On month change, run.py redirected the write to the previous month's GACampaignMes record but built the JSON from the new month's near-empty record, overwriting the previous month's full daily history with just the redirected day. Now it merges with the previous month's real MetricasDiarias (via get_metricas_diarias_prev_month) before writing. Also add get_metrics_for_date() and get_daily_metrics_for_month() to GoogleAdsClient for reconstructing historical daily cost/conversions, needed by the backfill scripts.
366 lines
14 KiB
Python
366 lines
14 KiB
Python
from google.ads.googleads.client import GoogleAdsClient as GAdsClient
|
|
from google.ads.googleads.errors import GoogleAdsException
|
|
from datetime import datetime
|
|
import config
|
|
|
|
|
|
class GoogleAdsClient:
|
|
def __init__(self):
|
|
self.client = GAdsClient.load_from_dict({
|
|
"developer_token": config.GOOGLE_ADS_DEVELOPER_TOKEN,
|
|
"client_id": config.GOOGLE_ADS_CLIENT_ID,
|
|
"client_secret": config.GOOGLE_ADS_CLIENT_SECRET,
|
|
"refresh_token": config.GOOGLE_ADS_REFRESH_TOKEN,
|
|
"login_customer_id": config.GOOGLE_ADS_LOGIN_CUSTOMER_ID,
|
|
"use_proto_plus": True,
|
|
})
|
|
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_monthly_metrics_all(self) -> dict:
|
|
"""
|
|
Devuelve métricas del mes actual para TODAS las campañas en una sola query.
|
|
Retorna dict {campaign_id: {conversions, cost}}.
|
|
"""
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = """
|
|
SELECT
|
|
campaign.id,
|
|
metrics.conversions,
|
|
metrics.cost_micros
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date DURING THIS_MONTH
|
|
"""
|
|
result = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
cid = str(row.campaign.id)
|
|
result[cid] = {
|
|
"conversions": row.metrics.conversions,
|
|
"cost": row.metrics.cost_micros / 1_000_000,
|
|
}
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas mensuales: {e}")
|
|
return result
|
|
|
|
def get_yesterday_metrics_all(self) -> dict:
|
|
"""
|
|
Devuelve métricas del día anterior para TODAS las campañas en una sola query.
|
|
Retorna dict {campaign_id: {conversions, cost}}.
|
|
"""
|
|
from datetime import timedelta
|
|
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = f"""
|
|
SELECT
|
|
campaign.id,
|
|
metrics.conversions,
|
|
metrics.cost_micros
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date = '{yesterday}'
|
|
"""
|
|
result = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
cid = str(row.campaign.id)
|
|
result[cid] = {
|
|
"conversions": row.metrics.conversions,
|
|
"cost": row.metrics.cost_micros / 1_000_000,
|
|
}
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas de hoy: {e}")
|
|
return result
|
|
|
|
def get_metrics_for_date(self, date_str: str) -> dict:
|
|
"""
|
|
Devuelve métricas de una fecha concreta ('YYYY-MM-DD') para TODAS las
|
|
campañas en una sola query. Retorna dict {campaign_id: {conversions, cost}}.
|
|
"""
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = f"""
|
|
SELECT
|
|
campaign.id,
|
|
metrics.conversions,
|
|
metrics.cost_micros
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date = '{date_str}'
|
|
"""
|
|
result = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
cid = str(row.campaign.id)
|
|
result[cid] = {
|
|
"conversions": row.metrics.conversions,
|
|
"cost": row.metrics.cost_micros / 1_000_000,
|
|
}
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas del {date_str}: {e}")
|
|
return result
|
|
|
|
def get_daily_metrics_for_month(self, year: int, month: int) -> dict:
|
|
"""
|
|
Devuelve coste y conversiones diarias de todas las campañas para un mes
|
|
completo. Retorna dict {date_str ('YYYY-MM-DD'): {campaign_id: {cost, conversions}}}.
|
|
"""
|
|
start = f"{year}-{month:02d}-01"
|
|
if month == 12:
|
|
end = f"{year + 1}-01-01"
|
|
else:
|
|
end = f"{year}-{month + 1:02d}-01"
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = f"""
|
|
SELECT
|
|
campaign.id,
|
|
segments.date,
|
|
metrics.conversions,
|
|
metrics.cost_micros
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date >= '{start}'
|
|
AND segments.date < '{end}'
|
|
"""
|
|
result: dict = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
date_str = row.segments.date
|
|
cid = str(row.campaign.id)
|
|
result.setdefault(date_str, {})[cid] = {
|
|
"conversions": row.metrics.conversions,
|
|
"cost": row.metrics.cost_micros / 1_000_000,
|
|
}
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas diarias del mes: {e}")
|
|
return result
|
|
|
|
def get_daily_conversions_for_month(self, year: int, month: int) -> dict:
|
|
"""
|
|
Devuelve conversiones diarias de todas las campañas para un mes completo.
|
|
Retorna dict {date_str ('YYYY-MM-DD'): {campaign_id: conversions}}.
|
|
"""
|
|
start = f"{year}-{month:02d}-01"
|
|
# último día del mes
|
|
if month == 12:
|
|
end = f"{year + 1}-01-01"
|
|
else:
|
|
end = f"{year}-{month + 1:02d}-01"
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = f"""
|
|
SELECT
|
|
campaign.id,
|
|
segments.date,
|
|
metrics.conversions
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date >= '{start}'
|
|
AND segments.date < '{end}'
|
|
"""
|
|
result: dict = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
date_str = row.segments.date
|
|
cid = str(row.campaign.id)
|
|
result.setdefault(date_str, {})[cid] = row.metrics.conversions
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas mensuales: {e}")
|
|
return result
|
|
|
|
def get_monthly_metrics_all(self) -> dict:
|
|
"""
|
|
Métricas del mes en curso para TODAS las campañas en una sola query.
|
|
Retorna dict {campaign_id: {cost, conversions, clicks, impressions, ctr,
|
|
status, budget_daily, budget_resource_name, name}}.
|
|
"""
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = """
|
|
SELECT
|
|
campaign.id,
|
|
campaign.name,
|
|
campaign.status,
|
|
campaign_budget.amount_micros,
|
|
campaign_budget.resource_name,
|
|
metrics.cost_micros,
|
|
metrics.conversions,
|
|
metrics.clicks,
|
|
metrics.impressions
|
|
FROM campaign
|
|
WHERE campaign.status != 'REMOVED'
|
|
AND segments.date DURING THIS_MONTH
|
|
"""
|
|
raw: dict = {}
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
for row in response:
|
|
cid = str(row.campaign.id)
|
|
if cid not in raw:
|
|
raw[cid] = {
|
|
"name": row.campaign.name,
|
|
"status": row.campaign.status.name,
|
|
"budget_daily": row.campaign_budget.amount_micros / 1_000_000,
|
|
"budget_resource_name": row.campaign_budget.resource_name,
|
|
"cost": 0.0, "conversions": 0.0, "clicks": 0, "impressions": 0,
|
|
}
|
|
m = row.metrics
|
|
raw[cid]["cost"] += m.cost_micros / 1_000_000
|
|
raw[cid]["conversions"] += m.conversions
|
|
raw[cid]["clicks"] += m.clicks
|
|
raw[cid]["impressions"] += m.impressions
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error obteniendo métricas mensuales bulk: {e}")
|
|
|
|
result = {}
|
|
for cid, d in raw.items():
|
|
imp = d["impressions"]
|
|
result[cid] = {
|
|
"campaign_id": cid,
|
|
"name": d["name"],
|
|
"status": d["status"],
|
|
"budget_daily": round(d["budget_daily"], 2),
|
|
"budget_resource_name": d["budget_resource_name"],
|
|
"cost": round(d["cost"], 2),
|
|
"conversions": d["conversions"],
|
|
"clicks": d["clicks"],
|
|
"impressions": imp,
|
|
"ctr": round(d["clicks"] / imp * 100, 2) if imp > 0 else 0.0,
|
|
}
|
|
return result
|
|
|
|
def get_campaign_metrics(self, campaign_id: str) -> dict:
|
|
"""Métricas del mes en curso para una campaña concreta (acumulado mensual)."""
|
|
ga_service = self.client.get_service("GoogleAdsService")
|
|
query = f"""
|
|
SELECT
|
|
campaign.id,
|
|
campaign.name,
|
|
campaign.status,
|
|
campaign_budget.amount_micros,
|
|
campaign_budget.resource_name,
|
|
metrics.cost_micros,
|
|
metrics.conversions,
|
|
metrics.clicks,
|
|
metrics.impressions
|
|
FROM campaign
|
|
WHERE campaign.id = {campaign_id}
|
|
AND segments.date DURING THIS_MONTH
|
|
"""
|
|
try:
|
|
response = ga_service.search(customer_id=self.customer_id, query=query)
|
|
cost = conversions = clicks = impressions = 0
|
|
meta = {}
|
|
for row in response:
|
|
m = row.metrics
|
|
cost += m.cost_micros / 1_000_000
|
|
conversions += m.conversions
|
|
clicks += m.clicks
|
|
impressions += m.impressions
|
|
if not meta:
|
|
meta = {
|
|
"name": row.campaign.name,
|
|
"status": row.campaign.status.name,
|
|
"budget_daily": row.campaign_budget.amount_micros / 1_000_000,
|
|
"budget_resource_name": row.campaign_budget.resource_name,
|
|
}
|
|
if not meta:
|
|
return {}
|
|
ctr = round(clicks / impressions * 100, 2) if impressions > 0 else 0.0
|
|
return {
|
|
"campaign_id": campaign_id,
|
|
"name": meta["name"],
|
|
"status": meta["status"],
|
|
"budget_daily": meta["budget_daily"],
|
|
"budget_resource_name": meta["budget_resource_name"],
|
|
"cost": round(cost, 2),
|
|
"conversions": conversions,
|
|
"clicks": clicks,
|
|
"impressions": impressions,
|
|
"ctr": ctr,
|
|
}
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error Google Ads para campaña {campaign_id}: {e}")
|
|
return {}
|
|
return {}
|
|
|
|
def set_campaign_budget(self, budget_resource_name: str, new_daily_budget: float):
|
|
"""Ajusta el presupuesto diario de una campaña."""
|
|
if config.DRY_RUN:
|
|
print(f" [DRY RUN] Nuevo presupuesto diario → {new_daily_budget:.2f}€")
|
|
return True
|
|
|
|
try:
|
|
budget_service = self.client.get_service("CampaignBudgetService")
|
|
campaign_budget = self.client.get_type("CampaignBudget")
|
|
campaign_budget.resource_name = budget_resource_name
|
|
campaign_budget.amount_micros = int(new_daily_budget * 1_000_000)
|
|
|
|
operation = self.client.get_type("CampaignBudgetOperation")
|
|
operation.update = campaign_budget
|
|
operation.update_mask.paths.append("amount_micros")
|
|
|
|
budget_service.mutate_campaign_budgets(
|
|
customer_id=self.customer_id,
|
|
operations=[operation]
|
|
)
|
|
return True
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error ajustando presupuesto: {e}")
|
|
return False
|
|
|
|
def pause_campaign(self, campaign_id: str):
|
|
"""Pausa una campaña."""
|
|
if config.DRY_RUN:
|
|
print(f" [DRY RUN] Pausar campaña {campaign_id}")
|
|
return True
|
|
|
|
try:
|
|
campaign_service = self.client.get_service("CampaignService")
|
|
campaign = self.client.get_type("Campaign")
|
|
campaign.resource_name = campaign_service.campaign_path(
|
|
self.customer_id, campaign_id
|
|
)
|
|
campaign.status = self.client.enums.CampaignStatusEnum.PAUSED
|
|
|
|
operation = self.client.get_type("CampaignOperation")
|
|
operation.update = campaign
|
|
operation.update_mask.paths.append("status")
|
|
|
|
campaign_service.mutate_campaigns(
|
|
customer_id=self.customer_id,
|
|
operations=[operation]
|
|
)
|
|
return True
|
|
except GoogleAdsException as e:
|
|
print(f" ❌ Error pausando campaña: {e}")
|
|
return False
|