Created
February 21, 2015 05:07
-
-
Save dbernheisel/d7a2680742a8f9562cca to your computer and use it in GitHub Desktop.
Some ONIX 3.0 files have complete sales rights expressed, but a contract may need to override what's in the ONIX. For example, the publisher may have world rights, but the given contract limits their ability to sell in the US.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/local/bin/python3 | |
| from lxml import etree | |
| import sys | |
| import os | |
| import logging | |
| import shutil | |
| import argparse | |
| # This is designed to take an ONIX 3.0 file, analyze the Sales Rights, enforce the contract limitations, and then give pass it on for ingestion. | |
| # USAGE: python3 limit-sales-rights.py -f $FILE -c US CA | |
| # outputs a log file | |
| ALL_COUNTRIES = ["AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CS", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "YU", "ZA", "ZM", "ZW"] | |
| def intersect(a, b): | |
| return list(set(a) & set(b)) | |
| def main(onixfile, limit): | |
| print("Limiting the file to " + " ".join(sorted(limit))) | |
| onixfile = onixfile[0] | |
| print(onixfile) | |
| logging.basicConfig(filename=sys.argv[0]+'.log', level=logging.DEBUG, format='%(asctime)s %(message)s', datefmt='%Y/%m/%d %H:%M:%S') | |
| logging.debug("======================================") | |
| outputfile = os.path.join(os.path.split(os.path.abspath(onixfile))[0], "Adjusted", "Adjusted_" + os.path.split(os.path.abspath(onixfile))[1]) | |
| movedfile = os.path.join(os.path.split(os.path.abspath(onixfile))[0], "Originals", os.path.split(os.path.abspath(onixfile))[1]) | |
| # test that we can open the file | |
| try: | |
| with open(onixfile, 'r') as f: logging.debug("Tested file existence %s", onixfile) | |
| except IOError as e: | |
| logging.critical("Can't open the file: %s" % os.path.abspath(onixfile)) | |
| logging.critical(e) | |
| sys.exit(1) | |
| try: | |
| logging.debug("Parsing ONIX file.") | |
| root = etree.parse(onixfile, etree.XMLParser(encoding='utf-8', recover=True, dtd_validation=False, strip_cdata=False)).getroot() | |
| except etree.XMLSyntaxError as e: | |
| logging.critical("Could not parse %s", onixfile) | |
| logging.critical(e.error_log.filter_from_level(etree.ErrorLevels.FATAL)) | |
| sys.exit(1) | |
| productsadjusted1 = root.xpath('/ONIXmessage/product[publishingdetail/salesrights[b089[text()="01"] or b089[text()="02"] or b089[text()="07"] or b089[text()="08"]]]') | |
| productsadjusted2 = root.xpath('/ONIXmessage/product[publishingdetail[x456[text()="01"] or x456[text()="02"] or x456[text()="07"] or x456[text()="08"]]]') | |
| productsadjusted = productsadjusted1 + productsadjusted2 | |
| print(len(productsadjusted)) | |
| for product in productsadjusted: | |
| isbn = product.xpath('.//productidentifier[b221/text()="15"]/b244')[0].text | |
| print("================================") | |
| print(isbn) | |
| forsaleterritories = [] | |
| notforsaleterritories = [] | |
| hasForSaleROW = False | |
| hasNotForSaleROW = False | |
| try: salesrights01 = product.xpath('.//publishingdetail/salesrights[b089[text()="01"]]/territory'); forsaleterritories.append(salesrights01[0]) | |
| except: pass | |
| try: salesrights02 = product.xpath('.//publishingdetail/salesrights[b089[text()="02"]]/territory'); forsaleterritories.append(salesrights02[0]) | |
| except: pass | |
| try: salesrights03 = product.xpath('.//publishingdetail/salesrights[b089[text()="03"]]/territory'); notforsaleterritories.append(salesrights03[0]) | |
| except: pass | |
| try: salesrights04 = product.xpath('.//publishingdetail/salesrights[b089[text()="04"]]/territory'); notforsaleterritories.append(salesrights04[0]) | |
| except: pass | |
| try: salesrights05 = product.xpath('.//publishingdetail/salesrights[b089[text()="05"]]/territory'); notforsaleterritories.append(salesrights05[0]) | |
| except: pass | |
| try: salesrights06 = product.xpath('.//publishingdetail/salesrights[b089[text()="06"]]/territory'); notforsaleterritories.append(salesrights06[0]) | |
| except: pass | |
| try: salesrights07 = product.xpath('.//publishingdetail/salesrights[b089[text()="07"]]/territory'); forsaleterritories.append(salesrights07[0]) | |
| except: pass | |
| try: salesrights08 = product.xpath('.//publishingdetail/salesrights[b089[text()="08"]]/territory'); forsaleterritories.append(salesrights08[0]) | |
| except: pass | |
| # Remove NOT For Sale ROW tag. One day I can remove this useless section. | |
| try: | |
| rownotforsalesrights = product.xpath('.//publishingdetail/x456[text()="03" or text()="04" or text()="05" or text()="06"]') | |
| # Remove Not For Sale ROW tags, since they don't matter and introduce bad processing for some ingestions. | |
| logging.debug(isbn + "Has NOT For Sale ROW") | |
| publishingdetail = product.xpath('.//publishingdetail')[0] | |
| logging.debug(isbn + " Saw: " + str(etree.tostring(publishingdetail)).strip('\r\n')) | |
| publishingdetail.remove(rownotforsalesrights[0]) | |
| print("Removed the Not For Sale ROW tag") | |
| logging.debug(isbn + " Corrected: Removed the Not For Sale ROW tag x456 since !!! doesn't support it") | |
| except: pass | |
| # Interpret For Sale ROW tag into a <salesrights> | |
| try: | |
| rowsalesrights = product.xpath('.//publishingdetail/x456[text()="01" or text()="02" or text()="07" or text()="08"]') | |
| rowsalesrightstype = rowsalesrights[0].text | |
| # Set up the Sales Rights composite | |
| logging.debug(isbn + "Has For Sale ROW") | |
| print("processing ROW==============") | |
| publishingdetail = product.xpath('.//publishingdetail')[0] | |
| salesrights = etree.SubElement(publishingdetail, "salesrights") | |
| salesrightstype = etree.SubElement(salesrights, "b089") | |
| salesrightstype.text = rowsalesrightstype | |
| rowterritory = etree.SubElement(salesrights, "territory") | |
| rowCountriesIncluded = etree.SubElement(rowterritory, "x449") | |
| # Do the Country Code math on ROW means, depending on what's present | |
| # CAUTION THIS DOES NOT ACCOUNT FOR ALL SCENARIOS YET (ECZ or sub-territories) | |
| print("ROW starting with all countries") | |
| rowCountriesIncludedlist = ALL_COUNTRIES | |
| logging.debug(isbn + " Starting: " + " ".join(rowCountriesIncludedlist)) | |
| # Iterate Not For Sale CountriesIncluded so we remove them from ROW | |
| for territory in notforsaleterritories: | |
| try: CountriesIncluded = territory.xpath('.//x449')[0]; hasCountriesIncluded = True; print("Found Not For Sale x449: " + CountriesIncluded.text) | |
| except: pass | |
| if hasCountriesIncluded == True: | |
| CountriesIncludedlist = sorted(CountriesIncluded.text.split(" ")) | |
| adjustedCountriesIncluded = [x for x in rowCountriesIncludedlist if x not in CountriesIncludedlist] | |
| rowCountriesIncludedlist = adjustedCountriesIncluded | |
| print("rowCountriesIncludedlist is now: %s" % " ".join(adjustedCountriesIncluded)) | |
| logging.debug(isbn + " Removing: " + " ".join(CountriesIncludedlist)) | |
| logging.debug(isbn + " Adjusted: " + " ".join(adjustedCountriesIncluded)) | |
| # Iterate For Sale CountriesIncluded so we remove them from ROW | |
| for territory in forsaleterritories: | |
| try: CountriesIncluded = territory.xpath('.//x449')[0]; hasCountriesIncluded = True; print("Found For Sale x449: " + CountriesIncluded.text) | |
| except: pass | |
| if hasCountriesIncluded == True: | |
| CountriesIncludedlist = sorted(CountriesIncluded.text.split(" ")) | |
| adjustedCountriesIncluded = [x for x in rowCountriesIncludedlist if x not in CountriesIncludedlist] | |
| rowCountriesIncludedlist = adjustedCountriesIncluded | |
| print("rowCountriesIncludedlist is now: %s" % " ".join(adjustedCountriesIncluded)) | |
| logging.debug(isbn + " Removing: " + " ".join(CountriesIncludedlist)) | |
| logging.debug(isbn + " Adjusted: " + " ".join(adjustedCountriesIncluded)) | |
| # So now we have a representation of ROW, but we need to ensure it's limited now | |
| adjustedCountriesIncluded = " ".join(set(adjustedCountriesIncluded).intersection(limit)) | |
| print("rowCountriesIncludedlist is now: %s" % adjustedCountriesIncluded) | |
| logging.debug(isbn + " The stated limit is: " + " ".join(limit)) | |
| logging.debug(isbn + " ROW has been interpreted to: " + adjustedCountriesIncluded) | |
| rowCountriesIncluded.text = adjustedCountriesIncluded | |
| logging.debug(isbn + " Saw: " + str(etree.tostring(rowsalesrights[0])).strip('\r\n')) | |
| print(isbn + " Saw: " + str(etree.tostring(rowsalesrights[0])).strip('\r\n')) | |
| logging.debug(isbn + " Corrected: " + str(etree.tostring(rowterritory)).strip('\r\n')) | |
| # Remove original ROW tag since now it's represented by a <salesrights> composite | |
| print("Removed the original ROW tag") | |
| logging.debug(isbn + " Removed the ROW tag x456 since it was converted to a <salesrights>") | |
| publishingdetail.remove(rowsalesrights[0]) | |
| except: print("Something went wrong") | |
| # Remove Not For Sale composites. Hope to remove this useless section one day. | |
| for territory in notforsaleterritories: | |
| print("not for sale==============") | |
| salesrights = territory.getparent() | |
| salesrights.getparent().remove(salesrights) | |
| print("To: Removed this SalesRights, since !!! doesn't read Not For Sale SalesRights: %s" % str(etree.tostring(salesrights)).strip('\r\n')) | |
| logging.debug(isbn + " Corrected: " + "Removed this SalesRights, since !!! doesn't read Not For Sale SalesRights: %s" % str(etree.tostring(salesrights)).strip('\r\n')) | |
| for territory in forsaleterritories: | |
| print("territory==============") | |
| print("From:\n" + str(etree.tostring(territory))) | |
| hasCountriesIncluded = False | |
| hasRegionsIncluded = False | |
| hasCountriesExcluded = False | |
| hasRegionsExcluded = False | |
| CountriesIncluded = False | |
| RegionsIncluded = False | |
| CountriesExcluded = False | |
| RegionsExcluded = False | |
| try: CountriesIncluded = territory.xpath('.//x449')[0]; hasCountriesIncluded = True; print("Found x449: " + CountriesIncluded.text) | |
| except: pass | |
| try: RegionsIncluded = territory.xpath('.//x450')[0]; hasRegionsIncluded = True; print("Found x450: " + RegionsIncluded.text) | |
| except: pass | |
| try: CountriesExcluded = territory.xpath('.//x451')[0]; hasCountriesExcluded = True; print("Found x451: " + CountriesExcluded.text) | |
| except: pass | |
| try: RegionsExcluded = territory.xpath('.//x452')[0]; hasRegionsExcluded = True; print("Found x452: " + RegionsExcluded.text) | |
| except: pass | |
| print(hasCountriesIncluded, hasRegionsIncluded, hasCountriesExcluded, hasRegionsExcluded) | |
| # Regions Included WORLD only | |
| if hasCountriesIncluded == False and hasRegionsIncluded == True and hasCountriesExcluded == False and hasRegionsExcluded == False: | |
| logging.debug(isbn + " Detected Regions Included ONLY") | |
| logging.debug(isbn + " Saw: " + str(etree.tostring(territory)).strip('\r\n')) | |
| if RegionsIncluded.text.upper() == "WORLD": | |
| newCountriesIncluded = etree.Element("x449") | |
| newCountriesIncluded.text = " ".join(set(ALL_COUNTRIES).intersection(limit)) | |
| territory.replace(RegionsIncluded, newCountriesIncluded) | |
| print("To:\n" + str(etree.tostring(territory))) | |
| logging.debug(isbn + " Corrected: " + str(etree.tostring(territory)).strip('\r\n')) | |
| continue | |
| else: | |
| print("We have not accounted for this combination (RegionsIncluded only, but not WORLD) of Sales Rights") | |
| logging.error("We have not accounted for this combination (RegionsIncluded only, but not WORLD) of Sales Rights") | |
| sys.exit(1) | |
| # Countries Included only | |
| if hasCountriesIncluded == True and hasRegionsIncluded == False and hasCountriesExcluded == False and hasRegionsExcluded == False: | |
| logging.debug(isbn + " Detected Countries Included ONLY") | |
| logging.debug(isbn + " Saw: " + str(etree.tostring(territory)).strip('\r\n')) | |
| currentsalesrights = sorted(CountriesIncluded.text.split(" ")) | |
| adjustedsalesrights = set(currentsalesrights).intersection(limit) | |
| if len(adjustedsalesrights) == 0: # The intersection left this salesrights empty, therefore we must remove it | |
| salesrights = territory.getparent() | |
| salesrights.getparent().remove(salesrights) | |
| print("To:\nRemoved this SalesRights, since nothing remained") | |
| logging.debug(isbn + " Corrected: " + "Removed this SalesRights") | |
| continue | |
| CountriesIncluded.text = " ".join(sorted(adjustedsalesrights)) | |
| print("To:\n" + str(etree.tostring(territory))) | |
| logging.debug(isbn + " Corrected: " + str(etree.tostring(territory)).strip('\r\n')) | |
| continue | |
| # Regions Included WORLD with Countries Excluded | |
| if hasCountriesIncluded == False and hasRegionsIncluded == True and hasCountriesExcluded == True and hasRegionsExcluded == False: | |
| logging.debug(isbn + " Detected Regions Included with Countries Excluded") | |
| logging.debug(isbn + " Saw: " + str(etree.tostring(territory)).strip('\r\n')) | |
| if RegionsIncluded.text.upper() == "WORLD": | |
| newCountriesIncluded = etree.Element("x449") | |
| adjustedsalesrights = [x for x in ALL_COUNTRIES if x not in CountriesExcluded.text.split(" ")] | |
| adjustedsalesrights = set(adjustedsalesrights).intersection(limit) | |
| if len(adjustedsalesrights) == 0: # The intersection left this salesrights empty, therefore we must remove it | |
| salesrights = territory.getparent() | |
| salesrights.getparent().remove(salesrights) | |
| print("To:\nRemoved this SalesRights, since nothing remained") | |
| logging.debug(isbn + " Corrected: " + "Removed this SalesRights") | |
| continue | |
| newCountriesIncluded.text = " ".join(adjustedsalesrights) | |
| territory.replace(RegionsIncluded, newCountriesIncluded) | |
| territory.remove(CountriesExcluded) | |
| print("To:\n" + str(etree.tostring(territory))) | |
| logging.debug(isbn + " Corrected: " + str(etree.tostring(territory)).strip('\r\n')) | |
| continue | |
| else: | |
| print("We have not accounted for this combination (RegionsIncluded that is NOT WORLD, and also has CountriesExcluded") | |
| logging.error("We have not accounted for this combination (RegionsIncluded that is NOT WORLD, and also has CountriesExcluded") | |
| sys.exit(1) | |
| try: | |
| with open(outputfile, 'wb') as f: | |
| f.write(etree.tostring(root, xml_declaration=True, encoding='utf-8')) | |
| except IOError as e: | |
| logging.critical("Could not write adjusted ONIX file: %s" % os.path.abspath(movedfile)) | |
| logging.critical(e) | |
| sys.exit(1) | |
| try: | |
| logging.debug("Moving original file into /Originals") | |
| shutil.copy2(onixfile, movedfile) | |
| os.remove(onixfile) | |
| sys.exit(0) | |
| except IOError as e: | |
| logging.error("Move failed") | |
| logging.critical(e) | |
| sys.exit(1) | |
| parser = argparse.ArgumentParser(description="Modify SalesRights of each product by intelligently removing countries") | |
| parser.add_argument('-f', '--file', nargs=1, required=True, help="The ONIX file that you'd like to modify") | |
| parser.add_argument('-c', '--country', nargs='*', required=True, help="A list of ISO country codes with which you'd like to limit the the Sales Rights. eg: US CA FR") | |
| args = parser.parse_args() | |
| main(args.file, args.country) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment