- 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>
164 lines
6.4 KiB
Python
164 lines
6.4 KiB
Python
"""
|
|
One-time script: creates the meta_optimizer database in Baserow with all tables and fields.
|
|
Run once, then paste the printed IDs into your .env file.
|
|
|
|
Usage:
|
|
python setup_baserow.py
|
|
"""
|
|
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 your .env file.")
|
|
sys.exit(1)
|
|
|
|
# Authenticate to get a JWT token
|
|
_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: str, path: str, **kwargs) -> dict:
|
|
url = f"{BASE_URL}/api{path}"
|
|
resp = requests.request(method, url, 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()
|
|
|
|
|
|
def setup_table(db_id: int, table_name: str, fields: list) -> int:
|
|
"""Create table, rename the auto-created primary field, then add remaining fields."""
|
|
t = api("POST", f"/database/tables/database/{db_id}/", json={"name": table_name})
|
|
table_id = t["id"]
|
|
print(f"\n Table: {table_name} (id={table_id})")
|
|
|
|
existing = api("GET", f"/database/fields/table/{table_id}/")
|
|
primary_id = existing[0]["id"]
|
|
|
|
first = fields[0]
|
|
api("PATCH", f"/database/fields/{primary_id}/", json={"name": first["name"], "type": first["type"]})
|
|
print(f" ~ primary field renamed to: {first['name']}")
|
|
|
|
for field in fields[1:]:
|
|
api("POST", f"/database/fields/table/{table_id}/", json=field)
|
|
print(f" + {field['name']}")
|
|
|
|
return table_id
|
|
|
|
|
|
# ── 1. Workspace ──────────────────────────────────────────────────────────────
|
|
|
|
workspaces = api("GET", "/workspaces/")
|
|
if not workspaces:
|
|
print("No workspaces found. Create one in Baserow first.")
|
|
sys.exit(1)
|
|
workspace_id = workspaces[0]["id"]
|
|
print(f"Workspace: {workspaces[0]['name']} (id={workspace_id})")
|
|
|
|
# ── 2. Database ───────────────────────────────────────────────────────────────
|
|
|
|
db = api("POST", f"/applications/workspace/{workspace_id}/", json={
|
|
"type": "database",
|
|
"name": "meta_optimizer",
|
|
})
|
|
db_id = db["id"]
|
|
print(f"Database: meta_optimizer (id={db_id})")
|
|
|
|
# ── 3. Tables ─────────────────────────────────────────────────────────────────
|
|
|
|
tid_campaigns = setup_table(db_id, "campaigns", [
|
|
{"name": "campaign_id", "type": "text"},
|
|
{"name": "name", "type": "text"},
|
|
{"name": "max_cpl", "type": "number", "number_decimal_places": 2},
|
|
{"name": "daily_budget", "type": "number", "number_decimal_places": 2},
|
|
{"name": "is_active", "type": "boolean"},
|
|
{"name": "notes", "type": "long_text"},
|
|
])
|
|
|
|
tid_actions = setup_table(db_id, "proposed_actions", [
|
|
{"name": "campaign_id", "type": "text"},
|
|
{"name": "campaign_name", "type": "text"},
|
|
{
|
|
"name": "action_type",
|
|
"type": "single_select",
|
|
"select_options": [
|
|
{"value": "PAUSE", "color": "red"},
|
|
{"value": "REDUCE_BUDGET", "color": "orange"},
|
|
{"value": "INCREASE_BUDGET", "color": "green"},
|
|
{"value": "MAINTAIN", "color": "blue"},
|
|
{"value": "REVIEW_CREATIVES", "color": "purple"},
|
|
],
|
|
},
|
|
{"name": "parameter", "type": "number", "number_decimal_places": 2},
|
|
{"name": "justification", "type": "long_text"},
|
|
{"name": "advice", "type": "long_text"},
|
|
{"name": "alert", "type": "long_text"},
|
|
{"name": "confidence", "type": "number", "number_decimal_places": 2},
|
|
{
|
|
"name": "status",
|
|
"type": "single_select",
|
|
"select_options": [
|
|
{"value": "pending", "color": "yellow"},
|
|
{"value": "approved", "color": "green"},
|
|
{"value": "rejected", "color": "red"},
|
|
{"value": "executed", "color": "blue"},
|
|
],
|
|
},
|
|
{"name": "proposed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
|
|
{"name": "executed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
|
|
{"name": "slack_message_ts", "type": "text"},
|
|
])
|
|
|
|
tid_creatives = setup_table(db_id, "creative_analyses", [
|
|
{"name": "ad_id", "type": "text"},
|
|
{"name": "ad_name", "type": "text"},
|
|
{"name": "campaign_id", "type": "text"},
|
|
{"name": "image_url", "type": "url"},
|
|
{"name": "analysis", "type": "long_text"},
|
|
{"name": "score", "type": "number", "number_decimal_places": 1},
|
|
{"name": "recommendations", "type": "long_text"},
|
|
{"name": "created_at", "type": "date", "date_format": "ISO", "date_include_time": True},
|
|
])
|
|
|
|
tid_logs = setup_table(db_id, "execution_logs", [
|
|
{"name": "executed_at", "type": "date", "date_format": "ISO", "date_include_time": True},
|
|
{"name": "mode", "type": "text"},
|
|
{"name": "campaigns_analyzed", "type": "number"},
|
|
{"name": "actions_proposed", "type": "number"},
|
|
{"name": "actions_executed", "type": "number"},
|
|
{"name": "errors", "type": "long_text"},
|
|
{"name": "summary", "type": "long_text"},
|
|
{"name": "duration_seconds", "type": "number", "number_decimal_places": 1},
|
|
])
|
|
|
|
# ── 4. Output env vars ────────────────────────────────────────────────────────
|
|
|
|
print(f"""
|
|
{'='*55}
|
|
Add these to your .env file:
|
|
{'='*55}
|
|
BASEROW_DB_ID={db_id}
|
|
BASEROW_TABLE_CAMPAIGNS={tid_campaigns}
|
|
BASEROW_TABLE_ACTIONS={tid_actions}
|
|
BASEROW_TABLE_CREATIVES={tid_creatives}
|
|
BASEROW_TABLE_LOGS={tid_logs}
|
|
{'='*55}
|
|
""")
|