Feature: Apply naming-tool pre-upload metadata overrides on A2→A3 upload
The naming tool's metadata editor saves pre-upload overrides to the override_metadata table (shared ferrero_tracking DB), but until now the Python upload pipeline never read from it — every edit was being saved but never applied to DAM. This wires up the consumer side so user edits land on the uploaded asset. - database.py: get_override_metadata() / mark_override_applied(), resilient to a missing override_metadata table on dev DBs - metadata_extractor_mvp.py: OVERRIDE_FIELD_MAP (mirrors the naming tool's editor-field → DAM-field-ID map) + _apply_override_fields(). Applied after master/filename/forced/CreativeX values but before asset_type_overrides so EOL/LTD compliance still wins. Empty editor values are skipped (leaves inherited value alone). Validity ISO dates normalised to MM/DD/YYYY for DAM - a2_to_a3_upload_polling.py: lookup before building the asset rep, pass override_fields into build_mvp_asset_representation, mark applied only after confirmed upload success Override priority: user edit > master metadata > forced defaults > hardcoded today+365 validity — so the team's per-asset validity period (e.g. 1 month) now flows through end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4e9fb6d18f
commit
9e92db185a
3 changed files with 209 additions and 3 deletions
|
|
@ -254,7 +254,19 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not
|
|||
# 5. Get clean filename
|
||||
clean_filename = parser.strip_upload_components(filename)
|
||||
|
||||
# 6. Build MVP asset representation with CreativeX data from database
|
||||
# 6. Look up pre-upload metadata override saved by the naming tool's editor.
|
||||
# The naming tool stores filename without extension, so strip it here.
|
||||
filename_no_ext = os.path.splitext(filename)[0]
|
||||
override = db.get_override_metadata(filename_no_ext)
|
||||
override_fields = None
|
||||
if override:
|
||||
override_fields = override.get('override_fields')
|
||||
logger.info("Found pre-upload override (id={}) for {}: {} field(s)".format(
|
||||
override.get('id'), filename_no_ext,
|
||||
len(override_fields) if override_fields else 0
|
||||
))
|
||||
|
||||
# 7. Build MVP asset representation with CreativeX data from database
|
||||
asset_rep = mvp_extractor.build_mvp_asset_representation(
|
||||
master_metadata=master_asset['full_metadata'],
|
||||
clean_filename=clean_filename,
|
||||
|
|
@ -262,7 +274,8 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not
|
|||
box_metadata=box_metadata, # Pass CreativeX data from database
|
||||
tracking_mode=tracking_mode, # Pass tracking mode for folder-only handling
|
||||
master_opentext_id=master_asset['opentext_id'], # Primary master DAM ID
|
||||
master_opentext_ids=master_opentext_ids # All master IDs (multiple or single)
|
||||
master_opentext_ids=master_opentext_ids, # All master IDs (multiple or single)
|
||||
override_fields=override_fields # Pre-upload edits from naming tool
|
||||
)
|
||||
|
||||
# DRYRUN MODE: Display full asset representation and exit
|
||||
|
|
@ -355,6 +368,10 @@ def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config, not
|
|||
filename=clean_filename
|
||||
)
|
||||
|
||||
# Mark pre-upload override as applied (only after confirmed DAM upload success).
|
||||
if override:
|
||||
db.mark_override_applied(filename_no_ext)
|
||||
|
||||
# 9. Delete file from Box after successful upload (unless --keep-files flag set)
|
||||
if keep_files:
|
||||
logger.info("--keep-files flag set - File kept in Box: {}".format(filename))
|
||||
|
|
|
|||
|
|
@ -1104,6 +1104,88 @@ class Database:
|
|||
cursor.close()
|
||||
self.put_connection(conn)
|
||||
|
||||
def get_override_metadata(self, filename_without_ext):
|
||||
"""
|
||||
Look up pre-upload metadata override saved by the naming tool.
|
||||
|
||||
Returns the latest unapplied override row for this filename, or None.
|
||||
If the override_metadata table doesn't exist (e.g., on a dev DB where the
|
||||
naming tool migration hasn't been run), returns None — upload behaviour
|
||||
falls back to today's defaults.
|
||||
"""
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id, tracking_id, override_fields
|
||||
FROM override_metadata
|
||||
WHERE filename = %s
|
||||
AND applied_to_upload = FALSE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""", (filename_without_ext,))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
override_fields = row[2] if isinstance(row[2], dict) else json.loads(row[2])
|
||||
return {
|
||||
'id': row[0],
|
||||
'tracking_id': row[1],
|
||||
'override_fields': override_fields,
|
||||
}
|
||||
except psycopg2.errors.UndefinedTable:
|
||||
conn.rollback()
|
||||
logger.warning("override_metadata table does not exist - skipping override lookup")
|
||||
return None
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error("Failed to query override_metadata for '{}': {}".format(
|
||||
filename_without_ext, str(e)
|
||||
))
|
||||
return None
|
||||
finally:
|
||||
cursor.close()
|
||||
self.put_connection(conn)
|
||||
|
||||
def mark_override_applied(self, filename_without_ext):
|
||||
"""
|
||||
Mark a pre-upload override row as applied after a successful DAM upload.
|
||||
Only updates rows that are currently applied_to_upload = FALSE.
|
||||
"""
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE override_metadata
|
||||
SET applied_to_upload = TRUE,
|
||||
applied_at = CURRENT_TIMESTAMP
|
||||
WHERE filename = %s
|
||||
AND applied_to_upload = FALSE
|
||||
""", (filename_without_ext,))
|
||||
|
||||
updated = cursor.rowcount
|
||||
conn.commit()
|
||||
|
||||
if updated:
|
||||
logger.info("Marked {} override row(s) as applied for '{}'".format(
|
||||
updated, filename_without_ext
|
||||
))
|
||||
return updated
|
||||
except psycopg2.errors.UndefinedTable:
|
||||
conn.rollback()
|
||||
return 0
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error("Failed to mark override applied for '{}': {}".format(
|
||||
filename_without_ext, str(e)
|
||||
))
|
||||
return 0
|
||||
finally:
|
||||
cursor.close()
|
||||
self.put_connection(conn)
|
||||
|
||||
def close(self):
|
||||
"""Close all connections in pool"""
|
||||
if self.pool:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,36 @@ from shared.config_loader import load_country_code_mappings
|
|||
|
||||
logger = logging.getLogger('MetadataExtractorMVP')
|
||||
|
||||
# Editor field name -> DAM metadata field ID.
|
||||
# Mirrors the canonical mapping in the naming tool's public-v2/Database.php
|
||||
# so that pre-upload overrides saved via the metadata editor are applied to
|
||||
# the matching DAM fields on upload.
|
||||
OVERRIDE_FIELD_MAP = {
|
||||
'validity_start': 'FERRERO.FIELD.ASSET VALIDITY START PERIOD',
|
||||
'validity_end': 'FERRERO.FIELD.ASSET VALIDITY END PERIOD',
|
||||
'marketing_tag': 'MARKETING_TAG',
|
||||
'agency_name': 'FERRERO.MARKETING.FIELD.AGENCY NAME',
|
||||
'spot_version': 'FERRERO.MARKETING.FIELD.SPOT_VERSION',
|
||||
'director_name': 'FERRERO.MARKETING.FIELD.DIRECTOR_NAME',
|
||||
'video_post_prod_company': 'FERRERO.MARKETING.FIELD.VIDEO_POST_PROD_COMPANY',
|
||||
'video_post_prod_contact': 'FERRERO.MARKETING.FIELD.VID_POST_PROD_CONTACT',
|
||||
'audio_post_prod_company': 'FERRERO.MARKETING.FIELD.AUDIO_POST_PROD_COMPANY',
|
||||
'audio_post_prod_contact': 'FERRERO.MARKETING.FIELD.AUDIO_POST_PROD_CONTACT',
|
||||
'video_type': 'FERRERO.MARKET.FIELD.TYPE_VID',
|
||||
'ip_rights': 'FERRERO.MARKET.FIELD.IPRIGHT',
|
||||
'production_company': 'FERRERO.MARKET.PROD_COMPANY',
|
||||
'licensing': 'FERRERO.MARKET.FIELD.LICENSIN',
|
||||
'buyout': 'FERRERO.MARKET.FIELD.BUYOUT',
|
||||
'ferrero_property': 'FERRERO.MARKET.FIELD.FERRERO PROPERTY',
|
||||
'video_status': 'FERRERO.MARKET.VID_N_STAT',
|
||||
'license': 'FERRERO.MARKET.FIELD.LICENSE',
|
||||
'creativex_score': 'FERRERO.TAB.FIELD.CREATIVEX',
|
||||
'creativex_link': 'FERRERO.FIELD.CREATIVEX LINK',
|
||||
}
|
||||
|
||||
DATE_OVERRIDE_FIELDS = {'validity_start', 'validity_end'}
|
||||
|
||||
|
||||
class MetadataExtractorMVP:
|
||||
def __init__(self, field_mappings):
|
||||
"""
|
||||
|
|
@ -113,7 +143,7 @@ class MetadataExtractorMVP:
|
|||
|
||||
return extracted_fields
|
||||
|
||||
def build_mvp_asset_representation(self, master_metadata, clean_filename, parsed_filename, box_metadata=None, tracking_mode='full', master_opentext_id=None, master_opentext_ids=None):
|
||||
def build_mvp_asset_representation(self, master_metadata, clean_filename, parsed_filename, box_metadata=None, tracking_mode='full', master_opentext_id=None, master_opentext_ids=None, override_fields=None):
|
||||
"""
|
||||
Build asset representation with MVP fields + updates from filename
|
||||
|
||||
|
|
@ -124,6 +154,10 @@ class MetadataExtractorMVP:
|
|||
box_metadata: Optional Box metadata
|
||||
tracking_mode: 'full' (inherit all metadata) or 'folder_only' (only use folder)
|
||||
master_opentext_id: Optional DAM Asset ID of master asset (for derivative tracking)
|
||||
override_fields: Optional dict of pre-upload metadata overrides keyed by
|
||||
editor field name (e.g. {'validity_end': '...', 'ip_rights': 'Yes'}).
|
||||
Applied after master/filename/forced values but before asset-type
|
||||
overrides so EOL/LTD compliance still wins. Empty values are skipped.
|
||||
|
||||
Returns:
|
||||
Asset representation dict ready for upload
|
||||
|
|
@ -160,6 +194,13 @@ class MetadataExtractorMVP:
|
|||
if box_metadata:
|
||||
mvp_fields = self._update_creativex_fields(mvp_fields, box_metadata)
|
||||
|
||||
# Apply pre-upload metadata overrides from the naming tool's editor.
|
||||
# Runs after master/filename/forced/default/CreativeX values so it wins
|
||||
# over them, but before asset_type_overrides so EOL/LTD compliance rules
|
||||
# still take final precedence.
|
||||
if override_fields:
|
||||
mvp_fields = self._apply_override_fields(mvp_fields, override_fields)
|
||||
|
||||
# Apply asset type overrides (e.g., EOL, LTD) - takes final precedence over
|
||||
# forced values, defaults, and CreativeX (LTD removes CreativeX entirely).
|
||||
mvp_fields = self._apply_asset_type_overrides(mvp_fields, parsed_filename)
|
||||
|
|
@ -921,6 +962,72 @@ class MetadataExtractorMVP:
|
|||
return field['value']['value']['field_value'].get('value')
|
||||
return None
|
||||
|
||||
def _apply_override_fields(self, mvp_fields, override_fields):
|
||||
"""
|
||||
Apply pre-upload metadata overrides from the naming tool.
|
||||
|
||||
For each non-empty entry in override_fields, map the editor field name
|
||||
to its DAM field ID via OVERRIDE_FIELD_MAP and write the value into the
|
||||
matching field in mvp_fields. Empty strings are skipped (treat as
|
||||
"user didn't set this, leave inherited value alone"). Validity dates
|
||||
from the editor arrive as ISO 8601 strings and are normalised to the
|
||||
MM/DD/YYYY format DAM expects.
|
||||
"""
|
||||
if not override_fields:
|
||||
return mvp_fields
|
||||
|
||||
applied = 0
|
||||
for editor_field, raw_value in override_fields.items():
|
||||
if raw_value is None or raw_value == '':
|
||||
continue
|
||||
|
||||
dam_field_id = OVERRIDE_FIELD_MAP.get(editor_field)
|
||||
if not dam_field_id:
|
||||
logger.debug("Override: no DAM mapping for editor field '{}' - skipping".format(editor_field))
|
||||
continue
|
||||
|
||||
value = raw_value
|
||||
if editor_field in DATE_OVERRIDE_FIELDS:
|
||||
value = self._normalize_iso_date(raw_value)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
target = None
|
||||
for field in mvp_fields:
|
||||
if field.get('id') == dam_field_id:
|
||||
target = field
|
||||
break
|
||||
|
||||
if target is None:
|
||||
logger.warning("Override: field {} (DAM id {}) not present in mvp_fields - skipping".format(
|
||||
editor_field, dam_field_id
|
||||
))
|
||||
continue
|
||||
|
||||
if editor_field in DATE_OVERRIDE_FIELDS:
|
||||
self._set_date_field_value(target, value)
|
||||
else:
|
||||
self._set_field_value(target, value)
|
||||
|
||||
logger.info("Override applied: {} ({}) = {}".format(editor_field, dam_field_id, value))
|
||||
applied += 1
|
||||
|
||||
if applied:
|
||||
logger.info("Applied {} pre-upload override field(s) from naming tool".format(applied))
|
||||
return mvp_fields
|
||||
|
||||
def _normalize_iso_date(self, iso_str):
|
||||
"""Convert an ISO 8601 date string (with or without time/timezone) to MM/DD/YYYY."""
|
||||
if not iso_str:
|
||||
return None
|
||||
try:
|
||||
date_part = iso_str.split('T')[0]
|
||||
dt = datetime.strptime(date_part, '%Y-%m-%d')
|
||||
return dt.strftime('%m/%d/%Y')
|
||||
except Exception as e:
|
||||
logger.warning("Could not normalize override date '{}': {}".format(iso_str, str(e)))
|
||||
return None
|
||||
|
||||
def _set_field_value(self, field, value):
|
||||
"""Set field value handling different structures"""
|
||||
import json
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue