Files
bggpipe/scripts/write_stub_fixtures.py
Eric Wagoner 74fc4fe847 RPGs become local library citizens: identified, enriched, never uploaded
When the board-game search (and truncation heads) runs dry, resolve
falls back to type=rpgitem — the geekdo database is shared, so the same
API, token, cache, and classification machinery apply. Matched rpgitems
flow through review and enrich normally but diff routes them to a
local_only bucket, structurally outside to_add/to_update: their
collections live on RPGGeek, beyond this pipeline's write scope. The
library page gains an All/Board games/RPGs filter and an "RPG · local
only" badge; the catalog tags them too. Fixture generators write blanket
empty rpgitem stubs for every known query (the fallback fires for every
unmatched title) with real synthetic entries for Alice Is Missing.

Data: both Alice rows re-resolved from unmatched to auto rpgitem
matches. First diff since the audit reworks also lands their real-data
consequences: Dungeon! gains its TSR edition update on a versionless
copy the old claim ordering missed, to_add rows carry unioned reshoot
provenance, and the Herbaceous typo row's survivor is now the
correctly-spelled title.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 19:01:09 -04:00

122 lines
4.8 KiB
Python

"""Write hand-crafted stub XML fixtures into tests/fixtures/bgg_cache/.
BGG's XML API now requires a registered application token, which we don't
have yet, so these fixtures are realistic approximations of real responses
(shapes verified against the API docs) rather than recordings. Once a
BGG_API_TOKEN exists: delete tests/fixtures/bgg_cache/ and run
scripts/record_fixtures.py — it writes real recordings under the SAME
filenames, and the test suite must still pass.
Usage: uv run python scripts/write_stub_fixtures.py
"""
from __future__ import annotations
from fixture_common import FIXTURE_CACHE, esc, write_cache_marker
from bggpipe.bgg_client import SEARCH_TYPES, cache_key
def search_item(bgg_id: int, name: str, year: int | None, type_: str) -> str:
year_xml = f'<yearpublished value="{year}"/>' if year else ""
return (
f'<item type="{type_}" id="{bgg_id}">'
f'<name type="primary" value="{esc(name)}"/>{year_xml}</item>'
)
WINGSPAN_FAMILY = (
search_item(266192, "Wingspan", 2019, "boardgame")
+ search_item(290448, "Wingspan: European Expansion", 2019, "boardgameexpansion")
+ search_item(300580, "Wingspan: Oceania Expansion", 2020, "boardgameexpansion")
+ search_item(366161, "Wingspan Asia", 2022, "boardgame")
)
SEARCHES = {
"Catan": (
search_item(13, "CATAN", 1995, "boardgame")
+ search_item(926, "CATAN: Seafarers", 1997, "boardgameexpansion")
+ search_item(27760, "Catan Dice Game", 2007, "boardgame")
+ search_item(133038, "CATAN 3D Collector's Edition", 2005, "boardgame")
),
"Wingspan": WINGSPAN_FAMILY,
"Wingspan: European Expansion": WINGSPAN_FAMILY,
"Wingspan Europe": WINGSPAN_FAMILY,
"Café International": (
search_item(373, "Café International", 1989, "boardgame")
+ search_item(8103, "Café International: Das Kartenspiel", 2001, "boardgame")
),
"Citadels": (
search_item(478, "Citadels", 2000, "boardgame")
+ search_item(205398, "Citadels", 2016, "boardgame")
),
"Blorvath: Quest of the Zzyzx": "",
"Blorvath": "", # truncation-retry head of the nonsense title
}
THINGS = {
# Citadels tie-break: neither edition dominates 10x -> stays ambiguous.
("478,205398", "stats"): """<items>
<item type="boardgame" id="478">
<name type="primary" value="Citadels"/><yearpublished value="2000"/>
<statistics><ratings><owned value="85000"/>
<ranks><rank type="subtype" id="1" name="boardgame" value="250"/></ranks>
</ratings></statistics>
</item>
<item type="boardgame" id="205398">
<name type="primary" value="Citadels"/><yearpublished value="2016"/>
<statistics><ratings><owned value="24000"/>
<ranks><rank type="subtype" id="1" name="boardgame" value="400"/></ranks>
</ratings></statistics>
</item>
</items>""",
# Wingspan versions: 2019 Stonemaier English must beat the 2020 printing.
("266192", "versions"): """<items>
<item type="boardgame" id="266192">
<name type="primary" value="Wingspan"/><yearpublished value="2019"/>
<versions>
<item type="boardgameversion" id="465063">
<name type="primary" value="English first edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
<link type="language" id="2184" value="English"/>
</item>
<item type="boardgameversion" id="521212">
<name type="primary" value="English fourth printing"/>
<yearpublished value="2020"/>
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
<link type="language" id="2184" value="English"/>
</item>
<item type="boardgameversion" id="472430">
<name type="primary" value="German edition"/>
<yearpublished value="2019"/>
<link type="boardgamepublisher" id="22160" value="Feuerland Spiele"/>
<link type="language" id="2188" value="German"/>
</item>
</versions>
</item>
</items>""",
}
def main() -> None:
FIXTURE_CACHE.mkdir(parents=True, exist_ok=True)
write_cache_marker(FIXTURE_CACHE)
for query, items in SEARCHES.items():
key = cache_key("search", {"query": query, "type": SEARCH_TYPES})
total = items.count("<item ")
(FIXTURE_CACHE / key).write_text(f'<items total="{total}">{items}</items>')
# resolve falls back to an rpgitem search whenever the board-game
# search runs dry: every known query needs an (empty) answer there
rpg_key = cache_key("search", {"query": query, "type": "rpgitem"})
(FIXTURE_CACHE / rpg_key).write_text('<items total="0"></items>')
for (ids, flavor), xml in THINGS.items():
key = cache_key("thing", {"id": ids, flavor: "1"})
(FIXTURE_CACHE / key).write_text(xml)
for f in sorted(FIXTURE_CACHE.iterdir()):
print(f" {f.name}")
if __name__ == "__main__":
main()