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>
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""
|
|
Box Client - Box.com API Integration
|
|
Handles JWT authentication and Box operations
|
|
Compatible with Python 3.6+
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from boxsdk import Client, JWTAuth
|
|
|
|
logger = logging.getLogger('BoxClient')
|
|
|
|
class BoxClient:
|
|
def __init__(self, config):
|
|
self.config = config
|
|
self.root_folder_id = config['box']['root_folder_id']
|
|
|
|
# Load Box config for JWT
|
|
box_config_path = config['box']['rsa_private_key_path']
|
|
|
|
try:
|
|
with open(box_config_path, 'r') as f:
|
|
box_config = json.load(f)
|
|
|
|
# Initialize JWT authentication
|
|
auth = JWTAuth.from_settings_dictionary(box_config)
|
|
self.client = Client(auth)
|
|
|
|
logger.info("Box client initialized with JWT auth")
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to initialize Box client: {}".format(str(e)))
|
|
raise
|
|
|
|
def upload_with_tracking_id(self, file_path, campaign_id, campaign_name, tracking_id):
|
|
"""
|
|
Upload file to Box with tracking ID in filename
|
|
|
|
Args:
|
|
file_path: Path to local file
|
|
campaign_id: Campaign ID
|
|
campaign_name: Campaign name
|
|
tracking_id: 6-character tracking ID
|
|
|
|
Returns:
|
|
dict with file_id, url, folder_id
|
|
"""
|
|
try:
|
|
import os
|
|
|
|
# Create or find campaign folder
|
|
folder = self._get_or_create_campaign_folder(campaign_id, campaign_name)
|
|
|
|
# Get original filename
|
|
original_filename = os.path.basename(file_path)
|
|
name_without_ext, ext = os.path.splitext(original_filename)
|
|
|
|
# Add tracking ID to filename
|
|
box_filename = "{}_{}{}".format(name_without_ext, tracking_id, ext)
|
|
|
|
# Upload file
|
|
uploaded_file = folder.upload(file_path, box_filename)
|
|
|
|
# Set description with DAM asset info
|
|
description = "Tracking ID: {}\nOriginal: {}".format(
|
|
tracking_id, original_filename
|
|
)
|
|
uploaded_file.update_info({'description': description})
|
|
|
|
logger.info("Uploaded to Box: {} → File ID: {}".format(box_filename, uploaded_file.id))
|
|
|
|
return {
|
|
'file_id': uploaded_file.id,
|
|
'url': 'https://app.box.com/file/{}'.format(uploaded_file.id),
|
|
'folder_id': folder.id,
|
|
'box_filename': box_filename
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error("Box upload failed: {}".format(str(e)))
|
|
raise
|
|
|
|
def _get_or_create_campaign_folder(self, campaign_id, campaign_name):
|
|
"""Get or create campaign folder in Box"""
|
|
try:
|
|
root_folder = self.client.folder(self.root_folder_id)
|
|
|
|
# Folder name format: C000000078_Campaign_Name
|
|
folder_name = "{}_{}".format(campaign_id, campaign_name.replace(' ', '_'))
|
|
|
|
# Check if folder exists
|
|
items = root_folder.get_items()
|
|
for item in items:
|
|
if item.type == 'folder' and item.name == folder_name:
|
|
logger.info("Using existing Box folder: {}".format(folder_name))
|
|
return self.client.folder(item.id)
|
|
|
|
# Create new folder
|
|
new_folder = root_folder.create_subfolder(folder_name)
|
|
logger.info("Created new Box folder: {}".format(folder_name))
|
|
return new_folder
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to get/create Box folder: {}".format(str(e)))
|
|
raise
|
|
|
|
def download_file(self, file_id, output_path):
|
|
"""
|
|
Download file from Box
|
|
|
|
Args:
|
|
file_id: Box file ID
|
|
output_path: Path to save file
|
|
|
|
Returns:
|
|
Path to downloaded file
|
|
"""
|
|
try:
|
|
import os
|
|
|
|
file_obj = self.client.file(file_id)
|
|
file_info = file_obj.get()
|
|
|
|
# Ensure output directory exists
|
|
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True)
|
|
|
|
# Download file
|
|
with open(output_path, 'wb') as f:
|
|
file_obj.download_to(f)
|
|
|
|
file_size = os.path.getsize(output_path)
|
|
logger.info("Downloaded from Box: {} ({} bytes)".format(file_info.name, file_size))
|
|
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
logger.error("Box download failed: {}".format(str(e)))
|
|
raise
|
|
|
|
def list_folder_files(self, folder_id):
|
|
"""
|
|
List all files in a Box folder
|
|
|
|
Args:
|
|
folder_id: Box folder ID
|
|
|
|
Returns:
|
|
List of file dictionaries
|
|
"""
|
|
try:
|
|
folder = self.client.folder(folder_id)
|
|
items = folder.get_items()
|
|
|
|
files = []
|
|
for item in items:
|
|
if item.type == 'file':
|
|
files.append({
|
|
'id': item.id,
|
|
'name': item.name,
|
|
'size': item.size,
|
|
'modified_at': item.modified_at,
|
|
'url': 'https://app.box.com/file/{}'.format(item.id)
|
|
})
|
|
|
|
logger.info("Found {} files in Box folder {}".format(len(files), folder_id))
|
|
return files
|
|
|
|
except Exception as e:
|
|
logger.error("Failed to list Box folder: {}".format(str(e)))
|
|
raise
|
|
|
|
def test_connection(self):
|
|
"""Test Box connection"""
|
|
try:
|
|
user = self.client.user().get()
|
|
logger.info("Box connection OK - User: {}".format(user.name))
|
|
return True
|
|
except Exception as e:
|
|
logger.error("Box connection failed: {}".format(str(e)))
|
|
return False
|