Skip to content

Instantly share code, notes, and snippets.

@dbernheisel
Created February 21, 2015 05:07
Show Gist options
  • Select an option

  • Save dbernheisel/d7a2680742a8f9562cca to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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