"""Direct Delivery ERN XML model. Reads the DDEX ERN delivery file for a delivery job from the delivery XML (vector-audit) S3 bucket and derives delivery attributes from it. """ import hashlib import defusedxml.ElementTree as ElementTree from deliveryhistory import config from deliveryhistory.connectors import s3 # S3 key pattern for a delivery's ERN XML, keyed by the md5 hex digest of the # job id (encoding_queue_detail_id). Mirrors the layout written by the delivery # pipeline into the delivery XML bucket. XML_FILE_PATH_PATTERN = 'metadata/{0}-0.xml' # DDEX ERN tag holding the rights share for a delivery. A takedown delivers a # share of 0. RIGHT_SHARE_PERCENTAGE_TAG = 'RightSharePercentage' DELIVERY_TYPE_TAKEDOWN = 'takedown' DELIVERY_TYPE_METADATA_UPDATE = 'metadata_update' DELIVERY_TYPE_DELIVERY = 'delivery' def get_delivery_type(job_id): """Resolve the delivery type for a delivery job from its ERN XML. A delivery is a takedown when the RightSharePercentage in its ERN file is 0. Args: job_id (int): the delivery job id (encoding_queue_detail_id) Returns: str: DELIVERY_TYPE_TAKEDOWN when the delivery is a takedown, otherwise None. """ if job_id is None: return None xml_body = s3.get_object_body( config.DELIVERY_XML_S3_BUCKET_NAME, _build_xml_key(job_id)) if not xml_body: return None percentages = _right_share_percentages(xml_body) if percentages and all(value == 0 for value in percentages): return DELIVERY_TYPE_TAKEDOWN return None def _build_xml_key(job_id): """Build the S3 key for a delivery job's ERN XML.""" digest = hashlib.md5(str(job_id).encode(), usedforsecurity=False).hexdigest() return XML_FILE_PATH_PATTERN.format(digest) def _right_share_percentages(xml_body): """Extract all numeric RightSharePercentage values from an ERN file.""" root = ElementTree.fromstring(xml_body) values = [] for element in root.iter(): # ERN content elements are unqualified, but strip any namespace # prefix defensively before comparing the tag. if element.tag.rsplit('}', 1)[-1] != RIGHT_SHARE_PERCENTAGE_TAG: continue value = _to_number(element.text) if value is not None: values.append(value) return values def _to_number(text): """Parse element text into a float, or None when not numeric.""" try: return float(text) except (TypeError, ValueError): return None