overfast-client 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.
- overfast_client-0.1.0/.github/workflows/publish.yml +58 -0
- overfast_client-0.1.0/.github/workflows/test.yml +33 -0
- overfast_client-0.1.0/.gitignore +83 -0
- overfast_client-0.1.0/CHANGELOG.md +24 -0
- overfast_client-0.1.0/LICENSE +21 -0
- overfast_client-0.1.0/PKG-INFO +160 -0
- overfast_client-0.1.0/README.md +123 -0
- overfast_client-0.1.0/overwatch_py/__init__.py +4 -0
- overfast_client-0.1.0/overwatch_py/client.py +96 -0
- overfast_client-0.1.0/overwatch_py/config.py +11 -0
- overfast_client-0.1.0/overwatch_py/enums/__init__.py +28 -0
- overfast_client-0.1.0/overwatch_py/enums/background_image_size.py +10 -0
- overfast_client-0.1.0/overwatch_py/enums/career_stat_category.py +12 -0
- overfast_client-0.1.0/overwatch_py/enums/gamemode.py +13 -0
- overfast_client-0.1.0/overwatch_py/enums/hero.py +55 -0
- overfast_client-0.1.0/overwatch_py/enums/locale.py +17 -0
- overfast_client-0.1.0/overwatch_py/enums/map.py +61 -0
- overfast_client-0.1.0/overwatch_py/enums/map_gamemode.py +18 -0
- overfast_client-0.1.0/overwatch_py/enums/platform.py +6 -0
- overfast_client-0.1.0/overwatch_py/enums/rank.py +23 -0
- overfast_client-0.1.0/overwatch_py/enums/region.py +7 -0
- overfast_client-0.1.0/overwatch_py/enums/role.py +7 -0
- overfast_client-0.1.0/overwatch_py/exceptions.py +33 -0
- overfast_client-0.1.0/overwatch_py/models/__init__.py +61 -0
- overfast_client-0.1.0/overwatch_py/models/gamemode_details.py +11 -0
- overfast_client-0.1.0/overwatch_py/models/hero.py +72 -0
- overfast_client-0.1.0/overwatch_py/models/hero_short.py +11 -0
- overfast_client-0.1.0/overwatch_py/models/hero_stats.py +9 -0
- overfast_client-0.1.0/overwatch_py/models/map_details.py +13 -0
- overfast_client-0.1.0/overwatch_py/models/player.py +64 -0
- overfast_client-0.1.0/overwatch_py/models/player_stats.py +115 -0
- overfast_client-0.1.0/overwatch_py/py.typed +0 -0
- overfast_client-0.1.0/overwatch_py/services/__init__.py +6 -0
- overfast_client-0.1.0/overwatch_py/services/heros.py +110 -0
- overfast_client-0.1.0/overwatch_py/services/maps.py +21 -0
- overfast_client-0.1.0/overwatch_py/services/players.py +135 -0
- overfast_client-0.1.0/overwatch_py/session.py +64 -0
- overfast_client-0.1.0/pyproject.toml +62 -0
- overfast_client-0.1.0/tests/__init__.py +0 -0
- overfast_client-0.1.0/tests/conftest.py +31 -0
- overfast_client-0.1.0/tests/fixtures/openapi.json +1 -0
- overfast_client-0.1.0/tests/test_enums.py +55 -0
- overfast_client-0.1.0/tests/test_exceptions.py +48 -0
- overfast_client-0.1.0/tests/test_heros_service.py +71 -0
- overfast_client-0.1.0/tests/test_maps_service.py +46 -0
- overfast_client-0.1.0/tests/test_players_service.py +81 -0
- overfast_client-0.1.0/tests/test_smoke.py +24 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
# Allow manual runs (e.g. re-publishing the same tag after a build-only fix).
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Build sdist + wheel
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: Install build
|
|
24
|
+
run: |
|
|
25
|
+
python -m pip install --upgrade pip
|
|
26
|
+
pip install build
|
|
27
|
+
|
|
28
|
+
- name: Build
|
|
29
|
+
run: python -m build
|
|
30
|
+
|
|
31
|
+
- name: Upload artifacts
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish:
|
|
38
|
+
name: Publish to PyPI (Trusted Publishing)
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
# The environment name must match what you configure on PyPI under
|
|
42
|
+
# "Manage → Publishing → Trusted publishers".
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/project/overfast-client/
|
|
46
|
+
permissions:
|
|
47
|
+
# Required for OIDC token exchange with PyPI.
|
|
48
|
+
id-token: write
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- name: Download artifacts
|
|
52
|
+
uses: actions/download-artifact@v4
|
|
53
|
+
with:
|
|
54
|
+
name: dist
|
|
55
|
+
path: dist/
|
|
56
|
+
|
|
57
|
+
- name: Publish to PyPI
|
|
58
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
cache: pip
|
|
26
|
+
|
|
27
|
+
- name: Install package and dev deps
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e ".[dev]"
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: pytest -v
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Distribution / packaging
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
develop-eggs/
|
|
11
|
+
dist/
|
|
12
|
+
downloads/
|
|
13
|
+
eggs/
|
|
14
|
+
.eggs/
|
|
15
|
+
lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
share/python-wheels/
|
|
22
|
+
*.egg-info/
|
|
23
|
+
.installed.cfg
|
|
24
|
+
*.egg
|
|
25
|
+
MANIFEST
|
|
26
|
+
|
|
27
|
+
# PyInstaller
|
|
28
|
+
*.manifest
|
|
29
|
+
*.spec
|
|
30
|
+
|
|
31
|
+
# Installer logs
|
|
32
|
+
pip-log.txt
|
|
33
|
+
pip-delete-this-directory.txt
|
|
34
|
+
|
|
35
|
+
# Unit test / coverage reports
|
|
36
|
+
htmlcov/
|
|
37
|
+
.tox/
|
|
38
|
+
.nox/
|
|
39
|
+
.coverage
|
|
40
|
+
.coverage.*
|
|
41
|
+
.cache
|
|
42
|
+
nosetests.xml
|
|
43
|
+
coverage.xml
|
|
44
|
+
*.cover
|
|
45
|
+
*.py,cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
cover/
|
|
49
|
+
|
|
50
|
+
# Environments
|
|
51
|
+
.env
|
|
52
|
+
.env.*
|
|
53
|
+
.venv
|
|
54
|
+
env/
|
|
55
|
+
venv/
|
|
56
|
+
ENV/
|
|
57
|
+
env.bak/
|
|
58
|
+
venv.bak/
|
|
59
|
+
|
|
60
|
+
# IDEs / editors
|
|
61
|
+
.idea/
|
|
62
|
+
.vscode/
|
|
63
|
+
*.swp
|
|
64
|
+
*.swo
|
|
65
|
+
*~
|
|
66
|
+
.DS_Store
|
|
67
|
+
Thumbs.db
|
|
68
|
+
|
|
69
|
+
# mypy / ruff / pyright
|
|
70
|
+
.mypy_cache/
|
|
71
|
+
.dmypy.json
|
|
72
|
+
dmypy.json
|
|
73
|
+
.pyre/
|
|
74
|
+
.pytype/
|
|
75
|
+
.ruff_cache/
|
|
76
|
+
.pyright/
|
|
77
|
+
|
|
78
|
+
# Jupyter
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# Project-local scratch
|
|
82
|
+
openapi_tmp.json
|
|
83
|
+
*.zip
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-04-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release.
|
|
14
|
+
- Async client for the [OverFast API](https://overfast-api.tekrop.fr/) built on `httpx`.
|
|
15
|
+
- Services: `HeroesService`, `MapsService`, `GamemodesService`, `RolesService`, `PlayersService`.
|
|
16
|
+
- Pydantic v2 models for heroes, maps, gamemodes, roles, and player data (summary, stats, career, search).
|
|
17
|
+
- Enums synchronized with the OverFast API schema: `Hero`, `Role`, `Region`, `Platform`, `Rank`, `CompetitiveDivisionFilter`, `HeroGamemode`, `PlayerGamemode`, `Privacy`, `BackgroundImageSize`, `CareerStatCategory`.
|
|
18
|
+
- HTTP-layer caching via [hishel](https://hishel.com) (memory / sqlite / file backends), opt-in through `ClientConfig.cache`.
|
|
19
|
+
- Automatic retries for transient failures via `httpx-retries`.
|
|
20
|
+
- Typed exceptions mapped to OverFast API responses: `BadRequestError` (400), `NotFoundError` (404), `ValidationError` (422), `RateLimitError` (429), `InternalServerError` (500), `ServiceUnavailableError` (503), `ParserBlizzardError` (504).
|
|
21
|
+
- `py.typed` marker for PEP 561 type-checker support.
|
|
22
|
+
|
|
23
|
+
[Unreleased]: https://github.com/Leo890728/overwatch_py/compare/v0.1.0...HEAD
|
|
24
|
+
[0.1.0]: https://github.com/Leo890728/overwatch_py/releases/tag/v0.1.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leo890728
|
|
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,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: overfast-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Async Python client for the OverFast API (Overwatch data).
|
|
5
|
+
Project-URL: Homepage, https://github.com/Leo890728/overwatch_py
|
|
6
|
+
Project-URL: Repository, https://github.com/Leo890728/overwatch_py
|
|
7
|
+
Project-URL: Issues, https://github.com/Leo890728/overwatch_py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Leo890728/overwatch_py/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Leo890728 <Leo890728@users.noreply.github.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,async,httpx,overfast,overwatch,pydantic
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Framework :: Pydantic :: 2
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Games/Entertainment
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: hishel<0.2,>=0.0.30
|
|
29
|
+
Requires-Dist: httpx-retries>=0.3
|
|
30
|
+
Requires-Dist: httpx>=0.27
|
|
31
|
+
Requires-Dist: pydantic>=2.5
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# overfast-client
|
|
39
|
+
|
|
40
|
+
Async Python client for the [OverFast API](https://overfast-api.tekrop.fr) — comprehensive Overwatch data (heroes, maps, gamemodes, player stats) via a typed, pydantic-backed interface.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install overfast-client
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> The PyPI distribution is `overfast-client`; the import name is `overwatch_py`.
|
|
49
|
+
|
|
50
|
+
Or from source (editable):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import asyncio
|
|
60
|
+
from overwatch_py import Client
|
|
61
|
+
from overwatch_py.enums import Hero, Locale, Role, HeroGamemode, PlayerGamemode, Platform
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
client = Client()
|
|
65
|
+
|
|
66
|
+
# List heroes
|
|
67
|
+
heroes = await client.get_heroes(role=Role.SUPPORT, locale=Locale.EN_US)
|
|
68
|
+
for h in heroes:
|
|
69
|
+
print(h.key, h.name)
|
|
70
|
+
|
|
71
|
+
# Full hero data (abilities, story, hitpoints, ...)
|
|
72
|
+
ana = await client.get_hero_data(Hero.ANA)
|
|
73
|
+
print(ana.hitpoints, len(ana.abilities))
|
|
74
|
+
|
|
75
|
+
# Maps & gamemodes
|
|
76
|
+
maps = await client.get_maps()
|
|
77
|
+
gamemodes = await client.get_gamemode_details()
|
|
78
|
+
|
|
79
|
+
# Hero usage stats (pickrate / winrate)
|
|
80
|
+
stats = await client.get_heroes_stats(
|
|
81
|
+
platform=Platform.PC,
|
|
82
|
+
gamemode=PlayerGamemode.COMPETITIVE,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Players
|
|
86
|
+
result = await client.search_players("TeKrop", limit=10)
|
|
87
|
+
player = await client.get_player(result.results[0].player_id)
|
|
88
|
+
summary = await client.get_player_summary("TeKrop-2217")
|
|
89
|
+
stats_summary = await client.get_player_stats("TeKrop-2217", gamemode=PlayerGamemode.COMPETITIVE)
|
|
90
|
+
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from overwatch_py import Client
|
|
98
|
+
from overwatch_py.config import Config
|
|
99
|
+
from overwatch_py.session import HTTPSession
|
|
100
|
+
|
|
101
|
+
config = Config(
|
|
102
|
+
timeout=10, # seconds
|
|
103
|
+
retries=3, # retries on 5xx
|
|
104
|
+
cache=True, # HTTP-level cache (hishel), respects server Cache-Control
|
|
105
|
+
cache_backend="sqlite", # "memory" | "sqlite" | "file"
|
|
106
|
+
)
|
|
107
|
+
client = Client(HTTPSession(config))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Caching
|
|
111
|
+
|
|
112
|
+
Caching is disabled by default. When enabled, [hishel](https://hishel.com) sits in the httpx transport stack and honors each endpoint's `Cache-Control` / `Age` headers (e.g. `/heroes` is cached ~1 day, `/heroes/stats` ~1 hour by the upstream API).
|
|
113
|
+
|
|
114
|
+
## Exceptions
|
|
115
|
+
|
|
116
|
+
All non-2xx responses map to subclasses of `APIError`:
|
|
117
|
+
|
|
118
|
+
| Status | Exception |
|
|
119
|
+
|--------|-----------|
|
|
120
|
+
| 400 | `BadRequestError` |
|
|
121
|
+
| 404 | `NotFoundError` |
|
|
122
|
+
| 422 | `ValidationError` |
|
|
123
|
+
| 429 | `APIRateLimitError` |
|
|
124
|
+
| 500 | `InternalServerError` |
|
|
125
|
+
| 503 | `BlizzardRateLimitError` |
|
|
126
|
+
| 504 | `BlizzardServerError` |
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from overwatch_py.exceptions import NotFoundError, APIRateLimitError
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
await client.get_player_summary("does-not-exist-1234")
|
|
133
|
+
except NotFoundError:
|
|
134
|
+
...
|
|
135
|
+
except APIRateLimitError as e:
|
|
136
|
+
print("rate limited:", e.response.headers.get("retry-after"))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## API surface
|
|
140
|
+
|
|
141
|
+
Exposed on `Client`:
|
|
142
|
+
|
|
143
|
+
- **Heroes** — `get_heroes`, `get_hero_data`, `get_heroes_stats`
|
|
144
|
+
- **Maps / Gamemodes** — `get_maps`, `get_gamemode_details`
|
|
145
|
+
- **Players** — `search_players`, `get_player`, `get_player_summary`, `get_player_stats`, `get_player_career_stats`, `get_player_career_stats_with_labels`, `get_player_full_stats`
|
|
146
|
+
|
|
147
|
+
The underlying services (`client.heros`, `client.maps`, `client.players`) are also available if you prefer service-level access.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install -e ".[dev]"
|
|
153
|
+
pytest # unit + mocked HTTP tests
|
|
154
|
+
pytest -m live # hits the real API (rate-limited)
|
|
155
|
+
pytest -m 'live or not live' # run everything
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# overfast-client
|
|
2
|
+
|
|
3
|
+
Async Python client for the [OverFast API](https://overfast-api.tekrop.fr) — comprehensive Overwatch data (heroes, maps, gamemodes, player stats) via a typed, pydantic-backed interface.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install overfast-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> The PyPI distribution is `overfast-client`; the import name is `overwatch_py`.
|
|
12
|
+
|
|
13
|
+
Or from source (editable):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from overwatch_py import Client
|
|
24
|
+
from overwatch_py.enums import Hero, Locale, Role, HeroGamemode, PlayerGamemode, Platform
|
|
25
|
+
|
|
26
|
+
async def main():
|
|
27
|
+
client = Client()
|
|
28
|
+
|
|
29
|
+
# List heroes
|
|
30
|
+
heroes = await client.get_heroes(role=Role.SUPPORT, locale=Locale.EN_US)
|
|
31
|
+
for h in heroes:
|
|
32
|
+
print(h.key, h.name)
|
|
33
|
+
|
|
34
|
+
# Full hero data (abilities, story, hitpoints, ...)
|
|
35
|
+
ana = await client.get_hero_data(Hero.ANA)
|
|
36
|
+
print(ana.hitpoints, len(ana.abilities))
|
|
37
|
+
|
|
38
|
+
# Maps & gamemodes
|
|
39
|
+
maps = await client.get_maps()
|
|
40
|
+
gamemodes = await client.get_gamemode_details()
|
|
41
|
+
|
|
42
|
+
# Hero usage stats (pickrate / winrate)
|
|
43
|
+
stats = await client.get_heroes_stats(
|
|
44
|
+
platform=Platform.PC,
|
|
45
|
+
gamemode=PlayerGamemode.COMPETITIVE,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Players
|
|
49
|
+
result = await client.search_players("TeKrop", limit=10)
|
|
50
|
+
player = await client.get_player(result.results[0].player_id)
|
|
51
|
+
summary = await client.get_player_summary("TeKrop-2217")
|
|
52
|
+
stats_summary = await client.get_player_stats("TeKrop-2217", gamemode=PlayerGamemode.COMPETITIVE)
|
|
53
|
+
|
|
54
|
+
asyncio.run(main())
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from overwatch_py import Client
|
|
61
|
+
from overwatch_py.config import Config
|
|
62
|
+
from overwatch_py.session import HTTPSession
|
|
63
|
+
|
|
64
|
+
config = Config(
|
|
65
|
+
timeout=10, # seconds
|
|
66
|
+
retries=3, # retries on 5xx
|
|
67
|
+
cache=True, # HTTP-level cache (hishel), respects server Cache-Control
|
|
68
|
+
cache_backend="sqlite", # "memory" | "sqlite" | "file"
|
|
69
|
+
)
|
|
70
|
+
client = Client(HTTPSession(config))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Caching
|
|
74
|
+
|
|
75
|
+
Caching is disabled by default. When enabled, [hishel](https://hishel.com) sits in the httpx transport stack and honors each endpoint's `Cache-Control` / `Age` headers (e.g. `/heroes` is cached ~1 day, `/heroes/stats` ~1 hour by the upstream API).
|
|
76
|
+
|
|
77
|
+
## Exceptions
|
|
78
|
+
|
|
79
|
+
All non-2xx responses map to subclasses of `APIError`:
|
|
80
|
+
|
|
81
|
+
| Status | Exception |
|
|
82
|
+
|--------|-----------|
|
|
83
|
+
| 400 | `BadRequestError` |
|
|
84
|
+
| 404 | `NotFoundError` |
|
|
85
|
+
| 422 | `ValidationError` |
|
|
86
|
+
| 429 | `APIRateLimitError` |
|
|
87
|
+
| 500 | `InternalServerError` |
|
|
88
|
+
| 503 | `BlizzardRateLimitError` |
|
|
89
|
+
| 504 | `BlizzardServerError` |
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from overwatch_py.exceptions import NotFoundError, APIRateLimitError
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
await client.get_player_summary("does-not-exist-1234")
|
|
96
|
+
except NotFoundError:
|
|
97
|
+
...
|
|
98
|
+
except APIRateLimitError as e:
|
|
99
|
+
print("rate limited:", e.response.headers.get("retry-after"))
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## API surface
|
|
103
|
+
|
|
104
|
+
Exposed on `Client`:
|
|
105
|
+
|
|
106
|
+
- **Heroes** — `get_heroes`, `get_hero_data`, `get_heroes_stats`
|
|
107
|
+
- **Maps / Gamemodes** — `get_maps`, `get_gamemode_details`
|
|
108
|
+
- **Players** — `search_players`, `get_player`, `get_player_summary`, `get_player_stats`, `get_player_career_stats`, `get_player_career_stats_with_labels`, `get_player_full_stats`
|
|
109
|
+
|
|
110
|
+
The underlying services (`client.heros`, `client.maps`, `client.players`) are also available if you prefer service-level access.
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pip install -e ".[dev]"
|
|
116
|
+
pytest # unit + mocked HTTP tests
|
|
117
|
+
pytest -m live # hits the real API (rate-limited)
|
|
118
|
+
pytest -m 'live or not live' # run everything
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .services import HerosService, MapsService, PlayersService
|
|
4
|
+
from .services.players import HeroFilter
|
|
5
|
+
from .session import HTTPSession
|
|
6
|
+
from .models import (
|
|
7
|
+
GamemodeDetails,
|
|
8
|
+
HeroData,
|
|
9
|
+
HeroPlayerCareerStats,
|
|
10
|
+
HeroShort,
|
|
11
|
+
HeroStats,
|
|
12
|
+
MapDetails,
|
|
13
|
+
Player,
|
|
14
|
+
PlayerSearchResult,
|
|
15
|
+
PlayerStats,
|
|
16
|
+
PlayerStatsSummary,
|
|
17
|
+
PlayerSummary,
|
|
18
|
+
)
|
|
19
|
+
from .enums import (
|
|
20
|
+
CompetitiveDivisionFilter,
|
|
21
|
+
Hero,
|
|
22
|
+
HeroGamemode,
|
|
23
|
+
Locale,
|
|
24
|
+
Map,
|
|
25
|
+
MapGamemode,
|
|
26
|
+
Platform,
|
|
27
|
+
PlayerGamemode,
|
|
28
|
+
Region,
|
|
29
|
+
Role,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Client:
|
|
34
|
+
def __init__(self, session: HTTPSession = None):
|
|
35
|
+
self.session = session or HTTPSession()
|
|
36
|
+
self.heros = HerosService(self.session)
|
|
37
|
+
self.maps = MapsService(self.session)
|
|
38
|
+
self.players = PlayersService(self.session)
|
|
39
|
+
|
|
40
|
+
async def get_heroes(self, role: Role = None, locale: Locale = Locale.EN_US, gamemode: Optional[HeroGamemode] = None) -> list[HeroShort]:
|
|
41
|
+
"""Get a list of Overwatch heroes, which can be filtered using roles or gamemodes."""
|
|
42
|
+
return await self.heros.get_heroes(role, locale, gamemode)
|
|
43
|
+
|
|
44
|
+
async def get_hero_data(self, hero: Hero, locale: Locale = Locale.EN_US) -> HeroData:
|
|
45
|
+
"""Get data about an Overwatch hero : description, abilities, stadium powers, story, etc."""
|
|
46
|
+
return await self.heros.get_hero_data(hero, locale)
|
|
47
|
+
|
|
48
|
+
async def get_maps(self, map_gamemode: Optional[MapGamemode] = None) -> list[MapDetails]:
|
|
49
|
+
"""Get a list of Overwatch maps : Hanamura, King's Row, Dorado, etc."""
|
|
50
|
+
return await self.maps.get_maps(map_gamemode)
|
|
51
|
+
|
|
52
|
+
async def get_gamemode_details(self) -> list[GamemodeDetails]:
|
|
53
|
+
"""Get a list of Overwatch gamemodes : Assault, Escort, Flashpoint, Hybrid, etc."""
|
|
54
|
+
return await self.maps.get_gamemode_details()
|
|
55
|
+
|
|
56
|
+
async def get_heroes_stats(self, platform: Platform = Platform.PC, gamemode: PlayerGamemode = PlayerGamemode.COMPETITIVE,
|
|
57
|
+
region: Region = Region.ASIA, role: Optional[Role] = None, map: Optional[Map] = None,
|
|
58
|
+
competitive_division: Optional[CompetitiveDivisionFilter] = None, order_by: Optional[str] = "hero:asc") -> list[HeroStats]:
|
|
59
|
+
"""Get hero statistics usage, filtered by platform, region, role, etc. Only Role Queue gamemodes are concerned."""
|
|
60
|
+
return await self.heros.get_heroes_stats(platform, gamemode, region, role, map, competitive_division, order_by)
|
|
61
|
+
|
|
62
|
+
async def search_players(self, name: str, order_by: Optional[str] = None,
|
|
63
|
+
offset: Optional[int] = None, limit: Optional[int] = None) -> PlayerSearchResult:
|
|
64
|
+
"""Search for players by BattleTag name."""
|
|
65
|
+
return await self.players.search_players(name, order_by, offset, limit)
|
|
66
|
+
|
|
67
|
+
async def get_player(self, player_id: str, gamemode: Optional[PlayerGamemode] = None,
|
|
68
|
+
platform: Optional[Platform] = None) -> Player:
|
|
69
|
+
"""Get all player data: summary and statistics."""
|
|
70
|
+
return await self.players.get_player(player_id, gamemode, platform)
|
|
71
|
+
|
|
72
|
+
async def get_player_summary(self, player_id: str) -> PlayerSummary:
|
|
73
|
+
"""Get player summary: name, avatar, endorsement, competitive ranks."""
|
|
74
|
+
return await self.players.get_player_summary(player_id)
|
|
75
|
+
|
|
76
|
+
async def get_player_stats(self, player_id: str, gamemode: Optional[PlayerGamemode] = None,
|
|
77
|
+
platform: Optional[Platform] = None) -> PlayerStatsSummary:
|
|
78
|
+
"""Get player stats summary (general / roles / heroes)."""
|
|
79
|
+
return await self.players.get_player_stats(player_id, gamemode, platform)
|
|
80
|
+
|
|
81
|
+
async def get_player_career_stats(self, player_id: str, gamemode: PlayerGamemode,
|
|
82
|
+
platform: Optional[Platform] = None,
|
|
83
|
+
hero: Optional[HeroFilter] = None) -> dict[str, Optional[HeroPlayerCareerStats]]:
|
|
84
|
+
"""Get player career stats as flat {label: value} maps per hero."""
|
|
85
|
+
return await self.players.get_player_career_stats(player_id, gamemode, platform, hero)
|
|
86
|
+
|
|
87
|
+
async def get_player_career_stats_with_labels(self, player_id: str, gamemode: PlayerGamemode,
|
|
88
|
+
platform: Optional[Platform] = None,
|
|
89
|
+
hero: Optional[HeroFilter] = None):
|
|
90
|
+
"""Get player career stats with labels and categories per hero."""
|
|
91
|
+
return await self.players.get_player_career_stats_with_labels(player_id, gamemode, platform, hero)
|
|
92
|
+
|
|
93
|
+
async def get_player_full_stats(self, player_id: str, gamemode: Optional[PlayerGamemode] = None,
|
|
94
|
+
platform: Optional[Platform] = None) -> PlayerStats:
|
|
95
|
+
"""Get a player's full per-platform stats (pc/console)."""
|
|
96
|
+
return await self.players.get_player_full_stats(player_id, gamemode, platform)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, HttpUrl
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config(BaseModel):
|
|
7
|
+
base_url: HttpUrl = "https://overfast-api.tekrop.fr"
|
|
8
|
+
timeout: int = 10
|
|
9
|
+
retries: int = 3
|
|
10
|
+
cache: bool = False
|
|
11
|
+
cache_backend: Literal["memory", "sqlite", "file"] = "memory"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .background_image_size import BackgroundImageSize
|
|
2
|
+
from .career_stat_category import CareerStatCategory
|
|
3
|
+
from .map_gamemode import MapGamemode
|
|
4
|
+
from .hero import Hero
|
|
5
|
+
from .locale import Locale
|
|
6
|
+
from .platform import Platform
|
|
7
|
+
from .region import Region
|
|
8
|
+
from .role import Role
|
|
9
|
+
from .map import Map
|
|
10
|
+
from .gamemode import HeroGamemode, PlayerGamemode
|
|
11
|
+
from .rank import Rank, CompetitiveDivisionFilter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"BackgroundImageSize",
|
|
16
|
+
"CareerStatCategory",
|
|
17
|
+
"CompetitiveDivisionFilter",
|
|
18
|
+
"Hero",
|
|
19
|
+
"HeroGamemode",
|
|
20
|
+
"Locale",
|
|
21
|
+
"Map",
|
|
22
|
+
"MapGamemode",
|
|
23
|
+
"Platform",
|
|
24
|
+
"PlayerGamemode",
|
|
25
|
+
"Rank",
|
|
26
|
+
"Region",
|
|
27
|
+
"Role",
|
|
28
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CareerStatCategory(str, Enum):
|
|
5
|
+
ASSISTS = "assists"
|
|
6
|
+
AVERAGE = "average"
|
|
7
|
+
BEST = "best"
|
|
8
|
+
COMBAT = "combat"
|
|
9
|
+
GAME = "game"
|
|
10
|
+
HERO_SPECIFIC = "hero_specific"
|
|
11
|
+
MATCH_AWARDS = "match_awards"
|
|
12
|
+
MISCELLANEOUS = "miscellaneous"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HeroGamemode(str, Enum):
|
|
5
|
+
"""Gamemodes a hero can be played in (used by /heroes filter and HeroShort.gamemodes)."""
|
|
6
|
+
QUICKPLAY = "quickplay"
|
|
7
|
+
STADIUM = "stadium"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlayerGamemode(str, Enum):
|
|
11
|
+
"""Gamemodes used by player-stats endpoints."""
|
|
12
|
+
QUICKPLAY = "quickplay"
|
|
13
|
+
COMPETITIVE = "competitive"
|