From d68f9f5659745c37f2b26203057dbf817cb4cfb0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 11:33:37 +0000 Subject: [PATCH] Sync Google Ads campaigns to Airtable before analysis Before each run, fetch all non-removed campaigns from Google Ads and upsert them into the Airtable campaigns table: new campaigns are created (Activa=False, pending manual PPL/Cap/CPA setup) and existing ones get their name updated if it changed. https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2 --- airtable_client.py | 38 ++++++++++++++++++++++++++++++++++++++ google_ads_client.py | 26 ++++++++++++++++++++++++++ run.py | 16 ++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/airtable_client.py b/airtable_client.py index 6de3b95..d8ef43d 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -25,6 +25,44 @@ class AirtableClient: }) 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]) -> dict: + """ + Sincroniza campañas de Google Ads → Airtable. + - Crea las campañas nuevas (Activa=False, requiere configuración manual de PPL/Cap/CPA). + - Actualiza el nombre si ha cambiado. + Devuelve un resumen con listas de campañas creadas y actualizadas. + """ + 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 = [] + updated = [] + + for gc in google_campaigns: + gid = gc["id"] + at_record = at_by_gid.get(gid) + + if at_record is None: + self.campaigns.create({ + "Campaign Name": gc["name"], + "CampaignID": int(gid), + "Activa": False, + }) + created.append(gc["name"]) + else: + changes = {} + if at_record["fields"].get("Campaign Name") != gc["name"]: + changes["Campaign Name"] = gc["name"] + if changes: + self.campaigns.update(at_record["id"], changes) + updated.append(gc["name"]) + + return {"created": created, "updated": updated} + def get_leads_this_month(self, google_campaign_id: str) -> int: """ Cuenta todos los leads del mes actual para una campaña. diff --git a/google_ads_client.py b/google_ads_client.py index 1156cac..290c92e 100644 --- a/google_ads_client.py +++ b/google_ads_client.py @@ -15,6 +15,32 @@ class GoogleAdsClient: }) self.customer_id = config.GOOGLE_ADS_LOGIN_CUSTOMER_ID + def get_all_campaigns(self) -> list[dict]: + """Obtiene todas las campañas no eliminadas de la cuenta.""" + ga_service = self.client.get_service("GoogleAdsService") + query = """ + SELECT + campaign.id, + campaign.name, + campaign.status + FROM campaign + WHERE campaign.status != 'REMOVED' + ORDER BY campaign.name + """ + try: + response = ga_service.search(customer_id=self.customer_id, query=query) + return [ + { + "id": str(row.campaign.id), + "name": row.campaign.name, + "status": row.campaign.status.name, + } + for row in response + ] + except GoogleAdsException as e: + print(f" ❌ Error obteniendo campañas de Google Ads: {e}") + return [] + def get_campaign_metrics(self, campaign_id: str) -> dict: """Métricas del mes en curso para una campaña concreta.""" ga_service = self.client.get_service("GoogleAdsService") diff --git a/run.py b/run.py index e1c6588..e04b2e2 100644 --- a/run.py +++ b/run.py @@ -25,6 +25,22 @@ def run(): at = AirtableClient() gads = GoogleAdsClient() + # Sincronizar catálogo de campañas desde Google Ads → Airtable + print("→ Sincronizando campañas desde Google Ads...") + google_campaigns = gads.get_all_campaigns() + sync_result = at.sync_campaigns_from_google_ads(google_campaigns) + if sync_result["created"]: + print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):") + for name in sync_result["created"]: + print(f" + {name}") + if sync_result["updated"]: + print(f" 🔄 Campañas con nombre actualizado ({len(sync_result['updated'])}):") + for name in sync_result["updated"]: + print(f" ~ {name}") + if not sync_result["created"] and not sync_result["updated"]: + print(" ✓ Sin cambios en el catálogo.") + print() + campaigns = at.get_active_campaigns() print(f"→ {len(campaigns)} campañas activas encontradas\n")