#!/usr/bin/env python3 """ A1→A2 Master Asset Downloader Polls DAM for campaigns with status A1, downloads master assets, uploads to Box Updates status to A2 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 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('logs/a1_to_a2.log'), logging.StreamHandler() ] ) logger = logging.getLogger('A1toA2') def process_campaign(campaign, dam, box, db, notifier, config): """ Process single campaign - download all master 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 master assets master_assets = dam.get_master_assets(campaign_id) total_assets = len(master_assets) logger.info("Found {} master assets".format(total_assets)) if total_assets == 0: logger.warning("No master assets found, skipping campaign") return {'success': False, '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': total_assets} # Process each asset for asset in master_assets: asset_id = asset['asset_id'] asset_name = asset.get('name', 'unknown') try: logger.info("Processing: {}".format(asset_name)) # 1. Download from DAM file_path = dam.download_asset( asset_id, output_dir='temp/downloads/{}'.format(campaign_id) ) # 2. Generate tracking ID tracking_id = db.generate_unique_tracking_id() # 3. Upload to Box box_result = box.upload_with_tracking_id( file_path=file_path, campaign_id=campaign_id, campaign_name=campaign_name, tracking_id=tracking_id ) # 4. Store in database with FULL metadata 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 ) 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'] }) logger.info("✓ Success: {} → {}".format(asset_name, tracking_id)) 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 assets processed successfully? all_done = len(processed_assets) == total_assets logger.info("") logger.info("Campaign {} Results:".format(campaign_id)) logger.info(" Total: {}".format(total_assets)) logger.info(" Successful: {}".format(len(processed_assets))) logger.info(" Failed: {}".format(len(failed_assets))) logger.info(" All Done: {}".format("YES" if all_done else "NO")) logger.info("") if all_done: # ALL assets processed - update status logger.info("All assets processed - Updating status A1 → A2") status_result = dam.update_campaign_status(campaign_id, 'A2') if status_result['success']: logger.info("✓ Status updated successfully") # Send webhook notification if config['webhooks']['campaign_status_update']['enabled']: logger.info("Sending campaign status webhook...") notifier.send_webhook( url=config['webhooks']['campaign_status_update']['url'], payload={ 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'campaign_name': campaign_name, 'old_status': 'A1', 'new_status': 'A2', 'asset_count': len(processed_assets), 'processed_assets': processed_assets, 'timestamp': int(time.time()) } ) # Send success email notifier.send_email( template_name='a1_to_a2_complete', recipients=config['notifications']['recipients']['success'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'campaign_number': campaign_number, 'asset_count': len(processed_assets) } ) 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 A1)") # Send partial completion email notifier.send_email( template_name='a1_to_a2_partial', recipients=config['notifications']['recipients']['errors'], data={ 'campaign_name': campaign_name, 'campaign_id': campaign_id, 'total_assets': total_assets, 'successful': len(processed_assets), 'failed': len(failed_assets), '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': total_assets} def main(): """Main polling loop""" logger.info("=" * 60) logger.info("Ferrero A1→A2 Master Asset Downloader Starting") logger.info("=" * 60) # Load configuration config = load_config('config/config.yaml') # Initialize clients dam = DAMClient(config) box = BoxClient(config) 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("") poll_interval = config['polling']['interval_seconds'] max_campaigns = config['polling']['max_campaigns_per_run'] # Main polling loop while config['polling']['enabled']: try: logger.info("Polling for A1 campaigns...") # Search for campaigns with status A1 campaigns = dam.search_campaigns(status='A1') if not campaigns: logger.info("No A1 campaigns found") else: logger.info("Found {} A1 campaigns".format(len(campaigns))) # Limit campaigns per run campaigns_to_process = campaigns[:max_campaigns] # Process each campaign for campaign in campaigns_to_process: result = process_campaign(campaign, dam, box, db, notifier, config) if result['success']: logger.info("✓ Campaign completed successfully") else: logger.warning("✗ Campaign incomplete or failed") logger.info("") logger.info("Sleeping for {} seconds...".format(poll_interval)) logger.info("") time.sleep(poll_interval) except KeyboardInterrupt: logger.info("Shutdown requested by user") break 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': 'A1→A2 Script', 'tracking_id': 'N/A', 'error': str(e) } ) # Continue running after error time.sleep(poll_interval) logger.info("A1→A2 Script stopped") db.close() if __name__ == '__main__': main()