BGG API client: disk cache, rate limiting, 202-queue retry, XML parsing
httpx client with injectable clock/sleep/rng for testability. Successful responses cached under data/bgg_cache/ keyed by endpoint+params; 202 retries follow the spec schedule (2/5/10/30s, give up after 5); 429/503 get jittered exponential backoff; consecutive requests are spaced rate_limit_seconds apart. Parsers (via defusedxml, per security hook) cover search, thing (+stats/+versions), and collection, including the Not Ranked and error-document cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import pytest
|
||||
|
||||
from bggpipe.models import (
|
||||
BGGResponseError,
|
||||
parse_collection,
|
||||
parse_search,
|
||||
parse_things,
|
||||
)
|
||||
|
||||
SEARCH_XML = """<items total="2">
|
||||
<item type="boardgame" id="13">
|
||||
<name type="primary" value="CATAN"/><yearpublished value="1995"/>
|
||||
</item>
|
||||
<item type="boardgameexpansion" id="290448">
|
||||
<name type="alternate" value="Wingspan: Europa"/>
|
||||
</item>
|
||||
</items>"""
|
||||
|
||||
THING_XML = """<items>
|
||||
<item type="boardgame" id="266192">
|
||||
<name type="primary" sortindex="1" value="Wingspan"/>
|
||||
<name type="alternate" sortindex="1" value="Flügelschlag"/>
|
||||
<yearpublished value="2019"/>
|
||||
<statistics page="1"><ratings>
|
||||
<owned value="123456"/>
|
||||
<ranks>
|
||||
<rank type="subtype" id="1" name="boardgame" value="30"/>
|
||||
<rank type="family" id="5497" name="strategygames" value="25"/>
|
||||
</ranks>
|
||||
</ratings></statistics>
|
||||
<versions>
|
||||
<item type="boardgameversion" id="465063">
|
||||
<name type="primary" value="English edition"/>
|
||||
<yearpublished value="2019"/>
|
||||
<link type="boardgamepublisher" id="23202" value="Stonemaier Games"/>
|
||||
<link type="language" id="2184" value="English"/>
|
||||
</item>
|
||||
<item type="boardgameversion" id="465064">
|
||||
<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>"""
|
||||
|
||||
COLLECTION_XML = """<items totalitems="2">
|
||||
<item objecttype="thing" objectid="13" subtype="boardgame" collid="101">
|
||||
<name sortindex="1">Catan</name>
|
||||
<yearpublished>1995</yearpublished>
|
||||
<status own="1" wanttoplay="0"/>
|
||||
</item>
|
||||
<item objecttype="thing" objectid="266192" subtype="boardgame" collid="102">
|
||||
<name sortindex="1">Wingspan</name>
|
||||
<status own="0" wishlist="1"/>
|
||||
<version><item type="boardgameversion" id="465063"/></version>
|
||||
</item>
|
||||
</items>"""
|
||||
|
||||
NOT_RANKED_XML = """<items>
|
||||
<item type="boardgame" id="99999">
|
||||
<name type="primary" value="Obscurity"/>
|
||||
<statistics><ratings>
|
||||
<owned value="12"/>
|
||||
<ranks><rank type="subtype" id="1" name="boardgame" value="Not Ranked"/></ranks>
|
||||
</ratings></statistics>
|
||||
</item>
|
||||
</items>"""
|
||||
|
||||
ERROR_XML = (
|
||||
"<errors><error><message>Invalid username specified</message></error></errors>"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_search():
|
||||
results = parse_search(SEARCH_XML)
|
||||
assert [r.bgg_id for r in results] == [13, 290448]
|
||||
assert results[0].name == "CATAN"
|
||||
assert results[0].year == 1995
|
||||
assert results[1].type == "boardgameexpansion"
|
||||
assert results[1].name_type == "alternate"
|
||||
assert results[1].year is None
|
||||
|
||||
|
||||
def test_parse_things_with_stats_and_versions():
|
||||
(thing,) = parse_things(THING_XML)
|
||||
assert thing.name == "Wingspan" # primary, not alternate
|
||||
assert thing.owned == 123456
|
||||
assert thing.rank == 30 # boardgame rank, not the family rank
|
||||
assert len(thing.versions) == 2
|
||||
english = thing.versions[0]
|
||||
assert english.version_id == 465063
|
||||
assert english.publishers == ("Stonemaier Games",)
|
||||
assert english.languages == ("English",)
|
||||
|
||||
|
||||
def test_parse_things_not_ranked_is_none():
|
||||
(thing,) = parse_things(NOT_RANKED_XML)
|
||||
assert thing.rank is None
|
||||
assert thing.owned == 12
|
||||
|
||||
|
||||
def test_parse_collection():
|
||||
catan, wingspan = parse_collection(COLLECTION_XML)
|
||||
assert (catan.object_id, catan.coll_id, catan.own) == (13, 101, True)
|
||||
assert catan.version_id is None
|
||||
assert wingspan.own is False # wishlist item must not count as owned
|
||||
assert wingspan.version_id == 465063
|
||||
|
||||
|
||||
def test_error_document_raises():
|
||||
with pytest.raises(BGGResponseError, match="Invalid username"):
|
||||
parse_collection(ERROR_XML)
|
||||
Reference in New Issue
Block a user