pitchapi 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.
- pitchapi-0.1.0/.gitignore +11 -0
- pitchapi-0.1.0/LICENSE +21 -0
- pitchapi-0.1.0/PKG-INFO +178 -0
- pitchapi-0.1.0/README.md +148 -0
- pitchapi-0.1.0/pyproject.toml +114 -0
- pitchapi-0.1.0/src/pitchapi/__init__.py +44 -0
- pitchapi-0.1.0/src/pitchapi/_transport.py +101 -0
- pitchapi-0.1.0/src/pitchapi/client.py +184 -0
- pitchapi-0.1.0/src/pitchapi/errors.py +128 -0
- pitchapi-0.1.0/src/pitchapi/models/__init__.py +127 -0
- pitchapi-0.1.0/src/pitchapi/models/_convert.py +79 -0
- pitchapi-0.1.0/src/pitchapi/models/advanced.py +279 -0
- pitchapi-0.1.0/src/pitchapi/models/common.py +51 -0
- pitchapi-0.1.0/src/pitchapi/models/event.py +27 -0
- pitchapi-0.1.0/src/pitchapi/models/h2h.py +30 -0
- pitchapi-0.1.0/src/pitchapi/models/league.py +62 -0
- pitchapi-0.1.0/src/pitchapi/models/lineup.py +41 -0
- pitchapi-0.1.0/src/pitchapi/models/match.py +48 -0
- pitchapi-0.1.0/src/pitchapi/models/momentum.py +16 -0
- pitchapi-0.1.0/src/pitchapi/models/player.py +42 -0
- pitchapi-0.1.0/src/pitchapi/models/shot.py +54 -0
- pitchapi-0.1.0/src/pitchapi/models/stats.py +35 -0
- pitchapi-0.1.0/src/pitchapi/models/team.py +13 -0
- pitchapi-0.1.0/src/pitchapi/py.typed +0 -0
- pitchapi-0.1.0/src/pitchapi/resources/__init__.py +20 -0
- pitchapi-0.1.0/src/pitchapi/resources/_base.py +52 -0
- pitchapi-0.1.0/src/pitchapi/resources/date.py +40 -0
- pitchapi-0.1.0/src/pitchapi/resources/leagues.py +61 -0
- pitchapi-0.1.0/src/pitchapi/resources/matches.py +163 -0
- pitchapi-0.1.0/src/pitchapi/resources/players.py +21 -0
- pitchapi-0.1.0/src/pitchapi/resources/teams.py +17 -0
- pitchapi-0.1.0/tests/test_client.py +367 -0
- pitchapi-0.1.0/tests/test_convert.py +137 -0
pitchapi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PitchAPI
|
|
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.
|
pitchapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pitchapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the PitchAPI football-data API
|
|
5
|
+
Project-URL: Homepage, https://pitchapi.dev
|
|
6
|
+
Project-URL: Documentation, https://pitchapi.dev/#sdks
|
|
7
|
+
Project-URL: Changelog, https://pitchapi.dev/#changelog
|
|
8
|
+
Author: PitchAPI
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,football,opta,sdk,soccer,xg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.24
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy<2,>=1.5; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff<0.17,>=0.6; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# pitchapi — Python SDK
|
|
32
|
+
|
|
33
|
+
Typed Python client for the [PitchAPI](https://pitchapi.dev) football-data API.
|
|
34
|
+
Match results, shots, xG, lineups, momentum, events, player stats, head-to-head,
|
|
35
|
+
and Opta-derived advanced analytics.
|
|
36
|
+
|
|
37
|
+
- Sync **and** async clients over one shared core (`httpx`)
|
|
38
|
+
- Plain `dataclass` response models — a faithful mirror of the API's JSON, built
|
|
39
|
+
through a small `from_dict` converter (no Pydantic, one dependency)
|
|
40
|
+
- Typed exceptions mapped from the API's stable `error.code`
|
|
41
|
+
- Automatic retries on `429`/`5xx` with `Retry-After` honoured
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pitchapi
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.9+.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pitchapi import PitchAPI
|
|
55
|
+
|
|
56
|
+
with PitchAPI(api_key="pk_live_...") as client:
|
|
57
|
+
day = client.date.get("2026-08-27")
|
|
58
|
+
for m in day.matches:
|
|
59
|
+
print(m.home_team.name, m.score_home, "-", m.score_away, m.away_team.name)
|
|
60
|
+
|
|
61
|
+
match = client.matches.get("m_4DP2fy")
|
|
62
|
+
shots = client.matches.shots(match.id)
|
|
63
|
+
adv = client.matches.advanced(match.id) # team rollups
|
|
64
|
+
net = client.matches.advanced_network(match.id) # pass networks
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The key can also come from the `PITCHAPI_API_KEY` environment variable, in which
|
|
68
|
+
case `PitchAPI()` needs no arguments.
|
|
69
|
+
|
|
70
|
+
## Async
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from pitchapi import AsyncPitchAPI
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
async with AsyncPitchAPI() as client:
|
|
78
|
+
league = await client.leagues.get("l_0bfbkO")
|
|
79
|
+
matches = await client.leagues.matches(league.id, season="2025-2026")
|
|
80
|
+
print(len(matches.matches), "matches")
|
|
81
|
+
|
|
82
|
+
asyncio.run(main())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Namespaces
|
|
86
|
+
|
|
87
|
+
| Namespace | Methods |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `client.date` | `get(date, status=None)` — `date` is a `str` (`YYYY-MM-DD`) or a `datetime.date` |
|
|
90
|
+
| `client.matches` | `get`, `shots`, `shot`, `events`, `lineups`, `momentum`, `stats`, `players`, `player`, `player_shots`, `h2h`, `advanced`, `advanced_network`, `advanced_players`, `advanced_player` |
|
|
91
|
+
| `client.leagues` | `list`, `get`, `matches(id, season=None, status=None)` |
|
|
92
|
+
| `client.teams` | `get` |
|
|
93
|
+
| `client.players` | `get` |
|
|
94
|
+
|
|
95
|
+
Every method returns a typed dataclass from `pitchapi.models`. Per-player match
|
|
96
|
+
`stats` is a raw `dict` because its shape varies by position.
|
|
97
|
+
|
|
98
|
+
## Upcoming fixtures
|
|
99
|
+
|
|
100
|
+
Match listings return played matches by default. Pass `status` to reach
|
|
101
|
+
scheduled ones — `"upcoming"` for fixtures that have not kicked off, `"all"` for
|
|
102
|
+
both. An upcoming match carries a kickoff `time_utc` and a `status`, but its
|
|
103
|
+
scores are `None` until it is played.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
for m in client.date.get("2026-09-01", status="upcoming").matches:
|
|
107
|
+
print(m.time_utc, m.home_team.name, "vs", m.away_team.name)
|
|
108
|
+
|
|
109
|
+
# Within a season. The season is resolved first, so asking for upcoming
|
|
110
|
+
# fixtures of a finished campaign is an empty list, not next season's.
|
|
111
|
+
client.leagues.matches("l_0bfbkO", season="2025-2026", status="all")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Lineups for a fixture may be a pre-match prediction rather than the real XI.
|
|
115
|
+
`confirmed` is the flag to branch on; `lineup_type` carries the source's own
|
|
116
|
+
label for a prediction and is `None` once the lineup is confirmed.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
lineups = client.matches.lineups("m_4DP2fy")
|
|
120
|
+
if lineups.home.confirmed:
|
|
121
|
+
print(lineups.home.formation, [p.name for p in lineups.home.starters])
|
|
122
|
+
else:
|
|
123
|
+
print("predicted only:", lineups.home.lineup_type)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Errors
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from pitchapi import NotFoundError, PlanUpgradeRequiredError, RateLimitError
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
client.matches.advanced("m_unprocessed")
|
|
133
|
+
except NotFoundError as e:
|
|
134
|
+
# code is RESOURCE_NOT_FOUND or ANALYTICS_UNAVAILABLE
|
|
135
|
+
print(e.code, e.request_id)
|
|
136
|
+
except PlanUpgradeRequiredError:
|
|
137
|
+
... # league is Pro-only
|
|
138
|
+
except RateLimitError as e:
|
|
139
|
+
print("retry after", e.retry_after, "s")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
All exceptions derive from `pitchapi.PitchAPIError` and carry `code`,
|
|
143
|
+
`status_code`, and `request_id` where available.
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
PitchAPI(
|
|
149
|
+
api_key="pk_live_...",
|
|
150
|
+
base_url="https://api.pitchapi.dev", # override for self-hosting/tests
|
|
151
|
+
timeout=30.0,
|
|
152
|
+
max_retries=2, # 429/5xx + network errors; 0 disables
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pip install -e ".[dev]"
|
|
160
|
+
pytest # tests use httpx.MockTransport — no network
|
|
161
|
+
mypy # scoped to src/ by pyproject
|
|
162
|
+
ruff check .
|
|
163
|
+
ruff format --check .
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## OpenAPI
|
|
167
|
+
|
|
168
|
+
A full OpenAPI 3.1 description of the API lives at
|
|
169
|
+
[`openapi/openapi.yaml`](../../openapi/openapi.yaml) in the repository root. The
|
|
170
|
+
dataclass models in this SDK mirror its schemas one-to-one.
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
This client library is released under the [MIT License](LICENSE).
|
|
175
|
+
|
|
176
|
+
The licence covers the SDK source only. Access to the PitchAPI service and the
|
|
177
|
+
football data it returns is governed separately by the PitchAPI terms of
|
|
178
|
+
service — an MIT-licensed client does not grant any right to the data.
|
pitchapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# pitchapi — Python SDK
|
|
2
|
+
|
|
3
|
+
Typed Python client for the [PitchAPI](https://pitchapi.dev) football-data API.
|
|
4
|
+
Match results, shots, xG, lineups, momentum, events, player stats, head-to-head,
|
|
5
|
+
and Opta-derived advanced analytics.
|
|
6
|
+
|
|
7
|
+
- Sync **and** async clients over one shared core (`httpx`)
|
|
8
|
+
- Plain `dataclass` response models — a faithful mirror of the API's JSON, built
|
|
9
|
+
through a small `from_dict` converter (no Pydantic, one dependency)
|
|
10
|
+
- Typed exceptions mapped from the API's stable `error.code`
|
|
11
|
+
- Automatic retries on `429`/`5xx` with `Retry-After` honoured
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install pitchapi
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Python 3.9+.
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pitchapi import PitchAPI
|
|
25
|
+
|
|
26
|
+
with PitchAPI(api_key="pk_live_...") as client:
|
|
27
|
+
day = client.date.get("2026-08-27")
|
|
28
|
+
for m in day.matches:
|
|
29
|
+
print(m.home_team.name, m.score_home, "-", m.score_away, m.away_team.name)
|
|
30
|
+
|
|
31
|
+
match = client.matches.get("m_4DP2fy")
|
|
32
|
+
shots = client.matches.shots(match.id)
|
|
33
|
+
adv = client.matches.advanced(match.id) # team rollups
|
|
34
|
+
net = client.matches.advanced_network(match.id) # pass networks
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The key can also come from the `PITCHAPI_API_KEY` environment variable, in which
|
|
38
|
+
case `PitchAPI()` needs no arguments.
|
|
39
|
+
|
|
40
|
+
## Async
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from pitchapi import AsyncPitchAPI
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with AsyncPitchAPI() as client:
|
|
48
|
+
league = await client.leagues.get("l_0bfbkO")
|
|
49
|
+
matches = await client.leagues.matches(league.id, season="2025-2026")
|
|
50
|
+
print(len(matches.matches), "matches")
|
|
51
|
+
|
|
52
|
+
asyncio.run(main())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Namespaces
|
|
56
|
+
|
|
57
|
+
| Namespace | Methods |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `client.date` | `get(date, status=None)` — `date` is a `str` (`YYYY-MM-DD`) or a `datetime.date` |
|
|
60
|
+
| `client.matches` | `get`, `shots`, `shot`, `events`, `lineups`, `momentum`, `stats`, `players`, `player`, `player_shots`, `h2h`, `advanced`, `advanced_network`, `advanced_players`, `advanced_player` |
|
|
61
|
+
| `client.leagues` | `list`, `get`, `matches(id, season=None, status=None)` |
|
|
62
|
+
| `client.teams` | `get` |
|
|
63
|
+
| `client.players` | `get` |
|
|
64
|
+
|
|
65
|
+
Every method returns a typed dataclass from `pitchapi.models`. Per-player match
|
|
66
|
+
`stats` is a raw `dict` because its shape varies by position.
|
|
67
|
+
|
|
68
|
+
## Upcoming fixtures
|
|
69
|
+
|
|
70
|
+
Match listings return played matches by default. Pass `status` to reach
|
|
71
|
+
scheduled ones — `"upcoming"` for fixtures that have not kicked off, `"all"` for
|
|
72
|
+
both. An upcoming match carries a kickoff `time_utc` and a `status`, but its
|
|
73
|
+
scores are `None` until it is played.
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
for m in client.date.get("2026-09-01", status="upcoming").matches:
|
|
77
|
+
print(m.time_utc, m.home_team.name, "vs", m.away_team.name)
|
|
78
|
+
|
|
79
|
+
# Within a season. The season is resolved first, so asking for upcoming
|
|
80
|
+
# fixtures of a finished campaign is an empty list, not next season's.
|
|
81
|
+
client.leagues.matches("l_0bfbkO", season="2025-2026", status="all")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Lineups for a fixture may be a pre-match prediction rather than the real XI.
|
|
85
|
+
`confirmed` is the flag to branch on; `lineup_type` carries the source's own
|
|
86
|
+
label for a prediction and is `None` once the lineup is confirmed.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
lineups = client.matches.lineups("m_4DP2fy")
|
|
90
|
+
if lineups.home.confirmed:
|
|
91
|
+
print(lineups.home.formation, [p.name for p in lineups.home.starters])
|
|
92
|
+
else:
|
|
93
|
+
print("predicted only:", lineups.home.lineup_type)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Errors
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pitchapi import NotFoundError, PlanUpgradeRequiredError, RateLimitError
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
client.matches.advanced("m_unprocessed")
|
|
103
|
+
except NotFoundError as e:
|
|
104
|
+
# code is RESOURCE_NOT_FOUND or ANALYTICS_UNAVAILABLE
|
|
105
|
+
print(e.code, e.request_id)
|
|
106
|
+
except PlanUpgradeRequiredError:
|
|
107
|
+
... # league is Pro-only
|
|
108
|
+
except RateLimitError as e:
|
|
109
|
+
print("retry after", e.retry_after, "s")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
All exceptions derive from `pitchapi.PitchAPIError` and carry `code`,
|
|
113
|
+
`status_code`, and `request_id` where available.
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
PitchAPI(
|
|
119
|
+
api_key="pk_live_...",
|
|
120
|
+
base_url="https://api.pitchapi.dev", # override for self-hosting/tests
|
|
121
|
+
timeout=30.0,
|
|
122
|
+
max_retries=2, # 429/5xx + network errors; 0 disables
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pip install -e ".[dev]"
|
|
130
|
+
pytest # tests use httpx.MockTransport — no network
|
|
131
|
+
mypy # scoped to src/ by pyproject
|
|
132
|
+
ruff check .
|
|
133
|
+
ruff format --check .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## OpenAPI
|
|
137
|
+
|
|
138
|
+
A full OpenAPI 3.1 description of the API lives at
|
|
139
|
+
[`openapi/openapi.yaml`](../../openapi/openapi.yaml) in the repository root. The
|
|
140
|
+
dataclass models in this SDK mirror its schemas one-to-one.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
This client library is released under the [MIT License](LICENSE).
|
|
145
|
+
|
|
146
|
+
The licence covers the SDK source only. Access to the PitchAPI service and the
|
|
147
|
+
football data it returns is governed separately by the PitchAPI terms of
|
|
148
|
+
service — an MIT-licensed client does not grant any right to the data.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pitchapi"
|
|
7
|
+
# Single-sourced from src/pitchapi/__init__.py, which is also what the
|
|
8
|
+
# User-Agent reports — one place to bump, no way for the two to drift.
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
description = "Python SDK for the PitchAPI football-data API"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{ name = "PitchAPI" }]
|
|
16
|
+
keywords = ["football", "soccer", "opta", "xg", "api", "sdk"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["httpx>=0.24"]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
# Upper bounds on the linters only: both change their default behaviour between
|
|
33
|
+
# minor releases, and CI should fail on a real regression rather than on an
|
|
34
|
+
# upstream rule that did not exist when the code was written.
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=7",
|
|
37
|
+
"pytest-asyncio>=0.21",
|
|
38
|
+
"mypy>=1.5,<2",
|
|
39
|
+
"ruff>=0.6,<0.17",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Every URL here is rendered publicly on the PyPI project page. The source repo
|
|
43
|
+
# is private, so it is deliberately not listed: linking it would publish both the
|
|
44
|
+
# owner's account name and the repository name to anyone reading the page.
|
|
45
|
+
[project.urls]
|
|
46
|
+
Homepage = "https://pitchapi.dev"
|
|
47
|
+
Documentation = "https://pitchapi.dev/#sdks"
|
|
48
|
+
Changelog = "https://pitchapi.dev/#changelog"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.version]
|
|
51
|
+
path = "src/pitchapi/__init__.py"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/pitchapi"]
|
|
55
|
+
|
|
56
|
+
# Listed explicitly rather than defaulting to "everything git does not ignore".
|
|
57
|
+
# The sdist is a published artifact that anyone can download and read, so a file
|
|
58
|
+
# added to this directory later — a release runbook, internal notes — must not
|
|
59
|
+
# ship by accident. It ships only if it appears below.
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
include = [
|
|
62
|
+
"src",
|
|
63
|
+
"tests",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"pyproject.toml",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
asyncio_mode = "auto"
|
|
71
|
+
testpaths = ["tests"]
|
|
72
|
+
|
|
73
|
+
[tool.ruff]
|
|
74
|
+
line-length = 100
|
|
75
|
+
target-version = "py39"
|
|
76
|
+
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
# Selected explicitly rather than relying on ruff's defaults, which grow between
|
|
79
|
+
# minor releases — an unpinned ruff would otherwise start failing code that has
|
|
80
|
+
# not changed. Two families are left out on purpose:
|
|
81
|
+
#
|
|
82
|
+
# UP / FA — pyupgrade and the future-annotations rules want to rewrite
|
|
83
|
+
# `Optional[X]` to `X | None` and add `from __future__ import annotations`
|
|
84
|
+
# to the model modules. Both are wrong here. The package supports 3.9, where
|
|
85
|
+
# `X | None` is a runtime TypeError outside an annotation; and
|
|
86
|
+
# models/_convert.py reads `dataclasses.fields(cls)[i].type` as a real type
|
|
87
|
+
# object, which only holds while the model modules keep their annotations
|
|
88
|
+
# un-postponed. See the design notes in that module.
|
|
89
|
+
select = ["E", "W", "F", "I", "B", "C4", "SIM", "RUF"]
|
|
90
|
+
# `__all__` is grouped by meaning (clients, then models, then errors) and
|
|
91
|
+
# mirrors the import block above it; alphabetical order would lose that.
|
|
92
|
+
ignore = ["RUF022"]
|
|
93
|
+
|
|
94
|
+
[tool.mypy]
|
|
95
|
+
# Strict mode covers the shipped package. The tests are deliberately loose —
|
|
96
|
+
# the httpx mock handlers take whatever shape each case needs — so `files`
|
|
97
|
+
# scopes a bare `mypy` to src rather than leaving that to the caller.
|
|
98
|
+
files = ["src"]
|
|
99
|
+
# No `python_version`: mypy no longer accepts a 3.9 target, so the version under
|
|
100
|
+
# test is whichever interpreter CI runs it on. The matrix covers 3.9 upward.
|
|
101
|
+
strict = true
|
|
102
|
+
warn_unused_ignores = false
|
|
103
|
+
|
|
104
|
+
[tool.pyright]
|
|
105
|
+
include = ["src", "tests"]
|
|
106
|
+
extraPaths = ["src"]
|
|
107
|
+
|
|
108
|
+
[tool.ruff.format]
|
|
109
|
+
# The formatter owns Python code and nothing else. Recent ruff versions also
|
|
110
|
+
# reformat fenced code in Markdown and docstrings by default, which flattens the
|
|
111
|
+
# indented reST literal blocks the module docstrings rely on and rewraps the
|
|
112
|
+
# hand-aligned trailing comments in README examples.
|
|
113
|
+
docstring-code-format = false
|
|
114
|
+
exclude = ["*.md"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""PitchAPI — Python SDK for the PitchAPI football-data API.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
from pitchapi import PitchAPI
|
|
5
|
+
|
|
6
|
+
with PitchAPI(api_key="pk_live_...") as client:
|
|
7
|
+
for m in client.date.get("2026-08-27").matches:
|
|
8
|
+
print(m.home_team.name, m.score_home, "-", m.score_away, m.away_team.name)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
from . import models
|
|
16
|
+
from .client import AsyncPitchAPI, PitchAPI
|
|
17
|
+
from .errors import (
|
|
18
|
+
APIConnectionError,
|
|
19
|
+
AuthError,
|
|
20
|
+
InvalidParameterError,
|
|
21
|
+
NotFoundError,
|
|
22
|
+
PitchAPIError,
|
|
23
|
+
PlanUpgradeRequiredError,
|
|
24
|
+
RateLimitError,
|
|
25
|
+
ServerError,
|
|
26
|
+
SubscriptionSuspendedError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"__version__",
|
|
31
|
+
"PitchAPI",
|
|
32
|
+
"AsyncPitchAPI",
|
|
33
|
+
"models",
|
|
34
|
+
# errors
|
|
35
|
+
"PitchAPIError",
|
|
36
|
+
"APIConnectionError",
|
|
37
|
+
"InvalidParameterError",
|
|
38
|
+
"AuthError",
|
|
39
|
+
"SubscriptionSuspendedError",
|
|
40
|
+
"PlanUpgradeRequiredError",
|
|
41
|
+
"NotFoundError",
|
|
42
|
+
"RateLimitError",
|
|
43
|
+
"ServerError",
|
|
44
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Transport-level helpers shared by the sync and async clients.
|
|
2
|
+
|
|
3
|
+
Keeping envelope handling, error mapping, and the retry policy here means the
|
|
4
|
+
two clients differ only in how they run the HTTP call itself, not in how they
|
|
5
|
+
interpret the result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
from .errors import (
|
|
15
|
+
PitchAPIError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ServerError,
|
|
18
|
+
exception_for_code,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.pitchapi.dev"
|
|
22
|
+
API_PREFIX = "/v1"
|
|
23
|
+
|
|
24
|
+
# Statuses that a GET may safely be replayed on. Everything else (400/401/403/
|
|
25
|
+
# 404) is a deterministic client error and is surfaced immediately.
|
|
26
|
+
_RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def user_agent() -> str:
|
|
30
|
+
return f"pitchapi-python/{__version__}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_headers(api_key: str, extra: Optional[dict[str, str]] = None) -> dict[str, str]:
|
|
34
|
+
headers = {
|
|
35
|
+
"X-API-KEY": api_key,
|
|
36
|
+
"Accept": "application/json",
|
|
37
|
+
"User-Agent": user_agent(),
|
|
38
|
+
}
|
|
39
|
+
if extra:
|
|
40
|
+
headers.update(extra)
|
|
41
|
+
return headers
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def should_retry(status: int, attempt: int, max_retries: int) -> bool:
|
|
45
|
+
return attempt < max_retries and status in _RETRY_STATUSES
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def backoff_seconds(attempt: int, retry_after: Optional[int]) -> float:
|
|
49
|
+
"""Delay before the next attempt.
|
|
50
|
+
|
|
51
|
+
Honours a server-provided ``Retry-After`` exactly; otherwise falls back to
|
|
52
|
+
exponential backoff with full jitter so retried clients do not synchronise.
|
|
53
|
+
"""
|
|
54
|
+
if retry_after is not None and retry_after >= 0:
|
|
55
|
+
return float(retry_after)
|
|
56
|
+
base = min(2.0**attempt, 8.0)
|
|
57
|
+
return random.uniform(0.0, base)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_retry_after(value: Optional[str]) -> Optional[int]:
|
|
61
|
+
if not value:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
return int(value)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def unwrap(status: int, payload: Any, request_id: Optional[str], retry_after: Optional[int]) -> Any:
|
|
70
|
+
"""Return the ``data`` object on success, or raise the mapped exception.
|
|
71
|
+
|
|
72
|
+
``payload`` is the already-decoded JSON body, or ``None`` when the body was
|
|
73
|
+
absent or not JSON (which is itself an error for this API).
|
|
74
|
+
"""
|
|
75
|
+
if 200 <= status < 300:
|
|
76
|
+
if isinstance(payload, dict) and "data" in payload:
|
|
77
|
+
return payload["data"]
|
|
78
|
+
# A 2xx without the envelope should not happen; surface it rather than
|
|
79
|
+
# hand back a shape the models cannot parse.
|
|
80
|
+
raise PitchAPIError(
|
|
81
|
+
"malformed success response: missing 'data' envelope",
|
|
82
|
+
status_code=status,
|
|
83
|
+
request_id=request_id,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
code: Optional[str] = None
|
|
87
|
+
message = f"HTTP {status}"
|
|
88
|
+
if isinstance(payload, dict):
|
|
89
|
+
err = payload.get("error")
|
|
90
|
+
if isinstance(err, dict):
|
|
91
|
+
code = err.get("code")
|
|
92
|
+
message = err.get("message", message)
|
|
93
|
+
|
|
94
|
+
exc_cls = exception_for_code(code, status)
|
|
95
|
+
if exc_cls is RateLimitError:
|
|
96
|
+
raise RateLimitError(
|
|
97
|
+
message, retry_after=retry_after, code=code, status_code=status, request_id=request_id
|
|
98
|
+
)
|
|
99
|
+
if exc_cls is ServerError and code is None:
|
|
100
|
+
message = f"server error (HTTP {status})"
|
|
101
|
+
raise exc_cls(message, code=code, status_code=status, request_id=request_id)
|