From 36b228fa123945a277a9631989a5094559b9cbb3 Mon Sep 17 00:00:00 2001 From: Vidar Akselsen Date: Thu, 4 Jun 2026 07:56:24 +0200 Subject: [PATCH] fix: resolve HTTP 429 rate limit issues on Hardcover and Anthropic APIs --- colophon/anthropic.py | 22 ++++++++++++++++----- colophon/hardcover.py | 46 +++++++++++++++++++++++++++---------------- colophon/resolver.py | 4 +++- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/colophon/anthropic.py b/colophon/anthropic.py index 772f149..22ba13d 100644 --- a/colophon/anthropic.py +++ b/colophon/anthropic.py @@ -9,6 +9,8 @@ import json import os import subprocess +import time +import urllib.error import urllib.request MODEL = "claude-haiku-4-5-20251001" @@ -37,11 +39,21 @@ def _via_api(system, user, schema, key): API, data=json.dumps(body).encode(), headers={"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"}, ) - resp = json.load(urllib.request.urlopen(req, timeout=120)) - for block in resp.get("content", []): - if block.get("type") == "tool_use": - return block["input"] - raise RuntimeError("no tool_use block in Haiku response") + for attempt in range(5): + try: + with urllib.request.urlopen(req, timeout=120) as response: + resp = json.load(response) + for block in resp.get("content", []): + if block.get("type") == "tool_use": + return block["input"] + raise RuntimeError("no tool_use block in Haiku response") + except urllib.error.HTTPError as e: + if e.code == 429 and attempt < 4: + retry_after = e.headers.get("Retry-After") + sleep_time = int(retry_after) if (retry_after and retry_after.isdigit()) else (2 ** attempt * 2) + time.sleep(sleep_time) + continue + raise def _via_cli(system, user, schema): diff --git a/colophon/hardcover.py b/colophon/hardcover.py index 1fae77d..eedf5a5 100644 --- a/colophon/hardcover.py +++ b/colophon/hardcover.py @@ -9,6 +9,8 @@ import json import os import subprocess +import time +import urllib.error import urllib.request # Optional command that takes a GraphQL query on stdin and returns the JSON response, @@ -31,23 +33,33 @@ def _api_key(): def query(graphql): key = _api_key() - if key: - req = urllib.request.Request( - HC_ENDPOINT, data=json.dumps({"query": graphql}).encode(), - headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, - ) - data = json.load(urllib.request.urlopen(req, timeout=60)) - elif QUERY_SH: # delegate so the key never enters this process - r = subprocess.run([QUERY_SH], input=graphql, capture_output=True, text=True, timeout=60) - if r.returncode != 0: - raise HardcoverError(f"query command failed: {r.stderr.strip()[:200]}") - data = json.loads(r.stdout) - else: - raise HardcoverError("no Hardcover credentials — set COLOPHON_HARDCOVER_KEY " - "(or COLOPHON_HARDCOVER_QUERY_CMD)") - if data.get("errors"): - raise HardcoverError(f"hardcover errors: {data['errors']}") - return data.get("data") or {} + for attempt in range(5): + try: + if key: + req = urllib.request.Request( + HC_ENDPOINT, data=json.dumps({"query": graphql}).encode(), + headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req, timeout=60) as response: + data = json.load(response) + elif QUERY_SH: # delegate so the key never enters this process + r = subprocess.run([QUERY_SH], input=graphql, capture_output=True, text=True, timeout=60) + if r.returncode != 0: + raise HardcoverError(f"query command failed: {r.stderr.strip()[:200]}") + data = json.loads(r.stdout) + else: + raise HardcoverError("no Hardcover credentials — set COLOPHON_HARDCOVER_KEY " + "(or COLOPHON_HARDCOVER_QUERY_CMD)") + if data.get("errors"): + raise HardcoverError(f"hardcover errors: {data['errors']}") + return data.get("data") or {} + except urllib.error.HTTPError as e: + if e.code == 429 and attempt < 4: + retry_after = e.headers.get("Retry-After") + sleep_time = int(retry_after) if (retry_after and retry_after.isdigit()) else (2 ** attempt * 2) + time.sleep(sleep_time) + continue + raise def book_by_id(hcid): diff --git a/colophon/resolver.py b/colophon/resolver.py index 7736c1c..0743e04 100644 --- a/colophon/resolver.py +++ b/colophon/resolver.py @@ -90,7 +90,9 @@ def run_resolve(limit=None, book_ids=None, apply=False, min_conf=AUTO_CONF, g=No books = [b for b, _ in audit.run_audit(limit=limit)["categories"].get("review-misseed", [])] run_id = (store.new_run_id() + "-resolve") if (apply and store) else None proposals, errors = [], 0 - for b in books: + for i, b in enumerate(books): + if i > 0: + time.sleep(1.0) p = resolve(b) if (apply and p["action"] == "propose" and (p.get("confidence") or 0) >= min_conf and p.get("isbn")):