74fc4fe847
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>
122 lines
4.8 KiB
Python
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()
|