SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%

Couverture Apify forcée : mode APIFY_FORCE=1 (ignore fraîcheur + caps, délai élargi), tampon des miss définitifs (handle mort/mur de login → rotation hebdo au lieu d'un re-scan quotidien en pure perte), concurrence X réduite à 4 + retries 429 avec backoff et rotation d'IP dans ka-x (0.1.7)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 21, 2026) parent 8fa08f4

2 changed files +38 −7

modified actors/ka-x/src/main.py +9 −3
@@ -112,10 +112,16 @@ async def main() -> None:
112 112 try:
113 113 resp = await fetcher.get(SYNDICATION_URL.format(u=u),
114 114 session_id=u, impersonate=None)
115 − if resp.status_code == 429:
115 + # 429 : retries avec backoff + NOUVELLE session proxy
116 + # (nouvelle IP résidentielle) — une seule relance ne
117 + # suffisait pas (13/40 handles en 429, constat 2026-08-21)
118 + for i, imp in enumerate(("chrome", None, "chrome")):
119 + if resp.status_code != 429:
120 + break
121 + await asyncio.sleep(5 * (i + 1))
116 122 resp = await fetcher.get(SYNDICATION_URL.format(u=u),
117 − session_id=f"{u}b",
118 − impersonate="chrome")
123 + session_id=f"{u}r{i}",
124 + impersonate=imp)
119 125 except Exception as exc:
120 126 await Actor.push_data({**miss, "error": str(exc)[:200]})
121 127 return
modified creaka/connectors/apify_social.py +29 −4
@@ -44,6 +44,14 @@ APIFY_API = "https://api.apify.com/v2"
44 44 ACTOR_OWNER = "gorgeous_thistle"
45 45 RESIDENTIAL = {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]}
46 46
47 +# erreurs DÉFINITIVES des acteurs (handle mort, profil privé, mur de login…) :
48 +# on tamponne le compte quand même pour qu'il ne soit re-sondé qu'à la rotation
49 +# `revisit_days`, au lieu d'être re-payé chaque jour en pure perte. Les erreurs
50 +# transitoires (429, 5xx, shell, proxy) ne sont PAS tamponnées → re-tentées.
51 +MISS_PREFIXES = ("user_not_found", "private", "not_found", "no_profile",
52 + "no_channel", "login_wall", "invite_invalide", "http_404",
53 + "wall_")
54 +
47 55 # --- parallélisme : on dispose de 256 Go de RAM Apify → au lieu d'UN run à
48 56 # 1 Go, on éclate le lot en tranches lancées EN PARALLÈLE, chacune avec plus de
49 57 # mémoire (donc plus de CPU/vCPU côté Apify) et une concurrence interne accrue.
@@ -57,6 +65,9 @@ RUN_CONCURRENCY = int(os.environ.get("APIFY_RUN_CONCURRENCY", "10")) # interne
57 65 # rafraîchissement : un compte est re-sondé s'il n'a pas été enrichi depuis
58 66 # REVISIT_DAYS jours. Le watch quotidien couvre ainsi TOUT le bassin en ≤ N j.
59 67 REVISIT_DAYS = int(os.environ.get("APIFY_REVISIT_DAYS", "7"))
68 +# passage FORCÉ (APIFY_FORCE=1) : ignore fraîcheur ET caps — couvre TOUT le
69 +# bassin de chaque plateforme en un seul passage (rattrapage de couverture)
70 +FORCE_PASS = os.environ.get("APIFY_FORCE", "") == "1"
60 71
61 72
62 73 def _token() -> str:
@@ -178,6 +189,8 @@ class _ApifySocialEnrich(BaseConnector):
178 189 return f"{self.source_id}_at"
179 190
180 191 def _stale(self, acc) -> bool:
192 + if FORCE_PASS:
193 + return True
181 194 stamp = (acc.metrics or {}).get(self._stamp_key)
182 195 if not stamp:
183 196 return True
@@ -209,7 +222,7 @@ class _ApifySocialEnrich(BaseConnector):
209 222 if acc is None or not self._stale(acc):
210 223 continue
211 224 targets.setdefault(self.target_of(acc), []).append((cr, acc))
212 − if len(targets) >= self.cap:
225 + if not FORCE_PASS and len(targets) >= self.cap:
213 226 break
214 227 if not targets:
215 228 return []
@@ -218,7 +231,10 @@ class _ApifySocialEnrich(BaseConnector):
218 231 {"proxyConfiguration": RESIDENTIAL,
219 232 "concurrency": RUN_CONCURRENCY,
220 233 **self.extra_input()},
221 − sorted(targets))
234 + sorted(targets),
235 + # passage forcé : tranches plus grosses (bassin entier / 32 runs)
236 + # → délai par run élargi en proportion
237 + timeout_s=7200 if FORCE_PASS else 2400)
222 238 by_handle = {str(it.get("username") or "").lower(): it
223 239 for it in items if it.get("kind") == "profile"}
224 240 enriched: list[Creator] = []
@@ -228,8 +244,14 @@ class _ApifySocialEnrich(BaseConnector):
228 244 if it is None:
229 245 continue
230 246 if not it.get("found"):
231 − if str(it.get("error", "")).startswith(("http_5", "shell")):
247 + err = str(it.get("error", ""))
248 + if err.startswith(("http_5", "shell")):
232 249 self.errors += 1
250 + elif err.startswith(MISS_PREFIXES):
251 + for cr, acc in pairs: # miss définitif → rotation douce
252 + acc.metrics[self._stamp_key] = now_iso()
253 + acc.metrics[f"{self.source_id}_miss"] = err
254 + enriched.append(cr)
233 255 continue
234 256 for cr, acc in pairs:
235 257 self.apply(cr, acc, it)
@@ -252,6 +274,7 @@ class _ApifySocialEnrich(BaseConnector):
252 274 if it.get(img_key):
253 275 metrics[img_key] = it[img_key]
254 276 metrics[self._stamp_key] = now_iso() # tampon d'enrichissement propre
277 + acc.metrics.pop(f"{self.source_id}_miss", None) # ressuscité
255 278 acc.metrics.update(metrics)
256 279 # avatar/bannière de la FICHE : rafraîchis à chaque passage depuis la
257 280 # plateforme principale (les URLs CDN signées expirent — ex. Instagram) ;
@@ -362,7 +385,9 @@ class ApifyX(_ApifySocialEnrich):
362 385 "top_hashtags": "top_hashtags", "top_tweet": "top_tweet"}
363 386
364 387 def extra_input(self) -> dict:
365 − return {"maxTweets": 20}
388 + # concurrence réduite : l'endpoint syndication de X rend des 429 en
389 + # rafale au-delà de ~4 requêtes simultanées par run (constat 2026-08-21)
390 + return {"maxTweets": 20, "concurrency": 4}
366 391
367 392 def bio_urls(self, it: dict) -> list[str]:
368 393 return [it.get("website") or ""]
369 394