MAJOR MILESTONE: Complete Python automation system created! Components Implemented: ✅ Box Client (box_client.py) - JWT authentication via boxsdk - Upload with tracking ID suffix - Download files - Campaign folder creation - Connection testing ✅ Database Client (database.py) - PostgreSQL connection pooling - generate_unique_tracking_id() - store_master_asset() with full_metadata JSONB - get_master_asset(tracking_id) - check_campaign_upload_complete() - ALL-DONE CHECK! - store_derivative_asset() - Connection testing ✅ Filename Parser (filename_parser.py) - V2 naming convention parser (ported from PHP) - parse_filename() - 10 components - strip_upload_components() - Remove Job# and Tracking ID - Strict validation with detailed errors ✅ Metadata Extractor MVP (metadata_extractor_mvp.py) - Extract 28 MVP fields from master - Update fields from V2 filename (Description, Language, State) - Add missing fields with defaults - Build asset representation for upload ✅ Notifier (notifier.py) - Mailgun email integration - Outgoing webhook sender - Email templates (success, error, partial, critical) - Configurable recipients Main Scripts: ✅ A1→A2 Download (a1_to_a2_download.py) - Poll DAM every 5 minutes for A1 campaigns - Download all master assets - Upload to Box with tracking IDs - Store in DB with full metadata - ALL-DONE CHECK before status update - Update A1→A2 only if all assets successful - Send webhook with campaign ID/number - Email notifications ✅ A2→A3 Upload (a2_to_a3_upload.py) - Flask webhook receiver for Box uploads - Signature validation - Async task queue processing - Parse V2 filenames - Load master metadata - Extract MVP fields - Upload to DAM - ALL-DONE CHECK for campaign - Update A2→A3 when all assets uploaded - Send webhook notifications ✅ Test Connection Script (test_connection.py) - Verify DAM, Box, Database connectivity - Quick health check ✅ README.md - Complete setup guide - Usage instructions - Configuration examples - Troubleshooting Key Features: - Python 3.6+ compatible (server requirement) - Virtual environment isolated - Configuration-driven (YAML files) - Easy field updates (no code changes) - Environment switching (staging/production) - Comprehensive error handling - Email + webhook notifications - Retry logic - All-done checks before status updates - Campaign webhook notifications Ready for testing locally with Python 3.10! 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
299 lines
10 KiB
Python
Executable file
299 lines
10 KiB
Python
Executable file
#!/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()
|