Critical Fix: - extract_global_campaign_reference() now accepts campaign_id parameter - Always sets local_campaign_id to current campaign as fallback - Prevents NULL local_campaign_id when no Global Campaign Reference exists Root Cause: - Assets without Global Campaign Reference had NULL local_campaign_id - Caused derivatives to be linked to wrong campaigns - Same asset in multiple campaigns would share tracking IDs incorrectly Impact: - Every asset now has proper local_campaign_id - Derivatives correctly linked to their source campaign - Fixes issue where C000001177 assets were showing as C000002098 Changes: - database.py: Added campaign_id parameter with fallback logic - a1_to_a2_box_uploader.py: Pass campaign_number to function - a5_to_a6_download.py: Pass campaign_number to function
422 lines
17 KiB
Python
Executable file
422 lines
17 KiB
Python
Executable file
#!/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, check if we've already notified about this campaign
|
|
if len(not_approved_assets) == 0:
|
|
logger.info("No NOT APPROVED assets found - all assets are approved")
|
|
|
|
# Check if we've already sent notification for this campaign
|
|
campaign_check = db.check_campaign_processed(campaign_id)
|
|
|
|
# Only skip if:
|
|
# 1. Campaign exists in database
|
|
# 2. It was marked as notified (webhook_sent = True)
|
|
# 3. Status is still A5 (hasn't moved forward and back)
|
|
if (campaign_check['exists'] and
|
|
campaign_check['webhook_sent'] and
|
|
campaign_check['status'] == 'A5'):
|
|
logger.info("Campaign already notified about no rejections (status still A5)")
|
|
logger.info(" Notified at: {}".format(campaign_check['webhook_sent_at']))
|
|
logger.info("Skipping duplicate notification")
|
|
return {'success': True, 'processed': 0, 'failed': 0}
|
|
|
|
# Record in database to prevent future duplicate emails
|
|
# Note: If campaign was A5 before, this updates the record
|
|
logger.info("Recording campaign status in database (first notification or status changed)...")
|
|
db.record_campaign_status(
|
|
campaign_id=campaign_id,
|
|
campaign_number=campaign_number,
|
|
campaign_name=campaign_name,
|
|
live_campaign='N/A', # Not applicable for A5
|
|
status='A5',
|
|
webhook_sent=True # Mark as notified
|
|
)
|
|
|
|
# Send "no rejections" email (only once per A5 status period)
|
|
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 (will not send again unless status changes)")
|
|
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, campaign_number)
|
|
|
|
# 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 (Legacy APIM)')
|
|
parser.add_argument('--auth-pfx-v2', action='store_true',
|
|
help='Use mTLS V2 (Hybrid) authentication')
|
|
args = parser.parse_args()
|
|
|
|
logger.info("=" * 60)
|
|
logger.info("Ferrero A5→A6 Rework Asset Downloader Starting")
|
|
|
|
# Determine auth mode
|
|
auth_mode = 'oauth'
|
|
if args.auth_pfx_v2:
|
|
auth_mode = 'mtls_v2'
|
|
logger.info("Authentication: mTLS V2 (Hybrid)")
|
|
elif args.auth_pfx:
|
|
auth_mode = 'mtls'
|
|
logger.info("Authentication: mTLS Certificate (Legacy)")
|
|
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, auth_mode=auth_mode)
|
|
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']))
|
|
|
|
# Only show status update if assets were actually processed
|
|
if result['processed'] > 0:
|
|
logger.info(" Status updated: A5 → A6")
|
|
else:
|
|
logger.info(" Status NOT updated (no rejected assets found)")
|
|
|
|
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()
|