- Migrate from Airtable to Baserow: BaserowClient with snapshots, actions, creatives, logs - Claude agents (Haiku for decisions/units, Sonnet for creatives) with cost_cap_eur vs CPL comparison - Slack bot with colored action emojis, effect text before approval buttons, 500-char justifications - Streamlit dashboard with date-range navigation, campaign drill-down (adsets/ads), Histórico tab - Approval server (FastAPI + ngrok) for Slack button callbacks - backfill.py for historical snapshot regeneration with Claude re-analysis - Margin fix: 0-lead campaigns contribute -spend (not 0) to margin - CTR fix: Meta returns CTR as percentage already, removed *100 - Parameter fix: pass decision parameter to Slack action for correct budget effect display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
"""One-time: add daily_snapshots table to existing meta_optimizer database in Baserow."""
|
|
import os
|
|
import sys
|
|
import requests
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
BASE_URL = os.environ.get("BASEROW_URL", "").rstrip("/")
|
|
EMAIL = os.environ.get("BASEROW_EMAIL", "")
|
|
PASSWORD = os.environ.get("BASEROW_PASSWORD", "")
|
|
|
|
if not BASE_URL or not EMAIL or not PASSWORD:
|
|
print("Error: BASEROW_URL, BASEROW_EMAIL and BASEROW_PASSWORD must be set in .env")
|
|
sys.exit(1)
|
|
|
|
auth = requests.post(f"{BASE_URL}/api/user/token-auth/",
|
|
json={"email": EMAIL, "password": PASSWORD}, timeout=10)
|
|
if not auth.ok:
|
|
print(f"Auth error: {auth.text}")
|
|
sys.exit(1)
|
|
JWT = auth.json()["access_token"]
|
|
HEADERS = {"Authorization": f"JWT {JWT}", "Content-Type": "application/json"}
|
|
|
|
|
|
def api(method, path, **kwargs):
|
|
resp = requests.request(method, f"{BASE_URL}/api{path}", headers=HEADERS, **kwargs)
|
|
if not resp.ok:
|
|
print(f" API error {resp.status_code} {method} {path}: {resp.text[:300]}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
|
|
# Find meta_optimizer database
|
|
db_id = None
|
|
for ws in api("GET", "/workspaces/"):
|
|
for app in api("GET", f"/applications/workspace/{ws['id']}/"):
|
|
if app.get("name") == "meta_optimizer":
|
|
db_id = app["id"]
|
|
break
|
|
if db_id:
|
|
break
|
|
|
|
if not db_id:
|
|
db_id_env = os.environ.get("BASEROW_DB_ID")
|
|
if db_id_env:
|
|
db_id = int(db_id_env)
|
|
else:
|
|
print("Error: could not find meta_optimizer database. Set BASEROW_DB_ID in .env.")
|
|
sys.exit(1)
|
|
|
|
print(f"Database: meta_optimizer (id={db_id})")
|
|
|
|
# Create table
|
|
t = api("POST", f"/database/tables/database/{db_id}/", json={"name": "daily_snapshots"})
|
|
table_id = t["id"]
|
|
print(f"Table: daily_snapshots (id={table_id})")
|
|
|
|
# Rename primary field
|
|
primary_id = api("GET", f"/database/fields/table/{table_id}/")[0]["id"]
|
|
api("PATCH", f"/database/fields/{primary_id}/", json={"name": "run_date", "type": "text"})
|
|
print(" ~ primary field: run_date")
|
|
|
|
for f in [
|
|
{"name": "campaign_id", "type": "text"},
|
|
{"name": "campaign_name", "type": "text"},
|
|
{"name": "vertical", "type": "text"},
|
|
{"name": "spend", "type": "number", "number_decimal_places": 2},
|
|
{"name": "leads", "type": "number"},
|
|
{"name": "cpl", "type": "number", "number_decimal_places": 2},
|
|
{"name": "margin", "type": "number", "number_decimal_places": 2, "number_negative": True},
|
|
{"name": "action_type", "type": "text"},
|
|
{"name": "justification", "type": "long_text"},
|
|
{"name": "adsets_json", "type": "long_text"},
|
|
{"name": "ads_json", "type": "long_text"},
|
|
]:
|
|
api("POST", f"/database/fields/table/{table_id}/", json=f)
|
|
print(f" + {f['name']}")
|
|
|
|
print(f"""
|
|
{'='*50}
|
|
Añade esto a tu .env:
|
|
BASEROW_TABLE_SNAPSHOTS={table_id}
|
|
{'='*50}
|
|
""")
|