leads-optimizer/airtable_client.py
José Manuel Gómez 952fabe4ca Sync ConvMes and CosteMes to Airtable on each run
- Add get_monthly_metrics_all() in GoogleAdsClient: fetches
  conversions and cost for all campaigns in one query
- sync_campaigns_from_google_ads() now accepts monthly_metrics
  and updates ConvMes and CosteMes fields in Airtable
- run.py fetches monthly metrics before sync and passes them

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 21:12:05 +02:00

119 lines
4.9 KiB
Python

from pyairtable import Api
from datetime import datetime
import config
class AirtableClient:
def __init__(self):
self.api = Api(config.AIRTABLE_TOKEN)
self.leads = self.api.table(config.AIRTABLE_BASE_ID, config.LEADS_TABLE)
self.campaigns = self.api.table(config.AIRTABLE_BASE_ID, config.CAMPAIGNS_TABLE)
def get_active_campaigns(self) -> list[dict]:
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
records = self.campaigns.all(formula="{Status}='Activa'")
result = []
for r in records:
f = r["fields"]
result.append({
"airtable_id": r["id"],
"curso": f.get("Campaign Name", "Sin nombre"),
"cursoid_text": str(f.get("CursoID Text", "")).strip(),
"google_campaign_id": str(f.get("CampaignID", "")).strip(),
"ppl": float(f.get("PPL", 0)),
"capping_mensual": int(f.get("CapTotalMes", 0)),
"cpa_maximo": float(f.get("CPAMax", 0)),
"margen_objetivo": float(f.get("MargenObjetivo", 0)),
})
return [c for c in result if c["google_campaign_id"]] # descartar sin ID
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict], monthly_metrics: dict = None) -> dict:
"""
Sincroniza campañas de Google Ads → Airtable.
- Crea las campañas nuevas con el status real de Google Ads.
- Actualiza nombre y/o status si han cambiado.
Devuelve un resumen con listas de campañas creadas y actualizadas.
"""
STATUS_MAP = {"ENABLED": "Activa", "PAUSED": "Pausada"}
all_records = self.campaigns.all()
at_by_gid = {
str(r["fields"].get("CampaignID", "")).strip(): r
for r in all_records
if r["fields"].get("CampaignID")
}
created = [] # lista de dicts {name, id, status}
updated = [] # lista de dicts {name, id, changes}
to_create = []
to_update = [] # lista de (record_id, changes, name, gid, at_status)
for gc in google_campaigns:
gid = gc["id"]
at_status = STATUS_MAP.get(gc["status"], "Pausada")
at_record = at_by_gid.get(gid)
metrics = (monthly_metrics or {}).get(gid, {})
conv = round(metrics.get("conversions", 0), 2)
cost = round(metrics.get("cost", 0), 2)
if at_record is None:
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
if monthly_metrics is not None:
fields["ConvMes"] = conv
fields["CosteMes"] = cost
to_create.append(fields)
created.append({"name": gc["name"], "id": gid, "status": at_status})
else:
changes = {}
change_detail = {}
if at_record["fields"].get("Campaign Name") != gc["name"]:
changes["Campaign Name"] = gc["name"]
change_detail["nombre"] = {
"antes": at_record["fields"].get("Campaign Name"),
"ahora": gc["name"],
}
if at_record["fields"].get("Status") != at_status:
changes["Status"] = at_status
change_detail["status"] = {
"antes": at_record["fields"].get("Status"),
"ahora": at_status,
}
if monthly_metrics is not None:
if at_record["fields"].get("ConvMes") != conv:
changes["ConvMes"] = conv
if at_record["fields"].get("CosteMes") != cost:
changes["CosteMes"] = cost
if changes:
to_update.append((at_record["id"], changes, gc["name"]))
if change_detail:
updated.append({"name": gc["name"], "id": gid, "changes": change_detail})
# Crear en lotes de 10
for i in range(0, len(to_create), 10):
self.campaigns.batch_create(to_create[i:i+10])
# Actualizar en lotes de 10
for i in range(0, len(to_update), 10):
batch = [{"id": rid, "fields": changes} for rid, changes, _ in to_update[i:i+10]]
self.campaigns.batch_update(batch)
return {"created": created, "updated": updated}
def get_leads_this_month(self, cursoid_text: str) -> int:
"""
Cuenta todos los leads validados del mes actual para un curso.
Filtra por attr_cursoid (que coincide con CursoID Text de la campaña).
"""
now = datetime.now()
mes_inicio = f"{now.year}-{now.month:02d}-01"
formula = (
f"AND("
f"{{attr_cursoid}}='{cursoid_text}',"
f"{{creado}}>='{mes_inicio}'"
f")"
)
records = self.leads.all(formula=formula)
return len(records)