#!/usr/bin/env python3 """ A5→A6 Rework Asset Downloader Polls DAM for campaigns with status A5, downloads rework assets, uploads to Box Updates status to A6 only when ALL assets successfully processed Supports OAuth2 (default) and mTLS (--auth-pfx) authentication Compatible with Python 3.6+ """ import sys import os import time import logging import argparse # 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 # Setup logging with rotation from logging.handlers import RotatingFileHandler # Create logs directory if it doesn't exist os.makedirs('logs', exist_ok=True) os.makedirs('logs/backup', exist_ok=True) # Configure logging with rotation # Keep 1 week of active logs (7 days * 10MB = 70MB) # Backup rotates keep 4 weeks (28 backups * 10MB = 280MB total) log_handler = RotatingFileHandler( 'logs/a5_to_a6.log', maxBytes=10*1024*1024, # 10MB per file backupCount=28 # Keep 28 rotated files (approximately 1 month) ) log_handler.setLevel(logging.INFO) log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logging.basicConfig( level=logging.INFO, handlers=[log_handler, console_handler] ) logger = logging.getLogger('A5toA6') def process_campaign(campaign, dam, box, db, notifier, config): """ Process single campaign - download all rework assets Returns: dict with success, processed_count, failed_count """ campaign_id = campaign['asset_id'] campaign_name = campaign['campaign_name'] campaign_number = campaign.get('campaign_id', 'N/A') logger.info("=" * 60) logger.info("Processing campaign: {} ({})".format(campaign_name, campaign_number)) logger.info("=" * 60) try: # Get ALL assets from Final Assets folder (recursive search) # A5 campaigns are Local Adaptation but use Final Assets folder (like B1) # So we pass is_global=True to search Final Assets folder all_assets = dam.get_master_assets(campaign_id, is_global=True) logger.info("Found {} total assets in Final Assets folder".format(len(all_assets))) # Filter for NOT APPROVED assets only not_approved_assets = [] skipped_assets = [] for asset in all_assets: if dam.is_asset_not_approved(asset): not_approved_assets.append(asset) else: skipped_assets.append(asset) logger.info("NOT APPROVED (rejected) assets: {}".format(len(not_approved_assets))) logger.info("Approved/other status (skipped): {}".format(len(skipped_assets))) # If NO rejected assets found, send email and exit if len(not_approved_assets) == 0: logger.info("No NOT APPROVED assets found - all assets are approved") # Send "no rejections" email notifier.send_email( template_name='a5_to_a6_no_rejections', recipients=config['notifications']['recipients']['success'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'total_assets': len(all_assets), 'skipped_count': len(skipped_assets) } ) logger.info("✓ Email sent: No rework required") return {'success': True, 'processed': 0, 'failed': 0} # Track results processed_assets = [] failed_assets = [] # Get Final Assets folder for upload directory final_folder_id = dam.find_final_assets_folder(campaign_id) if not final_folder_id: logger.error("Final Assets folder not found") return {'success': False, 'processed': 0, 'failed': len(not_approved_assets)} # Process ONLY NOT APPROVED assets for asset in not_approved_assets: asset_id = asset['asset_id'] asset_name = asset.get('name', 'unknown') folder_path = asset.get('folder_path', '') # Get subfolder path from recursive search try: if folder_path: logger.info("Processing NOT APPROVED: {} (from subfolder: {})".format(asset_name, folder_path)) else: logger.info("Processing NOT APPROVED: {}".format(asset_name)) # 1. Extract rejection details for email rejection_details = dam.extract_rejection_details(asset) # 2. Download from DAM file_path = dam.download_asset( asset_id, output_dir='temp/downloads/{}'.format(campaign_id) ) # 3. Extract Global Campaign Reference and Local Campaign ID from asset metadata global_ref = db.extract_global_campaign_reference(asset) # 4. Check if asset already exists in database (from A1→A2) # This will either find existing tracking_id or generate new one tracking_result = db.find_or_create_tracking_id( opentext_id=asset_id, local_campaign_id=global_ref['local_campaign_id'] ) tracking_id = tracking_result['tracking_id'] is_existing = tracking_result['is_existing'] if is_existing: logger.info("Found existing tracking ID: {} (updating record)".format(tracking_id)) else: logger.info("Generated new tracking ID: {}".format(tracking_id)) # 5. Upload to Box (Revisions folder with -Revisions suffix, preserve folder structure) box_result = box.upload_with_tracking_id( file_path=file_path, campaign_id=campaign_number, # Use C000000078, not hex asset_id campaign_name=campaign_name + "-Revisions", # Add -Revisions suffix tracking_id=tracking_id, subfolder_path=folder_path # Preserve DAM folder structure ) # 5. Store in database with FULL metadata and campaign references db_result = db.store_master_asset( tracking_id=tracking_id, opentext_id=asset_id, asset_data=asset, box_file_id=box_result['file_id'], box_url=box_result['url'], upload_folder_id=final_folder_id, global_master_campaign_id=global_ref['global_master_campaign_id'], global_master_folder_id=global_ref['global_master_folder_id'], local_campaign_id=global_ref['local_campaign_id'] ) if db_result['success']: processed_assets.append({ 'asset_id': asset_id, 'asset_name': asset_name, 'tracking_id': tracking_id, 'box_file_id': box_result['file_id'], 'box_url': box_result['url'], 'is_existing': is_existing, 'folder_path': folder_path, 'rejection_details': rejection_details # Include rejection comments for email }) logger.info("✓ Success: {} → {}{}".format( asset_name, tracking_id, " (updated)" if is_existing else " (new)" )) else: raise Exception("Database storage failed") # Clean up temp file os.remove(file_path) except Exception as e: logger.error("✗ Failed: {} - {}".format(asset_name, str(e))) failed_assets.append({ 'asset_id': asset_id, 'asset_name': asset_name, 'error': str(e) }) # CHECK: All rejected assets processed successfully? all_done = len(processed_assets) == len(not_approved_assets) logger.info("") logger.info("Campaign {} Results:".format(campaign_id)) logger.info(" NOT APPROVED (rejected): {}".format(len(not_approved_assets))) logger.info(" Successfully processed: {}".format(len(processed_assets))) logger.info(" Failed: {}".format(len(failed_assets))) logger.info(" Approved/skipped: {}".format(len(skipped_assets))) logger.info(" All Done: {}".format("YES" if all_done else "NO")) logger.info("") if all_done: # ALL rejected assets processed - update status logger.info("All NOT APPROVED assets processed - Updating status A5 → A6") status_result = dam.update_campaign_status(campaign_id, 'A6') if status_result['success']: logger.info("✓ Status updated successfully") # NOTE: No webhook for A5→A6 (rework workflow) # Send success email with rejection details notifier.send_email( template_name='a5_to_a6_rejections', recipients=config['notifications']['recipients']['success'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'rejected_count': len(processed_assets), 'skipped_count': len(skipped_assets), 'rejected_assets': processed_assets # Includes rejection_details } ) return {'success': True, 'processed': len(processed_assets), 'failed': 0} else: logger.error("✗ Status update failed: {}".format(status_result.get('error'))) # Don't send success notification if status update failed return {'success': False, 'processed': len(processed_assets), 'failed': 0} else: # NOT all done - some failed logger.warning("Campaign incomplete - NOT updating status (remains A5)") # Send partial completion email with asset details notifier.send_email( template_name='a5_to_a6_partial', recipients=config['notifications']['recipients']['errors'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'total_assets': len(not_approved_assets), 'successful': len(processed_assets), 'failed': len(failed_assets), 'rejected_assets': processed_assets, # Includes rejection_details 'failed_assets': failed_assets } ) return {'success': False, 'processed': len(processed_assets), 'failed': len(failed_assets)} except Exception as e: logger.error("Campaign processing failed: {}".format(str(e))) return {'success': False, 'processed': 0, 'failed': 0} def main(): """Main polling loop""" # Parse command-line arguments parser = argparse.ArgumentParser(description='Ferrero A5→A6 Rework Asset Downloader') parser.add_argument('--auth-pfx', action='store_true', help='Use mTLS certificate authentication instead of OAuth2') args = parser.parse_args() logger.info("=" * 60) logger.info("Ferrero A5→A6 Rework Asset Downloader Starting") if args.auth_pfx: logger.info("Authentication: mTLS Certificate (--auth-pfx)") else: logger.info("Authentication: OAuth2 (default)") logger.info("=" * 60) # Load configuration config = load_config('config/config.yaml') # Initialize clients (pass mTLS flag to DAM, use Revisions Box folder) dam = DAMClient(config, use_mtls=args.auth_pfx) box = BoxClient(config, root_folder_id='349441822875') # Revisions folder db = Database(config) notifier = Notifier(config) # Test connections logger.info("Testing connections...") if not dam.test_connection(): logger.error("DAM connection failed - exiting") sys.exit(1) if not box.test_connection(): logger.error("Box connection failed - exiting") sys.exit(1) if not db.test_connection(): logger.error("Database connection failed - exiting") sys.exit(1) logger.info("All connections OK") logger.info("") # SINGLE RUN MODE - Process ONE campaign and exit # Cron will run this script every 5 minutes, processing one campaign at a time try: logger.info("Searching for A5 campaigns...") # Search for campaigns with status A5 campaigns = dam.search_campaigns(status='A5') if not campaigns: logger.info("No A5 campaigns found - exiting") db.close() sys.exit(0) # Process ONLY THE FIRST campaign campaign = campaigns[0] logger.info("Found {} A5 campaigns - processing first one only".format(len(campaigns))) logger.info("") result = process_campaign(campaign, dam, box, db, notifier, config) if result['success']: logger.info("") logger.info("=" * 60) logger.info("✓ Campaign completed successfully") logger.info(" Processed: {} assets".format(result['processed'])) logger.info(" Status updated: A5 → A6") logger.info("=" * 60) db.close() sys.exit(0) else: logger.warning("") logger.warning("=" * 60) logger.warning("✗ Campaign incomplete or failed") logger.warning(" Processed: {} assets".format(result['processed'])) logger.warning(" Failed: {} assets".format(result['failed'])) logger.warning(" Status NOT updated (remains A5)") logger.warning("=" * 60) db.close() sys.exit(1) except Exception as e: logger.critical("Script error: {}".format(str(e))) # Send critical error notification notifier.send_email( template_name='upload_failed', recipients=config['notifications']['recipients']['critical'], data={ 'filename': 'A5→A6 Script', 'tracking_id': 'N/A', 'error': str(e) } ) db.close() sys.exit(1) if __name__ == '__main__': main()