Fix: Merge A+B live campaigns into single CSV for OMG
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>
This commit is contained in:
parent
28586308d7
commit
6d6213024a
4 changed files with 33 additions and 208 deletions
|
|
@ -52,23 +52,20 @@ logger = logging.getLogger('A4Box')
|
|||
|
||||
def generate_and_upload_csv(db, box, config):
|
||||
"""
|
||||
Generate CSV of all live LOCAL campaigns and upload to Box.
|
||||
Filename prefix `live_campaigns_` is what OMG's Box automation watches for.
|
||||
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...")
|
||||
|
||||
# 1. Get all live campaigns from DB
|
||||
campaigns = db.get_all_live_campaigns()
|
||||
|
||||
if not campaigns:
|
||||
logger.warning("No live campaigns found to report")
|
||||
# Even if empty, we might want to upload an empty CSV to clear the list?
|
||||
# For now, let's upload it even if empty to reflect that no campaigns are live.
|
||||
|
||||
logger.info("Found {} live campaigns".format(len(campaigns)))
|
||||
|
||||
# 2. Generate CSV file
|
||||
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)
|
||||
|
|
@ -88,7 +85,6 @@ def generate_and_upload_csv(db, box, config):
|
|||
|
||||
logger.info("Generated CSV: {}".format(csv_path))
|
||||
|
||||
# 3. Upload to Box
|
||||
folder_id = config['box'].get('live_campaigns_folder_id')
|
||||
if not folder_id:
|
||||
logger.error("Box live_campaigns_folder_id not configured")
|
||||
|
|
@ -104,7 +100,6 @@ def generate_and_upload_csv(db, box, config):
|
|||
csv_filename, upload_result['file_id']
|
||||
))
|
||||
|
||||
# Clean up
|
||||
os.remove(csv_path)
|
||||
return True
|
||||
|
||||
|
|
@ -112,63 +107,6 @@ def generate_and_upload_csv(db, box, config):
|
|||
logger.error("Failed to generate/upload CSV: {}".format(str(e)))
|
||||
return False
|
||||
|
||||
def generate_and_upload_global_csv(db, box, config):
|
||||
"""
|
||||
Generate CSV of all live GLOBAL campaigns (B-series) and upload to Box.
|
||||
Goes to the same Box folder as the local CSV; filename keeps the
|
||||
`live_campaigns_` prefix so OMG's Box automation picks it up too.
|
||||
"""
|
||||
try:
|
||||
logger.info("Generating live GLOBAL campaigns CSV...")
|
||||
|
||||
campaigns = db.get_all_live_global_campaigns()
|
||||
|
||||
if not campaigns:
|
||||
logger.warning("No live global campaigns found to report")
|
||||
|
||||
logger.info("Found {} live global campaigns".format(len(campaigns)))
|
||||
|
||||
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S_UTC')
|
||||
csv_filename = 'live_campaigns_global_{}.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 global 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 global 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 global CSV: {}".format(str(e)))
|
||||
return False
|
||||
|
||||
def process_campaign(campaign, dam, box, db, notifier, config):
|
||||
"""
|
||||
Process A4 campaign - update status and upload CSV
|
||||
|
|
@ -195,15 +133,9 @@ def process_campaign(campaign, dam, box, db, notifier, config):
|
|||
logger.info("Skipping to avoid duplicate processing")
|
||||
return {'success': True, 'processed': False, 'already_processed': True}
|
||||
|
||||
# Look up the campaign's PRIOR status before we overwrite it.
|
||||
# If it was B-series (B2), this is a global campaign that a manager
|
||||
# closed via A4 — route the CSV regen to the global CSV instead.
|
||||
prior_status = (campaign_check.get('status') or '') if campaign_check.get('exists') else ''
|
||||
is_global = prior_status.startswith('B')
|
||||
|
||||
# Record campaign status in database
|
||||
# This marks it as NOT LIVE
|
||||
logger.info("Recording campaign status in database (Live: NO, prior status: {})...".format(prior_status or 'unknown'))
|
||||
logger.info("Recording campaign status in database (Live: NO)...")
|
||||
db.record_campaign_status(
|
||||
campaign_id=campaign_id,
|
||||
campaign_number=campaign_number,
|
||||
|
|
@ -213,14 +145,8 @@ def process_campaign(campaign, dam, box, db, notifier, config):
|
|||
webhook_sent=True # Mark as processed
|
||||
)
|
||||
|
||||
# Regenerate the CSV that this campaign was on. Global if prior was
|
||||
# B-series, local otherwise.
|
||||
if is_global:
|
||||
logger.info("Prior status was {} - regenerating GLOBAL live campaigns CSV...".format(prior_status))
|
||||
csv_success = generate_and_upload_global_csv(db, box, config)
|
||||
else:
|
||||
logger.info("Generating and uploading updated live campaigns CSV...")
|
||||
csv_success = generate_and_upload_csv(db, box, config)
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -110,25 +110,24 @@ def extract_creativex_from_dam_metadata(asset_metadata):
|
|||
return {'score': None, 'url': None}
|
||||
|
||||
|
||||
def generate_and_upload_global_csv(db, box, config):
|
||||
def generate_and_upload_csv(db, box, config):
|
||||
"""
|
||||
Generate CSV of all live GLOBAL campaigns (B-series) and upload to Box.
|
||||
Same Box folder as the local CSV; filename keeps the `live_campaigns_`
|
||||
prefix so OMG's Box automation picks it up.
|
||||
Mirrors the helper of the same name in a4_box_uploader.py.
|
||||
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 GLOBAL campaigns CSV...")
|
||||
logger.info("Generating live campaigns CSV...")
|
||||
|
||||
campaigns = db.get_all_live_global_campaigns()
|
||||
campaigns = db.get_all_live_campaigns()
|
||||
|
||||
if not campaigns:
|
||||
logger.warning("No live global campaigns found to report")
|
||||
logger.warning("No live campaigns found to report")
|
||||
|
||||
logger.info("Found {} live global campaigns".format(len(campaigns)))
|
||||
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_global_{}.csv'.format(timestamp)
|
||||
csv_filename = 'live_campaigns_{}.csv'.format(timestamp)
|
||||
csv_path = os.path.join('temp', csv_filename)
|
||||
|
||||
os.makedirs('temp', exist_ok=True)
|
||||
|
|
@ -144,7 +143,7 @@ def generate_and_upload_global_csv(db, box, config):
|
|||
'description': camp['campaign_name']
|
||||
})
|
||||
|
||||
logger.info("Generated global CSV: {}".format(csv_path))
|
||||
logger.info("Generated CSV: {}".format(csv_path))
|
||||
|
||||
folder_id = config['box'].get('live_campaigns_folder_id')
|
||||
if not folder_id:
|
||||
|
|
@ -157,7 +156,7 @@ def generate_and_upload_global_csv(db, box, config):
|
|||
target_filename=csv_filename
|
||||
)
|
||||
|
||||
logger.info("Uploaded global CSV to Box: {} (File ID: {})".format(
|
||||
logger.info("Uploaded CSV to Box: {} (File ID: {})".format(
|
||||
csv_filename, upload_result['file_id']
|
||||
))
|
||||
|
||||
|
|
@ -165,7 +164,7 @@ def generate_and_upload_global_csv(db, box, config):
|
|||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Failed to generate/upload global CSV: {}".format(str(e)))
|
||||
logger.error("Failed to generate/upload CSV: {}".format(str(e)))
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -404,14 +403,14 @@ def process_campaign(campaign, dam, box, db, notifier, config):
|
|||
webhook_sent=False # B-series workflow doesn't send a webhook
|
||||
)
|
||||
|
||||
# Regenerate and upload the global live campaigns CSV to Box.
|
||||
# Box automation forwards it to OMG.
|
||||
logger.info("Generating and uploading global live campaigns CSV...")
|
||||
csv_success = generate_and_upload_global_csv(db, box, config)
|
||||
# Regenerate and upload the combined live campaigns CSV to Box.
|
||||
# Box automation forwards it to OMG as a full-list replacement.
|
||||
logger.info("Generating and uploading live campaigns CSV...")
|
||||
csv_success = generate_and_upload_csv(db, box, config)
|
||||
if csv_success:
|
||||
logger.info("✓ Global CSV report uploaded successfully")
|
||||
logger.info("✓ CSV report uploaded successfully")
|
||||
else:
|
||||
logger.error("✗ Global CSV report generation/upload failed")
|
||||
logger.error("✗ CSV report generation/upload failed")
|
||||
|
||||
# NOTE: B1→B2 workflow does NOT send webhook (only email notification)
|
||||
# Webhook is only used for A1→A2 workflow
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ logger = logging.getLogger('B4Box')
|
|||
|
||||
def generate_and_upload_csv(db, box, config):
|
||||
"""
|
||||
Generate CSV of all live LOCAL campaigns and upload to Box.
|
||||
Used here only as a fallback when a campaign closed via B4 was
|
||||
actually a local one (defensive — DAM likely prevents this).
|
||||
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...")
|
||||
|
|
@ -111,64 +111,6 @@ def generate_and_upload_csv(db, box, config):
|
|||
return False
|
||||
|
||||
|
||||
def generate_and_upload_global_csv(db, box, config):
|
||||
"""
|
||||
Generate CSV of all live GLOBAL campaigns (B-series) and upload to Box.
|
||||
Same Box folder as the local CSV; filename keeps the `live_campaigns_`
|
||||
prefix so OMG's Box automation picks it up.
|
||||
"""
|
||||
try:
|
||||
logger.info("Generating live GLOBAL campaigns CSV...")
|
||||
|
||||
campaigns = db.get_all_live_global_campaigns()
|
||||
|
||||
if not campaigns:
|
||||
logger.warning("No live global campaigns found to report")
|
||||
|
||||
logger.info("Found {} live global campaigns".format(len(campaigns)))
|
||||
|
||||
timestamp = datetime.now(timezone.utc).strftime('%Y-%m-%d_%H%M%S_UTC')
|
||||
csv_filename = 'live_campaigns_global_{}.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 global 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 global 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 global CSV: {}".format(str(e)))
|
||||
return False
|
||||
|
||||
|
||||
def process_campaign(campaign, dam, box, db, notifier, config):
|
||||
"""
|
||||
Process B4 campaign - mark not-live and regenerate the global CSV.
|
||||
|
|
@ -192,13 +134,7 @@ def process_campaign(campaign, dam, box, db, notifier, config):
|
|||
logger.info("Skipping to avoid duplicate processing")
|
||||
return {'success': True, 'processed': False, 'already_processed': True}
|
||||
|
||||
# Symmetric prior-status routing: if a campaign reaches B4 but its prior
|
||||
# status was A-series, regenerate the LOCAL CSV instead. Defensive —
|
||||
# DAM workflows likely prevent this cross-type transition.
|
||||
prior_status = (campaign_check.get('status') or '') if campaign_check.get('exists') else ''
|
||||
is_local = prior_status.startswith('A')
|
||||
|
||||
logger.info("Recording campaign status in database (Live: NO, prior status: {})...".format(prior_status or 'unknown'))
|
||||
logger.info("Recording campaign status in database (Live: NO)...")
|
||||
db.record_campaign_status(
|
||||
campaign_id=campaign_id,
|
||||
campaign_number=campaign_number,
|
||||
|
|
@ -208,12 +144,8 @@ def process_campaign(campaign, dam, box, db, notifier, config):
|
|||
webhook_sent=True
|
||||
)
|
||||
|
||||
if is_local:
|
||||
logger.info("Prior status was {} - regenerating LOCAL live campaigns CSV...".format(prior_status))
|
||||
csv_success = generate_and_upload_csv(db, box, config)
|
||||
else:
|
||||
logger.info("Generating and uploading updated GLOBAL live campaigns CSV...")
|
||||
csv_success = generate_and_upload_global_csv(db, box, config)
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1074,9 +1074,8 @@ class Database:
|
|||
|
||||
def get_all_live_campaigns(self):
|
||||
"""
|
||||
Get all live LOCAL campaigns (A-series) for the local CSV report.
|
||||
Filters by status starting with 'A' to keep this CSV scoped to
|
||||
local campaigns even if a B-series row is ever marked live.
|
||||
Get all live campaigns (A-series local + B-series global) for the
|
||||
single combined CSV that OMG ingests as a full replacement list.
|
||||
"""
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
|
|
@ -1086,38 +1085,7 @@ class Database:
|
|||
SELECT campaign_number, campaign_name
|
||||
FROM campaign_status
|
||||
WHERE live_campaign = 'YES'
|
||||
AND status LIKE 'A%'
|
||||
ORDER BY campaign_number DESC
|
||||
""")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
campaigns = []
|
||||
for row in rows:
|
||||
campaigns.append({
|
||||
'campaign_number': row[0],
|
||||
'campaign_name': row[1]
|
||||
})
|
||||
|
||||
return campaigns
|
||||
|
||||
finally:
|
||||
cursor.close()
|
||||
self.put_connection(conn)
|
||||
|
||||
def get_all_live_global_campaigns(self):
|
||||
"""
|
||||
Get all live GLOBAL campaigns (B-series) for the global CSV report.
|
||||
"""
|
||||
conn = self.get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT campaign_number, campaign_name
|
||||
FROM campaign_status
|
||||
WHERE live_campaign = 'YES'
|
||||
AND status LIKE 'B%'
|
||||
AND (status LIKE 'A%' OR status LIKE 'B%')
|
||||
ORDER BY campaign_number DESC
|
||||
""")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue