Merge pull request #1 from jmgomezroi/claude/help-project-setup-t4hyw
Claude/help project setup t4hyw
This commit is contained in:
commit
ef7d494aeb
@ -11,32 +11,77 @@ class AirtableClient:
|
|||||||
|
|
||||||
def get_active_campaigns(self) -> list[dict]:
|
def get_active_campaigns(self) -> list[dict]:
|
||||||
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
||||||
records = self.campaigns.all(formula="{Activa}=TRUE()")
|
records = self.campaigns.all(formula="{Status}='Activa'")
|
||||||
result = []
|
result = []
|
||||||
for r in records:
|
for r in records:
|
||||||
f = r["fields"]
|
f = r["fields"]
|
||||||
result.append({
|
result.append({
|
||||||
"airtable_id": r["id"],
|
"airtable_id": r["id"],
|
||||||
"curso": f.get("Campaign Name", "Sin nombre"),
|
"curso": f.get("Campaign Name", "Sin nombre"),
|
||||||
|
"cursoid_text": str(f.get("CursoID Text", "")).strip(),
|
||||||
"google_campaign_id": str(f.get("CampaignID", "")).strip(),
|
"google_campaign_id": str(f.get("CampaignID", "")).strip(),
|
||||||
"ppl": float(f.get("PPL", 0)),
|
"ppl": float(f.get("PPL", 0)),
|
||||||
"capping_mensual": int(f.get("CapTotalMes", 0)),
|
"capping_mensual": int(f.get("CapTotalMes", 0)),
|
||||||
"cpa_maximo": float(f.get("CPAMaximo", 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
|
return [c for c in result if c["google_campaign_id"]] # descartar sin ID
|
||||||
|
|
||||||
def get_leads_this_month(self, google_campaign_id: str) -> int:
|
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict]) -> dict:
|
||||||
"""
|
"""
|
||||||
Cuenta todos los leads del mes actual para una campaña.
|
Sincroniza campañas de Google Ads → Airtable.
|
||||||
Todos los leads en Airtable están validados, sin filtro de estado.
|
- 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 = []
|
||||||
|
updated = []
|
||||||
|
|
||||||
|
for gc in google_campaigns:
|
||||||
|
gid = gc["id"]
|
||||||
|
at_status = STATUS_MAP.get(gc["status"], "Pausada")
|
||||||
|
at_record = at_by_gid.get(gid)
|
||||||
|
|
||||||
|
if at_record is None:
|
||||||
|
self.campaigns.create({
|
||||||
|
"Campaign Name": gc["name"],
|
||||||
|
"CampaignID": int(gid),
|
||||||
|
"Status": at_status,
|
||||||
|
})
|
||||||
|
created.append(gc["name"])
|
||||||
|
else:
|
||||||
|
changes = {}
|
||||||
|
if at_record["fields"].get("Campaign Name") != gc["name"]:
|
||||||
|
changes["Campaign Name"] = gc["name"]
|
||||||
|
if at_record["fields"].get("Status") != at_status:
|
||||||
|
changes["Status"] = at_status
|
||||||
|
if changes:
|
||||||
|
self.campaigns.update(at_record["id"], changes)
|
||||||
|
updated.append(gc["name"])
|
||||||
|
|
||||||
|
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()
|
now = datetime.now()
|
||||||
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
||||||
|
|
||||||
formula = (
|
formula = (
|
||||||
f"AND("
|
f"AND("
|
||||||
f"{{GoogleCampaignID}}='{google_campaign_id}',"
|
f"{{attr_cursoid}}='{cursoid_text}',"
|
||||||
f"{{Fecha}}>='{mes_inicio}'"
|
f"{{creado}}>='{mes_inicio}'"
|
||||||
f")"
|
f")"
|
||||||
)
|
)
|
||||||
records = self.leads.all(formula=formula)
|
records = self.leads.all(formula=formula)
|
||||||
|
|||||||
@ -11,6 +11,7 @@ def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) ->
|
|||||||
capping = campaign_config["capping_mensual"]
|
capping = campaign_config["capping_mensual"]
|
||||||
ppl = campaign_config["ppl"]
|
ppl = campaign_config["ppl"]
|
||||||
cpa_max = campaign_config["cpa_maximo"]
|
cpa_max = campaign_config["cpa_maximo"]
|
||||||
|
margen_objetivo = campaign_config.get("margen_objetivo", 0)
|
||||||
gasto = ads_metrics.get("cost", 0)
|
gasto = ads_metrics.get("cost", 0)
|
||||||
conversiones_google = ads_metrics.get("conversions", 0)
|
conversiones_google = ads_metrics.get("conversions", 0)
|
||||||
|
|
||||||
@ -42,6 +43,8 @@ def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) ->
|
|||||||
"campaign_id": campaign_config["google_campaign_id"],
|
"campaign_id": campaign_config["google_campaign_id"],
|
||||||
"ppl": ppl,
|
"ppl": ppl,
|
||||||
"cpa_maximo": cpa_max,
|
"cpa_maximo": cpa_max,
|
||||||
|
"margen_objetivo": margen_objetivo,
|
||||||
|
"margen_ok": margen >= margen_objetivo if margen_objetivo > 0 else True,
|
||||||
"capping": capping,
|
"capping": capping,
|
||||||
"leads_entregados": leads_entregados,
|
"leads_entregados": leads_entregados,
|
||||||
"leads_restantes": leads_restantes,
|
"leads_restantes": leads_restantes,
|
||||||
|
|||||||
@ -20,5 +20,4 @@ GOOGLE_ADS_LOGIN_CUSTOMER_ID = os.environ["GOOGLE_ADS_LOGIN_CUSTOMER_ID"]
|
|||||||
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
|
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
|
||||||
|
|
||||||
# Operación
|
# Operación
|
||||||
DRY_RUN = True # True = solo sugiere, no aplica cambios en Google Ads
|
DRY_RUN = True # True = solo sugiere, no aplica cambios en Google Ads
|
||||||
MARGEN_MINIMO = 0.40 # 40% mínimo de margen sobre PPL
|
|
||||||
|
|||||||
@ -15,6 +15,32 @@ class GoogleAdsClient:
|
|||||||
})
|
})
|
||||||
self.customer_id = config.GOOGLE_ADS_LOGIN_CUSTOMER_ID
|
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:
|
def get_campaign_metrics(self, campaign_id: str) -> dict:
|
||||||
"""Métricas del mes en curso para una campaña concreta."""
|
"""Métricas del mes en curso para una campaña concreta."""
|
||||||
ga_service = self.client.get_service("GoogleAdsService")
|
ga_service = self.client.get_service("GoogleAdsService")
|
||||||
|
|||||||
18
run.py
18
run.py
@ -25,6 +25,22 @@ def run():
|
|||||||
at = AirtableClient()
|
at = AirtableClient()
|
||||||
gads = GoogleAdsClient()
|
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()
|
campaigns = at.get_active_campaigns()
|
||||||
print(f"→ {len(campaigns)} campañas activas encontradas\n")
|
print(f"→ {len(campaigns)} campañas activas encontradas\n")
|
||||||
|
|
||||||
@ -37,7 +53,7 @@ def run():
|
|||||||
print(f" Campaign ID: {cid} | PPL: {campaign['ppl']}€ | Cap: {campaign['capping_mensual']} leads")
|
print(f" Campaign ID: {cid} | PPL: {campaign['ppl']}€ | Cap: {campaign['capping_mensual']} leads")
|
||||||
|
|
||||||
# 1. Leads reales desde Airtable
|
# 1. Leads reales desde Airtable
|
||||||
leads = at.get_leads_this_month(cid)
|
leads = at.get_leads_this_month(campaign["cursoid_text"])
|
||||||
|
|
||||||
# 2. Métricas de Google Ads
|
# 2. Métricas de Google Ads
|
||||||
metrics = gads.get_campaign_metrics(cid)
|
metrics = gads.get_campaign_metrics(cid)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user