Ports meta-optimizer's Meta Ads execution/approval/creative-analysis layer (agent.py, meta_ads_client.py, baserow_client.py, slack_notifier.py, approval_server.py) and replaces the per-vertical CPL model with the PPL + monthly-capping-per-course model already used by leads-optimizer, via a new airtable_client.py that shares Cursos/Familias/CentroCurso/ CursoMes/Leads Lake with that project and adds Meta Ads Campaigns / MetaCampaignMes alongside its Google Ads Campaigns / GACampaignMes.
175 lines
6.9 KiB
Python
175 lines
6.9 KiB
Python
"""
|
|
One-time script: creates the meta_optimizer_formacion database in Baserow
|
|
with all tables and fields. Run once, then paste the printed IDs into your
|
|
.env file.
|
|
|
|
A diferencia de meta-optimizer (Viviful), NO se crean tablas 'campaigns' ni
|
|
'verticals' — esa capa de negocio (PPL, capping, familia) vive en Airtable
|
|
(ver setup_airtable_meta_tables.py). daily_snapshots usa 'familia' en vez de
|
|
'vertical'.
|
|
|
|
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_formacion",
|
|
})
|
|
db_id = db["id"]
|
|
print(f"Database: meta_optimizer_formacion (id={db_id})")
|
|
|
|
# ── 3. Tables ─────────────────────────────────────────────────────────────────
|
|
|
|
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"},
|
|
],
|
|
},
|
|
{"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},
|
|
])
|
|
|
|
tid_snapshots = setup_table(db_id, "daily_snapshots", [
|
|
{"name": "run_date", "type": "text"},
|
|
{"name": "campaign_id", "type": "text"},
|
|
{"name": "campaign_name", "type": "text"},
|
|
{"name": "familia", "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"},
|
|
])
|
|
|
|
# ── 4. Output env vars ────────────────────────────────────────────────────────
|
|
|
|
print(f"""
|
|
{'='*55}
|
|
Add these to your .env file:
|
|
{'='*55}
|
|
BASEROW_DB_ID={db_id}
|
|
BASEROW_TABLE_ACTIONS={tid_actions}
|
|
BASEROW_TABLE_CREATIVES={tid_creatives}
|
|
BASEROW_TABLE_LOGS={tid_logs}
|
|
BASEROW_TABLE_SNAPSHOTS={tid_snapshots}
|
|
{'='*55}
|
|
""")
|