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>
170 lines
6.7 KiB
Python
170 lines
6.7 KiB
Python
"""
|
|
Notifier - Email and Webhook Notifications
|
|
Handles Mailgun emails and outgoing webhooks
|
|
Compatible with Python 3.6+
|
|
"""
|
|
|
|
import requests
|
|
import logging
|
|
from jinja2 import Template
|
|
|
|
logger = logging.getLogger('Notifier')
|
|
|
|
class Notifier:
|
|
def __init__(self, config):
|
|
self.config = config
|
|
self.enabled = config['notifications']['enabled']
|
|
self.mailgun_api_key = config['notifications']['mailgun']['api_key']
|
|
self.mailgun_domain = config['notifications']['mailgun']['domain']
|
|
self.recipients = config['notifications']['recipients']
|
|
self.webhook_config = config.get('webhooks', {})
|
|
|
|
def send_email(self, template_name, recipients, data):
|
|
"""
|
|
Send email via Mailgun
|
|
|
|
Args:
|
|
template_name: Name of email template
|
|
recipients: List of email addresses
|
|
data: Template data dict
|
|
"""
|
|
if not self.enabled:
|
|
logger.info("Notifications disabled, skipping email")
|
|
return
|
|
|
|
try:
|
|
# Simple templates (full template system would load from YAML)
|
|
templates = {
|
|
'a1_to_a2_complete': {
|
|
'subject': "✅ Master Assets Downloaded - Campaign {campaign_name}",
|
|
'html': """
|
|
<h2>Master Assets Downloaded Successfully</h2>
|
|
<p><strong>Campaign:</strong> {campaign_name} ({campaign_id})</p>
|
|
<p><strong>Campaign Number:</strong> {campaign_number}</p>
|
|
<p><strong>Assets Downloaded:</strong> {asset_count}</p>
|
|
<p><strong>Status Updated:</strong> A1 → A2</p>
|
|
<hr>
|
|
<p>All assets have been downloaded and uploaded to Box with tracking IDs.</p>
|
|
"""
|
|
},
|
|
'a2_to_a3_complete': {
|
|
'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}",
|
|
'html': """
|
|
<h2>Localized Assets Uploaded Successfully</h2>
|
|
<p><strong>Campaign:</strong> {campaign_name}</p>
|
|
<p><strong>Campaign ID:</strong> {campaign_id}</p>
|
|
<p><strong>Assets Uploaded:</strong> {asset_count}</p>
|
|
<p><strong>Status Updated:</strong> A2 → A3</p>
|
|
<hr>
|
|
<p>All localized assets have been uploaded to DAM.</p>
|
|
"""
|
|
},
|
|
'upload_failed': {
|
|
'subject': "❌ Upload Failed - {filename}",
|
|
'html': """
|
|
<h2 style="color: red;">Upload Failed</h2>
|
|
<p><strong>Filename:</strong> {filename}</p>
|
|
<p><strong>Tracking ID:</strong> {tracking_id}</p>
|
|
<p><strong>Error:</strong> {error}</p>
|
|
<hr>
|
|
<p>Please investigate the error.</p>
|
|
"""
|
|
},
|
|
'a1_to_a2_partial': {
|
|
'subject': "⚠️ Partial Download - Campaign {campaign_name}",
|
|
'html': """
|
|
<h2 style="color: orange;">Campaign Partially Processed</h2>
|
|
<p><strong>Campaign:</strong> {campaign_name} ({campaign_id})</p>
|
|
<p><strong>Total Assets:</strong> {total_assets}</p>
|
|
<p><strong>Successful:</strong> {successful}</p>
|
|
<p><strong>Failed:</strong> {failed}</p>
|
|
<hr>
|
|
<p style="color: red;"><strong>Status NOT updated.</strong> Campaign remains at A1.</p>
|
|
<p>Please review failed assets and retry.</p>
|
|
"""
|
|
}
|
|
}
|
|
|
|
template_config = templates.get(template_name, {
|
|
'subject': 'Ferrero Automation Notification',
|
|
'html': '<p>{}</p>'.format(data)
|
|
})
|
|
|
|
# Render subject and body
|
|
subject = template_config['subject'].format(**data)
|
|
html_template = Template(template_config['html'])
|
|
html_body = html_template.render(**data)
|
|
|
|
# Send via Mailgun
|
|
response = requests.post(
|
|
"https://api.mailgun.net/v3/{}/messages".format(self.mailgun_domain),
|
|
auth=("api", self.mailgun_api_key),
|
|
data={
|
|
"from": "Ferrero Automation <noreply@{}>".format(self.mailgun_domain),
|
|
"to": recipients if isinstance(recipients, list) else [recipients],
|
|
"subject": subject,
|
|
"html": html_body
|
|
},
|
|
timeout=10
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
logger.info("Email sent: {} to {}".format(template_name, recipients))
|
|
else:
|
|
logger.error("Email failed: HTTP {} - {}".format(
|
|
response.status_code, response.text
|
|
))
|
|
|
|
except Exception as e:
|
|
logger.error("Email error: {}".format(str(e)))
|
|
|
|
def send_webhook(self, url, payload):
|
|
"""
|
|
Send outgoing webhook notification
|
|
|
|
Args:
|
|
url: Webhook URL
|
|
payload: dict to send as JSON
|
|
|
|
Returns:
|
|
bool: Success status
|
|
"""
|
|
try:
|
|
# Get webhook config if exists
|
|
webhook_config = None
|
|
for name, config in self.webhook_config.items():
|
|
if config.get('url') == url:
|
|
webhook_config = config
|
|
break
|
|
|
|
if not webhook_config:
|
|
webhook_config = {'timeout_seconds': 10, 'auth': {}}
|
|
|
|
# Prepare headers
|
|
headers = {'Content-Type': 'application/json'}
|
|
|
|
# Add auth if configured
|
|
auth_config = webhook_config.get('auth', {})
|
|
if auth_config.get('type') == 'bearer' and auth_config.get('token'):
|
|
headers['Authorization'] = 'Bearer {}'.format(auth_config['token'])
|
|
|
|
# Send webhook
|
|
response = requests.post(
|
|
url,
|
|
json=payload,
|
|
headers=headers,
|
|
timeout=webhook_config.get('timeout_seconds', 10)
|
|
)
|
|
|
|
if response.status_code in [200, 201, 202]:
|
|
logger.info("Webhook sent successfully: {}".format(url))
|
|
return True
|
|
else:
|
|
logger.warning("Webhook failed: HTTP {} - {}".format(
|
|
response.status_code, response.text[:200]
|
|
))
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error("Webhook error: {}".format(str(e)))
|
|
return False
|