Skip to content

Commit 4451ca1

Browse files
mad4msclaude
andcommitted
revert: keep get_games() as list[dict] — Statistics Center spec too incomplete for typed models
The generated Games model only documents 3 fields (match_id, home_team, away_team); real API returns date, game_day, running etc. which land in additional_properties without .get() support, breaking all existing callers. The statistics_center_client is still vendored (Login/LoginSuccess models are available) but response parsing stays as plain dicts until the spec is complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0f43663 commit 4451ca1

6 files changed

Lines changed: 33 additions & 34 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Python wrapper for the Kinexon Handball API. Provides a user-friendly abstractio
1010
- `api.py`: Two-step authentication and `httpx` client initialization.
1111
- `handball.py`: Main entry point (`HandballAPI`). Convenience methods for common API operations.
1212
- `fetchers.py`: Static data loading (e.g., team IDs from `config/teams.yaml`).
13-
- `statistics_center.py`: Separate wrapper for the Statistics Center REST + WebSocket API. Uses `Games` and `LoginSuccess` from the generated `statistics_center_client`.
13+
- `statistics_center.py`: Separate wrapper for the Statistics Center REST + WebSocket API. All REST methods return plain `list[dict]` the Statistics Center OpenAPI spec is incomplete (missing fields), so generated models are vendored but not used for response parsing.
1414
- **Generated Clients (`src/_vendor/`)**: Two auto-generated OpenAPI clients via `openapi-python-client`.
1515
- `kinexon_client/` — generated from `openapi/sports_app.json` (main Kinexon Cloud REST API).
1616
- `statistics_center_client/` — generated from `openapi/statistics_center.json` (Statistics Center REST API).

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,10 @@ You can add new teams by modifying `config/teams.yaml`. Somehow there is no API
129129

130130
### Statistics Center (REST + Websocket)
131131

132-
The package also provides a dedicated wrapper for the Statistics Center API. REST responses use typed models from the generated `statistics_center_client`:
132+
The package also provides a dedicated wrapper for the Statistics Center API:
133133

134134
```python
135135
from kinexon_handball_api import StatisticsCenterAPI
136-
from statistics_center_client.models import Games
137136

138137
sc = StatisticsCenterAPI(
139138
username="<USERNAME>",
@@ -145,10 +144,10 @@ sc = StatisticsCenterAPI(
145144
# JWT login
146145
jwt = sc.login()
147146

148-
# Typed REST responses
149-
games: list[Games] = sc.get_games(season="2025_2026")
150-
stats = sc.get_stats("<MATCH_ID>") # list[dict]
151-
events = sc.get_events("<MATCH_ID>") # list[dict]
147+
# REST endpoints — all return list[dict]
148+
games = sc.get_games(season="2025_2026")
149+
stats = sc.get_stats("<MATCH_ID>")
150+
events = sc.get_events("<MATCH_ID>")
152151

153152
# Push endpoints
154153
all_endpoints = sc.list_endpoints()

docs/api-reference.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,28 +197,25 @@ sc = StatisticsCenterAPI(
197197

198198
### `login(force_refresh=False)`
199199

200-
Authenticates and returns the JWT. Cached on the instance after first call. The response is parsed via the generated `LoginSuccess` model.
200+
Authenticates and returns the JWT. Cached on the instance after first call.
201201

202202
```python
203203
jwt = sc.login()
204204
```
205205

206206
### `get_games(season="")`
207207

208-
Returns all matches for the given season. Returns `list[Games]` (from `statistics_center_client.models`).
208+
Returns all matches for the given season from `/games`. Returns `list[dict[str, Any]]`.
209209

210210
```python
211-
from statistics_center_client.models import Games
212-
213-
games: list[Games] = sc.get_games(season="2025_2026")
211+
games = sc.get_games(season="2025_2026")
214212
for g in games:
215-
print(g.match_id, g.home_team, g.away_team)
216-
# extra fields from the API are in g.additional_properties
213+
print(g.get("match_id"), g.get("date"), g.get("home_team"))
217214
```
218215

219216
### `get_stats(match_id)`
220217

221-
Returns player statistics for a match from `/stats/{match_id}`. Returns `list[dict[str, Any]]` (the Statistics Center spec has no field-level schema for stats responses).
218+
Returns player statistics for a match from `/stats/{match_id}`. Returns `list[dict[str, Any]]`.
222219

223220
```python
224221
stats = sc.get_stats("123456")
@@ -234,10 +231,10 @@ events = sc.get_events("123456", event_type="shot")
234231

235232
### `get_games_via_websocket(season, timeout_s=10.0)`
236233

237-
Discovers matches for a season via the WebSocket matches stream. Returns `list[Games]`.
234+
Discovers matches for a season via the WebSocket matches stream. Returns `list[dict[str, Any]]`.
238235

239236
```python
240-
games: list[Games] = sc.get_games_via_websocket("2025_2026")
237+
games = sc.get_games_via_websocket("2025_2026")
241238
```
242239

243240
### `list_endpoints(jwt=None)`

docs/architecture.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ Standalone client (does not inherit `KinexonAPI`). Uses:
104104

105105
- `httpx.Client` for REST calls.
106106
- JWT authentication via `/auth/login`. The login response is parsed into a `LoginSuccess` model from `statistics_center_client.models`.
107-
- `get_games()` and `get_games_via_websocket()` return `list[Games]` (typed attrs model from `statistics_center_client.models`).
108-
- `get_stats()` and `get_events()` return `list[dict[str, Any]]` — the Statistics Center spec has no field-level schema for these responses.
107+
- `get_games()`, `get_games_via_websocket()`, `get_stats()`, and `get_events()` all return `list[dict[str, Any]]` — the Statistics Center spec only documents 3 fields for games (`match_id`, `home_team`, `away_team`) while the real API returns many more; a generated model would bury the extra fields in `additional_properties` without `.get()` support, so plain dicts are used.
109108
- `python-socketio` for WebSocket subscriptions (lazy-imported so the dependency is optional at import time).
110109

111110
## Generated Clients

src/kinexon_handball_api/statistics_center.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from urllib.parse import urlsplit
1515

1616
import httpx
17-
from statistics_center_client.models import Games, LoginSuccess
1817

1918
SubscriptionType = Literal["matches", "stats", "events", "live_events"]
2019
HTTP_OK = 200
@@ -93,13 +92,7 @@ def login(self, *, force_refresh: bool = False) -> str:
9392
)
9493

9594
payload = response.json()
96-
raw = payload[0] if isinstance(payload, list) and payload else payload
97-
if not isinstance(raw, dict):
98-
raise StatisticsCenterAPIError(
99-
"Statistics Center login response has no jwt."
100-
)
101-
result = LoginSuccess.from_dict(raw)
102-
jwt = result.jwt if isinstance(result.jwt, str) else None
95+
jwt = self._extract_jwt(payload)
10396
if not jwt:
10497
raise StatisticsCenterAPIError(
10598
"Statistics Center login response has no jwt."
@@ -135,13 +128,13 @@ def _get_with_retry(
135128
)
136129
return response
137130

138-
def get_games(self, season: str = "") -> list[Games]:
131+
def get_games(self, season: str = "") -> list[dict[str, Any]]:
139132
"""Return all matches for the given season from /games."""
140133
params = {"season": season} if season else None
141134
payload = self._get_with_retry("/games", params=params).json()
142135
if not isinstance(payload, list):
143136
raise StatisticsCenterAPIError("Expected list from /games")
144-
return [Games.from_dict(g) for g in payload if isinstance(g, dict)]
137+
return payload
145138

146139
def get_stats(self, match_id: str | int) -> list[dict[str, Any]]:
147140
"""Return player statistics for a match from /stats/{match_id}."""
@@ -170,7 +163,7 @@ def get_games_via_websocket(
170163
season: str,
171164
timeout_s: float = 10.0,
172165
verbose: bool = False,
173-
) -> list[Games]:
166+
) -> list[dict[str, Any]]:
174167
"""Discover matches for a season via the WebSocket matches stream.
175168
176169
Connects, sends a matches subscription, waits up to timeout_s for the
@@ -185,7 +178,7 @@ def get_games_via_websocket(
185178
) from exc
186179

187180
done = threading.Event()
188-
games: list[Games] = []
181+
games: list[dict[str, Any]] = []
189182
token = self._jwt or self.login()
190183
sio = socketio.Client(
191184
reconnection=False,
@@ -215,7 +208,7 @@ def _on_message(data: Any) -> None:
215208
if isinstance(data, dict) and data.get("type") == "matches":
216209
payload = data.get("payload")
217210
if isinstance(payload, list):
218-
games = [Games.from_dict(r) for r in payload if isinstance(r, dict)]
211+
games = [r for r in payload if isinstance(r, dict)]
219212
done.set()
220213

221214
@sio.on("connect_error")
@@ -263,6 +256,18 @@ def delete_endpoint(self, endpoint_id: str, *, jwt: str | None = None) -> bool:
263256
del endpoint_id, jwt
264257
raise StatisticsCenterAPIError("delete_endpoint is disabled in read-only mode.")
265258

259+
@staticmethod
260+
def _extract_jwt(payload: Any) -> str | None:
261+
if isinstance(payload, dict):
262+
value = payload.get("jwt")
263+
return value if isinstance(value, str) else None
264+
if isinstance(payload, list) and payload:
265+
first = payload[0]
266+
if isinstance(first, dict):
267+
value = first.get("jwt")
268+
return value if isinstance(value, str) else None
269+
return None
270+
266271
@staticmethod
267272
def _auth_headers(jwt: str) -> dict[str, str]:
268273
return {

test/test_statistics_center.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import httpx
77
import pytest
8-
from statistics_center_client.models import Games
98

109
from kinexon_handball_api.statistics_center import (
1110
StatisticsCenterAPI,
@@ -159,7 +158,7 @@ def handler(request: httpx.Request) -> httpx.Response:
159158

160159
api = _make_rest_api(handler)
161160
games = api.get_games(season="2025_2026")
162-
assert games == [Games(match_id="42")]
161+
assert games == [{"match_id": "42"}]
163162
api.close()
164163

165164

0 commit comments

Comments
 (0)