py-understat 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.
- py_understat-0.1.0/.github/workflows/publish.yml +65 -0
- py_understat-0.1.0/.gitignore +9 -0
- py_understat-0.1.0/.python-version +1 -0
- py_understat-0.1.0/CONTEXT.md +25 -0
- py_understat-0.1.0/LICENSE +21 -0
- py_understat-0.1.0/PKG-INFO +96 -0
- py_understat-0.1.0/README.md +86 -0
- py_understat-0.1.0/main.py +6 -0
- py_understat-0.1.0/pyproject.toml +28 -0
- py_understat-0.1.0/src/py_understat/__init__.py +36 -0
- py_understat-0.1.0/src/py_understat/client.py +327 -0
- py_understat-0.1.0/src/py_understat/exceptions.py +27 -0
- py_understat-0.1.0/src/py_understat/models.py +228 -0
- py_understat-0.1.0/tests/test_client.py +212 -0
- py_understat-0.1.0/uv.lock +733 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions: {}
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
name: Test and build distributions
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
steps:
|
|
17
|
+
- name: Check out source
|
|
18
|
+
uses: actions/checkout@v5
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
22
|
+
|
|
23
|
+
- name: Install locked dependencies
|
|
24
|
+
run: uv sync --locked --all-groups
|
|
25
|
+
|
|
26
|
+
- name: Check formatting
|
|
27
|
+
run: uv run ruff format --check .
|
|
28
|
+
|
|
29
|
+
- name: Lint
|
|
30
|
+
run: uv run ruff check .
|
|
31
|
+
|
|
32
|
+
- name: Type check
|
|
33
|
+
run: uv run ty check
|
|
34
|
+
|
|
35
|
+
- name: Test
|
|
36
|
+
run: uv run pytest
|
|
37
|
+
|
|
38
|
+
- name: Build distributions
|
|
39
|
+
run: uv build
|
|
40
|
+
|
|
41
|
+
- name: Upload distributions
|
|
42
|
+
uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: python-package-distributions
|
|
45
|
+
path: dist/
|
|
46
|
+
if-no-files-found: error
|
|
47
|
+
|
|
48
|
+
publish:
|
|
49
|
+
name: Publish distributions to PyPI
|
|
50
|
+
needs: build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
environment:
|
|
53
|
+
name: pypi
|
|
54
|
+
url: https://pypi.org/p/py-understat
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write
|
|
57
|
+
steps:
|
|
58
|
+
- name: Download distributions
|
|
59
|
+
uses: actions/download-artifact@v4
|
|
60
|
+
with:
|
|
61
|
+
name: python-package-distributions
|
|
62
|
+
path: dist/
|
|
63
|
+
|
|
64
|
+
- name: Publish distributions
|
|
65
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Understat Data
|
|
2
|
+
|
|
3
|
+
This context retrieves football statistics published by Understat. It presents Understat resources through a stable Python API without redefining the source data.
|
|
4
|
+
|
|
5
|
+
## Language
|
|
6
|
+
|
|
7
|
+
**Understat resource**:
|
|
8
|
+
A top-level published data subject: a League, Team, Player, or Match.
|
|
9
|
+
_Avoid_: endpoint, entity
|
|
10
|
+
|
|
11
|
+
**Competition season**:
|
|
12
|
+
The campaign beginning in a given calendar year and ending in the next, represented publicly as `YYYY/YYYY+1`.
|
|
13
|
+
_Avoid_: year, calendar year
|
|
14
|
+
|
|
15
|
+
**Resource snapshot**:
|
|
16
|
+
The complete set of data Understat returns for one resource query.
|
|
17
|
+
_Avoid_: table, endpoint response
|
|
18
|
+
|
|
19
|
+
**Team handle**:
|
|
20
|
+
The exact URL-safe name Understat assigns to a Team, such as `Manchester_United`.
|
|
21
|
+
_Avoid_: team name, slug
|
|
22
|
+
|
|
23
|
+
**Understat ID**:
|
|
24
|
+
The positive integer that identifies a Player or Match in Understat data.
|
|
25
|
+
_Avoid_: identifier, string ID
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 buenavista62
|
|
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,96 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: py-understat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An asynchronous client for Understat football statistics
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.14
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.13.5
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# py-understat
|
|
12
|
+
|
|
13
|
+
An asynchronous, typed client for football statistics published by [Understat](https://understat.com/). This is an unofficial client and is not affiliated with Understat.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv add py-understat
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From a checkout, install the package and its development tools with:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv sync --all-groups
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Query data
|
|
28
|
+
|
|
29
|
+
`UnderstatClient` owns its HTTP resources. Use it as an async context manager.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import asyncio
|
|
33
|
+
|
|
34
|
+
from py_understat import League, UnderstatClient
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def main() -> None:
|
|
38
|
+
async with UnderstatClient() as client:
|
|
39
|
+
premier_league = await client.league(League.EPL).get("2025/2026")
|
|
40
|
+
|
|
41
|
+
top_scorer = max(premier_league.players, key=lambda player: player.goals)
|
|
42
|
+
print(top_scorer.player_name, top_scorer.goals)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Each `get()` call returns the complete snapshot Understat supplies for that resource:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
async with UnderstatClient() as client:
|
|
52
|
+
league = await client.league(League.BUNDESLIGA).get("2025/2026")
|
|
53
|
+
team = await client.team("Bayern_Munich").get("2025/2026")
|
|
54
|
+
player = await client.player(8260).get()
|
|
55
|
+
match = await client.match(28778).get()
|
|
56
|
+
|
|
57
|
+
league.players # PlayerStatistic records
|
|
58
|
+
league.matches # MatchRecord records
|
|
59
|
+
league.teams # TeamSeason records keyed by Understat team ID
|
|
60
|
+
team.statistics # Typed team statistic categories
|
|
61
|
+
player.matches # PlayerMatch records
|
|
62
|
+
player.shots # Shot records
|
|
63
|
+
match.rosters # RosterEntry records grouped by home/away side
|
|
64
|
+
match.shots # Shot records grouped by home/away side
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Identifiers
|
|
68
|
+
|
|
69
|
+
- `League` is an enum containing Understat's six supported competitions.
|
|
70
|
+
- Team queries take the exact Understat team handle, such as `"Manchester_United"`.
|
|
71
|
+
- Player and Match queries take a positive Understat integer ID.
|
|
72
|
+
- League and Team queries take a competition season written as `"YYYY/YYYY+1"`, for example `"2025/2026"`. `Season("2025/2026")` is available when a reusable value is useful.
|
|
73
|
+
|
|
74
|
+
Invalid identifiers fail locally with `InvalidIdentifierError` before making a request.
|
|
75
|
+
|
|
76
|
+
## Data and failures
|
|
77
|
+
|
|
78
|
+
Known fields are normalized to native Python values: IDs and counts become `int`, expected-goal values become `float`, and source timestamps become `datetime`. Models are frozen, and source fields that the package does not yet name are available through each model's read-only `extra` mapping.
|
|
79
|
+
|
|
80
|
+
The client retries temporary transport failures, `429`, and `5xx` responses with bounded exponential backoff. It honors `Retry-After` up to the configured maximum delay. Catch package-level errors rather than HTTPX implementation errors:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from py_understat import RateLimitError, ResourceNotFoundError, UnderstatError
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
async with UnderstatClient() as client:
|
|
87
|
+
snapshot = await client.player(8260).get()
|
|
88
|
+
except ResourceNotFoundError:
|
|
89
|
+
print("Unknown Understat player")
|
|
90
|
+
except RateLimitError:
|
|
91
|
+
print("Understat still rate-limited the request after retries")
|
|
92
|
+
except UnderstatError as error:
|
|
93
|
+
print(f"Understat is unavailable: {error}")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The client sends an identifying user agent. It intentionally has no automatic cache or global rate limiter; callers needing either should apply them around immutable snapshots.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# py-understat
|
|
2
|
+
|
|
3
|
+
An asynchronous, typed client for football statistics published by [Understat](https://understat.com/). This is an unofficial client and is not affiliated with Understat.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add py-understat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From a checkout, install the package and its development tools with:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv sync --all-groups
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Query data
|
|
18
|
+
|
|
19
|
+
`UnderstatClient` owns its HTTP resources. Use it as an async context manager.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
|
|
24
|
+
from py_understat import League, UnderstatClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
async with UnderstatClient() as client:
|
|
29
|
+
premier_league = await client.league(League.EPL).get("2025/2026")
|
|
30
|
+
|
|
31
|
+
top_scorer = max(premier_league.players, key=lambda player: player.goals)
|
|
32
|
+
print(top_scorer.player_name, top_scorer.goals)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each `get()` call returns the complete snapshot Understat supplies for that resource:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
async with UnderstatClient() as client:
|
|
42
|
+
league = await client.league(League.BUNDESLIGA).get("2025/2026")
|
|
43
|
+
team = await client.team("Bayern_Munich").get("2025/2026")
|
|
44
|
+
player = await client.player(8260).get()
|
|
45
|
+
match = await client.match(28778).get()
|
|
46
|
+
|
|
47
|
+
league.players # PlayerStatistic records
|
|
48
|
+
league.matches # MatchRecord records
|
|
49
|
+
league.teams # TeamSeason records keyed by Understat team ID
|
|
50
|
+
team.statistics # Typed team statistic categories
|
|
51
|
+
player.matches # PlayerMatch records
|
|
52
|
+
player.shots # Shot records
|
|
53
|
+
match.rosters # RosterEntry records grouped by home/away side
|
|
54
|
+
match.shots # Shot records grouped by home/away side
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Identifiers
|
|
58
|
+
|
|
59
|
+
- `League` is an enum containing Understat's six supported competitions.
|
|
60
|
+
- Team queries take the exact Understat team handle, such as `"Manchester_United"`.
|
|
61
|
+
- Player and Match queries take a positive Understat integer ID.
|
|
62
|
+
- League and Team queries take a competition season written as `"YYYY/YYYY+1"`, for example `"2025/2026"`. `Season("2025/2026")` is available when a reusable value is useful.
|
|
63
|
+
|
|
64
|
+
Invalid identifiers fail locally with `InvalidIdentifierError` before making a request.
|
|
65
|
+
|
|
66
|
+
## Data and failures
|
|
67
|
+
|
|
68
|
+
Known fields are normalized to native Python values: IDs and counts become `int`, expected-goal values become `float`, and source timestamps become `datetime`. Models are frozen, and source fields that the package does not yet name are available through each model's read-only `extra` mapping.
|
|
69
|
+
|
|
70
|
+
The client retries temporary transport failures, `429`, and `5xx` responses with bounded exponential backoff. It honors `Retry-After` up to the configured maximum delay. Catch package-level errors rather than HTTPX implementation errors:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from py_understat import RateLimitError, ResourceNotFoundError, UnderstatError
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
async with UnderstatClient() as client:
|
|
77
|
+
snapshot = await client.player(8260).get()
|
|
78
|
+
except ResourceNotFoundError:
|
|
79
|
+
print("Unknown Understat player")
|
|
80
|
+
except RateLimitError:
|
|
81
|
+
print("Understat still rate-limited the request after retries")
|
|
82
|
+
except UnderstatError as error:
|
|
83
|
+
print(f"Understat is unavailable: {error}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The client sends an identifying user agent. It intentionally has no automatic cache or global rate limiter; callers needing either should apply them around immutable snapshots.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "py-understat"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "An asynchronous client for Understat football statistics"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.28.1",
|
|
9
|
+
"pydantic>=2.13.5",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"ipykernel>=7.3.0",
|
|
15
|
+
"pytest>=9.1.1",
|
|
16
|
+
"ruff>=0.16.5",
|
|
17
|
+
"ty>=0.0.75",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling>=1.28.0"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
target-version = "py314"
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""An asynchronous, typed client for Understat football statistics."""
|
|
2
|
+
|
|
3
|
+
from .client import League, RetryPolicy, Season, UnderstatClient
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
InvalidIdentifierError,
|
|
6
|
+
PayloadValidationError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ResourceNotFoundError,
|
|
9
|
+
UnderstatError,
|
|
10
|
+
UpstreamError,
|
|
11
|
+
)
|
|
12
|
+
from .models import (
|
|
13
|
+
LeagueSnapshot,
|
|
14
|
+
MatchSnapshot,
|
|
15
|
+
PlayerSnapshot,
|
|
16
|
+
TeamSnapshot,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"InvalidIdentifierError",
|
|
21
|
+
"League",
|
|
22
|
+
"LeagueSnapshot",
|
|
23
|
+
"MatchSnapshot",
|
|
24
|
+
"PayloadValidationError",
|
|
25
|
+
"PlayerSnapshot",
|
|
26
|
+
"RateLimitError",
|
|
27
|
+
"ResourceNotFoundError",
|
|
28
|
+
"RetryPolicy",
|
|
29
|
+
"Season",
|
|
30
|
+
"TeamSnapshot",
|
|
31
|
+
"UnderstatClient",
|
|
32
|
+
"UnderstatError",
|
|
33
|
+
"UpstreamError",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|