Resolve degrades gracefully when BGG_API_TOKEN is missing
A 401 on an uncached title no longer aborts the run: cached titles resolve and save, blocked titles are listed with registration/token instructions and left out of matches.csv so a future run picks them up untouched. Supports the take-photos-now, resolve-later workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+17
-2
@@ -18,7 +18,7 @@ from pathlib import Path
|
|||||||
import typer
|
import typer
|
||||||
from rapidfuzz import fuzz
|
from rapidfuzz import fuzz
|
||||||
|
|
||||||
from bggpipe.bgg_client import BGGClient
|
from bggpipe.bgg_client import BGGAuthError, BGGClient
|
||||||
from bggpipe.config import Config
|
from bggpipe.config import Config
|
||||||
from bggpipe.models import GameVersion
|
from bggpipe.models import GameVersion
|
||||||
from bggpipe.normalize import normalize_title
|
from bggpipe.normalize import normalize_title
|
||||||
@@ -417,12 +417,20 @@ def run_resolve(
|
|||||||
|
|
||||||
new_rows: list[MatchRow] = []
|
new_rows: list[MatchRow] = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
|
blocked: list[str] = []
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
key = _row_key(entry.title_raw, ";".join(entry.source_photos))
|
key = _row_key(entry.title_raw, ";".join(entry.source_photos))
|
||||||
if key in existing:
|
if key in existing:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
row = resolve_entry(client, entry)
|
try:
|
||||||
|
row = resolve_entry(client, entry)
|
||||||
|
except BGGAuthError:
|
||||||
|
# No API token yet: cached titles still resolve; the rest wait.
|
||||||
|
# No row is written, so a future run picks them up untouched.
|
||||||
|
blocked.append(entry.title_raw)
|
||||||
|
typer.echo(f" {entry.title_raw!r} -> waiting on BGG API token")
|
||||||
|
continue
|
||||||
new_rows.append(row)
|
new_rows.append(row)
|
||||||
detail = f"{row.bgg_name} ({row.bgg_id})" if row.bgg_id else "-"
|
detail = f"{row.bgg_name} ({row.bgg_id})" if row.bgg_id else "-"
|
||||||
version = f" [{row.version_status}]" if row.version_status else ""
|
version = f" [{row.version_status}]" if row.version_status else ""
|
||||||
@@ -438,4 +446,11 @@ def run_resolve(
|
|||||||
f"Resolved {len(new_rows)} title(s) ({summary or 'nothing new'}); "
|
f"Resolved {len(new_rows)} title(s) ({summary or 'nothing new'}); "
|
||||||
f"skipped {skipped} already in {cfg.matches_path}."
|
f"skipped {skipped} already in {cfg.matches_path}."
|
||||||
)
|
)
|
||||||
|
if blocked:
|
||||||
|
typer.echo(
|
||||||
|
f"\n{len(blocked)} title(s) are waiting on the BGG API "
|
||||||
|
"(register at https://boardgamegeek.com/applications, then set "
|
||||||
|
"BGG_API_TOKEN and re-run resolve — everything above is saved): "
|
||||||
|
+ ", ".join(blocked)
|
||||||
|
)
|
||||||
return new_rows
|
return new_rows
|
||||||
|
|||||||
@@ -294,3 +294,52 @@ def test_wrong_year_hint_never_drives_a_version(client):
|
|||||||
assert (row.match_status, row.bgg_id) == ("auto", 2529)
|
assert (row.match_status, row.bgg_id) == ("auto", 2529)
|
||||||
assert row.version_status == "version_unknown"
|
assert row.version_status == "version_unknown"
|
||||||
assert row.version_id is None
|
assert row.version_id is None
|
||||||
|
|
||||||
|
|
||||||
|
# -- graceful degradation without a BGG token ---------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_resolve_saves_progress_when_token_missing(tmp_path):
|
||||||
|
"""Cached titles resolve; uncached ones wait for the token instead of
|
||||||
|
crashing the run and losing everything."""
|
||||||
|
import shutil as _shutil
|
||||||
|
|
||||||
|
partial_cache = tmp_path / "cache"
|
||||||
|
partial_cache.mkdir()
|
||||||
|
for f in FIXTURES.glob("search_query=Catan-*"):
|
||||||
|
_shutil.copy(f, partial_cache / f.name)
|
||||||
|
|
||||||
|
data_dir = tmp_path / "data"
|
||||||
|
data_dir.mkdir()
|
||||||
|
(data_dir / "titles.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
[
|
||||||
|
{"title_raw": "Catan", "source_photos": ["a.jpg"]},
|
||||||
|
{"title_raw": "Wingspan", "source_photos": ["a.jpg"]},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cfg = Config(data_dir=data_dir)
|
||||||
|
unauthorized = BGGClient(
|
||||||
|
cache_dir=partial_cache,
|
||||||
|
transport=httpx.MockTransport(
|
||||||
|
lambda req: httpx.Response(401, text="Unauthorized")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = run_resolve(cfg, client=unauthorized)
|
||||||
|
assert [r.title_raw for r in rows] == ["Catan"] # cached one made it
|
||||||
|
|
||||||
|
with cfg.matches_path.open(newline="") as f:
|
||||||
|
saved = list(csv.DictReader(f))
|
||||||
|
assert len(saved) == 1 # blocked title left for a future run
|
||||||
|
|
||||||
|
# future run (fixtures now "recorded"): picks up only the blocked title
|
||||||
|
full = BGGClient(
|
||||||
|
cache_dir=FIXTURES,
|
||||||
|
transport=httpx.MockTransport(
|
||||||
|
lambda req: (_ for _ in ()).throw(AssertionError("network"))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rows2 = run_resolve(cfg, client=full)
|
||||||
|
assert [r.title_raw for r in rows2] == ["Wingspan"]
|
||||||
|
|||||||
Reference in New Issue
Block a user