From 2b6349e84eaa48a2fef46e4af1b7f6fe4ba8eddd Mon Sep 17 00:00:00 2001 From: Omar Musayev Date: Tue, 19 May 2026 20:37:01 +0400 Subject: [PATCH] fetcher: retry transient Codeforces API failures; fix README OAuth advice cf_get() now retries transient failures - read timeouts, connection errors, and non-OK Codeforces responses - for 4 attempts with 3s/8s/20s backoff. Previously a single Codeforces API blip failed the whole scheduled run; one such read-timeout did exactly that today. README: the OAuth setup step said the consent screen "can stay in Testing status". That makes Google expire the refresh token after 7 days, which silently breaks send_outbox.py. It now says to publish the app. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- fetch_data.py | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9ea31d0..73fb54c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The agent can't send mail directly — its Gmail connector only exposes `create_ ### 1. Google Cloud credentials - In Google Cloud Console, create a project and enable both the **Google Calendar API** and the **Gmail API** -- Configure the **OAuth consent screen**: External user type, app name of your choice, scopes `.../auth/calendar` and `.../auth/gmail.modify`, add yourself as a Test user (the app can stay in "Testing" status) +- Configure the **OAuth consent screen**: External user type, app name of your choice, scopes `.../auth/calendar` and `.../auth/gmail.modify`, add yourself as a Test user, then **click "Publish app"** so the status is "In production" — an app left in "Testing" makes Google expire the refresh token after 7 days, which silently breaks `send_outbox.py` - Create an **OAuth client ID** of type **Desktop app** and download the JSON as `client_secret.json` ### 2. Mint a refresh token locally diff --git a/fetch_data.py b/fetch_data.py index 8997dcc..cb0a601 100644 --- a/fetch_data.py +++ b/fetch_data.py @@ -15,6 +15,7 @@ data/leetcode_daily.json — LeetCode's daily challenge (fresh each day) """ import json +import time from datetime import datetime, timedelta, timezone from pathlib import Path @@ -23,16 +24,33 @@ HANDLE = "Omar_Musayev" DATA_DIR = Path(__file__).parent / "data" TIMEOUT = 60 +RETRIES = 4 +RETRY_BACKOFF = [3, 8, 20] # seconds to wait before retry attempts 2, 3, 4 NOW = datetime.now(timezone.utc) def cf_get(url): - r = requests.get(url, timeout=TIMEOUT) - r.raise_for_status() - data = r.json() - if data.get("status") != "OK": - raise RuntimeError(f"{url}: {data.get('status')}: {data.get('comment')}") - return data["result"] + """GET a Codeforces API endpoint, retrying transient network/API failures. + + The Codeforces API intermittently times out or returns a non-OK status; + a single blip should not fail the whole run, so retry with backoff. + """ + last_err = None + for attempt in range(RETRIES): + try: + r = requests.get(url, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + if data.get("status") != "OK": + raise RuntimeError(f"{url}: {data.get('status')}: {data.get('comment')}") + return data["result"] + except (requests.exceptions.RequestException, RuntimeError) as e: + last_err = e + if attempt + 1 < RETRIES: + delay = RETRY_BACKOFF[attempt] + print(f"cf_get failed (attempt {attempt + 1}/{RETRIES}): {e} — retrying in {delay}s") + time.sleep(delay) + raise RuntimeError(f"cf_get gave up after {RETRIES} attempts: {url}: {last_err}") def write_json(path: Path, payload: dict):