transfermarkt-api 5.0.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 (87) hide show
  1. transfermarkt_api-5.0.0/.gitignore +164 -0
  2. transfermarkt_api-5.0.0/LICENSE +21 -0
  3. transfermarkt_api-5.0.0/PKG-INFO +234 -0
  4. transfermarkt_api-5.0.0/README.md +216 -0
  5. transfermarkt_api-5.0.0/app/__init__.py +0 -0
  6. transfermarkt_api-5.0.0/app/api/__init__.py +0 -0
  7. transfermarkt_api-5.0.0/app/api/api.py +11 -0
  8. transfermarkt_api-5.0.0/app/api/dependencies.py +13 -0
  9. transfermarkt_api-5.0.0/app/api/endpoints/__init__.py +0 -0
  10. transfermarkt_api-5.0.0/app/api/endpoints/clubs.py +41 -0
  11. transfermarkt_api-5.0.0/app/api/endpoints/coaches.py +19 -0
  12. transfermarkt_api-5.0.0/app/api/endpoints/competitions.py +39 -0
  13. transfermarkt_api-5.0.0/app/api/endpoints/countries.py +12 -0
  14. transfermarkt_api-5.0.0/app/api/endpoints/games.py +12 -0
  15. transfermarkt_api-5.0.0/app/api/endpoints/players.py +83 -0
  16. transfermarkt_api-5.0.0/app/api/params.py +5 -0
  17. transfermarkt_api-5.0.0/app/main.py +122 -0
  18. transfermarkt_api-5.0.0/app/rate_limit.py +32 -0
  19. transfermarkt_api-5.0.0/app/settings.py +17 -0
  20. transfermarkt_api-5.0.0/pyproject.toml +70 -0
  21. transfermarkt_api-5.0.0/transfermarkt_api/__init__.py +23 -0
  22. transfermarkt_api-5.0.0/transfermarkt_api/_version.py +3 -0
  23. transfermarkt_api-5.0.0/transfermarkt_api/cache.py +20 -0
  24. transfermarkt_api-5.0.0/transfermarkt_api/client.py +226 -0
  25. transfermarkt_api-5.0.0/transfermarkt_api/exceptions.py +31 -0
  26. transfermarkt_api-5.0.0/transfermarkt_api/py.typed +0 -0
  27. transfermarkt_api-5.0.0/transfermarkt_api/schemas/__init__.py +24 -0
  28. transfermarkt_api-5.0.0/transfermarkt_api/schemas/achievements.py +25 -0
  29. transfermarkt_api-5.0.0/transfermarkt_api/schemas/base.py +17 -0
  30. transfermarkt_api-5.0.0/transfermarkt_api/schemas/clubs/__init__.py +3 -0
  31. transfermarkt_api-5.0.0/transfermarkt_api/schemas/clubs/listing.py +14 -0
  32. transfermarkt_api-5.0.0/transfermarkt_api/schemas/clubs/players.py +30 -0
  33. transfermarkt_api-5.0.0/transfermarkt_api/schemas/clubs/profile.py +82 -0
  34. transfermarkt_api-5.0.0/transfermarkt_api/schemas/clubs/search.py +19 -0
  35. transfermarkt_api-5.0.0/transfermarkt_api/schemas/coaches/__init__.py +31 -0
  36. transfermarkt_api-5.0.0/transfermarkt_api/schemas/competitions/__init__.py +4 -0
  37. transfermarkt_api-5.0.0/transfermarkt_api/schemas/competitions/clubs.py +15 -0
  38. transfermarkt_api-5.0.0/transfermarkt_api/schemas/competitions/search.py +21 -0
  39. transfermarkt_api-5.0.0/transfermarkt_api/schemas/competitions/seasons.py +15 -0
  40. transfermarkt_api-5.0.0/transfermarkt_api/schemas/competitions/table.py +31 -0
  41. transfermarkt_api-5.0.0/transfermarkt_api/schemas/countries.py +16 -0
  42. transfermarkt_api-5.0.0/transfermarkt_api/schemas/games/__init__.py +83 -0
  43. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/__init__.py +9 -0
  44. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/absences.py +22 -0
  45. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/injuries.py +20 -0
  46. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/market_value.py +19 -0
  47. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/matches.py +56 -0
  48. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/national_career.py +21 -0
  49. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/profile.py +81 -0
  50. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/search.py +25 -0
  51. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/stats.py +24 -0
  52. transfermarkt_api-5.0.0/transfermarkt_api/schemas/players/transfers.py +38 -0
  53. transfermarkt_api-5.0.0/transfermarkt_api/services/__init__.py +0 -0
  54. transfermarkt_api-5.0.0/transfermarkt_api/services/achievements.py +50 -0
  55. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/__init__.py +0 -0
  56. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/achievements.py +10 -0
  57. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/listing.py +22 -0
  58. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/players.py +88 -0
  59. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/profile.py +114 -0
  60. transfermarkt_api-5.0.0/transfermarkt_api/services/clubs/search.py +33 -0
  61. transfermarkt_api-5.0.0/transfermarkt_api/services/coaches/__init__.py +0 -0
  62. transfermarkt_api-5.0.0/transfermarkt_api/services/coaches/profile.py +29 -0
  63. transfermarkt_api-5.0.0/transfermarkt_api/services/coaches/search.py +18 -0
  64. transfermarkt_api-5.0.0/transfermarkt_api/services/competitions/__init__.py +0 -0
  65. transfermarkt_api-5.0.0/transfermarkt_api/services/competitions/clubs.py +30 -0
  66. transfermarkt_api-5.0.0/transfermarkt_api/services/competitions/search.py +61 -0
  67. transfermarkt_api-5.0.0/transfermarkt_api/services/competitions/seasons.py +28 -0
  68. transfermarkt_api-5.0.0/transfermarkt_api/services/competitions/table.py +61 -0
  69. transfermarkt_api-5.0.0/transfermarkt_api/services/countries.py +12 -0
  70. transfermarkt_api-5.0.0/transfermarkt_api/services/games/__init__.py +0 -0
  71. transfermarkt_api-5.0.0/transfermarkt_api/services/games/game.py +136 -0
  72. transfermarkt_api-5.0.0/transfermarkt_api/services/players/__init__.py +0 -0
  73. transfermarkt_api-5.0.0/transfermarkt_api/services/players/absences.py +33 -0
  74. transfermarkt_api-5.0.0/transfermarkt_api/services/players/achievements.py +10 -0
  75. transfermarkt_api-5.0.0/transfermarkt_api/services/players/injuries.py +42 -0
  76. transfermarkt_api-5.0.0/transfermarkt_api/services/players/market_value.py +35 -0
  77. transfermarkt_api-5.0.0/transfermarkt_api/services/players/matches.py +107 -0
  78. transfermarkt_api-5.0.0/transfermarkt_api/services/players/national_career.py +31 -0
  79. transfermarkt_api-5.0.0/transfermarkt_api/services/players/profile.py +122 -0
  80. transfermarkt_api-5.0.0/transfermarkt_api/services/players/search.py +44 -0
  81. transfermarkt_api-5.0.0/transfermarkt_api/services/players/stats.py +74 -0
  82. transfermarkt_api-5.0.0/transfermarkt_api/services/players/transfers.py +89 -0
  83. transfermarkt_api-5.0.0/transfermarkt_api/tfmkt/__init__.py +1 -0
  84. transfermarkt_api-5.0.0/transfermarkt_api/tfmkt/client.py +249 -0
  85. transfermarkt_api-5.0.0/transfermarkt_api/tfmkt/concurrency.py +17 -0
  86. transfermarkt_api-5.0.0/transfermarkt_api/tfmkt/freshness.py +38 -0
  87. transfermarkt_api-5.0.0/transfermarkt_api/tfmkt/reference.py +116 -0
@@ -0,0 +1,164 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+ *.ipynb
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # poetry
99
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103
+ #poetry.lock
104
+
105
+ # pdm
106
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107
+ #pdm.lock
108
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109
+ # in version control.
110
+ # https://pdm.fming.dev/#use-with-ide
111
+ .pdm.toml
112
+
113
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114
+ __pypackages__/
115
+
116
+ # Celery stuff
117
+ celerybeat-schedule
118
+ celerybeat.pid
119
+
120
+ # SageMath parsed files
121
+ *.sage.py
122
+
123
+ # Environments
124
+ .env
125
+ .venv
126
+ env/
127
+ venv/
128
+ ENV/
129
+ env.bak/
130
+ venv.bak/
131
+
132
+ # Spyder project settings
133
+ .spyderproject
134
+ .spyproject
135
+
136
+ # Rope project settings
137
+ .ropeproject
138
+
139
+ # mkdocs documentation
140
+ /site
141
+
142
+ # mypy
143
+ .mypy_cache/
144
+ .dmypy.json
145
+ dmypy.json
146
+
147
+ # Pyre type checker
148
+ .pyre/
149
+
150
+ # pytype static type analyzer
151
+ .pytype/
152
+
153
+ # Cython debug symbols
154
+ cython_debug/
155
+
156
+ # PyCharm
157
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
160
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161
+ .idea/
162
+
163
+ # Vercel
164
+ .vercel/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Felipe Allegretti
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,234 @@
1
+ Metadata-Version: 2.5
2
+ Name: transfermarkt-api
3
+ Version: 5.0.0
4
+ Summary: Async Python SDK and HTTP service for Transfermarkt data
5
+ Author-email: Felipe Allegretti <felipe@allegretti.me>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <3.14,>=3.12
9
+ Requires-Dist: cachetools<8,>=7.2
10
+ Requires-Dist: httpx<1,>=0.28.1
11
+ Requires-Dist: pydantic<3,>=2.13
12
+ Provides-Extra: server
13
+ Requires-Dist: fastapi<1,>=0.142.2; extra == 'server'
14
+ Requires-Dist: limits<6,>=5.8; extra == 'server'
15
+ Requires-Dist: pydantic-settings<3,>=2.15; extra == 'server'
16
+ Requires-Dist: uvicorn[standard]<1,>=0.54; extra == 'server'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # transfermarkt-api
20
+
21
+ An async Python SDK and RESTful API (FastAPI) for football data from [Transfermarkt](https://www.transfermarkt.com/): players, clubs and competitions.
22
+
23
+ Since version 4.0.0, data comes from Transfermarkt's JSON API (`tmapi.transfermarkt.technology`), the same API the website uses, instead of scraping HTML pages. The website blocks requests from most cloud hosts, which broke v3 deployments; the JSON API does not. See [CHANGELOG.md](CHANGELOG.md) for what changed in the response format.
24
+
25
+ Please note that the deployed application is used only for testing purposes and is rate limited to 2 requests
26
+ every 3 seconds per client IP. If you'd like to customize it, consider hosting in your own cloud service.
27
+
28
+ The JSON API is internal to Transfermarkt and undocumented: it can change without notice. This project's MIT license covers its code, not the data; check [Transfermarkt's terms](https://www.transfermarkt.com/intern/) before using the data in your own services.
29
+
30
+ ### Python SDK
31
+
32
+ Install the library without server dependencies:
33
+
34
+ ```bash
35
+ pip install transfermarkt-api
36
+ # Until the SDK release is published, install from this repository:
37
+ # pip install .
38
+ ```
39
+
40
+ Release setup and the publishing procedure are documented in [Python package releases](docs/releases.md).
41
+
42
+ ```python
43
+ import asyncio
44
+
45
+ from transfermarkt_api import Transfermarkt
46
+
47
+
48
+ async def main() -> None:
49
+ async with Transfermarkt() as tm:
50
+ squad = await tm.club_players("985")
51
+ for player in squad.players:
52
+ print(player.shirt_number, player.name, player.position, player.market_value)
53
+ print(squad.model_dump(mode="json", by_alias=True))
54
+
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ The facade has one method per data endpoint: `player_profile`, `player_transfers`, `club_players`,
60
+ `competition_clubs`, `competition_table`, `game`, and the remaining methods listed in the
61
+ [SDK method table](docs/plans/0001-python-sdk.md#public-contract). Methods return typed Pydantic models exported
62
+ from `transfermarkt_api.schemas`. Nested models remain available in their resource modules. Use string entity
63
+ and season IDs, integer country IDs, and 1-based page numbers. Calls return one page; callers control pagination.
64
+
65
+ Constructor settings are explicit: `base_url`, `timeout` (seconds, `httpx.Timeout`, or `None`),
66
+ `max_concurrency` (default `10`), `transport`, and `cache`. The SDK reads no `.env` or environment settings on import.
67
+ `CacheConfig(ttl=600, reference_ttl=86400, max_entries=2000)` configures per-client caching. Pass `cache=None` to
68
+ disable both response and reference caches so every request reaches the transport.
69
+
70
+ One client supports concurrent calls within one event loop. Its async context manager, or explicit `aclose()`,
71
+ closes the HTTP client and supplied transport. Do not share a supplied transport across client lifetimes unless
72
+ your wrapper owns and controls its closure. Cache hits remain local to each SDK instance.
73
+
74
+ The SDK raises `NotFound`, `UpstreamBlocked`, `UpstreamTimeout`, `UpstreamError`, or `EndpointUnavailable`, all
75
+ derived from `TransfermarktError` and exported at the package root. Available upstream URL/status metadata lives
76
+ on `error.url` and `error.status_code`; the status is the upstream status, not the server's mapped HTTP status.
77
+ The unsupported `player_jersey_numbers` method raises `EndpointUnavailable` without fetching data.
78
+ Invalid constructor/page/country arguments raise `ValueError`. Custom transport exceptions propagate unchanged.
79
+
80
+ ### Custom transports and replay
81
+
82
+ Supply an `httpx.AsyncBaseTransport` to control every upstream request, including reference lookups, batches,
83
+ and per-player fan-out. The SDK bounds in-flight concurrency; the caller's transport owns rate limits, retries,
84
+ proxy selection, recording and circuit breakers. The SDK performs no automatic retries.
85
+
86
+ ```python
87
+ import httpx
88
+
89
+ from transfermarkt_api import Transfermarkt
90
+
91
+ # Run inside an async function. A custom browser-capable backend can implement the same transport interface.
92
+ async with Transfermarkt(
93
+ transport=httpx.AsyncHTTPTransport(proxy="http://127.0.0.1:8080"),
94
+ cache=None,
95
+ ) as tm:
96
+ squad = await tm.club_players("985", season_id="2024")
97
+ ```
98
+
99
+ [Recording and replay examples](examples/transports.py) wrap a caller-selected backend and save the response
100
+ before SDK parsing. Persist the method, full URL with repeated query parameters, status, relevant headers (including
101
+ `x-amzn-waf-action`), decoded body, and original fetch timestamp. The example store is in memory; replace it with
102
+ durable storage in your application. When reconstructing decoded responses, drop content-encoding/content-length
103
+ headers so the body is not decoded twice.
104
+
105
+ A replay transport returns an `httpx.Response` with
106
+ `extensions={"transfermarkt_fetched_at": captured_datetime}`. This optional metadata accepts naive or timezone-aware
107
+ datetimes. Aware inputs are converted to UTC; naive inputs are interpreted as UTC. `updatedAt` retains its existing
108
+ JSON shape without a timezone suffix and always represents UTC. Without metadata, it reports the current UTC fetch
109
+ time. Each model reports the oldest fetch time used in that call, including cache hits. Exact replay
110
+ needs the same response bodies and timestamps. Missing recordings must raise a transport error rather than fetch
111
+ live data. Batch URLs are stable; concurrent request completion order is not part of the replay key.
112
+
113
+ ```python
114
+ from examples.transports import RecordingTransport, ReplayTransport
115
+
116
+ records = {}
117
+ async with Transfermarkt(transport=RecordingTransport(httpx.AsyncHTTPTransport(), records), cache=None) as tm:
118
+ captured = await tm.club_players("985")
119
+ async with Transfermarkt(transport=ReplayTransport(records), cache=None) as tm:
120
+ replayed = await tm.club_players("985")
121
+ assert captured == replayed
122
+ ```
123
+
124
+ The examples and HTTP server are repository files. The wheel installs only the `transfermarkt_api` package,
125
+ avoiding collisions with a consumer's own `app` package. The `server` extra installs the server dependencies;
126
+ run `uvicorn app.main:app` from a repository checkout, or use the Docker image.
127
+
128
+ ### API Swagger
129
+ https://transfermarkt-api.fly.dev/
130
+
131
+ ### Endpoints
132
+
133
+ | Endpoint | Notes |
134
+ | --- | --- |
135
+ | `GET /players/search/{player_name}?page_number=` | 10 results per page |
136
+ | `GET /players/{player_id}/profile` | |
137
+ | `GET /players/{player_id}/market_value` | |
138
+ | `GET /players/{player_id}/transfers` | Transfer type, age, contract dates, remaining contract days, and each club's league and country at the time |
139
+ | `GET /players/{player_id}/stats` | Per season, competition and club; club competitions only |
140
+ | `GET /players/{player_id}/matches?season_id=&page_number=` | Past matches of the player's teams, most recent first, 50 per page; no upcoming fixtures |
141
+ | `GET /players/{player_id}/injuries?page_number=` | 15 injuries per page |
142
+ | `GET /players/{player_id}/absences?page_number=` | Suspensions, call-ups, leave; 15 per page |
143
+ | `GET /players/{player_id}/achievements` | Titles and awards with season, club and competition |
144
+ | `GET /players/{player_id}/national_career` | Senior and youth national teams |
145
+ | `GET /players/{player_id}/jersey_numbers` | `501`: no data source |
146
+ | `GET /countries/` | Country IDs and metadata, including historical entries; cached for 24 hours |
147
+ | `GET /clubs/?country_id=` | Available club IDs and names for a country; limited upstream coverage, no pagination |
148
+ | `GET /clubs/search/{club_name}?page_number=` | 10 results per page |
149
+ | `GET /clubs/{club_id}/profile` | Squad value details, historical names and stadium details |
150
+ | `GET /clubs/{club_id}/players?season_id=` | Current squad by default; shirt number and captaincy for the selected season |
151
+ | `GET /clubs/{club_id}/achievements` | Club titles by season |
152
+ | `GET /coaches/search/{coach_name}?page_number=` | 10 results per page |
153
+ | `GET /coaches/{coach_id}/profile` | |
154
+ | `GET /competitions/search/{competition_name}?page_number=` | 10 results per page |
155
+ | `GET /competitions/{competition_id}/clubs?season_id=` | Current season by default |
156
+ | `GET /competitions/{competition_id}/table?season_id=` | League table; one table per group for group stages |
157
+ | `GET /competitions/{competition_id}/seasons` | Valid `season_id` values |
158
+ | `GET /games/{game_id}` | Score, lineups, coaches, stadium, team stats, goals, cards and substitutions |
159
+
160
+ Errors: `404` unknown ID, `501` endpoint without data source, `502` unexpected upstream response, `503` upstream blocked or refused the request, `504` upstream timeout.
161
+
162
+ Fields without an upstream value are returned as `null`. `updatedAt` is when the underlying data was fetched from Transfermarkt; responses are cached for 10 minutes by default.
163
+
164
+ ### Running Locally
165
+
166
+ Requires [uv](https://docs.astral.sh/uv/).
167
+
168
+ ````bash
169
+ # Clone the repository
170
+ $ git clone https://github.com/felipeall/transfermarkt-api.git
171
+
172
+ # Go to the project's root folder
173
+ $ cd transfermarkt-api
174
+
175
+ # Install Python 3.12 and the dependencies
176
+ $ uv sync --extra server
177
+
178
+ # Start the API server
179
+ $ uv run uvicorn app.main:app --reload
180
+
181
+ # Access the API local page
182
+ $ open http://localhost:8000/
183
+ ````
184
+
185
+ ### Running Tests
186
+
187
+ ````bash
188
+ # Offline tests (recorded upstream responses, no network)
189
+ $ uv run --extra server pytest
190
+
191
+ # Live smoke tests against Transfermarkt's JSON API
192
+ $ uv run --extra server pytest -m live
193
+
194
+ # Re-record upstream fixtures and response snapshots
195
+ $ rm -rf tests/fixtures/tfmkt && uv run python scripts/capture_fixtures.py
196
+ ````
197
+
198
+ ### Running via Docker
199
+
200
+ ````bash
201
+ # Clone the repository
202
+ $ git clone https://github.com/felipeall/transfermarkt-api.git
203
+
204
+ # Go to the project's root folder
205
+ $ cd transfermarkt-api
206
+
207
+ # Build the Docker image
208
+ $ docker build -t transfermarkt-api .
209
+
210
+ # Instantiate the Docker container
211
+ $ docker run -d -p 8000:8000 transfermarkt-api
212
+
213
+ # Access the API local page
214
+ $ open http://localhost:8000/
215
+ ````
216
+
217
+ ### Environment Variables
218
+
219
+ | Variable | Description | Default |
220
+ |-------------------------------|--------------------------------------------------------------------------------------------------------|------------------------------------------|
221
+ | `RATE_LIMITING_ENABLE` | Enable rate limiting feature for API calls | `false` |
222
+ | `RATE_LIMITING_FREQUENCY` | Requests allowed per client IP across all routes, in [limits notation](https://limits.readthedocs.io/en/stable/quickstart.html#rate-limit-string-notation) | `2/3seconds` |
223
+ | `TFMKT_BASE_URL` | Transfermarkt JSON API base URL | `https://tmapi.transfermarkt.technology` |
224
+ | `TFMKT_TIMEOUT_SECONDS` | Timeout for each upstream request | `15` |
225
+ | `TFMKT_MAX_CONCURRENCY` | Maximum concurrent upstream requests across the whole server | `10` |
226
+ | `CACHE_TTL_SECONDS` | How long successful upstream responses are cached | `600` |
227
+ | `CACHE_REFERENCE_TTL_SECONDS` | Cache duration for reference data (countries, positions, ...) | `86400` |
228
+ | `CACHE_MAX_ENTRIES` | Maximum number of cached upstream responses | `2000` |
229
+
230
+ Rate limits apply to the API routes only: `/health`, the docs (`/`, `/docs`, `/redoc`) and `/openapi.json` are not
231
+ counted. They are counted per client IP: the `Fly-Client-IP` header set by the Fly.io proxy, or the address of the
232
+ connection when that header is missing. `X-Forwarded-For` is not trusted. Behind another reverse proxy, all
233
+ requests share the proxy's address, and outside Fly.io clients can send `Fly-Client-IP` themselves, so adjust
234
+ `client_ip` in `app/main.py` before enabling rate limiting on another host.
@@ -0,0 +1,216 @@
1
+ # transfermarkt-api
2
+
3
+ An async Python SDK and RESTful API (FastAPI) for football data from [Transfermarkt](https://www.transfermarkt.com/): players, clubs and competitions.
4
+
5
+ Since version 4.0.0, data comes from Transfermarkt's JSON API (`tmapi.transfermarkt.technology`), the same API the website uses, instead of scraping HTML pages. The website blocks requests from most cloud hosts, which broke v3 deployments; the JSON API does not. See [CHANGELOG.md](CHANGELOG.md) for what changed in the response format.
6
+
7
+ Please note that the deployed application is used only for testing purposes and is rate limited to 2 requests
8
+ every 3 seconds per client IP. If you'd like to customize it, consider hosting in your own cloud service.
9
+
10
+ The JSON API is internal to Transfermarkt and undocumented: it can change without notice. This project's MIT license covers its code, not the data; check [Transfermarkt's terms](https://www.transfermarkt.com/intern/) before using the data in your own services.
11
+
12
+ ### Python SDK
13
+
14
+ Install the library without server dependencies:
15
+
16
+ ```bash
17
+ pip install transfermarkt-api
18
+ # Until the SDK release is published, install from this repository:
19
+ # pip install .
20
+ ```
21
+
22
+ Release setup and the publishing procedure are documented in [Python package releases](docs/releases.md).
23
+
24
+ ```python
25
+ import asyncio
26
+
27
+ from transfermarkt_api import Transfermarkt
28
+
29
+
30
+ async def main() -> None:
31
+ async with Transfermarkt() as tm:
32
+ squad = await tm.club_players("985")
33
+ for player in squad.players:
34
+ print(player.shirt_number, player.name, player.position, player.market_value)
35
+ print(squad.model_dump(mode="json", by_alias=True))
36
+
37
+
38
+ asyncio.run(main())
39
+ ```
40
+
41
+ The facade has one method per data endpoint: `player_profile`, `player_transfers`, `club_players`,
42
+ `competition_clubs`, `competition_table`, `game`, and the remaining methods listed in the
43
+ [SDK method table](docs/plans/0001-python-sdk.md#public-contract). Methods return typed Pydantic models exported
44
+ from `transfermarkt_api.schemas`. Nested models remain available in their resource modules. Use string entity
45
+ and season IDs, integer country IDs, and 1-based page numbers. Calls return one page; callers control pagination.
46
+
47
+ Constructor settings are explicit: `base_url`, `timeout` (seconds, `httpx.Timeout`, or `None`),
48
+ `max_concurrency` (default `10`), `transport`, and `cache`. The SDK reads no `.env` or environment settings on import.
49
+ `CacheConfig(ttl=600, reference_ttl=86400, max_entries=2000)` configures per-client caching. Pass `cache=None` to
50
+ disable both response and reference caches so every request reaches the transport.
51
+
52
+ One client supports concurrent calls within one event loop. Its async context manager, or explicit `aclose()`,
53
+ closes the HTTP client and supplied transport. Do not share a supplied transport across client lifetimes unless
54
+ your wrapper owns and controls its closure. Cache hits remain local to each SDK instance.
55
+
56
+ The SDK raises `NotFound`, `UpstreamBlocked`, `UpstreamTimeout`, `UpstreamError`, or `EndpointUnavailable`, all
57
+ derived from `TransfermarktError` and exported at the package root. Available upstream URL/status metadata lives
58
+ on `error.url` and `error.status_code`; the status is the upstream status, not the server's mapped HTTP status.
59
+ The unsupported `player_jersey_numbers` method raises `EndpointUnavailable` without fetching data.
60
+ Invalid constructor/page/country arguments raise `ValueError`. Custom transport exceptions propagate unchanged.
61
+
62
+ ### Custom transports and replay
63
+
64
+ Supply an `httpx.AsyncBaseTransport` to control every upstream request, including reference lookups, batches,
65
+ and per-player fan-out. The SDK bounds in-flight concurrency; the caller's transport owns rate limits, retries,
66
+ proxy selection, recording and circuit breakers. The SDK performs no automatic retries.
67
+
68
+ ```python
69
+ import httpx
70
+
71
+ from transfermarkt_api import Transfermarkt
72
+
73
+ # Run inside an async function. A custom browser-capable backend can implement the same transport interface.
74
+ async with Transfermarkt(
75
+ transport=httpx.AsyncHTTPTransport(proxy="http://127.0.0.1:8080"),
76
+ cache=None,
77
+ ) as tm:
78
+ squad = await tm.club_players("985", season_id="2024")
79
+ ```
80
+
81
+ [Recording and replay examples](examples/transports.py) wrap a caller-selected backend and save the response
82
+ before SDK parsing. Persist the method, full URL with repeated query parameters, status, relevant headers (including
83
+ `x-amzn-waf-action`), decoded body, and original fetch timestamp. The example store is in memory; replace it with
84
+ durable storage in your application. When reconstructing decoded responses, drop content-encoding/content-length
85
+ headers so the body is not decoded twice.
86
+
87
+ A replay transport returns an `httpx.Response` with
88
+ `extensions={"transfermarkt_fetched_at": captured_datetime}`. This optional metadata accepts naive or timezone-aware
89
+ datetimes. Aware inputs are converted to UTC; naive inputs are interpreted as UTC. `updatedAt` retains its existing
90
+ JSON shape without a timezone suffix and always represents UTC. Without metadata, it reports the current UTC fetch
91
+ time. Each model reports the oldest fetch time used in that call, including cache hits. Exact replay
92
+ needs the same response bodies and timestamps. Missing recordings must raise a transport error rather than fetch
93
+ live data. Batch URLs are stable; concurrent request completion order is not part of the replay key.
94
+
95
+ ```python
96
+ from examples.transports import RecordingTransport, ReplayTransport
97
+
98
+ records = {}
99
+ async with Transfermarkt(transport=RecordingTransport(httpx.AsyncHTTPTransport(), records), cache=None) as tm:
100
+ captured = await tm.club_players("985")
101
+ async with Transfermarkt(transport=ReplayTransport(records), cache=None) as tm:
102
+ replayed = await tm.club_players("985")
103
+ assert captured == replayed
104
+ ```
105
+
106
+ The examples and HTTP server are repository files. The wheel installs only the `transfermarkt_api` package,
107
+ avoiding collisions with a consumer's own `app` package. The `server` extra installs the server dependencies;
108
+ run `uvicorn app.main:app` from a repository checkout, or use the Docker image.
109
+
110
+ ### API Swagger
111
+ https://transfermarkt-api.fly.dev/
112
+
113
+ ### Endpoints
114
+
115
+ | Endpoint | Notes |
116
+ | --- | --- |
117
+ | `GET /players/search/{player_name}?page_number=` | 10 results per page |
118
+ | `GET /players/{player_id}/profile` | |
119
+ | `GET /players/{player_id}/market_value` | |
120
+ | `GET /players/{player_id}/transfers` | Transfer type, age, contract dates, remaining contract days, and each club's league and country at the time |
121
+ | `GET /players/{player_id}/stats` | Per season, competition and club; club competitions only |
122
+ | `GET /players/{player_id}/matches?season_id=&page_number=` | Past matches of the player's teams, most recent first, 50 per page; no upcoming fixtures |
123
+ | `GET /players/{player_id}/injuries?page_number=` | 15 injuries per page |
124
+ | `GET /players/{player_id}/absences?page_number=` | Suspensions, call-ups, leave; 15 per page |
125
+ | `GET /players/{player_id}/achievements` | Titles and awards with season, club and competition |
126
+ | `GET /players/{player_id}/national_career` | Senior and youth national teams |
127
+ | `GET /players/{player_id}/jersey_numbers` | `501`: no data source |
128
+ | `GET /countries/` | Country IDs and metadata, including historical entries; cached for 24 hours |
129
+ | `GET /clubs/?country_id=` | Available club IDs and names for a country; limited upstream coverage, no pagination |
130
+ | `GET /clubs/search/{club_name}?page_number=` | 10 results per page |
131
+ | `GET /clubs/{club_id}/profile` | Squad value details, historical names and stadium details |
132
+ | `GET /clubs/{club_id}/players?season_id=` | Current squad by default; shirt number and captaincy for the selected season |
133
+ | `GET /clubs/{club_id}/achievements` | Club titles by season |
134
+ | `GET /coaches/search/{coach_name}?page_number=` | 10 results per page |
135
+ | `GET /coaches/{coach_id}/profile` | |
136
+ | `GET /competitions/search/{competition_name}?page_number=` | 10 results per page |
137
+ | `GET /competitions/{competition_id}/clubs?season_id=` | Current season by default |
138
+ | `GET /competitions/{competition_id}/table?season_id=` | League table; one table per group for group stages |
139
+ | `GET /competitions/{competition_id}/seasons` | Valid `season_id` values |
140
+ | `GET /games/{game_id}` | Score, lineups, coaches, stadium, team stats, goals, cards and substitutions |
141
+
142
+ Errors: `404` unknown ID, `501` endpoint without data source, `502` unexpected upstream response, `503` upstream blocked or refused the request, `504` upstream timeout.
143
+
144
+ Fields without an upstream value are returned as `null`. `updatedAt` is when the underlying data was fetched from Transfermarkt; responses are cached for 10 minutes by default.
145
+
146
+ ### Running Locally
147
+
148
+ Requires [uv](https://docs.astral.sh/uv/).
149
+
150
+ ````bash
151
+ # Clone the repository
152
+ $ git clone https://github.com/felipeall/transfermarkt-api.git
153
+
154
+ # Go to the project's root folder
155
+ $ cd transfermarkt-api
156
+
157
+ # Install Python 3.12 and the dependencies
158
+ $ uv sync --extra server
159
+
160
+ # Start the API server
161
+ $ uv run uvicorn app.main:app --reload
162
+
163
+ # Access the API local page
164
+ $ open http://localhost:8000/
165
+ ````
166
+
167
+ ### Running Tests
168
+
169
+ ````bash
170
+ # Offline tests (recorded upstream responses, no network)
171
+ $ uv run --extra server pytest
172
+
173
+ # Live smoke tests against Transfermarkt's JSON API
174
+ $ uv run --extra server pytest -m live
175
+
176
+ # Re-record upstream fixtures and response snapshots
177
+ $ rm -rf tests/fixtures/tfmkt && uv run python scripts/capture_fixtures.py
178
+ ````
179
+
180
+ ### Running via Docker
181
+
182
+ ````bash
183
+ # Clone the repository
184
+ $ git clone https://github.com/felipeall/transfermarkt-api.git
185
+
186
+ # Go to the project's root folder
187
+ $ cd transfermarkt-api
188
+
189
+ # Build the Docker image
190
+ $ docker build -t transfermarkt-api .
191
+
192
+ # Instantiate the Docker container
193
+ $ docker run -d -p 8000:8000 transfermarkt-api
194
+
195
+ # Access the API local page
196
+ $ open http://localhost:8000/
197
+ ````
198
+
199
+ ### Environment Variables
200
+
201
+ | Variable | Description | Default |
202
+ |-------------------------------|--------------------------------------------------------------------------------------------------------|------------------------------------------|
203
+ | `RATE_LIMITING_ENABLE` | Enable rate limiting feature for API calls | `false` |
204
+ | `RATE_LIMITING_FREQUENCY` | Requests allowed per client IP across all routes, in [limits notation](https://limits.readthedocs.io/en/stable/quickstart.html#rate-limit-string-notation) | `2/3seconds` |
205
+ | `TFMKT_BASE_URL` | Transfermarkt JSON API base URL | `https://tmapi.transfermarkt.technology` |
206
+ | `TFMKT_TIMEOUT_SECONDS` | Timeout for each upstream request | `15` |
207
+ | `TFMKT_MAX_CONCURRENCY` | Maximum concurrent upstream requests across the whole server | `10` |
208
+ | `CACHE_TTL_SECONDS` | How long successful upstream responses are cached | `600` |
209
+ | `CACHE_REFERENCE_TTL_SECONDS` | Cache duration for reference data (countries, positions, ...) | `86400` |
210
+ | `CACHE_MAX_ENTRIES` | Maximum number of cached upstream responses | `2000` |
211
+
212
+ Rate limits apply to the API routes only: `/health`, the docs (`/`, `/docs`, `/redoc`) and `/openapi.json` are not
213
+ counted. They are counted per client IP: the `Fly-Client-IP` header set by the Fly.io proxy, or the address of the
214
+ connection when that header is missing. `X-Forwarded-For` is not trusted. Behind another reverse proxy, all
215
+ requests share the proxy's address, and outside Fly.io clients can send `Fly-Client-IP` themselves, so adjust
216
+ `client_ip` in `app/main.py` before enabling rate limiting on another host.
File without changes
File without changes
@@ -0,0 +1,11 @@
1
+ from fastapi import APIRouter
2
+
3
+ from app.api.endpoints import clubs, coaches, competitions, countries, games, players
4
+
5
+ api_router = APIRouter()
6
+ api_router.include_router(competitions.router, prefix="/competitions", tags=["competitions"])
7
+ api_router.include_router(clubs.router, prefix="/clubs", tags=["clubs"])
8
+ api_router.include_router(players.router, prefix="/players", tags=["players"])
9
+ api_router.include_router(coaches.router, prefix="/coaches", tags=["coaches"])
10
+ api_router.include_router(countries.router, prefix="/countries", tags=["countries"])
11
+ api_router.include_router(games.router, prefix="/games", tags=["games"])
@@ -0,0 +1,13 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import Depends, Request
4
+
5
+ from transfermarkt_api import Transfermarkt
6
+
7
+
8
+ def get_tfmkt(request: Request) -> Transfermarkt:
9
+ """Return the shared upstream client created during server startup."""
10
+ return request.app.state.tfmkt
11
+
12
+
13
+ Tfmkt = Annotated[Transfermarkt, Depends(get_tfmkt)]
File without changes