chore: scripts Dokploy/BPS (cron secret, Ads MCC, sync y probes)
Incluye utilidades de ops para CRON_SECRET, login customer Google Ads, sync de seguimiento BPS y probes de conexiones. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
592
scripts/refresh_agency_connections.py
Normal file
592
scripts/refresh_agency_connections.py
Normal file
@@ -0,0 +1,592 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refresh Meta + Google agency matches into client fichas and audit."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
META = "https://graph.facebook.com/v21.0"
|
||||
GADS = "https://googleads.googleapis.com/v18"
|
||||
|
||||
|
||||
def load_env() -> dict[str, str]:
|
||||
vals: dict[str, str] = {}
|
||||
for p in (ROOT / ".env.local", Path("/Users/4ventures/Projects/bps-facturacion/.env")):
|
||||
if not p.is_file():
|
||||
continue
|
||||
for line in p.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip() or line.strip().startswith("#") or "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
vals.setdefault(k.strip(), v.strip().strip('"').strip("'"))
|
||||
return vals
|
||||
|
||||
|
||||
ENV = load_env()
|
||||
BASE = ENV["NEXT_PUBLIC_SUPABASE_URL"].rstrip("/")
|
||||
KEY = ENV["SUPABASE_SERVICE_ROLE_KEY"]
|
||||
SH = {"apikey": KEY, "Authorization": f"Bearer {KEY}"}
|
||||
|
||||
|
||||
def sb_get(path: str):
|
||||
req = urllib.request.Request(BASE + path, headers=SH)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def sb_patch(path: str, body: dict) -> bool:
|
||||
data = json.dumps(body).encode()
|
||||
req = urllib.request.Request(
|
||||
BASE + path,
|
||||
data=data,
|
||||
headers={**SH, "Content-Type": "application/json", "Prefer": "return=minimal"},
|
||||
method="PATCH",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return 200 <= r.status < 300
|
||||
except urllib.error.HTTPError:
|
||||
return False
|
||||
|
||||
|
||||
def sb_upsert_integ(tenant_id: str, provider: str, patch: dict) -> bool:
|
||||
path = (
|
||||
f"/rest/v1/integrations?tenant_id=eq.{urllib.parse.quote(tenant_id)}"
|
||||
f"&provider=eq.{urllib.parse.quote(provider)}"
|
||||
)
|
||||
body = {**patch, "updated_at": datetime.now(timezone.utc).isoformat()}
|
||||
if sb_patch(path, body):
|
||||
return True
|
||||
data = json.dumps(
|
||||
{"tenant_id": tenant_id, "provider": provider, "enabled": True, **body}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
BASE + "/rest/v1/integrations",
|
||||
data=data,
|
||||
headers={
|
||||
**SH,
|
||||
"Content-Type": "application/json",
|
||||
"Prefer": "resolution=merge-duplicates,return=minimal",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as _:
|
||||
return True
|
||||
except urllib.error.HTTPError as e:
|
||||
print("upsert fail", provider, e.read().decode()[:200])
|
||||
return False
|
||||
|
||||
|
||||
def norm(s: str) -> str:
|
||||
s = s.lower()
|
||||
s = (
|
||||
s.replace("á", "a")
|
||||
.replace("é", "e")
|
||||
.replace("í", "i")
|
||||
.replace("ó", "o")
|
||||
.replace("ú", "u")
|
||||
.replace("ñ", "n")
|
||||
)
|
||||
return re.sub(r"[^a-z0-9]+", " ", s).strip()
|
||||
|
||||
|
||||
def score_name(tenant_name: str, tenant_slug: str, account_name: str) -> int:
|
||||
t = norm(tenant_name)
|
||||
s = norm(tenant_slug.replace("-", " "))
|
||||
a = norm(account_name)
|
||||
if not a:
|
||||
return 0
|
||||
if a == t or a == s:
|
||||
return 100
|
||||
if t and (t in a or a in t):
|
||||
return 85
|
||||
if s and (s in a or a in s):
|
||||
return 80
|
||||
tt = set(x for x in t.split() if len(x) > 2)
|
||||
aa = set(x for x in a.split() if len(x) > 2)
|
||||
if not tt or not aa:
|
||||
return 0
|
||||
hit = len(tt & aa) / len(tt)
|
||||
if hit >= 0.8:
|
||||
return 70
|
||||
if hit >= 0.5:
|
||||
return 55
|
||||
return int(hit * 50)
|
||||
|
||||
|
||||
def best_matches(tenants, accounts, threshold=55):
|
||||
"""Greedy 1:1 match like portal."""
|
||||
scored = []
|
||||
for t in tenants:
|
||||
for acc in accounts:
|
||||
sc = score_name(t["name"], t["slug"], acc["name"])
|
||||
if sc >= threshold:
|
||||
scored.append((sc, t["id"], acc))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
used_t, used_a = set(), set()
|
||||
out = {}
|
||||
for sc, tid, acc in scored:
|
||||
if tid in used_t or acc["id"] in used_a:
|
||||
continue
|
||||
used_t.add(tid)
|
||||
used_a.add(acc["id"])
|
||||
out[tid] = (acc, sc)
|
||||
return out
|
||||
|
||||
|
||||
def meta_get(token: str, path: str):
|
||||
url = f"{META}/{path}"
|
||||
sep = "&" if "?" in path else "?"
|
||||
req = urllib.request.Request(url + sep + "access_token=" + urllib.parse.quote(token))
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
|
||||
def meta_paginate(token: str, path: str):
|
||||
out = []
|
||||
url = f"{META}/{path}"
|
||||
sep = "&" if "?" in path else "?"
|
||||
url = url + sep + "access_token=" + urllib.parse.quote(token)
|
||||
while url:
|
||||
req = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
j = json.loads(r.read().decode())
|
||||
out.extend(j.get("data") or [])
|
||||
url = (j.get("paging") or {}).get("next")
|
||||
return out
|
||||
|
||||
|
||||
def google_refresh(refresh: str) -> str:
|
||||
body = urllib.parse.urlencode(
|
||||
{
|
||||
"refresh_token": refresh,
|
||||
"client_id": ENV["GOOGLE_OAUTH_CLIENT_ID"],
|
||||
"client_secret": ENV["GOOGLE_OAUTH_CLIENT_SECRET"],
|
||||
"grant_type": "refresh_token",
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read().decode())["access_token"]
|
||||
|
||||
|
||||
def gads_access() -> str:
|
||||
refresh = ENV.get("GOOGLE_ADS_REFRESH_TOKEN") or ENV.get("GOOGLE_OAUTH_REFRESH_TOKEN")
|
||||
cid = ENV.get("GOOGLE_ADS_CLIENT_ID") or ENV.get("GOOGLE_OAUTH_CLIENT_ID")
|
||||
secret = ENV.get("GOOGLE_ADS_CLIENT_SECRET") or ENV.get("GOOGLE_OAUTH_CLIENT_SECRET")
|
||||
body = urllib.parse.urlencode(
|
||||
{
|
||||
"refresh_token": refresh,
|
||||
"client_id": cid,
|
||||
"client_secret": secret,
|
||||
"grant_type": "refresh_token",
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
"https://oauth2.googleapis.com/token",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read().decode())["access_token"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
tenants = sb_get("/rest/v1/tenants?select=id,name,slug&order=name.asc")
|
||||
print(f"tenants={len(tenants)}")
|
||||
|
||||
# --- tokens ---
|
||||
g_rows = sb_get(
|
||||
"/rest/v1/agency_oauth?provider=eq.google_drive&select=access_token,refresh_token,expires_at,account_email,scopes"
|
||||
)
|
||||
m_rows = sb_get(
|
||||
"/rest/v1/agency_oauth?provider=eq.meta_ads&select=access_token,account_email,scopes,updated_at"
|
||||
)
|
||||
if not g_rows:
|
||||
print("FAIL: no google_drive oauth")
|
||||
return 1
|
||||
if not m_rows:
|
||||
print("FAIL: no meta_ads oauth")
|
||||
return 1
|
||||
|
||||
grow = g_rows[0]
|
||||
mrow = m_rows[0]
|
||||
print("Google:", grow.get("account_email"))
|
||||
print("Meta:", mrow.get("account_email"), "updated", mrow.get("updated_at"))
|
||||
|
||||
gtoken = grow["access_token"]
|
||||
exp = grow.get("expires_at")
|
||||
if exp:
|
||||
try:
|
||||
et = datetime.fromisoformat(exp.replace("Z", "+00:00"))
|
||||
if et.timestamp() < datetime.now(timezone.utc).timestamp() + 60:
|
||||
print("Refreshing Google access token…")
|
||||
gtoken = google_refresh(grow["refresh_token"])
|
||||
except Exception as e:
|
||||
print("google refresh warn", e)
|
||||
gtoken = google_refresh(grow["refresh_token"])
|
||||
|
||||
mtoken = mrow["access_token"]
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
linked_counts = defaultdict(int)
|
||||
|
||||
# ========== META ==========
|
||||
print("\n=== META Ads + Pages + IG ===")
|
||||
ad_accounts = []
|
||||
for row in meta_paginate(
|
||||
mtoken, "me/adaccounts?fields=id,account_id,name,account_status&limit=200"
|
||||
):
|
||||
aid = row.get("account_id") or row["id"].replace("act_", "")
|
||||
ad_accounts.append({"id": f"act_{aid}", "name": row.get("name") or aid})
|
||||
print(f"Meta ad accounts visible: {len(ad_accounts)}")
|
||||
|
||||
pages = []
|
||||
igs = []
|
||||
for page in meta_paginate(
|
||||
mtoken,
|
||||
"me/accounts?fields=id,name,instagram_business_account{id,username,name}&limit=100",
|
||||
):
|
||||
pages.append({"id": page["id"], "name": page.get("name") or page["id"]})
|
||||
ig = page.get("instagram_business_account") or {}
|
||||
if ig.get("id"):
|
||||
igs.append(
|
||||
{
|
||||
"id": ig["id"],
|
||||
"name": f"@{ig['username']}" if ig.get("username") else (ig.get("name") or page.get("name") or ig["id"]),
|
||||
}
|
||||
)
|
||||
print(f"FB pages visible: {len(pages)} · IG: {len(igs)}")
|
||||
|
||||
for provider, accounts, label in (
|
||||
("meta_ads", ad_accounts, "Meta Ads"),
|
||||
("facebook", pages, "Facebook Page"),
|
||||
("instagram", igs, "Instagram"),
|
||||
):
|
||||
matches = best_matches(tenants, accounts, threshold=55)
|
||||
for tid, (acc, sc) in matches.items():
|
||||
ok = sb_upsert_integ(
|
||||
tid,
|
||||
provider,
|
||||
{
|
||||
"status": "connected",
|
||||
"external_account_id": acc["id"],
|
||||
"label": acc["name"],
|
||||
"last_sync_at": now,
|
||||
"error_message": None,
|
||||
"enabled": True,
|
||||
"meta": {
|
||||
"account_name": acc["name"],
|
||||
"match_score": sc,
|
||||
"matched_at": now,
|
||||
"source": "refresh_script",
|
||||
},
|
||||
},
|
||||
)
|
||||
if ok:
|
||||
linked_counts[provider] += 1
|
||||
print(f" linked {provider}: {linked_counts[provider]}/{len(accounts)}")
|
||||
|
||||
# ========== GOOGLE DRIVE ==========
|
||||
print("\n=== Google Drive folders ===")
|
||||
folders = []
|
||||
page_token = None
|
||||
while True:
|
||||
params = {
|
||||
"q": "mimeType='application/vnd.google-apps.folder' and trashed=false",
|
||||
"fields": "nextPageToken,files(id,name)",
|
||||
"pageSize": "1000",
|
||||
"corpora": "allDrives",
|
||||
"includeItemsFromAllDrives": "true",
|
||||
"supportsAllDrives": "true",
|
||||
}
|
||||
if page_token:
|
||||
params["pageToken"] = page_token
|
||||
req = urllib.request.Request(
|
||||
"https://www.googleapis.com/drive/v3/files?" + urllib.parse.urlencode(params),
|
||||
headers={"Authorization": f"Bearer {gtoken}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
j = json.loads(r.read().decode())
|
||||
for f in j.get("files") or []:
|
||||
folders.append({"id": f["id"], "name": f["name"]})
|
||||
page_token = j.get("nextPageToken")
|
||||
if not page_token:
|
||||
break
|
||||
print(f"Drive folders: {len(folders)}")
|
||||
matches = best_matches(tenants, folders, threshold=55)
|
||||
for tid, (acc, sc) in matches.items():
|
||||
if sb_upsert_integ(
|
||||
tid,
|
||||
"google_drive",
|
||||
{
|
||||
"status": "connected",
|
||||
"external_account_id": acc["id"],
|
||||
"label": acc["name"],
|
||||
"last_sync_at": now,
|
||||
"error_message": None,
|
||||
"meta": {"folder_name": acc["name"], "match_score": sc, "source": "refresh_script"},
|
||||
},
|
||||
):
|
||||
linked_counts["google_drive"] += 1
|
||||
print(f" linked google_drive: {linked_counts['google_drive']}")
|
||||
|
||||
# ========== GA4 ==========
|
||||
print("\n=== GA4 properties ===")
|
||||
ga4 = []
|
||||
req = urllib.request.Request(
|
||||
"https://analyticsadmin.googleapis.com/v1beta/accountSummaries",
|
||||
headers={"Authorization": f"Bearer {gtoken}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
summaries = json.loads(r.read().decode())
|
||||
for acc in summaries.get("accountSummaries") or []:
|
||||
for p in acc.get("propertySummaries") or []:
|
||||
prop = p.get("property")
|
||||
if not prop:
|
||||
continue
|
||||
# measurement id
|
||||
mid = None
|
||||
try:
|
||||
req2 = urllib.request.Request(
|
||||
f"https://analyticsadmin.googleapis.com/v1beta/{prop}/dataStreams",
|
||||
headers={"Authorization": f"Bearer {gtoken}"},
|
||||
)
|
||||
with urllib.request.urlopen(req2, timeout=60) as r2:
|
||||
streams = json.loads(r2.read().decode())
|
||||
for s in streams.get("dataStreams") or []:
|
||||
mid = (s.get("webStreamData") or {}).get("measurementId")
|
||||
if mid:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
ga4.append(
|
||||
{
|
||||
"id": mid or prop,
|
||||
"name": p.get("displayName") or prop,
|
||||
"property_id": prop,
|
||||
"measurement_id": mid,
|
||||
}
|
||||
)
|
||||
print(f"GA4 properties: {len(ga4)}")
|
||||
matches = best_matches(tenants, ga4, threshold=50)
|
||||
for tid, (acc, sc) in matches.items():
|
||||
if sb_upsert_integ(
|
||||
tid,
|
||||
"ga4",
|
||||
{
|
||||
"status": "connected",
|
||||
"external_account_id": acc["id"],
|
||||
"label": acc["name"],
|
||||
"last_sync_at": now,
|
||||
"error_message": None,
|
||||
"meta": {
|
||||
"property_id": acc.get("property_id"),
|
||||
"measurement_id": acc.get("measurement_id"),
|
||||
"display_name": acc["name"],
|
||||
"match_score": sc,
|
||||
"source": "refresh_script",
|
||||
},
|
||||
},
|
||||
):
|
||||
linked_counts["ga4"] += 1
|
||||
print(f" linked ga4: {linked_counts['ga4']}")
|
||||
|
||||
# ========== GSC ==========
|
||||
print("\n=== Search Console ===")
|
||||
req = urllib.request.Request(
|
||||
"https://www.googleapis.com/webmasters/v3/sites",
|
||||
headers={"Authorization": f"Bearer {gtoken}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
sites_j = json.loads(r.read().decode())
|
||||
gsc = []
|
||||
for s in sites_j.get("siteEntry") or []:
|
||||
url = s.get("siteUrl")
|
||||
if url:
|
||||
gsc.append({"id": url, "name": url})
|
||||
print(f"GSC sites: {len(gsc)}")
|
||||
matches = best_matches(tenants, gsc, threshold=40)
|
||||
for tid, (acc, sc) in matches.items():
|
||||
if sb_upsert_integ(
|
||||
tid,
|
||||
"search_console",
|
||||
{
|
||||
"status": "connected",
|
||||
"external_account_id": acc["id"],
|
||||
"label": acc["id"],
|
||||
"last_sync_at": now,
|
||||
"error_message": None,
|
||||
"meta": {"site_url": acc["id"], "match_score": sc, "source": "refresh_script"},
|
||||
},
|
||||
):
|
||||
linked_counts["gsc"] += 1
|
||||
print(f" linked gsc: {linked_counts['gsc']}")
|
||||
|
||||
# ========== Google Ads ==========
|
||||
print("\n=== Google Ads ===")
|
||||
try:
|
||||
at = gads_access()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {at}",
|
||||
"developer-token": ENV["GOOGLE_ADS_DEVELOPER_TOKEN"],
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
login = (ENV.get("GOOGLE_ADS_LOGIN_CUSTOMER_ID") or "").replace("-", "")
|
||||
if login:
|
||||
headers["login-customer-id"] = login
|
||||
req = urllib.request.Request(
|
||||
f"{GADS}/customers:listAccessibleCustomers", headers=headers
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=90) as r:
|
||||
listed = json.loads(r.read().decode())
|
||||
ids = [rn.replace("customers/", "") for rn in listed.get("resourceNames") or []]
|
||||
gads_accounts = []
|
||||
for cid in ids:
|
||||
h = dict(headers)
|
||||
h["login-customer-id"] = cid
|
||||
body = json.dumps(
|
||||
{
|
||||
"query": "SELECT customer.id, customer.descriptive_name, customer.manager FROM customer LIMIT 1"
|
||||
}
|
||||
).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{GADS}/customers/{cid}/googleAds:search",
|
||||
data=body,
|
||||
headers=h,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
j = json.loads(r.read().decode())
|
||||
except Exception:
|
||||
continue
|
||||
c = ((j.get("results") or [{}])[0]).get("customer") or {}
|
||||
if not c:
|
||||
continue
|
||||
name = c.get("descriptiveName") or cid
|
||||
if c.get("manager"):
|
||||
# pull clients
|
||||
body2 = json.dumps(
|
||||
{
|
||||
"query": (
|
||||
"SELECT customer_client.id, customer_client.descriptive_name, "
|
||||
"customer_client.manager, customer_client.status FROM customer_client "
|
||||
"WHERE customer_client.status = 'ENABLED' AND customer_client.manager = FALSE"
|
||||
)
|
||||
}
|
||||
).encode()
|
||||
req2 = urllib.request.Request(
|
||||
f"{GADS}/customers/{cid}/googleAds:search",
|
||||
data=body2,
|
||||
headers=h,
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req2, timeout=90) as r2:
|
||||
j2 = json.loads(r2.read().decode())
|
||||
for row in j2.get("results") or []:
|
||||
cc = row.get("customerClient") or {}
|
||||
if cc.get("id"):
|
||||
gads_accounts.append(
|
||||
{
|
||||
"id": str(cc["id"]),
|
||||
"name": cc.get("descriptiveName") or str(cc["id"]),
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
gads_accounts.append({"id": str(c.get("id") or cid), "name": name})
|
||||
# dedupe
|
||||
mmap = {a["id"]: a for a in gads_accounts}
|
||||
gads_accounts = list(mmap.values())
|
||||
print(f"Google Ads accounts: {len(gads_accounts)}")
|
||||
matches = best_matches(tenants, gads_accounts, threshold=55)
|
||||
for tid, (acc, sc) in matches.items():
|
||||
if sb_upsert_integ(
|
||||
tid,
|
||||
"google_ads",
|
||||
{
|
||||
"status": "connected",
|
||||
"external_account_id": acc["id"],
|
||||
"label": acc["name"],
|
||||
"last_sync_at": now,
|
||||
"error_message": None,
|
||||
"meta": {
|
||||
"account_name": acc["name"],
|
||||
"customer_id": acc["id"],
|
||||
"match_score": sc,
|
||||
"source": "refresh_script",
|
||||
},
|
||||
},
|
||||
):
|
||||
linked_counts["google_ads"] += 1
|
||||
print(f" linked google_ads: {linked_counts['google_ads']}")
|
||||
except Exception as e:
|
||||
print("Google Ads ERROR:", e)
|
||||
|
||||
# ========== AUDIT ==========
|
||||
print("\n=== AUDIT ===")
|
||||
integs = sb_get(
|
||||
"/rest/v1/integrations?provider=in.(ga4,search_console,google_drive,google_ads,meta_ads,facebook,instagram)"
|
||||
"&select=tenant_id,provider,status,external_account_id,error_message"
|
||||
)
|
||||
by = defaultdict(dict)
|
||||
for i in integs:
|
||||
by[i["tenant_id"]][i["provider"]] = i
|
||||
|
||||
def flag(i):
|
||||
if not i:
|
||||
return "—"
|
||||
if i.get("status") == "connected" and i.get("external_account_id"):
|
||||
return "OK"
|
||||
if i.get("status") == "error":
|
||||
return "ERR"
|
||||
return "—"
|
||||
|
||||
print(
|
||||
f"{'Cliente':30} {'GAds':5} {'Meta':5} {'GA4':5} {'GSC':5} {'Drive':5} {'FB':5} {'IG':5} Org"
|
||||
)
|
||||
for t in tenants:
|
||||
m = by[t["id"]]
|
||||
# organic count
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/rest/v1/organic_daily?tenant_id=eq.{t['id']}&select=day",
|
||||
headers={**SH, "Prefer": "count=exact", "Range": "0-0"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
cr = r.headers.get("content-range") or "*/0"
|
||||
org = cr.split("/")[-1]
|
||||
print(
|
||||
f"{t['name'][:30]:30} "
|
||||
f"{flag(m.get('google_ads')):5} "
|
||||
f"{flag(m.get('meta_ads')):5} "
|
||||
f"{flag(m.get('ga4')):5} "
|
||||
f"{flag(m.get('search_console')):5} "
|
||||
f"{flag(m.get('google_drive')):5} "
|
||||
f"{flag(m.get('facebook')):5} "
|
||||
f"{flag(m.get('instagram')):5} "
|
||||
f"{org}"
|
||||
)
|
||||
|
||||
print("\n=== Linked counts this run ===")
|
||||
print(dict(linked_counts))
|
||||
print("\nDone. Orgánico days still need app sync (Emparejar todo Google / abrir Orgánico).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user