universal-game-api 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. universal_game_api-0.1.0/.github/workflows/python-package.yml +40 -0
  2. universal_game_api-0.1.0/.gitignore +15 -0
  3. universal_game_api-0.1.0/CHANGELOG.md +38 -0
  4. universal_game_api-0.1.0/CONTRIBUTING.md +85 -0
  5. universal_game_api-0.1.0/LICENSE +21 -0
  6. universal_game_api-0.1.0/PKG-INFO +291 -0
  7. universal_game_api-0.1.0/README.md +239 -0
  8. universal_game_api-0.1.0/demo-project/pyproject.toml +24 -0
  9. universal_game_api-0.1.0/demo-project/src/demo_project/__init__.py +1 -0
  10. universal_game_api-0.1.0/demo-project/src/demo_project/__main__.py +6 -0
  11. universal_game_api-0.1.0/demo-project/src/demo_project/dashboard.py +46 -0
  12. universal_game_api-0.1.0/demo-project/src/demo_project/main.py +60 -0
  13. universal_game_api-0.1.0/demo-project/tests/test_demo.py +29 -0
  14. universal_game_api-0.1.0/examples/async_usage.py +18 -0
  15. universal_game_api-0.1.0/examples/basic_usage.py +18 -0
  16. universal_game_api-0.1.0/examples/compare_players.py +8 -0
  17. universal_game_api-0.1.0/pyproject.toml +64 -0
  18. universal_game_api-0.1.0/src/gameapi/__init__.py +48 -0
  19. universal_game_api-0.1.0/src/gameapi/_base.py +59 -0
  20. universal_game_api-0.1.0/src/gameapi/async_client.py +40 -0
  21. universal_game_api-0.1.0/src/gameapi/cache/__init__.py +5 -0
  22. universal_game_api-0.1.0/src/gameapi/cache/memory.py +40 -0
  23. universal_game_api-0.1.0/src/gameapi/client.py +43 -0
  24. universal_game_api-0.1.0/src/gameapi/exceptions.py +95 -0
  25. universal_game_api-0.1.0/src/gameapi/games/__init__.py +6 -0
  26. universal_game_api-0.1.0/src/gameapi/games/base.py +60 -0
  27. universal_game_api-0.1.0/src/gameapi/games/chess_com/__init__.py +5 -0
  28. universal_game_api-0.1.0/src/gameapi/games/chess_com/client.py +271 -0
  29. universal_game_api-0.1.0/src/gameapi/games/chess_com/endpoints.py +21 -0
  30. universal_game_api-0.1.0/src/gameapi/games/chess_com/models.py +35 -0
  31. universal_game_api-0.1.0/src/gameapi/games/lichess/__init__.py +5 -0
  32. universal_game_api-0.1.0/src/gameapi/games/lichess/client.py +156 -0
  33. universal_game_api-0.1.0/src/gameapi/games/lichess/endpoints.py +21 -0
  34. universal_game_api-0.1.0/src/gameapi/games/lichess/models.py +41 -0
  35. universal_game_api-0.1.0/src/gameapi/games/registry.py +25 -0
  36. universal_game_api-0.1.0/src/gameapi/http/__init__.py +5 -0
  37. universal_game_api-0.1.0/src/gameapi/http/client.py +203 -0
  38. universal_game_api-0.1.0/src/gameapi/models/__init__.py +15 -0
  39. universal_game_api-0.1.0/src/gameapi/models/leaderboard.py +34 -0
  40. universal_game_api-0.1.0/src/gameapi/models/match.py +31 -0
  41. universal_game_api-0.1.0/src/gameapi/models/player.py +27 -0
  42. universal_game_api-0.1.0/src/gameapi/models/stats.py +27 -0
  43. universal_game_api-0.1.0/tests/__init__.py +0 -0
  44. universal_game_api-0.1.0/tests/conftest.py +119 -0
  45. universal_game_api-0.1.0/tests/test_cache.py +53 -0
  46. universal_game_api-0.1.0/tests/test_chess_com.py +155 -0
  47. universal_game_api-0.1.0/tests/test_client.py +72 -0
  48. universal_game_api-0.1.0/tests/test_exceptions.py +47 -0
  49. universal_game_api-0.1.0/tests/test_http_client.py +118 -0
  50. universal_game_api-0.1.0/tests/test_integration.py +46 -0
  51. universal_game_api-0.1.0/tests/test_lichess.py +61 -0
  52. universal_game_api-0.1.0/tests/test_live.py +52 -0
  53. universal_game_api-0.1.0/tests/test_models.py +56 -0
@@ -0,0 +1,40 @@
1
+ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
+
4
+ name: Python package
5
+
6
+ on:
7
+ push:
8
+ branches: [ "main" ]
9
+ pull_request:
10
+ branches: [ "main" ]
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v3
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ python -m pip install flake8 pytest
31
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
32
+ - name: Lint with flake8
33
+ run: |
34
+ # stop the build if there are Python syntax errors or undefined names
35
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
36
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
37
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
38
+ - name: Test with pytest
39
+ run: |
40
+ pytest
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ venv/
9
+ .env
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .coverage
14
+ htmlcov/
15
+ .DS_Store
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.2.0] — 2026-08-25
6
+
7
+ ### Added
8
+ - **Lichess integration** (`lichess` slug) — player profiles and leaderboards via the public Lichess API.
9
+ - **`compare_players(game, identifiers)`** — batch-fetch multiple players in one call (sync + async).
10
+ - **`game_info(game)`** — introspect integration metadata (slug, auth requirements, source URL).
11
+ - **Model helpers**:
12
+ - `Match.is_win`, `Match.is_loss`, `Match.is_draw`
13
+ - `Player.win_rate_pct()` — returns win rate as a 0–100 percentage
14
+ - `Leaderboard.top(n)` — slice the top N entries
15
+ - **Live test suite** (`tests/test_live.py`) — run with `pytest -m live` to test against real APIs.
16
+ - **Demo project** (`demo-project/`) — CLI tool and Rich dashboard built on top of `gameapi`.
17
+ - **Examples** (`examples/`) — `basic_usage.py`, `async_usage.py`, `compare_players.py`.
18
+
19
+ ### Fixed
20
+ - **HTTP base headers** — `HTTPClient` now correctly forwards `base_headers` to `httpx.Client` and `httpx.AsyncClient` (fixes missing `User-Agent` on requests).
21
+ - **Retry-After handling** — 429 responses now respect the `Retry-After` header instead of always using exponential backoff.
22
+ - **Missing import** — `GameAPI.__repr__` now properly imports `supported_games`.
23
+ - **Pylance warnings** — guarded `game_data` access in tests to satisfy `reportOptionalMemberAccess`.
24
+
25
+ ### Changed
26
+ - Bumped version to `0.2.0`.
27
+ - `pyproject.toml` now includes `lichess` in keywords and classifiers.
28
+ - `pytest.ini_options` adds a `live` marker for optional live API tests.
29
+
30
+ ## [0.1.0] — 2026-08-20
31
+
32
+ ### Added
33
+ - Initial release with Chess.com integration.
34
+ - Unified models: `Player`, `PlayerStats`, `Rank`, `Match`, `Leaderboard`, `LeaderboardEntry`.
35
+ - Sync (`GameAPI`) and async (`AsyncGameAPI`) clients.
36
+ - In-memory TTL cache (`MemoryCache`).
37
+ - Retry logic with exponential backoff for 429/500/502/503/504.
38
+ - Exception hierarchy: `GameAPIError`, `PlayerNotFoundError`, `RateLimitError`, etc.
@@ -0,0 +1,85 @@
1
+ # Contributing to gameapi
2
+
3
+ Thanks for considering a contribution! The most common contribution is a
4
+ new game integration, so that's covered in detail below.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ git clone https://github.com/F0xyN0xy/universal-game-api
10
+ cd gameapi
11
+ pip install -e ".[dev]"
12
+ pytest
13
+ ```
14
+
15
+ ## Adding a new game integration
16
+
17
+ 1. **Confirm there's a legitimate public data source.** Only integrate APIs
18
+ that developers are permitted to access under their terms of service —
19
+ don't scrape sites in ways that violate their terms, and don't bypass
20
+ auth, rate limits, or anti-bot systems. If a game doesn't have a
21
+ suitable public API, don't add a fake/partial integration for it.
22
+
23
+ 2. **Create a new module** under `src/gameapi/games/<your_game>/`:
24
+
25
+ ```text
26
+ games/
27
+ └── your_game/
28
+ ├── __init__.py
29
+ ├── client.py # subclasses GameIntegration
30
+ ├── models.py # game-specific dataclasses (exposed via game_data)
31
+ └── endpoints.py # URL builders
32
+ ```
33
+
34
+ Use `games/chess_com/` as the reference implementation.
35
+
36
+ 3. **Subclass `GameIntegration`** (`gameapi.games.base.GameIntegration`) and
37
+ implement at minimum `get_player` and `get_player_async`. Implement
38
+ `get_matches`/`get_matches_async` and `get_leaderboard`/
39
+ `get_leaderboard_async` only if the underlying API actually supports
40
+ them — the base class already raises a clear `NotImplementedError`
41
+ otherwise.
42
+
43
+ All HTTP calls should go through `self.http` (a shared `HTTPClient`) —
44
+ never call `httpx`/`requests` directly from a game integration. This
45
+ keeps retries, timeouts, and error translation consistent everywhere.
46
+
47
+ 4. **Map the game's data onto the unified models** (`Player`, `PlayerStats`,
48
+ `Rank`, `Match`, `Leaderboard`) wherever a field genuinely corresponds.
49
+ Don't force a field that doesn't generalize — put it on `game_data`
50
+ instead (a dataclass you define in your integration's `models.py`).
51
+
52
+ 5. **Register your integration** in `src/gameapi/games/registry.py`:
53
+
54
+ ```python
55
+ from .your_game.client import YourGameIntegration
56
+
57
+ GAME_REGISTRY["your_game"] = YourGameIntegration
58
+ ```
59
+
60
+ 6. **Translate errors.** Catch the upstream API's "not found" signal (often
61
+ an HTTP 404) and re-raise it as `PlayerNotFoundError(self.slug, identifier)`
62
+ with useful context. Let other `GameAPIError` subclasses raised by the
63
+ shared `HTTPClient` propagate as-is.
64
+
65
+ 7. **Write tests** in `tests/test_your_game.py` using `respx` to mock HTTP
66
+ responses — don't depend on the live API in the standard test suite.
67
+ See `tests/test_chess_com.py` for the pattern. If you want, add a
68
+ separate integration test (clearly marked) that hits the real API.
69
+
70
+ 8. **Document it** — add a row to the "Supported games" table in
71
+ `README.md`, including the data source, whether auth is required, and
72
+ a link to the upstream API's documentation.
73
+
74
+ ## Code style
75
+
76
+ - Type hints on all public functions/methods.
77
+ - Docstrings on all public classes and methods (Google style, as used
78
+ throughout the codebase).
79
+ - Run `ruff check .` and `mypy src/` before submitting.
80
+
81
+ ## Pull requests
82
+
83
+ - Keep PRs focused — one game integration or one fix per PR.
84
+ - Include tests for anything you add or change.
85
+ - Update `CHANGELOG.md` under an "Unreleased" heading.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 F0xyN0xy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.5
2
+ Name: universal-game-api
3
+ Version: 0.1.0
4
+ Summary: A unified, developer-friendly Python interface for public game data and statistics.
5
+ Project-URL: Homepage, https://github.com/F0xyN0xy/universal-game-api
6
+ Project-URL: Documentation, https://github.com/F0xyN0xy/universal-game-api#readme
7
+ Project-URL: Issues, https://github.com/F0xyN0xy/universal-game-api/issues
8
+ Project-URL: Changelog, https://github.com/F0xyN0xy/universal-game-api/blob/main/CHANGELOG.md
9
+ Author-email: F0xyN0xy <foxynoxy07@proton.me>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 F0xyN0xy
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: api,chess,games,gaming,lichess,python,statistics
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
42
+ Classifier: Typing :: Typed
43
+ Requires-Python: >=3.9
44
+ Requires-Dist: httpx<1.0,>=0.25
45
+ Provides-Extra: dev
46
+ Requires-Dist: mypy>=1.5; extra == 'dev'
47
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
48
+ Requires-Dist: pytest>=7.4; extra == 'dev'
49
+ Requires-Dist: respx>=0.20; extra == 'dev'
50
+ Requires-Dist: ruff>=0.1; extra == 'dev'
51
+ Description-Content-Type: text/markdown
52
+
53
+ # Universal Game API (`gameapi`)
54
+
55
+ > One interface. Every game. No API archaeology.
56
+
57
+ `gameapi` is a Python library that unifies public game APIs behind a single, consistent interface. Instead of learning a new client for every game, you write the same code for Chess.com, Lichess, and whatever comes next.
58
+
59
+ ```python
60
+ from gameapi import GameAPI
61
+
62
+ with GameAPI() as api:
63
+ player = api.player(game="chess_com", identifier="hikaru")
64
+ print(player.name, player.rank.rating) # hikaru 2800
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Why
70
+
71
+ Every game API looks different:
72
+
73
+ ```python
74
+ # Without gameapi
75
+ chess_client.get_profile(...)
76
+ rl_client.fetch_stats(...)
77
+ mc_client.player_lookup(...)
78
+ ```
79
+
80
+ `gameapi` gives you one shape instead:
81
+
82
+ ```python
83
+ # With gameapi
84
+ api.player(game="chess_com", identifier="...")
85
+ api.player(game="lichess", identifier="...")
86
+ api.player(game="rocket_league", identifier="...") # once implemented
87
+ ```
88
+
89
+ Common fields (`name`, `stats`, `rank`) are normalized. Anything that doesn't generalize lives on `player.game_data`.
90
+
91
+ ---
92
+
93
+ ## Supported Games
94
+
95
+ | Game | Slug | Auth Required | Data Source |
96
+ |-----------|-------------|---------------|--------------------------------------|
97
+ | Chess.com | `chess_com` | No | Chess.com Published-Data API |
98
+ | Lichess | `lichess` | No | Lichess Public API |
99
+
100
+ ```python
101
+ from gameapi import supported_games
102
+ print(supported_games()) # ['chess_com', 'lichess']
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Installation
108
+
109
+ ```bash
110
+ pip install gameapi
111
+ ```
112
+
113
+ Requires Python 3.9+.
114
+
115
+ ### Development
116
+
117
+ ```bash
118
+ git clone https://github.com/F0xyN0xy/universal-game-api.git
119
+ cd universal-game-api
120
+ pip install -e ".[dev]"
121
+ pytest
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Quick Start
127
+
128
+ ### Player Profile
129
+
130
+ ```python
131
+ from gameapi import GameAPI
132
+
133
+ with GameAPI() as api:
134
+ player = api.player(game="chess_com", identifier="hikaru")
135
+ print(player.name) # hikaru
136
+ print(player.rank.tier) # GM
137
+ print(player.rank.rating) # 2800
138
+ print(player.stats) # PlayerStats(games_played=..., wins=...)
139
+ ```
140
+
141
+ ### Recent Matches
142
+
143
+ ```python
144
+ for match in api.matches(game="chess_com", identifier="hikaru", limit=10):
145
+ print(match.result, match.opponent, match.played_at)
146
+ ```
147
+
148
+ ### Leaderboard
149
+
150
+ ```python
151
+ board = api.leaderboard(game="chess_com")
152
+ for entry in board.top(5):
153
+ print(entry.position, entry.name, entry.rating)
154
+ ```
155
+
156
+ ### Batch Lookups
157
+
158
+ ```python
159
+ players = api.compare_players("chess_com", ["hikaru", "magnuscarlsen", "nihalsarin"])
160
+ for p in players:
161
+ print(p.name, p.rank.rating)
162
+ ```
163
+
164
+ ### Async
165
+
166
+ ```python
167
+ import asyncio
168
+ from gameapi import AsyncGameAPI
169
+
170
+ async def main():
171
+ async with AsyncGameAPI() as api:
172
+ player = await api.player(game="lichess", identifier="drnykterstein")
173
+ print(player.name)
174
+
175
+ asyncio.run(main())
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Caching
181
+
182
+ Optional in-process caching reduces redundant requests:
183
+
184
+ ```python
185
+ api = GameAPI(cache=True, cache_ttl=60) # seconds
186
+ ```
187
+
188
+ Nothing sensitive is ever cached — only parsed response data.
189
+
190
+ ---
191
+
192
+ ## Rate Limits & Retries
193
+
194
+ `gameapi` retries transient failures (HTTP 429/500/502/503/504) with exponential backoff, then raises typed exceptions:
195
+
196
+ ```python
197
+ from gameapi import RateLimitError
198
+
199
+ try:
200
+ player = api.player(game="chess_com", identifier="hikaru")
201
+ except RateLimitError as e:
202
+ print(f"Rate limited, retry after {e.retry_after}s")
203
+ ```
204
+
205
+ 429 responses respect the upstream `Retry-After` header when provided.
206
+
207
+ ---
208
+
209
+ ## Error Handling
210
+
211
+ All exceptions inherit from `GameAPIError`:
212
+
213
+ ```python
214
+ from gameapi import (
215
+ GameAPIError,
216
+ GameNotSupportedError,
217
+ PlayerNotFoundError,
218
+ AuthenticationError,
219
+ RateLimitError,
220
+ APIUnavailableError,
221
+ InvalidResponseError,
222
+ )
223
+ ```
224
+
225
+ ---
226
+
227
+ ## API Reference
228
+
229
+ ### `GameAPI(api_key=None, cache=False, cache_ttl=60.0, timeout=10.0, max_retries=2)`
230
+
231
+ | Method | Returns |
232
+ |--------|---------|
233
+ | `player(game, identifier)` | `Player` |
234
+ | `matches(game, identifier, limit=20)` | `list[Match]` |
235
+ | `leaderboard(game, region=None)` | `Leaderboard` |
236
+ | `compare_players(game, identifiers)` | `list[Player]` |
237
+ | `game_info(game)` | `dict` |
238
+ | `close()` | — |
239
+
240
+ Context-manager compatible: `with GameAPI() as api:`
241
+
242
+ `AsyncGameAPI` has identical signatures, `await`-ed.
243
+
244
+ ### Models
245
+
246
+ - `Player` — `name`, `game`, `identifier`, `stats`, `rank`, `game_data`, `avatar_url`
247
+ - `PlayerStats` — `games_played`, `wins`, `losses`, `draws`, `win_rate`
248
+ - `Rank` — `tier`, `rating`, `position`, `raw`
249
+ - `Match` — `id`, `game`, `played_at`, `result`, `opponent`, `game_data`
250
+ - `Leaderboard` / `LeaderboardEntry` — `position`, `name`, `rating`
251
+
252
+ Every model is a `@dataclass`, so `dataclasses.asdict(player)` works out of the box.
253
+
254
+ ---
255
+
256
+ ## Demo Project
257
+
258
+ A small CLI and dashboard built on `gameapi` lives in `demo-project/`:
259
+
260
+ ```bash
261
+ cd demo-project
262
+ pip install -e "."
263
+
264
+ # Look up a player
265
+ python -m demo_project chess_com hikaru -m 5 -l
266
+
267
+ # Compare two players side-by-side
268
+ python src/demo_project/dashboard.py chess_com hikaru magnuscarlsen
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Contributing
274
+
275
+ See `CONTRIBUTING.md` for how to add a new game integration. The pattern is:
276
+
277
+ 1. Create `src/gameapi/games/<game>/`
278
+ 2. Subclass `GameIntegration`
279
+ 3. Register it in `games/registry.py`
280
+
281
+ No changes to `client.py` or `async_client.py` are needed.
282
+
283
+ ---
284
+
285
+ ## License
286
+
287
+ MIT
288
+
289
+ ## Legal
290
+
291
+ `gameapi` only integrates with public APIs that permit this kind of access under their terms of service. It does not scrape websites in ways that violate their terms, and does not attempt to bypass authentication, rate limits, or anti-bot protections.