def from_isbn(cls, isbn): """Attempts to fetch an edition by isbn, or if no edition is found, attempts to import from amazon :param str isbn: :rtype: edition|None :return: an open library work for this isbn """ isbn = canonical(isbn) if len(isbn) not in [10, 13]: return None # consider raising ValueError isbn13 = to_isbn_13(isbn) isbn10 = isbn_13_to_isbn_10(isbn13) # Attempt to fetch book from OL for isbn in [isbn13, isbn10]: if isbn: matches = web.ctx.site.things({ "type": "/type/edition", 'isbn_%s' % len(isbn): isbn }) if matches: return web.ctx.site.get(matches[0]) # Attempt to create from amazon, then fetch from OL key = (isbn10 or isbn13) and create_edition_from_amazon_metadata(isbn10 or isbn13) if key: return web.ctx.site.get(key)
def _get_amazon_metadata(id_, id_type='isbn', resources=None): """Uses the Amazon Product Advertising API ItemLookup operation to locatate a specific book by identifier; either 'isbn' or 'asin'. https://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html :param str id_: The item id: isbn (10/13), or Amazon ASIN. :param str id_type: 'isbn' or 'asin'. :return: A single book item's metadata, or None. :rtype: dict or None """ if not affiliate_server_url: return None if id_type == 'isbn': id_ = normalize_isbn(id_) if len(id_) == 13 and id_.startswith('978'): id_ = isbn_13_to_isbn_10(id_) try: r = requests.get('http://%s/isbn/%s' % (affiliate_server_url, id_)) r.raise_for_status() return r.json().get('hit') or None except requests.exceptions.ConnectionError: logger.exception("Affiliate Server unreachable") except requests.exceptions.HTTPError: logger.exception("Affiliate Server: id {} not found".format(id_)) return None
def get_isbn10(self): """Fetches either isbn_10 or isbn_13 from record and returns canonical isbn_10 """ isbn_10 = self.isbn_10 and canonical(self.isbn_10[0]) if not isbn_10: isbn_13 = self.get_isbn13() return isbn_13 and isbn_13_to_isbn_10(isbn_13) return isbn_10
def _get_amazon_metadata(isbn): # XXX some esbns may be < 10! isbn_10 = isbn if len(isbn) == 10 else isbn_13_to_isbn_10(isbn) isbn_13 = isbn if len(isbn) == 13 else isbn_10_to_isbn_13(isbn) try: if not lending.amazon_api: raise Exception product = lending.amazon_api.lookup(ItemId=isbn_10) except Exception as e: return None price_fmt, price, qlt = (None, None, None) used = product._safe_get_element_text( 'OfferSummary.LowestUsedPrice.Amount') new = product._safe_get_element_text('OfferSummary.LowestNewPrice.Amount') # prioritize lower prices and newer, all things being equal if used and new: price, qlt = (used, 'used') if int(used) < int(new) else (new, 'new') # accept whichever is available elif used or new: price, qlt = (used, 'used') if used else (new, 'new') if price: price = '{:00,.2f}'.format(int(price) / 100.) if qlt: price_fmt = "$%s (%s)" % (price, qlt) return { 'url': "https://www.amazon.com/dp/%s/?tag=%s" % (isbn, h.affiliate_id('amazon')), 'price': price_fmt, 'price_amt': price, 'qlt': qlt, 'title': product.title, 'authors': [{ 'name': name } for name in product.authors], 'publish_date': product.publication_date.strftime('%b %d, %Y'), 'source_records': ['amazon:%s' % product.asin], 'number_of_pages': product.pages, 'languages': list(product.languages), # needs to be normalized 'publishers': [product.publisher], 'cover': product.large_image_url, 'isbn_10': [isbn_10], 'isbn_13': [isbn_13] }
def _get_amazon_metadata(isbn=None): # XXX @hornc, you should be extending this to work with # isbn=, asin=, title=, authors=, etc isbn = normalize_isbn(isbn) try: if not lending.amazon_api: raise Exception product = lending.amazon_api.lookup( ItemId=isbn, IdType="ISBN", SearchIndex="Books") except Exception as e: return None price_fmt, price, qlt = (None, None, None) used = product._safe_get_element_text('OfferSummary.LowestUsedPrice.Amount') new = product._safe_get_element_text('OfferSummary.LowestNewPrice.Amount') # prioritize lower prices and newer, all things being equal if used and new: price, qlt = (used, 'used') if int(used) < int(new) else (new, 'new') # accept whichever is available elif used or new: price, qlt = (used, 'used') if used else (new, 'new') if price: price = '{:00,.2f}'.format(int(price)/100.) if qlt: price_fmt = "$%s (%s)" % (price, qlt) data = { 'url': "https://www.amazon.com/dp/%s/?tag=%s" % ( isbn, h.affiliate_id('amazon')), 'price': price_fmt, 'price_amt': price, 'qlt': qlt, 'title': product.title, 'authors': [{'name': name} for name in product.authors], 'publish_date': product.publication_date.strftime('%b %d, %Y'), 'source_records': ['amazon:%s' % product.asin], 'number_of_pages': product.pages, 'languages': list(product.languages), # needs to be normalized 'cover': product.large_image_url, } if product.publisher: data['publishers'] = [product.publisher] if len(isbn) == 10: data['isbn_10'] = [isbn] data['isbn_13'] = [isbn_10_to_isbn_13(isbn)] if len(isbn) == 13: data['isbn_13'] = [isbn] if isbn.startswith('978'): data['isbn_10'] = [isbn_13_to_isbn_10(isbn)] return data
def _get_amazon_metadata(id_, id_type='isbn', resources=None): """Uses the Amazon Product Advertising API ItemLookup operation to locatate a specific book by identifier; either 'isbn' or 'asin'. https://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html :param str id_: The item id: isbn (10/13), or Amazon ASIN. :param str id_type: 'isbn' or 'asin'. :return: A single book item's metadata, or None. :rtype: dict or None """ if id_type == 'isbn': id_ = normalize_isbn(id_) if len(id_) == 13 and id_.startswith('978'): id_ = isbn_13_to_isbn_10(id_) if amazon_api: try: return amazon_api.get_product(id_, serialize=True, resources=resources) except Exception: return None
def _get_amazon_metadata( id_: str, id_type: str = 'isbn', resources=None, retries: int = 3, sleep_sec: float = 0.1, ) -> Optional[dict]: """Uses the Amazon Product Advertising API ItemLookup operation to locatate a specific book by identifier; either 'isbn' or 'asin'. https://docs.aws.amazon.com/AWSECommerceService/latest/DG/ItemLookup.html :param str id_: The item id: isbn (10/13), or Amazon ASIN. :param str id_type: 'isbn' or 'asin'. :param resources: Used for AWSE Commerce Service lookup -- See Amazon docs :param int retries: Number of times to query affiliate server before returning None :param float sleep_sec: Delay time.sleep(sleep_sec) seconds before each retry :return: A single book item's metadata, or None. :rtype: dict or None """ if not affiliate_server_url: return None if id_type == 'isbn': id_ = normalize_isbn(id_) if len(id_) == 13 and id_.startswith('978'): id_ = isbn_13_to_isbn_10(id_) try: r = requests.get(f'http://{affiliate_server_url}/isbn/{id_}') r.raise_for_status() if hit := r.json().get('hit'): return hit if retries <= 1: return None time.sleep(sleep_sec) # sleep before recursive call return _get_amazon_metadata(id_, id_type, resources, retries - 1, sleep_sec)
def _serialize_amazon_product(product): """Takes a full Amazon product Advertising API returned AmazonProduct with multiple ResponseGroups, and extracts the data we are interested in. :param amazon.api.AmazonProduct product: :return: Amazon metadata for one product :rtype: dict """ price_fmt = price = qlt = None used = product._safe_get_element_text( 'OfferSummary.LowestUsedPrice.Amount') new = product._safe_get_element_text('OfferSummary.LowestNewPrice.Amount') # prioritize lower prices and newer, all things being equal if used and new: price, qlt = (used, 'used') if int(used) < int(new) else (new, 'new') # accept whichever is available elif used or new: price, qlt = (used, 'used') if used else (new, 'new') if price: price = '{:00,.2f}'.format(int(price) / 100.) if qlt: price_fmt = "$%s (%s)" % (price, qlt) data = { 'url': "https://www.amazon.com/dp/%s/?tag=%s" % (product.asin, h.affiliate_id('amazon')), 'price': price_fmt, 'price_amt': price, 'qlt': qlt, 'title': product.title, 'authors': [{ 'name': name } for name in product.authors], 'source_records': ['amazon:%s' % product.asin], 'number_of_pages': product.pages, 'languages': list(product.languages), 'cover': product.large_image_url, 'product_group': product.product_group, } if product._safe_get_element('OfferSummary') is not None: data['offer_summary'] = { 'total_new': int(product._safe_get_element_text('OfferSummary.TotalNew')), 'total_used': int(product._safe_get_element_text('OfferSummary.TotalUsed')), 'total_collectible': int(product._safe_get_element_text( 'OfferSummary.TotalCollectible')), } collectible = product._safe_get_element_text( 'OfferSummary.LowestCollectiblePrice.Amount') if new: data['offer_summary']['lowest_new'] = int(new) if used: data['offer_summary']['lowest_used'] = int(used) if collectible: data['offer_summary']['lowest_collectible'] = int(collectible) amazon_offers = product._safe_get_element_text('Offers.TotalOffers') if amazon_offers: data['offer_summary']['amazon_offers'] = int(amazon_offers) if product.publication_date: data['publish_date'] = product._safe_get_element_text( 'ItemAttributes.PublicationDate') if re.match(AMAZON_FULL_DATE_RE, data['publish_date']): data['publish_date'] = product.publication_date.strftime( '%b %d, %Y') if product.binding: data['physical_format'] = product.binding.lower() if product.edition: data['edition'] = product.edition if product.publisher: data['publishers'] = [product.publisher] if product.isbn: isbn = product.isbn if len(isbn) == 10: data['isbn_10'] = [isbn] data['isbn_13'] = [isbn_10_to_isbn_13(isbn)] elif len(isbn) == 13: data['isbn_13'] = [isbn] if isbn.startswith('978'): data['isbn_10'] = [isbn_13_to_isbn_10(isbn)] return data
def _get_amazon_metadata(id_=None, id_type='isbn'): # TODO: extend this to work with # isbn=, asin=, title=, authors=, etc kwargs = {} if id_type == 'isbn': id_ = normalize_isbn(id_) kwargs = {'SearchIndex': 'Books', 'IdType': 'ISBN'} kwargs['ItemId'] = id_ try: if not lending.amazon_api: raise Exception product = lending.amazon_api.lookup(**kwargs) # sometimes more than one product can be returned, choose first if isinstance(product, list): product = product[0] except Exception as e: return None price_fmt, price, qlt = (None, None, None) used = product._safe_get_element_text( 'OfferSummary.LowestUsedPrice.Amount') new = product._safe_get_element_text('OfferSummary.LowestNewPrice.Amount') # prioritize lower prices and newer, all things being equal if used and new: price, qlt = (used, 'used') if int(used) < int(new) else (new, 'new') # accept whichever is available elif used or new: price, qlt = (used, 'used') if used else (new, 'new') if price: price = '{:00,.2f}'.format(int(price) / 100.) if qlt: price_fmt = "$%s (%s)" % (price, qlt) data = { 'url': "https://www.amazon.com/dp/%s/?tag=%s" % (id_, h.affiliate_id('amazon')), 'price': price_fmt, 'price_amt': price, 'qlt': qlt, 'title': product.title, 'authors': [{ 'name': name } for name in product.authors], 'source_records': ['amazon:%s' % product.asin], 'number_of_pages': product.pages, 'languages': list(product.languages), # needs to be normalized 'cover': product.large_image_url, 'product_group': product.product_group, } if product.publication_date: # TODO: Don't populate false month and day for older products data['publish_date'] = product.publication_date.strftime('%b %d, %Y') if product.binding: data['physical_format'] = product.binding.lower() if product.edition: data['edition'] = product.edition if product.publisher: data['publishers'] = [product.publisher] if product.isbn: isbn = product.isbn if len(isbn) == 10: data['isbn_10'] = [isbn] data['isbn_13'] = [isbn_10_to_isbn_13(isbn)] elif len(isbn) == 13: data['isbn_13'] = [isbn] if isbn.startswith('978'): data['isbn_10'] = [isbn_13_to_isbn_10(isbn)] return data
def test_isbn_13_to_isbn_10(): assert isbn_13_to_isbn_10('978-0-940787-08-7') == '0940787083' assert isbn_13_to_isbn_10('9780940787087') == '0940787083' assert isbn_13_to_isbn_10('BAD-ISBN') is None