#!/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 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 # 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""" logger.info("=" * 60) logger.info("Ferrero A5→A6 Rework Asset Downloader Starting") logger.info("=" * 60) # Load configuration config = load_config('config/config.yaml') # Initialize clients (use Revisions Box folder) dam = DAMClient(config) 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()