ferrero-opentext/Python-Version/scripts/a2_to_a3_upload_polling.py
DJP 65f2c9c68e Add Box file deletion, email notifications, and log rotation for A2→A3
Major Features Added:

1. Delete Files from Box After Upload
   - After successful DAM upload, delete file from Box
   - Prevents reprocessing same files
   - Keeps Box folder clean
   - Only deletes on success (keeps on failure for retry)

2. Email Notification for Each Upload
   - New template: a2_to_a3_file_uploaded
   - Sends email immediately after each successful upload
   - Includes: filename, clean filename, asset ID, tracking ID
   - Don't wait for "all done" - notify per file
   - Recipients: configured in .env (REPORT_EMAILS)

3. Log Rotation for Both Scripts
   - Uses RotatingFileHandler
   - Max file size: 10MB per log file
   - Backup count: 28 files (approximately 1 month)
   - Auto-rotates when log reaches 10MB
   - Keeps logs/a1_to_a2.log (current)
   - Backups: logs/a1_to_a2.log.1, .2, .3, etc.
   - Automatically deletes logs older than 28 rotations
   - Applied to both A1→A2 and A2→A3 scripts

Flow Changes:
A2→A3 now:
1. Poll Box folder
2. Find V2 files
3. Download from Box
4. Upload to DAM
5. Delete from Box  NEW
6. Send email notification  NEW
7. Store derivative record
8. Exit

Log Management:
- Active logs: ~10MB max
- Rotated backups: 28 files = ~280MB total
- Automatic cleanup (no manual intervention needed)
- 1 week of detailed logs + 3 weeks of backups

Database:
- Added dam_asset_id and upload_status columns to derivative_assets
- Fixed store_derivative_asset() to use existing schema columns

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 19:41:02 -04:00

273 lines
8.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""
A2→A3 Upload Handler - Box Folder Polling Version
Polls Box folder for new files with V2 naming, uploads to DAM
Updates status to A3 only when ALL assets for campaign uploaded
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
from shared.filename_parser import FilenameParser
from shared.metadata_extractor_mvp import MetadataExtractorMVP
# Load configuration
config = load_config('config/config.yaml')
field_mappings = load_field_mappings(config)
# 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/a2_to_a3.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('A2toA3')
def process_box_file(file_info, dam, box, db, parser, mvp_extractor, config):
"""
Process a single file from Box folder
Returns:
dict with success, asset_id, tracking_id
"""
file_id = file_info['id']
filename = file_info['name']
logger.info("Processing: {}".format(filename))
try:
# 1. Parse V2 filename
parsed = parser.parse_filename(filename)
if not parsed['is_valid']:
raise ValueError("Invalid V2 filename: {} - {}".format(
filename, ', '.join(parsed['validation_errors'])
))
tracking_id = parsed['tracking_id']
if not tracking_id:
raise ValueError("No tracking ID in filename")
# 2. Load master metadata from database
master_asset = db.get_master_asset(tracking_id)
if not master_asset:
raise ValueError("No master asset for tracking ID: {}".format(tracking_id))
# 3. Download from Box
temp_file = os.path.join('temp/downloads', filename)
box.download_file(file_id, temp_file)
# 4. Get clean filename
clean_filename = parser.strip_upload_components(filename)
# 5. Build MVP asset representation
asset_rep = mvp_extractor.build_mvp_asset_representation(
master_metadata=master_asset['full_metadata'],
clean_filename=clean_filename,
parsed_filename=parsed
)
# 6. Rename to clean filename
clean_temp_file = os.path.join('temp/downloads', clean_filename)
if os.path.exists(clean_temp_file):
os.remove(clean_temp_file)
os.rename(temp_file, clean_temp_file)
# 7. Upload to DAM
upload_result = dam.upload_asset(
file_path=clean_temp_file,
folder_id=master_asset['upload_directory'],
asset_representation=asset_rep
)
if not upload_result['success']:
raise Exception("Upload failed: {}".format(upload_result.get('error')))
# 8. Store derivative record
db.store_derivative_asset(
tracking_id=tracking_id,
master_asset_id=None,
dam_asset_id=upload_result['asset_id'],
filename=clean_filename
)
# 9. Delete file from Box after successful upload
try:
box_file = box.client.file(file_id)
box_file.delete()
logger.info("Deleted file from Box: {}".format(filename))
except Exception as e:
logger.warning("Could not delete file from Box: {}".format(str(e)))
# 10. Clean up local temp file
os.remove(clean_temp_file)
logger.info("✓ Success: {} → Asset ID: {}".format(filename, upload_result['asset_id']))
return {
'success': True,
'asset_id': upload_result['asset_id'],
'tracking_id': tracking_id,
'filename': filename,
'clean_filename': clean_filename
}
except Exception as e:
logger.error("✗ Failed: {} - {}".format(filename, str(e)))
return {
'success': False,
'error': str(e),
'filename': filename,
'tracking_id': tracking_id if 'tracking_id' in locals() else None
}
def main():
"""Main entry point - single run mode"""
logger.info("=" * 60)
logger.info("Ferrero A2→A3 Upload Handler Starting (Polling Mode)")
logger.info("=" * 60)
# Initialize clients
dam = DAMClient(config)
# Use A2→A3 Box folder for polling
box = BoxClient(config, root_folder_id=config['box'].get('root_folder_a2_a3'))
db = Database(config)
notifier = Notifier(config)
parser = FilenameParser()
mvp_extractor = MetadataExtractorMVP(field_mappings)
# Test connections
logger.info("Testing connections...")
if not dam.test_connection():
logger.error("DAM connection failed")
sys.exit(1)
if not box.test_connection():
logger.error("Box connection failed")
sys.exit(1)
if not db.test_connection():
logger.error("Database connection failed")
sys.exit(1)
logger.info("All connections OK")
logger.info("")
try:
# Get Box folder ID to poll
box_folder_id = config['box'].get('root_folder_a2_a3', config['box'].get('root_folder_id'))
logger.info("Polling Box folder: {}".format(box_folder_id))
# List files in Box folder
files = box.list_folder_files(box_folder_id)
if not files:
logger.info("No files found in Box folder - exiting")
db.close()
sys.exit(0)
logger.info("Found {} files in Box folder".format(len(files)))
# Filter for V2 filenames only
valid_files = []
for file_info in files:
parsed = parser.parse_filename(file_info['name'])
if parsed['is_valid'] and parsed.get('tracking_id'):
valid_files.append(file_info)
else:
logger.debug("Skipping invalid file: {}".format(file_info['name']))
logger.info("Found {} valid V2 files to process".format(len(valid_files)))
if not valid_files:
logger.info("No valid V2 files to process - exiting")
db.close()
sys.exit(0)
# Process files one at a time (process first file only)
file_info = valid_files[0]
logger.info("Processing first file only (more will be processed on next run)")
logger.info("")
result = process_box_file(file_info, dam, box, db, parser, mvp_extractor, config)
if result['success']:
logger.info("")
logger.info("=" * 60)
logger.info("✓ File processed successfully")
logger.info(" Filename: {}".format(result['filename']))
logger.info(" Clean filename: {}".format(result['clean_filename']))
logger.info(" Asset ID: {}".format(result['asset_id']))
logger.info(" Tracking ID: {}".format(result['tracking_id']))
logger.info("=" * 60)
# Send success email notification
notifier.send_email(
template_name='a2_to_a3_file_uploaded',
recipients=config['notifications']['recipients']['success'],
data={
'filename': result['filename'],
'clean_filename': result['clean_filename'],
'asset_id': result['asset_id'],
'tracking_id': result['tracking_id']
}
)
# TODO: Check if all campaign assets uploaded and update status A2→A3
# Would need to track campaign_id in master_assets table
db.close()
sys.exit(0)
else:
logger.warning("")
logger.warning("=" * 60)
logger.warning("✗ File processing failed")
logger.warning(" Filename: {}".format(result['filename']))
logger.warning(" Error: {}".format(result['error']))
logger.warning("=" * 60)
db.close()
sys.exit(1)
except Exception as e:
logger.critical("Script error: {}".format(str(e)))
db.close()
sys.exit(1)
if __name__ == '__main__':
main()