OMG's Box automation treats each new live_campaigns_*.csv as a full-list replacement, so the per-series global CSV introduced 2026-04-30 stomped the local list whenever a B1→B2 ran. Collapse to one combined CSV (A-series + B-series) emitted by every handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
287 lines
9.4 KiB
Python
287 lines
9.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
A4 Box Uploader
|
|
Monitors campaigns with status A4 (Not Going Live)
|
|
Updates status in DB to live_campaign='NO'
|
|
Generates and uploads updated CSV of live campaigns to Box
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import time
|
|
import logging
|
|
import argparse
|
|
import csv
|
|
from datetime import datetime, timezone
|
|
|
|
# Add shared library to path
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
|
|
from shared.config_loader import load_config
|
|
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
|
|
log_handler = RotatingFileHandler(
|
|
'logs/a4_box.log',
|
|
maxBytes=10*1024*1024, # 10MB per file
|
|
backupCount=28
|
|
)
|
|
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('A4Box')
|
|
|
|
def generate_and_upload_csv(db, box, config):
|
|
"""
|
|
Generate the combined live-campaigns CSV (A-series + B-series) and upload
|
|
to Box. OMG's automation treats each new file as a full replacement of
|
|
its live list, so we always emit the complete list under one filename.
|
|
"""
|
|
try:
|
|
logger.info("Generating live campaigns CSV...")
|
|
|
|
campaigns = db.get_all_live_campaigns()
|
|
|
|
if not campaigns:
|
|
logger.warning("No live campaigns found to report")
|
|
|
|
logger.info("Found {} live campaigns".format(len(campaigns)))
|
|
|
|
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S_UTC')
|
|
csv_filename = 'live_campaigns_{}.csv'.format(timestamp)
|
|
csv_path = os.path.join('temp', csv_filename)
|
|
|
|
os.makedirs('temp', exist_ok=True)
|
|
|
|
with open(csv_path, 'w', newline='') as csvfile:
|
|
fieldnames = ['code', 'description']
|
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
|
|
writer.writeheader()
|
|
for camp in campaigns:
|
|
writer.writerow({
|
|
'code': "{}-{}".format(camp['campaign_number'], camp['campaign_name']),
|
|
'description': camp['campaign_name']
|
|
})
|
|
|
|
logger.info("Generated CSV: {}".format(csv_path))
|
|
|
|
folder_id = config['box'].get('live_campaigns_folder_id')
|
|
if not folder_id:
|
|
logger.error("Box live_campaigns_folder_id not configured")
|
|
return False
|
|
|
|
upload_result = box.upload_file(
|
|
file_path=csv_path,
|
|
folder_id=folder_id,
|
|
target_filename=csv_filename
|
|
)
|
|
|
|
logger.info("Uploaded CSV to Box: {} (File ID: {})".format(
|
|
csv_filename, upload_result['file_id']
|
|
))
|
|
|
|
os.remove(csv_path)
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to generate/upload CSV: {}".format(str(e)))
|
|
return False
|
|
|
|
def process_campaign(campaign, dam, box, db, notifier, config):
|
|
"""
|
|
Process A4 campaign - update status and upload CSV
|
|
"""
|
|
campaign_id = campaign['asset_id']
|
|
campaign_name = campaign['campaign_name']
|
|
campaign_number = campaign.get('campaign_id') or 'UNKNOWN'
|
|
|
|
logger.info("=" * 60)
|
|
logger.info("Processing A4 campaign: {} ({})".format(campaign_name, campaign_number))
|
|
logger.info("=" * 60)
|
|
|
|
try:
|
|
# Check if campaign already processed
|
|
campaign_check = db.check_campaign_processed(campaign_id)
|
|
|
|
if campaign_check['exists'] and campaign_check['webhook_sent']:
|
|
# Note: We reuse 'webhook_sent' column to track if we've processed it,
|
|
# even though we aren't sending a webhook anymore.
|
|
logger.info("Campaign already processed")
|
|
logger.info(" Processed at: {}".format(campaign_check['webhook_sent_at']))
|
|
logger.info(" Status: {}".format(campaign_check['status']))
|
|
logger.info(" Live Campaign: {}".format(campaign_check['live_campaign']))
|
|
logger.info("Skipping to avoid duplicate processing")
|
|
return {'success': True, 'processed': False, 'already_processed': True}
|
|
|
|
# Record campaign status in database
|
|
# This marks it as NOT LIVE
|
|
logger.info("Recording campaign status in database (Live: NO)...")
|
|
db.record_campaign_status(
|
|
campaign_id=campaign_id,
|
|
campaign_number=campaign_number,
|
|
campaign_name=campaign_name,
|
|
live_campaign='NO', # A4 campaigns are NOT going live
|
|
status='A4',
|
|
webhook_sent=True # Mark as processed
|
|
)
|
|
|
|
logger.info("Generating and uploading updated live campaigns CSV...")
|
|
csv_success = generate_and_upload_csv(db, box, config)
|
|
|
|
if csv_success:
|
|
logger.info("✓ CSV report uploaded successfully")
|
|
else:
|
|
logger.error("✗ CSV report generation/upload failed")
|
|
|
|
# Send email notification (internal)
|
|
notifier.send_email(
|
|
template_name='a4_webhook_sent', # Reuse template or create new one? Reusing for now as it conveys "A4 processed"
|
|
recipients=config['notifications']['recipients']['success'],
|
|
data={
|
|
'campaign_name': campaign_name,
|
|
'campaign_id': campaign_id,
|
|
'campaign_number': campaign_number,
|
|
'webhook_url': 'CSV Uploaded to Box' # Placeholder
|
|
}
|
|
)
|
|
|
|
return {'success': True, 'processed': True}
|
|
|
|
except Exception as e:
|
|
logger.error("Campaign processing failed: {}".format(str(e)))
|
|
return {'success': False, 'processed': False}
|
|
|
|
def main():
|
|
"""Main polling loop"""
|
|
parser = argparse.ArgumentParser(description='Ferrero A4 Box Uploader')
|
|
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 A4 Box Uploader 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
|
|
dam = DAMClient(config, auth_mode=auth_mode)
|
|
box = BoxClient(config) # Need Box client now
|
|
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("")
|
|
|
|
try:
|
|
logger.info("Searching for A4 campaigns...")
|
|
|
|
campaigns = dam.search_campaigns(status='A4')
|
|
|
|
if not campaigns:
|
|
logger.info("No A4 campaigns found - exiting")
|
|
db.close()
|
|
sys.exit(0)
|
|
|
|
logger.info("Found {} A4 campaign(s) - processing all".format(len(campaigns)))
|
|
logger.info("")
|
|
|
|
processed_count = 0
|
|
failed_count = 0
|
|
already_processed_count = 0
|
|
|
|
for campaign in campaigns:
|
|
result = process_campaign(campaign, dam, box, db, notifier, config)
|
|
|
|
if result['success']:
|
|
if result.get('processed'):
|
|
processed_count += 1
|
|
if result.get('already_processed'):
|
|
already_processed_count += 1
|
|
else:
|
|
failed_count += 1
|
|
|
|
logger.info("")
|
|
logger.info("=" * 60)
|
|
logger.info("A4 Box Uploader Summary")
|
|
logger.info("=" * 60)
|
|
logger.info("Total campaigns found: {}".format(len(campaigns)))
|
|
logger.info("Processed (CSV updated): {}".format(processed_count))
|
|
logger.info("Already processed: {}".format(already_processed_count))
|
|
logger.info("Failed: {}".format(failed_count))
|
|
logger.info("=" * 60)
|
|
|
|
db.close()
|
|
|
|
if failed_count == 0:
|
|
sys.exit(0)
|
|
elif processed_count > 0:
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(1)
|
|
|
|
except Exception as e:
|
|
logger.critical("Script error: {}".format(str(e)))
|
|
notifier.send_email(
|
|
template_name='upload_failed',
|
|
recipients=config['notifications']['recipients']['critical'],
|
|
data={
|
|
'filename': 'A4 Box Uploader',
|
|
'tracking_id': 'N/A',
|
|
'error': str(e)
|
|
}
|
|
)
|
|
db.close()
|
|
sys.exit(1)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|