#!/usr/bin/env python3 """ A2→A3 Upload Handler - Box Folder Polling Version Polls Box folder for new files with V2 naming, uploads to DAM Updates status to A3 only when ALL assets for campaign uploaded Compatible with Python 3.6+ """ import sys import os import time import logging # Add shared library to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from shared.config_loader import load_config, load_field_mappings from shared.dam_client import DAMClient from shared.box_client import BoxClient from shared.database import Database from shared.notifier import Notifier from shared.filename_parser import FilenameParser from shared.metadata_extractor_mvp import MetadataExtractorMVP # Load configuration config = load_config('config/config.yaml') field_mappings = load_field_mappings(config) # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('logs/a2_to_a3.log'), logging.StreamHandler() ] ) logger = logging.getLogger('A2toA3') def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config): """ Process a single file from Box folder Returns: dict with success, asset_id, tracking_id """ file_id = file_info['id'] filename = file_info['name'] logger.info("Processing: {}".format(filename)) try: # 1. Parse V2 filename parsed = parser.parse_filename(filename) if not parsed['is_valid']: raise ValueError("Invalid V2 filename: {} - {}".format( filename, ', '.join(parsed['validation_errors']) )) tracking_id = parsed['tracking_id'] if not tracking_id: raise ValueError("No tracking ID in filename") # 2. Load master metadata from database master_asset = db.get_master_asset(tracking_id) if not master_asset: raise ValueError("No master asset for tracking ID: {}".format(tracking_id)) # 3. Download from Box temp_file = os.path.join('temp/downloads', filename) box.download_file(file_id, temp_file) # 4. Get clean filename clean_filename = parser.strip_upload_components(filename) # 5. Build MVP asset representation asset_rep = mvp_extractor.build_mvp_asset_representation( master_metadata=master_asset['full_metadata'], clean_filename=clean_filename, parsed_filename=parsed ) # 6. Rename to clean filename clean_temp_file = os.path.join('temp/downloads', clean_filename) if os.path.exists(clean_temp_file): os.remove(clean_temp_file) os.rename(temp_file, clean_temp_file) # 7. Upload to DAM upload_result = dam.upload_asset( file_path=clean_temp_file, folder_id=master_asset['upload_directory'], asset_representation=asset_rep ) if not upload_result['success']: raise Exception("Upload failed: {}".format(upload_result.get('error'))) # 8. Store derivative record db.store_derivative_asset( tracking_id=tracking_id, master_asset_id=None, dam_asset_id=upload_result['asset_id'], filename=clean_filename ) # 9. Clean up os.remove(clean_temp_file) logger.info("✓ Success: {} → Asset ID: {}".format(filename, upload_result['asset_id'])) return { 'success': True, 'asset_id': upload_result['asset_id'], 'tracking_id': tracking_id, 'filename': filename, 'clean_filename': clean_filename } except Exception as e: logger.error("✗ Failed: {} - {}".format(filename, str(e))) return { 'success': False, 'error': str(e), 'filename': filename, 'tracking_id': tracking_id if 'tracking_id' in locals() else None } def main(): """Main entry point - single run mode""" logger.info("=" * 60) logger.info("Ferrero A2→A3 Upload Handler Starting (Polling Mode)") logger.info("=" * 60) # Initialize clients dam = DAMClient(config) # Use A2→A3 Box folder for polling box = BoxClient(config, root_folder_id=config['box'].get('root_folder_a2_a3')) db = Database(config) notifier = Notifier(config) parser = FilenameParser() mvp_extractor = MetadataExtractorMVP(field_mappings) # Test connections logger.info("Testing connections...") if not dam.test_connection(): logger.error("DAM connection failed") sys.exit(1) if not box.test_connection(): logger.error("Box connection failed") sys.exit(1) if not db.test_connection(): logger.error("Database connection failed") sys.exit(1) logger.info("All connections OK") logger.info("") try: # Get Box folder ID to poll box_folder_id = config['box'].get('root_folder_a2_a3', config['box'].get('root_folder_id')) logger.info("Polling Box folder: {}".format(box_folder_id)) # List files in Box folder files = box.list_folder_files(box_folder_id) if not files: logger.info("No files found in Box folder - exiting") db.close() sys.exit(0) logger.info("Found {} files in Box folder".format(len(files))) # Filter for V2 filenames only valid_files = [] for file_info in files: parsed = parser.parse_filename(file_info['name']) if parsed['is_valid'] and parsed.get('tracking_id'): valid_files.append(file_info) else: logger.debug("Skipping invalid file: {}".format(file_info['name'])) logger.info("Found {} valid V2 files to process".format(len(valid_files))) if not valid_files: logger.info("No valid V2 files to process - exiting") db.close() sys.exit(0) # Process files one at a time (process first file only) file_info = valid_files[0] logger.info("Processing first file only (more will be processed on next run)") logger.info("") result = process_box_file(file_info, dam, box, db, parser, mvp_extractor, config) if result['success']: logger.info("") logger.info("=" * 60) logger.info("✓ File processed successfully") logger.info(" Filename: {}".format(result['filename'])) logger.info(" Clean filename: {}".format(result['clean_filename'])) logger.info(" Asset ID: {}".format(result['asset_id'])) logger.info(" Tracking ID: {}".format(result['tracking_id'])) logger.info("=" * 60) # TODO: Check if all campaign assets uploaded and update status A2→A3 # Would need to track campaign_id in master_assets table db.close() sys.exit(0) else: logger.warning("") logger.warning("=" * 60) logger.warning("✗ File processing failed") logger.warning(" Filename: {}".format(result['filename'])) logger.warning(" Error: {}".format(result['error'])) logger.warning("=" * 60) db.close() sys.exit(1) except Exception as e: logger.critical("Script error: {}".format(str(e))) db.close() sys.exit(1) if __name__ == '__main__': main()