├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .travis.yml ├── AUTHORS.rst ├── LICENSE ├── README.md ├── docs ├── Makefile ├── classes │ └── understat.rst ├── conf.py ├── contributing │ ├── authors.rst │ └── contributing.rst ├── index.rst ├── make.bat └── user │ └── installation.rst ├── setup.py ├── tests ├── __init__.py ├── conftest.py ├── test_understat.py └── test_utils.py ├── tox.ini └── understat ├── __init__.py ├── constants.py ├── understat.py └── utils.py /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: [push] 3 | jobs: 4 | unit-tests: 5 | 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | python-version: ["3.7", "3.8", "3.9"] 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install . 21 | - name: Run tests 22 | run: | 23 | pytest tests/ 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 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 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | test.py 107 | .vscode/ 108 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | dist: xenial 4 | sudo: required 5 | 6 | python: 7 | - "3.6" 8 | - "3.7" 9 | 10 | install: 11 | - python setup.py install 12 | 13 | script: 14 | - pytest --cov=fpl 15 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Maintainer 2 | `````````` 3 | 4 | - Amos Bastian `@amosbastian `_ 5 | 6 | Contributors 7 | ```````````` 8 | 9 | - Chris Musson `@ChrisMusson `_ 10 | - Gracjan Strzelec `@gracjans `_ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Amos Bastian 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | A Python package for https://understat.com/. 3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

13 | 14 | Join the [Discord server](https://discord.gg/cjY37fv) or submit [an issue](https://github.com/amosbastian/understat/issues) for help and / or suggestions! 15 | 16 | ## Installing understat 17 | 18 | The recommended way to install understat is via `pip`. 19 | 20 | pip install understat 21 | 22 | To install it directly from GitHub you can do the following: 23 | 24 | git clone git://github.com/amosbastian/understat.git 25 | 26 | You can also install a [.tar file](https://github.com/requests/requests/tarball/master) 27 | or [.zip file](https://github.com/requests/requests/tarball/master) 28 | 29 | curl -OL https://github.com/amosbastian/understat/tarball/master 30 | curl -OL https://github.com/amosbastian/understat/zipball/master # Windows 31 | 32 | Once it has been downloaded you can easily install it using `pip`: 33 | 34 | cd understat 35 | pip install . 36 | 37 | ## Usage 38 | 39 | An example of using `understat` can be found below: 40 | 41 | ```python 42 | import asyncio 43 | import json 44 | 45 | import aiohttp 46 | 47 | from understat import Understat 48 | 49 | 50 | async def main(): 51 | async with aiohttp.ClientSession() as session: 52 | understat = Understat(session) 53 | data = await understat.get_league_players("epl", 2018, {"team_title": "Manchester United"}) 54 | print(json.dumps(data)) 55 | 56 | 57 | if __name__ == "__main__": 58 | loop = asyncio.new_event_loop() 59 | asyncio.set_event_loop(loop) 60 | loop.run_until_complete(main()) 61 | ``` 62 | 63 | 64 | ## Contributing 65 | 66 | 1. Fork the repository on GitHub. 67 | 2. Run the tests with `pytest tests/` to confirm they all pass on your system. 68 | If the tests fail, then try and find out why this is happening. If you aren't 69 | able to do this yourself, then don't hesitate to either create an issue on 70 | GitHub, or send an email to [amosbastian@gmail.com](mailto:amosbastian@gmail.com>). 71 | 3. Either create your feature and then write tests for it, or do this the other 72 | way around. 73 | 4. Run all tests again with with `pytest tests/` to confirm that everything 74 | still passes, including your newly added test(s). 75 | 5. Create a pull request for the main repository's `master` branch. 76 | 77 | ## Documentation 78 | 79 | Documentation and examples for **understat** can be found at http://understat.readthedocs.io/en/latest/. 80 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Understat 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/classes/understat.rst: -------------------------------------------------------------------------------- 1 | Understat 2 | ================ 3 | 4 | .. module:: understat 5 | 6 | The :class:`Understat ` class is the main, and only class 7 | used for interacting with Understat's data. It requires a 8 | ``aiohttp.ClientSession`` for sending requests, so typical usage of the 9 | :class:`Understat ` class can look something like this: 10 | 11 | .. code-block:: python 12 | 13 | import asyncio 14 | import json 15 | 16 | import aiohttp 17 | 18 | from understat import Understat 19 | 20 | 21 | async def main(): 22 | async with aiohttp.ClientSession() as session: 23 | understat = Understat(session) 24 | player = await understat.get_league_players( 25 | "epl", 2018, 26 | player_name="Paul Pogba", 27 | team_title="Manchester United" 28 | ) 29 | print(json.dumps(player)) 30 | 31 | loop = asyncio.get_event_loop() 32 | loop.run_until_complete(main()) 33 | 34 | >>>[{"id": "1740", "player_name": "Paul Pogba", "games": "27", "time": "2293", "goals": "11", "xG": "13.361832823604345", "assists": "9", "xA": "4.063152700662613", "shots": "87", "key_passes": "40", "yellow_cards": "5", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "6", "npxG": "7.272482139989734", "xGChain": "17.388037759810686", "xGBuildup": "8.965998269617558"}] 35 | 36 | The functions 37 | ------------- 38 | 39 | Below each function of the :class:`Understat ` class will 40 | be documented separately. It will also show a screenshot of the equivalent data 41 | on `understat.com `_, and an example of how the function 42 | itself could be used. 43 | 44 | Most of the functions come with the `options` keyword argument, and the 45 | `**kwargs` magic variable, which means that their output can be filtered 46 | (the ways this can be done depends entirely on the output). It was the easiest 47 | way to implement something like this, but may not always be optimal (e.g. 48 | filtering by home team may require an object for example), and so this could be 49 | changed in the future. 50 | 51 | If you have any suggestions on what kind of filtering 52 | options you'd like to see for certain functions, then you can create an 53 | `issue `_ for this. Also, any 54 | help with adding better filtering, if necessary, is also very much appreciated! 55 | 56 | --- 57 | 58 | .. automethod:: understat.Understat.get_league_fixtures 59 | 60 | It returns the fixtures (not results) of the given league, in the given season. 61 | So for example, the fixtures as seen in the screenshot below 62 | 63 | .. image:: https://i.imgur.com/dE54ox0.png 64 | 65 | The function comes with the `options` keyword argument, and the `**kwargs` 66 | magic variable, and so that can be used to filter the output. 67 | So for example, if you wanted to get all Manchester United's upcoming fixtures 68 | at **home**, then you could do the following 69 | 70 | .. code-block:: python 71 | 72 | async def main(): 73 | async with aiohttp.ClientSession() as session: 74 | understat = Understat(session) 75 | fixtures = await understat.get_league_fixtures( 76 | "epl", 77 | 2018, 78 | { 79 | "h": {"id": "89", 80 | "title": "Manchester United", 81 | "short_title": "MUN"} 82 | } 83 | ) 84 | print(json.dumps(fixtures)) 85 | 86 | loop = asyncio.get_event_loop() 87 | loop.run_until_complete(main()) 88 | 89 | which outputs (with parts omitted) 90 | 91 | .. code-block:: javascript 92 | 93 | [ 94 | { 95 | "id": "9501", 96 | "isResult": false, 97 | "h": { 98 | "id": "89", 99 | "title": "Manchester United", 100 | "short_title": "MUN" 101 | }, 102 | "a": { 103 | "id": "88", 104 | "title": "Manchester City", 105 | "short_title": "MCI" 106 | }, 107 | "goals": { 108 | "h": null, 109 | "a": null 110 | }, 111 | "xG": { 112 | "h": null, 113 | "a": null 114 | }, 115 | "datetime": "2019-03-16 18:00:00" 116 | }, 117 | ... 118 | { 119 | "id": "9570", 120 | "isResult": false, 121 | "h": { 122 | "id": "89", 123 | "title": "Manchester United", 124 | "short_title": "MUN" 125 | }, 126 | "a": { 127 | "id": "227", 128 | "title": "Cardiff", 129 | "short_title": "CAR" 130 | }, 131 | "goals": { 132 | "h": null, 133 | "a": null 134 | }, 135 | "xG": { 136 | "h": null, 137 | "a": null 138 | }, 139 | "datetime": "2019-05-12 17:00:00" 140 | } 141 | ] 142 | 143 | --- 144 | 145 | .. automethod:: understat.Understat.get_league_table 146 | 147 | It returns the standings of the given league in the given year, as seen in the screenshot below 148 | 149 | .. image:: https://i.imgur.com/fYo9zkz.png 150 | 151 | There are also optional "start_date" and "end_date" arguments, 152 | which can be used to get the table from a specific date range from given season, like on screenshot below 153 | 154 | .. image:: https://i.imgur.com/r30bdpn.png 155 | 156 | An example of getting the standings from the EPL in 2019 can be found below 157 | 158 | .. code-block:: python 159 | 160 | async def main(): 161 | async with aiohttp.ClientSession() as session: 162 | understat = Understat(session) 163 | table = await understat.get_league_table("EPL", "2019") 164 | print(table) 165 | 166 | loop = asyncio.get_event_loop() 167 | loop.run_until_complete(main()) 168 | 169 | which outputs (with parts omitted) 170 | 171 | .. code-block:: javascript 172 | 173 | [ 174 | ['Team', 'M', 'W', 'D', 'L', 'G', 'GA', 'PTS', 'xG', 'NPxG', 'xGA', 'NPxGA', 'NPxGD', 'PPDA', 'OPPDA', 'DC', 'ODC', 'xPTS'], 175 | ['Liverpool', 38, 32, 3, 3, 85, 33, 99, 75.19, 71.39, 39.57, 38.81, 32.58, 8.01, 21.33, 429, 145, 74.28], 176 | ['Manchester City', 38, 26, 3, 9, 102, 35, 81, 102.21, 93.53, 37.0, 34.71, 58.82, 8.49, 23.77, 547, 135, 86.76], 177 | ['Manchester United', 38, 18, 12, 8, 66, 36, 66, 66.19, 55.53, 38.06, 35.78, 19.75, 9.64, 11.1, 290, 178, 70.99], 178 | ..., 179 | ['Aston Villa', 38, 9, 8, 21, 41, 67, 35, 45.09, 42.65, 71.6, 66.88, -24.23, 12.34, 7.89, 186, 343, 37.23], 180 | ['Bournemouth', 38, 9, 7, 22, 40, 65, 34, 44.67, 41.63, 63.29, 58.73, -17.1, 13.38, 9.15, 210, 326, 39.2], 181 | ['Watford', 38, 8, 10, 20, 36, 64, 34, 48.56, 42.47, 59.53, 52.52, -10.05, 12.2, 9.64, 227, 259, 47.87], 182 | ['Norwich', 38, 5, 6, 27, 26, 75, 21, 37.23, 35.71, 71.61, 66.13, -30.41, 12.59, 9.65, 207, 345, 33.12] 183 | ] 184 | 185 | --- 186 | 187 | .. automethod:: understat.Understat.get_player_grouped_stats 188 | 189 | It returns all the statistics of a given player, which includes stuff like 190 | their performance per season, position and more. Basically, it's everything 191 | that can be found in the table shown in the screenshot below 192 | 193 | .. image:: https://i.imgur.com/gEMSKin.png 194 | 195 | An example of getting Sergio Agüero's grouped data can be found below 196 | 197 | .. code-block:: python 198 | 199 | async def main(): 200 | async with aiohttp.ClientSession() as session: 201 | understat = Understat(session) 202 | grouped_stats = await understat.get_player_grouped_stats(619) 203 | print(json.dumps(grouped_stats)) 204 | 205 | loop = asyncio.get_event_loop() 206 | loop.run_until_complete(main()) 207 | 208 | which outputs (with parts omitted) 209 | 210 | .. code-block:: javascript 211 | 212 | { 213 | "season": [ 214 | { 215 | "position": "FW", 216 | "games": "26", 217 | "goals": "18", 218 | "shots": "95", 219 | "time": "1960", 220 | "xG": "17.515484783798456", 221 | "assists": "6", 222 | "xA": "3.776376834139228", 223 | "key_passes": "25", 224 | "season": "2018", 225 | "team": "Manchester City", 226 | "yellow": "3", 227 | "red": "0", 228 | "npg": "16", 229 | "npxG": "15.9931472055614", 230 | "xGChain": "23.326821692287922", 231 | "xGBuildup": "6.351545065641403" 232 | }, 233 | ..., 234 | { 235 | "position": "Sub", 236 | "games": "33", 237 | "goals": "26", 238 | "shots": "148", 239 | "time": "2551", 240 | "xG": "25.270159743726254", 241 | "assists": "8", 242 | "xA": "5.568922242149711", 243 | "key_passes": "33", 244 | "season": "2014", 245 | "team": "Manchester City", 246 | "yellow": "4", 247 | "red": "0", 248 | "npg": "21", 249 | "npxG": "20.70318364351988", 250 | "xGChain": "27.805154908448458", 251 | "xGBuildup": "6.878173082135618" 252 | } 253 | ], 254 | "position": { 255 | "2018": { 256 | "FW": { 257 | "position": "FW", 258 | "games": "24", 259 | "goals": "18", 260 | "shots": "94", 261 | "time": "1911", 262 | "xG": "17.464063242077827", 263 | "assists": "6", 264 | "xA": "3.776376834139228", 265 | "key_passes": "25", 266 | "season": "2018", 267 | "yellow": "3", 268 | "red": "0", 269 | "npg": "16", 270 | "npxG": "15.94172566384077", 271 | "xGChain": "23.258203461766243", 272 | "xGBuildup": "6.334348376840353" 273 | }, 274 | "Sub": { 275 | "position": "Sub", 276 | "games": "2", 277 | "goals": "0", 278 | "shots": "1", 279 | "time": "49", 280 | "xG": "0.05142154172062874", 281 | "assists": "0", 282 | "xA": "0", 283 | "key_passes": "0", 284 | "season": "2018", 285 | "yellow": "0", 286 | "red": "0", 287 | "npg": "0", 288 | "npxG": "0.05142154172062874", 289 | "xGChain": "0.06861823052167892", 290 | "xGBuildup": "0.017196688801050186" 291 | } 292 | }, 293 | ..., 294 | }, 295 | "2014": { 296 | "FW": { 297 | "position": "FW", 298 | "games": "30", 299 | "goals": "24", 300 | "shots": "142", 301 | "time": "2504", 302 | "xG": "24.362012460827827", 303 | "assists": "8", 304 | "xA": "5.568922242149711", 305 | "key_passes": "33", 306 | "season": "2014", 307 | "yellow": "4", 308 | "red": "0", 309 | "npg": "19", 310 | "npxG": "19.795036360621452", 311 | "xGChain": "26.94415594637394", 312 | "xGBuildup": "6.878173082135618" 313 | }, 314 | "Sub": { 315 | "position": "Sub", 316 | "games": "3", 317 | "goals": "2", 318 | "shots": "6", 319 | "time": "47", 320 | "xG": "0.9081472828984261", 321 | "assists": "0", 322 | "xA": "0", 323 | "key_passes": "0", 324 | "season": "2014", 325 | "yellow": "0", 326 | "red": "0", 327 | "npg": "2", 328 | "npxG": "0.9081472828984261", 329 | "xGChain": "0.8609989620745182", 330 | "xGBuildup": "0" 331 | } 332 | } 333 | }, 334 | "situation": { 335 | "2015": { 336 | "OpenPlay": { 337 | "situation": "OpenPlay", 338 | "season": "2015", 339 | "goals": "17", 340 | "shots": "97", 341 | "xG": "13.971116883680224", 342 | "assists": "2", 343 | "key_passes": "26", 344 | "xA": "2.0287596937268972", 345 | "npg": "17", 346 | "npxG": "13.971116883680224", 347 | "time": 2399 348 | }, 349 | "FromCorner": { 350 | "situation": "FromCorner", 351 | "season": "2015", 352 | "goals": "2", 353 | "shots": "11", 354 | "xG": "1.8276203628629446", 355 | "assists": "0", 356 | "key_passes": "0", 357 | "xA": "0", 358 | "npg": "2", 359 | "npxG": "1.8276203628629446", 360 | "time": 2399 361 | }, 362 | "Penalty": { 363 | "situation": "Penalty", 364 | "season": "2015", 365 | "goals": "4", 366 | "shots": "5", 367 | "xG": "3.8058441877365112", 368 | "assists": "0", 369 | "key_passes": "0", 370 | "xA": "0", 371 | "npg": "0", 372 | "npxG": "0", 373 | "time": 2399 374 | }, 375 | ..., 376 | "2014": { 377 | "OpenPlay": { 378 | "situation": "OpenPlay", 379 | "season": "2014", 380 | "goals": "19", 381 | "shots": "128", 382 | "xG": "18.23446972388774", 383 | "assists": "7", 384 | "key_passes": "32", 385 | "xA": "4.622839629650116", 386 | "npg": "19", 387 | "npxG": "18.23446972388774", 388 | "time": 2551 389 | }, 390 | "FromCorner": { 391 | "situation": "FromCorner", 392 | "season": "2014", 393 | "goals": "1", 394 | "shots": "12", 395 | "xG": "1.8788630235940218", 396 | "assists": "1", 397 | "key_passes": "1", 398 | "xA": "0.9460826516151428", 399 | "npg": "1", 400 | "npxG": "1.8788630235940218", 401 | "time": 2551 402 | }, 403 | "Penalty": { 404 | "situation": "Penalty", 405 | "season": "2014", 406 | "goals": "5", 407 | "shots": "6", 408 | "xG": "4.566976249217987", 409 | "assists": "0", 410 | "key_passes": "0", 411 | "xA": "0", 412 | "npg": "0", 413 | "npxG": "0", 414 | "time": 2551 415 | }, 416 | "SetPiece": { 417 | "situation": "SetPiece", 418 | "season": "2014", 419 | "goals": "1", 420 | "shots": "2", 421 | "xG": "0.5898510366678238", 422 | "assists": "0", 423 | "key_passes": "0", 424 | "xA": "0", 425 | "npg": "1", 426 | "npxG": "0.5898510366678238", 427 | "time": 2551 428 | } 429 | } 430 | }, 431 | "shotZones": { 432 | "2014": { 433 | "shotOboxTotal": { 434 | "shotZones": "shotOboxTotal", 435 | "season": "2014", 436 | "goals": "2", 437 | "shots": "33", 438 | "xG": "1.5900825830176473", 439 | "assists": "2", 440 | "key_passes": "9", 441 | "xA": "0.3100438117980957", 442 | "npg": "2", 443 | "npxG": "1.5900825830176473" 444 | }, 445 | "shotPenaltyArea": { 446 | "shotZones": "shotPenaltyArea", 447 | "season": "2014", 448 | "goals": "22", 449 | "shots": "108", 450 | "xG": "19.79369100742042", 451 | "assists": "5", 452 | "key_passes": "22", 453 | "xA": "3.9576267898082733", 454 | "npg": "17", 455 | "npxG": "15.226714758202434" 456 | }, 457 | "shotSixYardBox": { 458 | "shotZones": "shotSixYardBox", 459 | "season": "2014", 460 | "goals": "2", 461 | "shots": "7", 462 | "xG": "3.8863864429295063", 463 | "assists": "1", 464 | "key_passes": "2", 465 | "xA": "1.3012516796588898", 466 | "npg": "2", 467 | "npxG": "3.8863864429295063" 468 | } 469 | }, 470 | ..., 471 | "2018": { 472 | "shotOboxTotal": { 473 | "shotZones": "shotOboxTotal", 474 | "season": "2018", 475 | "goals": "2", 476 | "shots": "21", 477 | "xG": "0.8707829182967544", 478 | "assists": "1", 479 | "key_passes": "9", 480 | "xA": "0.31408058758825064", 481 | "npg": "2", 482 | "npxG": "0.8707829182967544" 483 | }, 484 | "shotPenaltyArea": { 485 | "shotZones": "shotPenaltyArea", 486 | "season": "2018", 487 | "goals": "12", 488 | "shots": "65", 489 | "xG": "11.844964944757521", 490 | "assists": "4", 491 | "key_passes": "14", 492 | "xA": "2.1070052348077297", 493 | "npg": "10", 494 | "npxG": "10.322627269662917" 495 | }, 496 | "shotSixYardBox": { 497 | "shotZones": "shotSixYardBox", 498 | "season": "2018", 499 | "goals": "4", 500 | "shots": "9", 501 | "xG": "4.799736991524696", 502 | "assists": "1", 503 | "key_passes": "2", 504 | "xA": "1.3552910089492798", 505 | "npg": "4", 506 | "npxG": "4.799736991524696" 507 | } 508 | } 509 | }, 510 | "shotTypes": { 511 | "2014": { 512 | "RightFoot": { 513 | "shotTypes": "RightFoot", 514 | "season": "2014", 515 | "goals": "18", 516 | "shots": "96", 517 | "xG": "17.13349057827145", 518 | "assists": "5", 519 | "key_passes": "19", 520 | "xA": "3.883937703445554", 521 | "npg": "13", 522 | "npxG": "12.566514329053462" 523 | }, 524 | "LeftFoot": { 525 | "shotTypes": "LeftFoot", 526 | "season": "2014", 527 | "goals": "7", 528 | "shots": "40", 529 | "xG": "6.236775731667876", 530 | "assists": "3", 531 | "key_passes": "13", 532 | "xA": "1.6454832945019007", 533 | "npg": "7", 534 | "npxG": "6.236775731667876" 535 | }, 536 | "Head": { 537 | "shotTypes": "Head", 538 | "season": "2014", 539 | "goals": "1", 540 | "shots": "12", 541 | "xG": "1.8998937234282494", 542 | "assists": "0", 543 | "key_passes": "1", 544 | "xA": "0.03950128331780434", 545 | "npg": "1", 546 | "npxG": "1.8998937234282494" 547 | } 548 | }, 549 | ..., 550 | }, 551 | "2018": { 552 | "RightFoot": { 553 | "shotTypes": "RightFoot", 554 | "season": "2018", 555 | "goals": "9", 556 | "shots": "58", 557 | "xG": "9.876922971569002", 558 | "assists": "3", 559 | "key_passes": "9", 560 | "xA": "1.6752301333472133", 561 | "npg": "7", 562 | "npxG": "8.354585296474397" 563 | }, 564 | "LeftFoot": { 565 | "shotTypes": "LeftFoot", 566 | "season": "2018", 567 | "goals": "6", 568 | "shots": "26", 569 | "xG": "4.921279687434435", 570 | "assists": "3", 571 | "key_passes": "16", 572 | "xA": "2.101146697998047", 573 | "npg": "6", 574 | "npxG": "4.921279687434435" 575 | }, 576 | "Head": { 577 | "shotTypes": "Head", 578 | "season": "2018", 579 | "goals": "2", 580 | "shots": "10", 581 | "xG": "1.8183354930952191", 582 | "assists": "0", 583 | "key_passes": "0", 584 | "xA": "0", 585 | "npg": "2", 586 | "npxG": "1.8183354930952191" 587 | }, 588 | "OtherBodyPart": { 589 | "shotTypes": "OtherBodyPart", 590 | "season": "2018", 591 | "goals": "1", 592 | "shots": "1", 593 | "xG": "0.8989467024803162", 594 | "assists": "0", 595 | "key_passes": "0", 596 | "xA": "0", 597 | "npg": "1", 598 | "npxG": "0.8989467024803162" 599 | } 600 | } 601 | } 602 | } 603 | 604 | --- 605 | 606 | .. automethod:: understat.Understat.get_match_stats 607 | 608 | It returns the stats for the given match. 609 | So for example, the match stats from Wolves v Chelsea 2023-12-24, as seen in the screenshot: 610 | 611 | .. image:: https://i.imgur.com/JJA1wnx.png 612 | 613 | To view the match stats for Wolves v Chelsea 2023-12-24, you can use the following code: 614 | 615 | .. code-block:: python 616 | 617 | async def main(): 618 | async with aiohttp.ClientSession() as session: 619 | understat = Understat(session) 620 | match_stats = await understat.get_match_stats(22073) 621 | print(json.dumps(match_stats)) 622 | 623 | loop = asyncio.get_event_loop() 624 | loop.run_until_complete(main()) 625 | 626 | which outputs 627 | 628 | .. code-block:: javascript 629 | 630 | { 631 | "id": "22073", 632 | "fid": "1729511", 633 | "h": "229", 634 | "a": "80", 635 | "date": "2023-12-24 13:00:00", 636 | "league_id": "1", 637 | "season": "2023", 638 | "h_goals": "2", 639 | "a_goals": "1", 640 | "team_h": "Wolverhampton Wanderers", 641 | "team_a": "Chelsea", 642 | "h_xg": "1.74626", 643 | "a_xg": "1.75447", 644 | "h_w": "0.3603", 645 | "h_d": "0.2626", 646 | "h_l": "0.3771", 647 | "league": "EPL", 648 | "h_shot": "14", 649 | "a_shot": "16", 650 | "h_shotOnTarget": "6", 651 | "a_shotOnTarget": "5", 652 | "h_deep": "6", 653 | "a_deep": "19", 654 | "a_ppda": "6.7059", 655 | "h_ppda": "11.5172" 656 | } 657 | 658 | --- 659 | 660 | .. automethod:: understat.Understat.get_player_matches 661 | 662 | It returns the information about the matches played by the given player. So for 663 | example, the matches Sergio Agüero has played, as seen in the screenshot 664 | 665 | .. image:: https://i.imgur.com/p7jh1mh.png 666 | 667 | This function also comes with the `options` keyword argument, and also the 668 | `**kwargs` magic variable. An example of how you could 669 | use either of these to filter Sergio Agüero's matches to only include matches 670 | where Manchester United were the home team is shown below 671 | 672 | .. code-block:: python 673 | 674 | async def main(): 675 | async with aiohttp.ClientSession() as session: 676 | understat = Understat(session) 677 | # Using **kwargs 678 | player_matches = await understat.get_player_matches( 679 | 619, h_team="Manchester United") 680 | # Or using options keyword arugment 681 | player_matches = await understat.get_player_matches( 682 | 619, {"h_team": "Manchester United"}) 683 | print(json.dumps(player_matches)) 684 | 685 | loop = asyncio.get_event_loop() 686 | loop.run_until_complete(main()) 687 | 688 | which outputs 689 | 690 | .. code-block:: javascript 691 | 692 | [ 693 | { 694 | "goals": "2", 695 | "shots": "5", 696 | "xG": "1.4754852056503296", 697 | "time": "90", 698 | "position": "FW", 699 | "h_team": "Manchester United", 700 | "a_team": "Manchester City", 701 | "h_goals": "4", 702 | "a_goals": "2", 703 | "date": "2015-04-12", 704 | "id": "4459", 705 | "season": "2014", 706 | "roster_id": "23306", 707 | "xA": "0", 708 | "assists": "0", 709 | "key_passes": "0", 710 | "npg": "2", 711 | "npxG": "1.4754852056503296", 712 | "xGChain": "1.4855852127075195", 713 | "xGBuildup": "0.04120262712240219" 714 | } 715 | ] 716 | 717 | Since the usage of both the `options` keyword argument and the `**kwargs` magic 718 | variable have been shown, the examples following this will only show *one* of 719 | the two. 720 | 721 | --- 722 | 723 | .. automethod:: understat.Understat.get_player_shots 724 | 725 | It returns the given player's shot data, which includes information about the 726 | situation (open play, freekick etc.), if it hit the post or was a goal, and 727 | more. Basically, all the information that you can get from a player's page in 728 | the section shown below 729 | 730 | .. image:: https://i.imgur.com/t80WF5r.png 731 | 732 | The function comes with the `options` keyword argument, and the `**kwargs` 733 | magic variable, and so that can be used to filter the output. So for example, 734 | if you wanted to get all Sergio Agüero's shots (not necessarily goals) that 735 | were assisted by Fernandinho, then you could do the following 736 | 737 | .. code-block:: python 738 | 739 | async def main(): 740 | async with aiohttp.ClientSession() as session: 741 | understat = Understat(session) 742 | player_shots = await understat.get_player_shots( 743 | 619, {"player_assisted": "Fernandinho"}) 744 | print(json.dumps(player_shots)) 745 | 746 | loop = asyncio.get_event_loop() 747 | loop.run_until_complete(main()) 748 | 749 | which outputs (with parts omitted) 750 | 751 | .. code-block:: javascript 752 | 753 | [ 754 | { 755 | "id": "14552", 756 | "minute": "91", 757 | "result": "SavedShot", 758 | "X": "0.9259999847412109", 759 | "Y": "0.6809999847412109", 760 | "xG": "0.0791548416018486", 761 | "player": "Sergio Ag\u00fcero", 762 | "h_a": "a", 763 | "player_id": "619", 764 | "situation": "OpenPlay", 765 | "season": "2014", 766 | "shotType": "LeftFoot", 767 | "match_id": "4757", 768 | "h_team": "Newcastle United", 769 | "a_team": "Manchester City", 770 | "h_goals": "0", 771 | "a_goals": "2", 772 | "date": "2014-08-17 16:00:00", 773 | "player_assisted": "Fernandinho", 774 | "lastAction": "Pass" 775 | }, 776 | ..., 777 | { 778 | "id": "233670", 779 | "minute": "15", 780 | "result": "MissedShots", 781 | "X": "0.7419999694824219", 782 | "Y": "0.5359999847412109", 783 | "xG": "0.029104366898536682", 784 | "player": "Sergio Ag\u00fcero", 785 | "h_a": "h", 786 | "player_id": "619", 787 | "situation": "OpenPlay", 788 | "season": "2018", 789 | "shotType": "RightFoot", 790 | "match_id": "9234", 791 | "h_team": "Manchester City", 792 | "a_team": "Newcastle United", 793 | "h_goals": "2", 794 | "a_goals": "1", 795 | "date": "2018-09-01 16:30:00", 796 | "player_assisted": "Fernandinho", 797 | "lastAction": "Pass" 798 | } 799 | ] 800 | 801 | --- 802 | 803 | .. automethod:: understat.Understat.get_player_stats 804 | 805 | It returns the player's average stats overall, which includes stuff like their 806 | average goals per 90 minutes, average expected assists per 90 minutes and more. 807 | Basically everything you can see on a player's page in the section shown below 808 | 809 | .. image:: https://i.imgur.com/uJ2o0zi.png 810 | 811 | The function comes with the `positions` argument, which can be used to filter 812 | the stats by position(s). So for example, if you wanted to get Sergio Agüero's 813 | performance as a forward, then you could do the following 814 | 815 | .. code-block:: python 816 | 817 | async def main(): 818 | async with aiohttp.ClientSession() as session: 819 | understat = Understat(session) 820 | player_stats = await understat.get_player_stats(619, ["FW"]) 821 | print(json.dumps(player_stats)) 822 | 823 | loop = asyncio.get_event_loop() 824 | loop.run_until_complete(main()) 825 | 826 | which outputs 827 | 828 | .. code-block:: javascript 829 | 830 | [ 831 | { 832 | "goals": { 833 | "min": 0.0011, 834 | "max": 0.0126, 835 | "avg": 0.0042 836 | }, 837 | "xG": { 838 | "min": 0.00172821, 839 | "max": 0.0120816, 840 | "avg": 0.00415549 841 | }, 842 | "shots": { 843 | "min": 0.015, 844 | "max": 0.0737, 845 | "avg": 0.028 846 | }, 847 | "assists": { 848 | "min": 0, 849 | "max": 0.0048, 850 | "avg": 0.0014 851 | }, 852 | "xA": { 853 | "min": 0.000264191, 854 | "max": 0.00538174, 855 | "avg": 0.00131568 856 | }, 857 | "key_passes": { 858 | "min": 0.0036, 859 | "max": 0.0309, 860 | "avg": 0.012 861 | }, 862 | "xGChain": { 863 | "min": 0.00272705, 864 | "max": 0.0169137, 865 | "avg": 0.00533791 866 | }, 867 | "xGBuildup": { 868 | "min": 0.000243189, 869 | "max": 0.00671256, 870 | "avg": 0.00131848 871 | }, 872 | "position": "FW" 873 | } 874 | ] 875 | 876 | --- 877 | 878 | .. automethod:: understat.Understat.get_league_players 879 | 880 | It returns all the information about the players in a given league in the given 881 | season. This includes stuff like their number of goals scored, their total 882 | expected assists and more. Basically, it's all the information you can find 883 | in the player table shown on all league overview pages on 884 | `understat.com `_. 885 | 886 | .. image:: https://i.imgur.com/vPJzqnd.png 887 | 888 | The function comes with the `options` keyword argument, and the `**kwargs` 889 | magic variable, and so that can be used to filter the output. So for example, 890 | if you wanted to get all the players who play for Manchester United, then you 891 | could do the following 892 | 893 | .. code-block:: python 894 | 895 | async def main(): 896 | async with aiohttp.ClientSession() as session: 897 | understat = Understat(session) 898 | players = await understat.get_league_players( 899 | "epl", 900 | 2018, 901 | team_title="Manchester United" 902 | ) 903 | print(json.dumps(players)) 904 | 905 | loop = asyncio.get_event_loop() 906 | loop.run_until_complete(main()) 907 | 908 | which outputs (with parts omitted) 909 | 910 | .. code-block:: javascript 911 | 912 | [ 913 | { 914 | "id": "594", 915 | "player_name": "Romelu Lukaku", 916 | "games": "27", 917 | "time": "1768", 918 | "goals": "12", 919 | "xG": "12.054240763187408", 920 | "assists": "0", 921 | "xA": "1.6836179178208113", 922 | "shots": "50", 923 | "key_passes": "17", 924 | "yellow_cards": "4", 925 | "red_cards": "0", 926 | "position": "F S", 927 | "team_title": "Manchester United", 928 | "npg": "12", 929 | "npxG": "12.054240763187408", 930 | "xGChain": "12.832402393221855", 931 | "xGBuildup": "3.366600174456835" 932 | }, 933 | ..., 934 | { 935 | "id": "1740", 936 | "player_name": "Paul Pogba", 937 | "games": "27", 938 | "time": "2293", 939 | "goals": "11", 940 | "xG": "13.361832823604345", 941 | "assists": "9", 942 | "xA": "4.063152700662613", 943 | "shots": "87", 944 | "key_passes": "40", 945 | "yellow_cards": "5", 946 | "red_cards": "0", 947 | "position": "M S", 948 | "team_title": "Manchester United", 949 | "npg": "6", 950 | "npxG": "7.272482139989734", 951 | "xGChain": "17.388037759810686", 952 | "xGBuildup": "8.965998269617558" 953 | } 954 | ] 955 | 956 | --- 957 | 958 | .. automethod:: understat.Understat.get_league_results 959 | 960 | It returns the results (not fixtures) of the given league, in the given season. 961 | So for example, the results as seen in the screenshot below 962 | 963 | .. image:: https://i.imgur.com/LyWGAJw.png 964 | 965 | The function comes with the `options` keyword argument, and the `**kwargs` 966 | magic variable, and so that can be used to filter the output. So for example, 967 | if you wanted to get all Manchester United's results away from home, then you 968 | could do the following 969 | 970 | .. code-block:: python 971 | 972 | async def main(): 973 | async with aiohttp.ClientSession() as session: 974 | understat = Understat(session) 975 | fixtures = await understat.get_league_results( 976 | "epl", 977 | 2018, 978 | { 979 | "a": {"id": "89", 980 | "title": "Manchester United", 981 | "short_title": "MUN"} 982 | } 983 | ) 984 | print(json.dumps(fixtures)) 985 | 986 | loop = asyncio.get_event_loop() 987 | loop.run_until_complete(main()) 988 | 989 | which outputs (with parts omitted) 990 | 991 | .. code-block:: javascript 992 | 993 | [ 994 | { 995 | "id": "9215", 996 | "isResult": true, 997 | "h": { 998 | "id": "220", 999 | "title": "Brighton", 1000 | "short_title": "BRI" 1001 | }, 1002 | "a": { 1003 | "id": "89", 1004 | "title": "Manchester United", 1005 | "short_title": "MUN" 1006 | }, 1007 | "goals": { 1008 | "h": "3", 1009 | "a": "2" 1010 | }, 1011 | "xG": { 1012 | "h": "1.63672", 1013 | "a": "1.56579" 1014 | }, 1015 | "datetime": "2018-08-19 18:00:00", 1016 | "forecast": { 1017 | "w": "0.3538", 1018 | "d": "0.3473", 1019 | "l": "0.2989" 1020 | } 1021 | }, 1022 | ..., 1023 | { 1024 | "id": "9496", 1025 | "isResult": true, 1026 | "h": { 1027 | "id": "83", 1028 | "title": "Arsenal", 1029 | "short_title": "ARS" 1030 | }, 1031 | "a": { 1032 | "id": "89", 1033 | "title": "Manchester United", 1034 | "short_title": "MUN" 1035 | }, 1036 | "goals": { 1037 | "h": "2", 1038 | "a": "0" 1039 | }, 1040 | "xG": { 1041 | "h": "1.52723", 1042 | "a": "2.3703" 1043 | }, 1044 | "datetime": "2019-03-10 16:30:00", 1045 | "forecast": { 1046 | "w": "0.1667", 1047 | "d": "0.227", 1048 | "l": "0.6063" 1049 | } 1050 | } 1051 | ] 1052 | 1053 | --- 1054 | 1055 | .. automethod:: understat.Understat.get_match_players 1056 | 1057 | It returns information about the players who played in the given match. 1058 | So for example, the players seen in the screenshot below 1059 | 1060 | .. image:: https://i.imgur.com/b7etgfp.png 1061 | 1062 | An example of getting the players who played in the match between Manchester 1063 | United and Chelsea on 11 August, 2019 which ended 4-0 can be seen below: 1064 | 1065 | .. code-block:: python 1066 | 1067 | async def main(): 1068 | async with aiohttp.ClientSession() as session: 1069 | understat = Understat(session) 1070 | players = await understat.get_match_players(11652) 1071 | print(json.dumps(players)) 1072 | 1073 | loop = asyncio.get_event_loop() 1074 | loop.run_until_complete(main()) 1075 | 1076 | which outputs (with parts omitted) 1077 | 1078 | .. code-block:: javascript 1079 | 1080 | { 1081 | "h": { 1082 | "341628": { 1083 | "id": "341628", 1084 | "goals": "2", 1085 | "own_goals": "0", 1086 | "shots": "4", 1087 | "xG": "1.3030972480773926", 1088 | "time": "88", 1089 | "player_id": "556", 1090 | "team_id": "89", 1091 | "position": "AML", 1092 | "player": "Marcus Rashford", 1093 | "h_a": "h", 1094 | "yellow_card": "0", 1095 | "red_card": "0", 1096 | "roster_in": "341631", 1097 | "roster_out": "0", 1098 | "key_passes": "0", 1099 | "assists": "0", 1100 | "xA": "0", 1101 | "xGChain": "1.1517746448516846", 1102 | "xGBuildup": "0.6098462343215942", 1103 | "positionOrder": "13" 1104 | }, 1105 | ..., 1106 | "341629": { 1107 | "id": "341629", 1108 | "goals": "1", 1109 | "own_goals": "0", 1110 | "shots": "4", 1111 | "xG": "0.7688590884208679", 1112 | "time": "90", 1113 | "player_id": "553", 1114 | "team_id": "89", 1115 | "position": "FW", 1116 | "player": "Anthony Martial", 1117 | "h_a": "h", 1118 | "yellow_card": "1", 1119 | "red_card": "0", 1120 | "roster_in": "0", 1121 | "roster_out": "0", 1122 | "key_passes": "1", 1123 | "assists": "0", 1124 | "xA": "0.05561231076717377", 1125 | "xGChain": "0.9395027160644531", 1126 | "xGBuildup": "0.11503136157989502", 1127 | "positionOrder": "15" 1128 | } 1129 | }, 1130 | "a": { 1131 | "341633": { 1132 | "id": "341633", 1133 | "goals": "0", 1134 | "own_goals": "0", 1135 | "shots": "0", 1136 | "xG": "0", 1137 | "time": "90", 1138 | "player_id": "5061", 1139 | "team_id": "80", 1140 | "position": "GK", 1141 | "player": "Kepa", 1142 | "h_a": "a", 1143 | "yellow_card": "0", 1144 | "red_card": "0", 1145 | "roster_in": "0", 1146 | "roster_out": "0", 1147 | "key_passes": "0", 1148 | "assists": "0", 1149 | "xA": "0", 1150 | "xGChain": "0.04707280918955803", 1151 | "xGBuildup": "0.04707280918955803", 1152 | "positionOrder": "1" 1153 | }, 1154 | ..., 1155 | "341642": { 1156 | "id": "341642", 1157 | "goals": "0", 1158 | "own_goals": "0", 1159 | "shots": "2", 1160 | "xG": "0.08609434962272644", 1161 | "time": "60", 1162 | "player_id": "592", 1163 | "team_id": "80", 1164 | "position": "AML", 1165 | "player": "Ross Barkley", 1166 | "h_a": "a", 1167 | "yellow_card": "0", 1168 | "red_card": "0", 1169 | "roster_in": "341646", 1170 | "roster_out": "0", 1171 | "key_passes": "1", 1172 | "assists": "0", 1173 | "xA": "0.024473881348967552", 1174 | "xGChain": "0.11056823283433914", 1175 | "xGBuildup": "0", 1176 | "positionOrder": "13" 1177 | } 1178 | } 1179 | } 1180 | 1181 | --- 1182 | 1183 | .. automethod:: understat.Understat.get_match_shots 1184 | 1185 | It returns information about the shots made by players who played in the given 1186 | match. So for example, the shots seen in the screenshot below 1187 | 1188 | .. image:: https://i.imgur.com/3z5wkAV.png 1189 | 1190 | An example of getting the shots made in the match between Manchester United and 1191 | Chelsea on 11 August, 2019 which ended 4-0 can be seen below: 1192 | 1193 | .. code-block:: python 1194 | 1195 | async def main(): 1196 | async with aiohttp.ClientSession() as session: 1197 | understat = Understat(session) 1198 | players = await understat.get_match_shots(11652) 1199 | print(json.dumps(players)) 1200 | 1201 | loop = asyncio.get_event_loop() 1202 | loop.run_until_complete(main()) 1203 | 1204 | which outputs (with parts omitted) 1205 | 1206 | .. code-block:: javascript 1207 | 1208 | { 1209 | "h": [ 1210 | { 1211 | "id": "310295", 1212 | "minute": "6", 1213 | "result": "SavedShot", 1214 | "X": "0.8280000305175781", 1215 | "Y": "0.639000015258789", 1216 | "xG": "0.04247729107737541", 1217 | "player": "Anthony Martial", 1218 | "h_a": "h", 1219 | "player_id": "553", 1220 | "situation": "OpenPlay", 1221 | "season": "2019", 1222 | "shotType": "RightFoot", 1223 | "match_id": "11652", 1224 | "h_team": "Manchester United", 1225 | "a_team": "Chelsea", 1226 | "h_goals": "4", 1227 | "a_goals": "0", 1228 | "date": "2019-08-11 16:30:00", 1229 | "player_assisted": null, 1230 | "lastAction": "None" 1231 | }, 1232 | ..., 1233 | { 1234 | "id": "310318", 1235 | "minute": "86", 1236 | "result": "BlockedShot", 1237 | "X": "0.8669999694824219", 1238 | "Y": "0.47299999237060547", 1239 | "xG": "0.11503136157989502", 1240 | "player": "Mason Greenwood", 1241 | "h_a": "h", 1242 | "player_id": "7490", 1243 | "situation": "OpenPlay", 1244 | "season": "2019", 1245 | "shotType": "RightFoot", 1246 | "match_id": "11652", 1247 | "h_team": "Manchester United", 1248 | "a_team": "Chelsea", 1249 | "h_goals": "4", 1250 | "a_goals": "0", 1251 | "date": "2019-08-11 16:30:00", 1252 | "player_assisted": "Aaron Wan-Bissaka", 1253 | "lastAction": "Cross" 1254 | } 1255 | ], 1256 | "a": [ 1257 | { 1258 | "id": "310293", 1259 | "minute": "3", 1260 | "result": "ShotOnPost", 1261 | "X": "0.835999984741211", 1262 | "Y": "0.38599998474121094", 1263 | "xG": "0.03392893448472023", 1264 | "player": "Tammy Abraham", 1265 | "h_a": "a", 1266 | "player_id": "702", 1267 | "situation": "FromCorner", 1268 | "season": "2019", 1269 | "shotType": "RightFoot", 1270 | "match_id": "11652", 1271 | "h_team": "Manchester United", 1272 | "a_team": "Chelsea", 1273 | "h_goals": "4", 1274 | "a_goals": "0", 1275 | "date": "2019-08-11 16:30:00", 1276 | "player_assisted": "Mateo Kovacic", 1277 | "lastAction": "BallTouch" 1278 | }, 1279 | ..., 1280 | { 1281 | "id": "310321", 1282 | "minute": "93", 1283 | "result": "SavedShot", 1284 | "X": "0.850999984741211", 1285 | "Y": "0.7", 1286 | "xG": "0.043492574244737625", 1287 | "player": "Emerson", 1288 | "h_a": "a", 1289 | "player_id": "1245", 1290 | "situation": "OpenPlay", 1291 | "season": "2019", 1292 | "shotType": "LeftFoot", 1293 | "match_id": "11652", 1294 | "h_team": "Manchester United", 1295 | "a_team": "Chelsea", 1296 | "h_goals": "4", 1297 | "a_goals": "0", 1298 | "date": "2019-08-11 16:30:00", 1299 | "player_assisted": "Christian Pulisic", 1300 | "lastAction": "Pass" 1301 | } 1302 | ] 1303 | } 1304 | 1305 | --- 1306 | 1307 | .. automethod:: understat.Understat.get_stats 1308 | 1309 | It returns the average stats of all the leagues tracked on 1310 | `understat.com `_, split by month. Basically, it is all 1311 | the information you see on their homepage, as seen in the screenshot below 1312 | 1313 | .. image:: https://i.imgur.com/5rf0ACo.png 1314 | 1315 | The function comes with the `options` keyword argument, and the `**kwargs` 1316 | magic variable, and so that can be used to filter the output. So for example, 1317 | if you wanted to gets the stats for the Premier League in the 8th month of each 1318 | year they have been tracking the stats, then you could do the following 1319 | 1320 | .. code-block:: python 1321 | 1322 | async def main(): 1323 | async with aiohttp.ClientSession() as session: 1324 | understat = Understat(session) 1325 | stats = await understat.get_stats({"league": "EPL", "month": "8"}) 1326 | print(json.dumps(stats)) 1327 | 1328 | loop = asyncio.get_event_loop() 1329 | loop.run_until_complete(main()) 1330 | 1331 | which outputs 1332 | 1333 | .. code-block:: javascript 1334 | 1335 | [ 1336 | { 1337 | "league_id": "1", 1338 | "league": "EPL", 1339 | "h": "1.3000", 1340 | "a": "1.4000", 1341 | "hxg": "1.141921697060267", 1342 | "axg": "1.110964298248291", 1343 | "year": "2014", 1344 | "month": "8", 1345 | "matches": "30" 1346 | }, 1347 | { 1348 | "league_id": "1", 1349 | "league": "EPL", 1350 | "h": "1.1000", 1351 | "a": "1.3750", 1352 | "hxg": "1.2151590750552714", 1353 | "axg": "1.221375621855259", 1354 | "year": "2015", 1355 | "month": "8", 1356 | "matches": "40" 1357 | }, 1358 | { 1359 | "league_id": "1", 1360 | "league": "EPL", 1361 | "h": "1.2000", 1362 | "a": "1.2000", 1363 | "hxg": "1.3605596815546355", 1364 | "axg": "1.145853524406751", 1365 | "year": "2016", 1366 | "month": "8", 1367 | "matches": "30" 1368 | }, 1369 | { 1370 | "league_id": "1", 1371 | "league": "EPL", 1372 | "h": "1.3000", 1373 | "a": "1.1333", 1374 | "hxg": "1.4422248949607213", 1375 | "axg": "1.096401752779881", 1376 | "year": "2017", 1377 | "month": "8", 1378 | "matches": "30" 1379 | }, 1380 | { 1381 | "league_id": "1", 1382 | "league": "EPL", 1383 | "h": "1.6333", 1384 | "a": "1.3333", 1385 | "hxg": "1.453833992779255", 1386 | "axg": "1.4325587471326193", 1387 | "year": "2018", 1388 | "month": "8", 1389 | "matches": "30" 1390 | } 1391 | ] 1392 | 1393 | --- 1394 | 1395 | .. automethod:: understat.Understat.get_team_fixtures 1396 | 1397 | It returns the upcoming fixtures (not results) of the given team, in the given 1398 | season. So for example, the fixtures as seen in the screenshot below 1399 | 1400 | .. image:: https://i.imgur.com/0qZbE8a.png 1401 | 1402 | The function comes with the `options` keyword argument, and the `**kwargs` 1403 | magic variable, and so that can be used to filter the output. This is similar 1404 | to the `get_league_fixtures` function, but it makes certain options for 1405 | filtering much easier. For example, if you, once again, wanted to get all 1406 | Manchester United's upcoming fixtures at **home**, then instead of passing a 1407 | dictionary as keyword argument, you could simply do the following 1408 | 1409 | .. code-block:: python 1410 | 1411 | async def main(): 1412 | async with aiohttp.ClientSession() as session: 1413 | understat = Understat(session) 1414 | results = await understat.get_team_fixtures( 1415 | "Manchester United", 1416 | 2018, 1417 | side="h" 1418 | ) 1419 | print(json.dumps(results)) 1420 | 1421 | loop = asyncio.get_event_loop() 1422 | loop.run_until_complete(main()) 1423 | 1424 | which outputs (with parts omitted) 1425 | 1426 | .. code-block:: javascript 1427 | 1428 | [ 1429 | { 1430 | "id": "9501", 1431 | "isResult": false, 1432 | "side": "h", 1433 | "h": { 1434 | "id": "89", 1435 | "title": "Manchester United", 1436 | "short_title": "MUN" 1437 | }, 1438 | "a": { 1439 | "id": "88", 1440 | "title": "Manchester City", 1441 | "short_title": "MCI" 1442 | }, 1443 | "goals": { 1444 | "h": null, 1445 | "a": null 1446 | }, 1447 | "xG": { 1448 | "h": null, 1449 | "a": null 1450 | }, 1451 | "datetime": "2019-03-16 18:00:00" 1452 | }, 1453 | ..., 1454 | { 1455 | "id": "9570", 1456 | "isResult": false, 1457 | "side": "h", 1458 | "h": { 1459 | "id": "89", 1460 | "title": "Manchester United", 1461 | "short_title": "MUN" 1462 | }, 1463 | "a": { 1464 | "id": "227", 1465 | "title": "Cardiff", 1466 | "short_title": "CAR" 1467 | }, 1468 | "goals": { 1469 | "h": null, 1470 | "a": null 1471 | }, 1472 | "xG": { 1473 | "h": null, 1474 | "a": null 1475 | }, 1476 | "datetime": "2019-05-12 17:00:00" 1477 | } 1478 | ] 1479 | 1480 | --- 1481 | 1482 | .. automethod:: understat.Understat.get_team_players 1483 | 1484 | It returns all the information about the players of a given team in the given 1485 | season. This includes stuff like their number of goals scored, their total 1486 | expected assists and more. Basically, it's all the information you can find 1487 | in the player table shown on all team overview pages on 1488 | `understat.com `_. 1489 | 1490 | .. image:: https://i.imgur.com/N53k9Ao.png 1491 | 1492 | The function comes with the `options` keyword argument, and the `**kwargs` 1493 | magic variable, and so that can be used to filter the output. This is similar 1494 | to the `get_league_players` function, but is quicker and easier. For example, 1495 | if you, once again, wanted to get all Manchester United's players who have only 1496 | played games as a forward, then you could do the following 1497 | 1498 | .. code-block:: python 1499 | 1500 | async def main(): 1501 | async with aiohttp.ClientSession() as session: 1502 | understat = Understat(session) 1503 | results = await understat.get_team_players( 1504 | "Manchester United", 1505 | 2018, 1506 | position="F S" 1507 | ) 1508 | print(json.dumps(results)) 1509 | 1510 | loop = asyncio.get_event_loop() 1511 | loop.run_until_complete(main()) 1512 | 1513 | which outputs 1514 | 1515 | .. code-block:: javascript 1516 | 1517 | [ 1518 | { 1519 | "id": "594", 1520 | "player_name": "Romelu Lukaku", 1521 | "games": "27", 1522 | "time": "1768", 1523 | "goals": "12", 1524 | "xG": "12.054240763187408", 1525 | "assists": "0", 1526 | "xA": "1.6836179178208113", 1527 | "shots": "50", 1528 | "key_passes": "17", 1529 | "yellow_cards": "4", 1530 | "red_cards": "0", 1531 | "position": "F S", 1532 | "team_title": "Manchester United", 1533 | "npg": "12", 1534 | "npxG": "12.054240763187408", 1535 | "xGChain": "12.832402393221855", 1536 | "xGBuildup": "3.366600174456835" 1537 | } 1538 | ] 1539 | 1540 | --- 1541 | 1542 | .. automethod:: understat.Understat.get_team_results 1543 | 1544 | It returns the results (not fixtures) of the given team, in the given season. 1545 | So for example, the fixtures as seen in the screenshot below 1546 | 1547 | .. image:: https://i.imgur.com/Q9KC5f9.png 1548 | 1549 | The function comes with the `options` keyword argument, and the `**kwargs` 1550 | magic variable, and so that can be used to filter the output. This is similar 1551 | to the `get_league_results` function, but it makes certain options for 1552 | filtering much easier. For example, if you, once again, wanted to get all 1553 | Manchester United's results at **home**, then instead of passing a dictionary 1554 | as keyword argument, you could simply do the following 1555 | 1556 | .. code-block:: python 1557 | 1558 | async def main(): 1559 | async with aiohttp.ClientSession() as session: 1560 | understat = Understat(session) 1561 | results = await understat.get_team_results( 1562 | "Manchester United", 1563 | 2018, 1564 | side="h" 1565 | ) 1566 | print(json.dumps(results)) 1567 | 1568 | loop = asyncio.get_event_loop() 1569 | loop.run_until_complete(main()) 1570 | 1571 | which outputs (with parts omitted) 1572 | 1573 | .. code-block:: javascript 1574 | 1575 | [ 1576 | { 1577 | "id": "9197", 1578 | "isResult": true, 1579 | "side": "h", 1580 | "h": { 1581 | "id": "89", 1582 | "title": "Manchester United", 1583 | "short_title": "MUN" 1584 | }, 1585 | "a": { 1586 | "id": "75", 1587 | "title": "Leicester", 1588 | "short_title": "LEI" 1589 | }, 1590 | "goals": { 1591 | "h": "2", 1592 | "a": "1" 1593 | }, 1594 | "xG": { 1595 | "h": "1.5137", 1596 | "a": "1.73813" 1597 | }, 1598 | "datetime": "2018-08-10 22:00:00", 1599 | "forecast": { 1600 | "w": 0.33715468577027, 1601 | "d": 0.23067469101496, 1602 | "l": 0.43217062251974 1603 | }, 1604 | "result": "w" 1605 | }, 1606 | ..., 1607 | { 1608 | "id": "9226", 1609 | "isResult": true, 1610 | "side": "h", 1611 | "h": { 1612 | "id": "89", 1613 | "title": "Manchester United", 1614 | "short_title": "MUN" 1615 | }, 1616 | "a": { 1617 | "id": "82", 1618 | "title": "Tottenham", 1619 | "short_title": "TOT" 1620 | }, 1621 | "goals": { 1622 | "h": "0", 1623 | "a": "3" 1624 | }, 1625 | "xG": { 1626 | "h": "1.40321", 1627 | "a": "1.80811" 1628 | }, 1629 | "datetime": "2018-08-27 22:00:00", 1630 | "forecast": { 1631 | "w": 0.29970781519619, 1632 | "d": 0.22891929318443, 1633 | "l": 0.47137289056693 1634 | }, 1635 | "result": "l" 1636 | } 1637 | ] 1638 | 1639 | --- 1640 | 1641 | .. automethod:: understat.Understat.get_team_stats 1642 | 1643 | It returns all the statistics of a given team, which includes stuff like 1644 | their performance per season, formation and more. Basically, it's everything 1645 | that can be found in the table shown in the screenshot below 1646 | 1647 | .. image:: https://i.imgur.com/RlWzExr.png 1648 | 1649 | An example of getting Manchester United's data can be found below 1650 | 1651 | .. code-block:: python 1652 | 1653 | async def main(): 1654 | async with aiohttp.ClientSession() as session: 1655 | understat = Understat(session) 1656 | team_stats = await understat.get_team_stats("Manchester United", 2018) 1657 | print(json.dumps(team_stats)) 1658 | 1659 | loop = asyncio.get_event_loop() 1660 | loop.run_until_complete(main()) 1661 | 1662 | which outputs (with parts omitted) 1663 | 1664 | .. code-block:: javascript 1665 | 1666 | { 1667 | "situation": { 1668 | "OpenPlay": { 1669 | "shots": 297, 1670 | "goals": 39, 1671 | "xG": 36.671056651045, 1672 | "against": { 1673 | "shots": 279, 1674 | "goals": 25, 1675 | "xG": 28.870285989717 1676 | } 1677 | }, 1678 | ..., 1679 | "Penalty": { 1680 | "shots": 10, 1681 | "goals": 7, 1682 | "xG": 7.611688375473, 1683 | "against": { 1684 | "shots": 5, 1685 | "goals": 5, 1686 | "xG": 3.8058441877365 1687 | } 1688 | } 1689 | }, 1690 | "formation": { 1691 | "4-3-3": { 1692 | "stat": "4-3-3", 1693 | "time": 1295, 1694 | "shots": 185, 1695 | "goals": 30, 1696 | "xG": 27.7899469533, 1697 | "against": { 1698 | "shots": 176, 1699 | "goals": 18, 1700 | "xG": 20.478145442903 1701 | } 1702 | }, 1703 | ..., 1704 | "4-4-2": { 1705 | "stat": "4-4-2", 1706 | "time": 38, 1707 | "shots": 8, 1708 | "goals": 0, 1709 | "xG": 0.87938431277871, 1710 | "against": { 1711 | "shots": 11, 1712 | "goals": 1, 1713 | "xG": 0.66449437476695 1714 | } 1715 | } 1716 | }, 1717 | "gameState": { 1718 | "Goal diff 0": { 1719 | "stat": "Goal diff 0", 1720 | "time": 1284, 1721 | "shots": 154, 1722 | "goals": 20, 1723 | "xG": 20.433959940448, 1724 | "against": { 1725 | "shots": 170, 1726 | "goals": 15, 1727 | "xG": 17.543024708517 1728 | } 1729 | }, 1730 | ..., 1731 | "Goal diff < -1": { 1732 | "stat": "Goal diff < -1", 1733 | "time": 253, 1734 | "shots": 43, 1735 | "goals": 7, 1736 | "xG": 6.4928285568021, 1737 | "against": { 1738 | "shots": 21, 1739 | "goals": 1, 1740 | "xG": 2.9283153852448 1741 | } 1742 | } 1743 | }, 1744 | "timing": { 1745 | "1-15": { 1746 | "stat": "1-15", 1747 | "shots": 51, 1748 | "goals": 6, 1749 | "xG": 7.2566251829267, 1750 | "against": { 1751 | "shots": 72, 1752 | "goals": 7, 1753 | "xG": 8.5656435946003 1754 | } 1755 | }, 1756 | ..., 1757 | "76+": { 1758 | "stat": "76+", 1759 | "shots": 70, 1760 | "goals": 12, 1761 | "xG": 10.272770666517, 1762 | "against": { 1763 | "shots": 77, 1764 | "goals": 8, 1765 | "xG": 10.18940022774 1766 | } 1767 | } 1768 | }, 1769 | "shotZone": { 1770 | "ownGoals": { 1771 | "stat": "ownGoals", 1772 | "shots": 0, 1773 | "goals": 0, 1774 | "xG": 0, 1775 | "against": { 1776 | "shots": 2, 1777 | "goals": 2, 1778 | "xG": 2 1779 | } 1780 | }, 1781 | "shotOboxTotal": { 1782 | "stat": "shotOboxTotal", 1783 | "shots": 158, 1784 | "goals": 8, 1785 | "xG": 4.8084309450351, 1786 | "against": { 1787 | "shots": 170, 1788 | "goals": 6, 1789 | "xG": 5.4022304248065 1790 | } 1791 | }, 1792 | ..., 1793 | "shotSixYardBox": { 1794 | "stat": "shotSixYardBox", 1795 | "shots": 36, 1796 | "goals": 13, 1797 | "xG": 13.912872407585, 1798 | "against": { 1799 | "shots": 32, 1800 | "goals": 8, 1801 | "xG": 11.533062046394 1802 | } 1803 | } 1804 | }, 1805 | "attackSpeed": { 1806 | "Normal": { 1807 | "stat": "Normal", 1808 | "shots": 258, 1809 | "goals": 34, 1810 | "xG": 30.690259062219, 1811 | "against": { 1812 | "shots": 230, 1813 | "goals": 18, 1814 | "xG": 23.094043077901 1815 | } 1816 | }, 1817 | ..., 1818 | "Slow": { 1819 | "stat": "Slow", 1820 | "shots": 18, 1821 | "goals": 2, 1822 | "xG": 0.71848054975271, 1823 | "against": { 1824 | "shots": 26, 1825 | "goals": 5, 1826 | "xG": 2.9855494443327 1827 | } 1828 | } 1829 | }, 1830 | "result": { 1831 | "MissedShots": { 1832 | "shots": 122, 1833 | "goals": 0, 1834 | "xG": 12.353983599227, 1835 | "against": { 1836 | "shots": 155, 1837 | "goals": 0, 1838 | "xG": 13.091518453322 1839 | } 1840 | }, 1841 | ..., 1842 | "ShotOnPost": { 1843 | "shots": 4, 1844 | "goals": 0, 1845 | "xG": 0.81487018615007, 1846 | "against": { 1847 | "shots": 2, 1848 | "goals": 0, 1849 | "xG": 0.61989105120301 1850 | } 1851 | } 1852 | } 1853 | } 1854 | 1855 | --- 1856 | 1857 | .. automethod:: understat.Understat.get_teams 1858 | 1859 | It returns all the information for the teams in a given league, in a given 1860 | season. Basically it is all the information that is shown in the league's 1861 | table, as shown in the screenshot below 1862 | 1863 | .. image:: https://i.imgur.com/tQO7cnO.png 1864 | 1865 | The function comes with the `options` keyword argument, and the `**kwargs` 1866 | magic variable, and so that can be used to filter the output. So for example, 1867 | if you wanted to get Manchester United's stats (as shown in the table), you 1868 | could do the following 1869 | 1870 | .. code-block:: python 1871 | 1872 | async def main(): 1873 | async with aiohttp.ClientSession() as session: 1874 | understat = Understat(session) 1875 | teams = await understat.get_teams( 1876 | "epl", 1877 | 2018, 1878 | title="Manchester United" 1879 | ) 1880 | print(json.dumps(teams)) 1881 | 1882 | loop = asyncio.get_event_loop() 1883 | loop.run_until_complete(main()) 1884 | 1885 | which outputs (with parts omitted) 1886 | 1887 | .. code-block:: javascript 1888 | 1889 | [ 1890 | { 1891 | "id": "89", 1892 | "title": "Manchester United", 1893 | "history": [ 1894 | { 1895 | "h_a": "h", 1896 | "xG": 1.5137, 1897 | "xGA": 1.73813, 1898 | "npxG": 0.75253, 1899 | "npxGA": 1.73813, 1900 | "ppda": { 1901 | "att": 285, 1902 | "def": 18 1903 | }, 1904 | "ppda_allowed": { 1905 | "att": 298, 1906 | "def": 26 1907 | }, 1908 | "deep": 3, 1909 | "deep_allowed": 10, 1910 | "scored": 2, 1911 | "missed": 1, 1912 | "xpts": 1.1711, 1913 | "result": "w", 1914 | "date": "2018-08-10 22:00:00", 1915 | "wins": 1, 1916 | "draws": 0, 1917 | "loses": 0, 1918 | "pts": 3, 1919 | "npxGD": -0.9856 1920 | }, 1921 | ..., 1922 | { 1923 | "h_a": "a", 1924 | "xG": 2.3703, 1925 | "xGA": 1.52723, 1926 | "npxG": 2.3703, 1927 | "npxGA": 0.766059, 1928 | "ppda": { 1929 | "att": 203, 1930 | "def": 25 1931 | }, 1932 | "ppda_allowed": { 1933 | "att": 271, 1934 | "def": 21 1935 | }, 1936 | "deep": 7, 1937 | "deep_allowed": 9, 1938 | "scored": 0, 1939 | "missed": 2, 1940 | "xpts": 2.0459, 1941 | "result": "l", 1942 | "date": "2019-03-10 16:30:00", 1943 | "wins": 0, 1944 | "draws": 0, 1945 | "loses": 1, 1946 | "pts": 0, 1947 | "npxGD": 1.604241 1948 | } 1949 | ] 1950 | } 1951 | ] 1952 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath('..')) 19 | 20 | import understat 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = 'Understat' 25 | copyright = '2019, Amos Bastian' 26 | author = 'Amos Bastian' 27 | 28 | # The short X.Y version 29 | version = '' 30 | # The full version, including alpha/beta/rc tags 31 | release = '0.1.1' 32 | 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # If your documentation needs a minimal Sphinx version, state it here. 37 | # 38 | # needs_sphinx = '1.0' 39 | 40 | # Add any Sphinx extension module names here, as strings. They can be 41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 42 | # ones. 43 | extensions = [ 44 | 'sphinx.ext.autodoc', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path . 69 | exclude_patterns = [] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = 'sphinx' 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'alabaster' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | # html_theme_options = {} 87 | 88 | # Add any paths that contain custom static files (such as style sheets) here, 89 | # relative to this directory. They are copied after the builtin static files, 90 | # so a file named "default.css" will overwrite the builtin "default.css". 91 | html_static_path = ['_static'] 92 | 93 | # Custom sidebar templates, must be a dictionary that maps document names 94 | # to template names. 95 | # 96 | # The default sidebars (for documents that don't match any pattern) are 97 | # defined by theme itself. Builtin themes are using these templates by 98 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 99 | # 'searchbox.html']``. 100 | # 101 | # html_sidebars = {} 102 | 103 | 104 | # -- Options for HTMLHelp output --------------------------------------------- 105 | 106 | # Output file base name for HTML help builder. 107 | htmlhelp_basename = 'Understatdoc' 108 | 109 | 110 | # -- Options for LaTeX output ------------------------------------------------ 111 | 112 | latex_elements = { 113 | # The paper size ('letterpaper' or 'a4paper'). 114 | # 115 | # 'papersize': 'letterpaper', 116 | 117 | # The font size ('10pt', '11pt' or '12pt'). 118 | # 119 | # 'pointsize': '10pt', 120 | 121 | # Additional stuff for the LaTeX preamble. 122 | # 123 | # 'preamble': '', 124 | 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | (master_doc, 'Understat.tex', 'Understat Documentation', 135 | 'Amos Bastian', 'manual'), 136 | ] 137 | 138 | 139 | # -- Options for manual page output ------------------------------------------ 140 | 141 | # One entry per manual page. List of tuples 142 | # (source start file, name, description, authors, manual section). 143 | man_pages = [ 144 | (master_doc, 'understat', 'Understat Documentation', 145 | [author], 1) 146 | ] 147 | 148 | 149 | # -- Options for Texinfo output ---------------------------------------------- 150 | 151 | # Grouping the document tree into Texinfo files. List of tuples 152 | # (source start file, target name, title, author, 153 | # dir menu entry, description, category) 154 | texinfo_documents = [ 155 | (master_doc, 'Understat', 'Understat Documentation', 156 | author, 'Understat', 'One line description of project.', 157 | 'Miscellaneous'), 158 | ] 159 | 160 | 161 | # -- Extension configuration ------------------------------------------------- -------------------------------------------------------------------------------- /docs/contributing/authors.rst: -------------------------------------------------------------------------------- 1 | .. _authors: 2 | 3 | Authors 4 | ======= 5 | 6 | .. include:: ../../AUTHORS.rst 7 | -------------------------------------------------------------------------------- /docs/contributing/contributing.rst: -------------------------------------------------------------------------------- 1 | .. _contributing: 2 | 3 | Contributing 4 | ============ 5 | 6 | If you're reading this, then you're probably interested in helping out with 7 | the development of **understat**! On this page you will be able to find information 8 | that *should* make it easier for you to start contributing. Since contributions 9 | can be in all kinds of different forms, the contributing guide has been split 10 | up into sections. 11 | 12 | To contact me directly you can send an email to 13 | `amosbastian@gmail.com `_. If you are looking 14 | for other people interested in programming stuff related to football, then you 15 | can also join our `Discord server `_. 16 | 17 | Code contributions 18 | ------------------ 19 | 20 | Submitting code 21 | ~~~~~~~~~~~~~~~ 22 | 23 | When contributing code, you'll want to follow this checklist: 24 | 25 | 1. Fork the repository on GitHub. 26 | 2. Run the tests with `pytest tests/` to confirm they all pass on your system. 27 | If the tests fail, then try and find out why this is happening. If you aren't 28 | able to do this yourself, then don't hesitate to either create an issue on 29 | GitHub (see :ref:`reporting-bugs`), contact me on Discord or send an email 30 | to `amosbastian@gmail.com `_. 31 | 3. Either create your feature and then write tests for it, or do this the other 32 | way around. 33 | 4. Run all tests again with with `pytest tests/` to confirm that everything 34 | still passes, including your newly added test(s). 35 | 5. Create a pull request for the main repository's ``master`` branch. 36 | 37 | If you want, you can also add your name 38 | `AUTHORS `_. 39 | 40 | Code review 41 | ~~~~~~~~~~~ 42 | 43 | Currently I am the only maintainer of this project. Because of this I will review 44 | each pull request myself and provide feedback if necessary. I would like this to 45 | happen in a clear and calm manner (from both sides)! 46 | 47 | New contributors 48 | ~~~~~~~~~~~~~~~~ 49 | 50 | If you are new or relatively new to contributing to open source projects, then 51 | please don't hesitate to contact me directly! I am more than willing to help 52 | out, and will try and assign issues to you if possible. 53 | 54 | Code style 55 | ~~~~~~~~~~ 56 | 57 | The `understat` package follows `PEP 8`_ code style. Currently there is only one 58 | specific additions to this, but if you think more should be added, then this 59 | can always be discussed. 60 | 61 | - Always use double-quoted strings, unless it is not possible. 62 | 63 | .. _PEP 8: https://pep8.org/ 64 | 65 | Documentation contributions 66 | --------------------------- 67 | 68 | Documentation improvements and suggestions are always welcome! The 69 | documentation files live in the ``docs/`` directory. They're written in 70 | `reStructuredText`_, and use `Sphinx`_ to generate the full suite of 71 | documentation. 72 | 73 | Of course the documentation doesn't have to be too serious, but try and keep it 74 | semi-formal. 75 | 76 | .. _reStructuredText: http://docutils.sourceforge.net/rst.html 77 | .. _Sphinx: http://sphinx-doc.org/index.html 78 | 79 | 80 | .. _reporting-bugs: 81 | 82 | Reporting bugs 83 | -------------- 84 | 85 | If you encounter any bugs while using **understat** then please don't hesitate to 86 | open an issue. However, before you do, please check the `GitHub issues`_ (make 87 | sure to also check closed ones) to see if the bug has already been reported. 88 | 89 | A template is provided below to make it easier to understand the issue: 90 | 91 | .. code-block:: none 92 | 93 | #### Expected behaviour 94 | What did you expect to happen? 95 | 96 | #### Actual behaviour 97 | What actually happened? 98 | 99 | #### How to reproduce 100 | When did it happen? Include a code snippet if possible! 101 | 102 | .. _GitHub issues: https://github.com/amosbastian/understat/issues 103 | 104 | 105 | Feature requests 106 | ---------------- 107 | 108 | Currently **understat** is in active development, so feature requests are more 109 | than welcome. If you have any ideas for features you'd like to see added, then 110 | simply create an `issue`_ with an **enhancement** label. 111 | 112 | .. _issue: https://github.com/amosbastian/understat/issues 113 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | A Python package for Understat_ 2 | ========================================================= 3 | 4 | .. image:: https://api.codacy.com/project/badge/Grade/716b2c24086a41d7a79481ac89748861 5 | :target: https://www.codacy.com/app/amosbastian/understat?utm_source=github.com&utm_medium=referral&utm_content=amosbastian/understat&utm_campaign=Badge_Grade 6 | 7 | .. image:: https://travis-ci.com/amosbastian/understat.svg?branch=master 8 | :target: https://travis-ci.com/amosbastian/understat 9 | 10 | .. image:: https://img.shields.io/badge/Supported%20by-Utopian.io-%23B10DC9.svg 11 | :target: https://utopian.io/ 12 | 13 | .. image:: https://badge.fury.io/py/understat.svg 14 | :target: https://pypi.org/project/understat/ 15 | 16 | .. image:: https://img.shields.io/badge/Python-3.6%2B-blue.svg 17 | :target: https://pypi.org/project/understat/ 18 | 19 | 20 | .. note:: I have nothing to do with Understat, and have simply created this 21 | package for fun! 22 | 23 | .. note:: The latest version of **understat** is asynchronous, and requires 24 | Python 3.6+! 25 | 26 | If you're interested in helping out the development of **understat**, or have 27 | suggestions and ideas then please don't hesitate to create an issue on GitHub, 28 | join our `Discord server `_ or send an email to 29 | `amosbastian@gmail.com `_! 30 | 31 | -------------- 32 | 33 | **A simple example**:: 34 | 35 | import asyncio 36 | import json 37 | 38 | import aiohttp 39 | 40 | from understat import Understat 41 | 42 | 43 | async def main(): 44 | async with aiohttp.ClientSession() as session: 45 | understat = Understat(session) 46 | player = await understat.get_players( 47 | "epl", 2018, 48 | player_name="Paul Pogba", 49 | team_title="Manchester United" 50 | ) 51 | print(json.dumps(player)) 52 | 53 | loop = asyncio.get_event_loop() 54 | loop.run_until_complete(main()) 55 | 56 | >>>[{"id": "1740", "player_name": "Paul Pogba", "games": "27", "time": "2293", "goals": "11", "xG": "13.361832823604345", "assists": "9", "xA": "4.063152700662613", "shots": "87", "key_passes": "40", "yellow_cards": "5", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "6", "npxG": "7.272482139989734", "xGChain": "17.388037759810686", "xGBuildup": "8.965998269617558"}] 57 | 58 | 59 | With **understat** you can easily get all the data available on Understat_! 60 | 61 | The User Guide 62 | -------------- 63 | 64 | This part of the documentation simply shows you have to install **understat**. 65 | 66 | .. toctree:: 67 | :maxdepth: 2 68 | 69 | user/installation 70 | 71 | The Class Documentation / Guide 72 | ------------------------------- 73 | 74 | This part of the documentation is for people who want or need more information 75 | bout specific functions and classes found in **understat**. It includes example 76 | output for each of the functions, and also screenshots showing where you would 77 | find the equivalent data on Understat_. 78 | 79 | .. toctree:: 80 | :maxdepth: 2 81 | 82 | classes/understat 83 | 84 | 85 | The Contributor Guide 86 | --------------------- 87 | 88 | If you want to help **understat** out and contribute to the project, be it via 89 | development, suggestions, hunting bugs etc. then this part of the documentation 90 | is for you! 91 | 92 | .. toctree:: 93 | :maxdepth: 2 94 | 95 | contributing/contributing 96 | contributing/authors 97 | 98 | .. _Understat: https://understat.com -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=Understat 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/user/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation: 2 | 3 | Installing **understat** 4 | ======================== 5 | 6 | The recommended way to install ``understat`` is via ``pip``. 7 | 8 | .. code-block:: bash 9 | 10 | pip install understat 11 | 12 | .. note:: Depending on your system, you may need to use ``pip3`` to install 13 | packages for Python 3. 14 | 15 | Updating **understat** with pip 16 | ------------------------------- 17 | 18 | To update **understat** you can run: 19 | 20 | .. code-block:: bash 21 | 22 | pip install --upgrade understat 23 | 24 | Example output: 25 | 26 | .. code-block:: bash 27 | 28 | Installing collected packages: understat 29 | Found existing installation: understat 0.1.0 30 | Uninstalling understat-0.1.0: 31 | Successfully uninstalled understat-0.1.0 32 | Successfully installed understat-0.1.1 33 | 34 | Installing older versions 35 | ------------------------- 36 | 37 | Older versions of **understat** can be installed by specifying the version number 38 | as part of the installation command: 39 | 40 | .. code-block:: bash 41 | 42 | pip install understat==0.1.1 43 | 44 | Installing from GitHub 45 | ---------------------- 46 | 47 | The source code for **understat** is available on GitHub repository 48 | ``_. To install the most recent 49 | version of **understat** from here you can use the following command:: 50 | 51 | $ git clone git://github.com/amosbastian/understat.git 52 | 53 | You can also install a `.tar file `_ 54 | or `.zip file `_ 55 | 56 | $ curl -OL https://github.com/amosbastian/understat/tarball/master 57 | $ curl -OL https://github.com/amosbastian/understat/zipball/master # Windows 58 | 59 | Once it has been downloaded you can easily install it using `pip`:: 60 | 61 | $ cd understat 62 | $ pip install . 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="understat", 8 | version="0.1.12", 9 | packages=find_packages(), 10 | description="A Python wrapper for https://understat.com/", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="https://github.com/amosbastian/understat", 14 | author="amosbastian", 15 | author_email="amosbastian@gmail.com", 16 | license="MIT", 17 | classifiers=[ 18 | "Development Status :: 4 - Beta", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: MIT License", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9" 25 | ], 26 | keywords="fpl fantasy premier league understat football", 27 | project_urls={ 28 | "Documentation": "http://fpl.readthedocs.io/en/latest/", 29 | "Source": "https://github.com/amosbastian/fpl" 30 | }, 31 | install_requires=[ 32 | "beautifulsoup4==4.11.1", 33 | "pytest-aiohttp==1.0.4", 34 | "pytest-cov==4.0.0", 35 | "pytest-mock==3.6.0", 36 | "pytest==7.2.0", 37 | "aiohttp==3.8.3" 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/understat/9a02234379d7e39f87b1d0d4ee0e6de5f4d5247c/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import pytest 3 | 4 | from understat import Understat 5 | 6 | 7 | @pytest.fixture() 8 | async def understat(): 9 | session = aiohttp.ClientSession() 10 | fpl = Understat(session) 11 | yield fpl 12 | await session.close() 13 | -------------------------------------------------------------------------------- /tests/test_understat.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from understat import Understat 4 | 5 | leagues = ["epl", "la_liga", "bundesliga", "serie_a", "ligue_1", "rfpl"] 6 | 7 | 8 | class TestUnderstat(object): 9 | async def test_init(self, loop): 10 | session = aiohttp.ClientSession() 11 | understat = Understat(session) 12 | assert understat.session is session 13 | await session.close() 14 | 15 | async def test_get_stats(self, loop, understat): 16 | stats = await understat.get_stats() 17 | assert isinstance(stats, list) 18 | 19 | async def test_get_stats_with_options(self, loop, understat): 20 | stats = await understat.get_stats(league="EPL") 21 | assert isinstance(stats, list) 22 | 23 | stats = await understat.get_stats({"league": "EPL", "month": "8"}) 24 | assert isinstance(stats, list) 25 | 26 | async def test_get_teams(self, loop, understat): 27 | for league in leagues: 28 | teams = await understat.get_teams(league, 2018) 29 | assert isinstance(teams, list) 30 | 31 | async def test_get_team_with_options(self, loop, understat): 32 | team = await understat.get_teams( 33 | "epl", 2018, {"title": "Manchester United"}) 34 | assert isinstance(team, list) 35 | assert len(team) == 1 36 | 37 | team = await understat.get_teams( 38 | "epl", 2018, title="Manchester United") 39 | assert isinstance(team, list) 40 | assert len(team) == 1 41 | 42 | team = await understat.get_teams("epl", 2018, title="Reddit United") 43 | assert isinstance(team, list) 44 | assert not team 45 | 46 | async def test_get_league_players(self, loop, understat): 47 | for league in leagues: 48 | players = await understat.get_league_players(league, 2018) 49 | assert isinstance(players, list) 50 | 51 | async def test_get_league_players_with_options(self, loop, understat): 52 | player = await understat.get_league_players( 53 | "epl", 2018, player_name="Paul Pogba", 54 | team_title="Manchester United") 55 | assert isinstance(player, list) 56 | assert len(player) == 1 57 | 58 | player = await understat.get_league_players( 59 | "epl", 2018, {"position": "F S", 60 | "yellow_cards": "3", 61 | "player_name": "Sergio Agüero"}) 62 | assert isinstance(player, list) 63 | assert len(player) == 1 64 | 65 | player = await understat.get_league_players( 66 | "epl", 2018, player_name="Lionel Messi") 67 | assert isinstance(player, list) 68 | assert not player 69 | 70 | async def test_get_league_results(self, loop, understat): 71 | for league in leagues: 72 | results = await understat.get_league_results(league, 2018) 73 | assert isinstance(results, list) 74 | 75 | for result in results: 76 | assert result["isResult"] 77 | 78 | async def test_get_league_results_with_options(self, loop, understat): 79 | results = await understat.get_league_results("epl", 2018, { 80 | "h": {"id": "89", 81 | "title": "Manchester United", 82 | "short_title": "MUN"} 83 | }) 84 | assert isinstance(results, list) 85 | assert len(results) > 0 86 | 87 | results_without_option = await understat.get_league_results( 88 | "epl", 2018, isResult=True) 89 | results_with_option = await understat.get_league_results( 90 | "epl", 2018, isResult=True) 91 | assert results_with_option == results_without_option 92 | 93 | async def test_get_league_fixtures(self, loop, understat): 94 | for league in leagues: 95 | fixtures = await understat.get_league_fixtures(league, 2018) 96 | assert isinstance(fixtures, list) 97 | 98 | for fixture in fixtures: 99 | assert not fixture["isResult"] 100 | 101 | async def test_get_league_fixtures_with_options(self, loop, understat): 102 | results = await understat.get_league_fixtures("epl", 2024, { 103 | "h": {"id": "89", 104 | "title": "Manchester United", 105 | "short_title": "MUN"} 106 | }) 107 | assert isinstance(results, list) 108 | assert results 109 | 110 | async def test_get_league_table(self, loop, understat): 111 | table = await understat.get_league_table("epl", 2020) 112 | assert isinstance(table, list) 113 | 114 | async def test_get_league_table_with_date(self, loop, understat): 115 | table = await understat.get_league_table("epl", 2020, start_date="2020-11-01", end_date="2021-01-31") 116 | assert isinstance(table, list) 117 | 118 | async def test_get_player_shots(self, loop, understat): 119 | player_shots = await understat.get_player_shots(619) 120 | assert isinstance(player_shots, list) 121 | 122 | async def test_get_player_shots_with_options(self, loop, understat): 123 | player_shots = await understat.get_player_shots( 124 | 619, {"player_assisted": "Fernandinho"}) 125 | assert isinstance(player_shots, list) 126 | 127 | player_shots = await understat.get_player_shots( 128 | 619, player_assisted="Fernandinho") 129 | assert isinstance(player_shots, list) 130 | 131 | async def test_get_matches(self, loop, understat): 132 | player_matches = await understat.get_player_matches(619) 133 | assert isinstance(player_matches, list) 134 | 135 | async def test_get_matches_with_options(self, loop, understat): 136 | player_matches = await understat.get_player_matches( 137 | 619, {"h_team": "Manchester United"}) 138 | assert isinstance(player_matches, list) 139 | 140 | player_matches = await understat.get_player_matches( 141 | 619, h_team="Manchester United") 142 | assert isinstance(player_matches, list) 143 | 144 | async def test_get_player_stats(self, loop, understat): 145 | player_stats = await understat.get_player_stats(619) 146 | assert isinstance(player_stats, list) 147 | assert len(player_stats) > 1 148 | 149 | player_stats = await understat.get_player_stats(619, ["FW"]) 150 | assert isinstance(player_stats, list) 151 | assert len(player_stats) == 1 152 | 153 | async def test_get_player_grouped_stats(self, loop, understat): 154 | grouped_stats = await understat.get_player_grouped_stats(619) 155 | assert isinstance(grouped_stats, dict) 156 | 157 | async def test_get_team_stats(self, loop, understat): 158 | team_stats = await understat.get_team_stats("Manchester United", 2018) 159 | assert isinstance(team_stats, dict) 160 | 161 | async def test_get_team_results(self, loop, understat): 162 | results = await understat.get_team_results("Manchester United", 2018) 163 | assert isinstance(results, list) 164 | 165 | async def test_get_team_results_with_options(self, loop, understat): 166 | results = await understat.get_team_results( 167 | "Manchester United", 2018, side="h") 168 | assert isinstance(results, list) 169 | 170 | results = await understat.get_team_results( 171 | "Manchester United", 2018, {"side": "h", "result": "w"}) 172 | assert isinstance(results, list) 173 | 174 | async def test_get_team_fixtures(self, loop, understat): 175 | fixtures = await understat.get_team_fixtures("Manchester United", 2018) 176 | assert isinstance(fixtures, list) 177 | 178 | async def test_get_team_fixtures_with_options(self, loop, understat): 179 | fixtures = await understat.get_team_fixtures( 180 | "Manchester United", 2018, side="h") 181 | assert isinstance(fixtures, list) 182 | 183 | fixtures = await understat.get_team_fixtures( 184 | "Manchester United", 2018, {"side": "h", "result": "w"}) 185 | assert isinstance(fixtures, list) 186 | 187 | async def test_get_team_players(self, loop, understat): 188 | players = await understat.get_team_players("Manchester United", 2018) 189 | assert isinstance(players, list) 190 | 191 | async def test_get_team_players_with_options(self, loop, understat): 192 | players = await understat.get_team_players( 193 | "Manchester United", 2018, position="F S") 194 | assert isinstance(players, list) 195 | 196 | players = await understat.get_team_players( 197 | "Manchester United", 2018, {"position": "F S", "red_cards": "0"}) 198 | assert isinstance(players, list) 199 | 200 | async def test_get_match_stats(self, loop, understat): 201 | stats = await understat.get_match_stats(11670) 202 | assert isinstance(stats, dict) 203 | 204 | async def test_get_match_players(self, loop, understat): 205 | players = await understat.get_match_players(11670) 206 | assert isinstance(players, dict) 207 | assert isinstance(players["h"], dict) 208 | assert isinstance(players["a"], dict) 209 | 210 | async def test_get_match_shots(self, loop, understat): 211 | shots = await understat.get_match_shots(11670) 212 | assert isinstance(shots, dict) 213 | assert isinstance(shots["h"], list) 214 | assert isinstance(shots["a"], list) 215 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from understat.utils import filter_by_positions, filter_data, to_league_name, filter_by_date 2 | 3 | 4 | class TestUtils(object): 5 | @staticmethod 6 | def test_to_league_name(): 7 | leagues = ["epl", "la_liga", "bundesliga", "serie_a", "ligue_1", "rfpl"] 8 | leagues = [to_league_name(league) for league in leagues] 9 | assert leagues == [ 10 | "EPL", "La_liga", "Bundesliga", "Serie_A", "Ligue_1", "RFPL"] 11 | 12 | @staticmethod 13 | def test_filter_data(): 14 | leagues = [ 15 | {"league": "epl", "players": 600}, 16 | {"league": "la_liga", "players": 300}, 17 | {"league": "bundesliga", "players": 400}, 18 | {"league": "serie_a", "players": 500}, 19 | {"league": "ligue_1", "players": 600}, 20 | {"league": "rfpl", "players": 700} 21 | ] 22 | filtered_leagues = filter_data(leagues, {"league": "epl"}) 23 | assert filtered_leagues == [{"league": "epl", "players": 600}] 24 | 25 | filtered_leagues = filter_data(leagues, {"players": 600}) 26 | assert filtered_leagues == [ 27 | {"league": "epl", "players": 600}, 28 | {"league": "ligue_1", "players": 600} 29 | ] 30 | 31 | @staticmethod 32 | def test_filter_by_positions(): 33 | data = {"FW": {"goals": {"avg": 0.0042}}, 34 | "Sub": {"goals": {"avg": 0.0026}}} 35 | filtered_data = filter_by_positions(data, None) 36 | assert filtered_data == [{"goals": {"avg": 0.0042}, "position": "FW"}, 37 | {"goals": {"avg": 0.0026}, "position": "Sub"}] 38 | 39 | filtered_data = filter_by_positions(data, "FW") 40 | assert filtered_data == [{"goals": {"avg": 0.0042}, "position": "FW"}] 41 | 42 | @staticmethod 43 | def test_filter_by_date(): 44 | data = [{'xG': 0.639599, 'xGA': 2.57262, 'date': '2022-03-16 17:30:00', 'wins': 0}, 45 | {'xG': 1.65069, 'xGA': 1.62777, 'date': '2022-07-27 15:00:00', 'wins': 0}, 46 | {'xG': 0.855926, 'xGA': 1.25668, 'date': '2022-09-05 20:00:00', 'wins': 1}] 47 | 48 | filtered_data = filter_by_date(data, 2021, None, None) 49 | assert filtered_data == [{'xG': 0.639599, 'xGA': 2.57262, 'date': '2022-03-16 17:30:00', 'wins': 0}, 50 | {'xG': 1.65069, 'xGA': 1.62777, 'date': '2022-07-27 15:00:00', 'wins': 0}, 51 | {'xG': 0.855926, 'xGA': 1.25668, 'date': '2022-09-05 20:00:00', 'wins': 1}] 52 | 53 | filtered_data = filter_by_date(data, 2021, '2022-04-01', '2022-09-05') 54 | assert filtered_data == [{'xG': 1.65069, 'xGA': 1.62777, 'date': '2022-07-27 15:00:00', 'wins': 0}, 55 | {'xG': 0.855926, 'xGA': 1.25668, 'date': '2022-09-05 20:00:00', 'wins': 1}] 56 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # pytest.ini 2 | [pytest] 3 | asyncio_mode=auto 4 | -------------------------------------------------------------------------------- /understat/__init__.py: -------------------------------------------------------------------------------- 1 | from .understat import Understat 2 | -------------------------------------------------------------------------------- /understat/constants.py: -------------------------------------------------------------------------------- 1 | BASE_URL = "https://understat.com/" 2 | LEAGUE_URL = "https://understat.com/league/{}/{}" 3 | PLAYER_URL = "https://understat.com/player/{}" 4 | TEAM_URL = "https://understat.com/team/{}/{}" 5 | MATCH_URL = "https://understat.com/match/{}/" 6 | PATTERN = r"{}\s+=\s+JSON.parse\(\'(.*?)\'\)" 7 | -------------------------------------------------------------------------------- /understat/understat.py: -------------------------------------------------------------------------------- 1 | from understat.constants import (BASE_URL, LEAGUE_URL, MATCH_URL, PLAYER_URL, 2 | TEAM_URL) 3 | from understat.utils import (filter_by_positions, filter_data, get_data, 4 | to_league_name, filter_by_date) 5 | 6 | 7 | class Understat(): 8 | def __init__(self, session): 9 | self.session = session 10 | 11 | async def get_stats(self, options=None, **kwargs): 12 | """Returns a list containing stats of every league, grouped by month. 13 | 14 | :param options: Options to filter the data by, defaults to None. 15 | :param options: dict, optional 16 | :return: List of dictionaries. 17 | :rtype: list 18 | """ 19 | 20 | stats = await get_data(self.session, BASE_URL, "statData") 21 | 22 | if options: 23 | kwargs = options 24 | 25 | filtered_data = filter_data(stats, kwargs) 26 | 27 | return filtered_data 28 | 29 | async def get_teams(self, league_name, season, options=None, **kwargs): 30 | """Returns a list containing information about all the teams in 31 | the given league in the given season. 32 | 33 | :param league_name: The league's name. 34 | :type league_name: str 35 | :param season: The season. 36 | :param options: Options to filter the data by, defaults to None. 37 | :param options: dict, optional 38 | :type season: str or int 39 | :return: A list of the league's table as seen on Understat's 40 | league overview. 41 | :rtype: list 42 | """ 43 | 44 | url = LEAGUE_URL.format(to_league_name(league_name), season) 45 | teams_data = await get_data(self.session, url, "teamsData") 46 | 47 | if options: 48 | kwargs = options 49 | 50 | filtered_data = filter_data(list(teams_data.values()), kwargs) 51 | 52 | return filtered_data 53 | 54 | async def get_league_players( 55 | self, league_name, season, options=None, **kwargs): 56 | """Returns a list containing information about all the players in 57 | the given league in the given season. 58 | 59 | :param league_name: The league's name. 60 | :type league_name: str 61 | :param season: The season. 62 | :param options: Options to filter the data by, defaults to None. 63 | :param options: dict, optional 64 | :type season: str or int 65 | :return: A list of the players as seen on Understat's league overview. 66 | :rtype: list 67 | """ 68 | 69 | url = LEAGUE_URL.format(to_league_name(league_name), season) 70 | players_data = await get_data(self.session, url, "playersData") 71 | 72 | if options: 73 | kwargs = options 74 | 75 | filtered_data = filter_data(players_data, kwargs) 76 | 77 | return filtered_data 78 | 79 | async def get_league_results( 80 | self, league_name, season, options=None, **kwargs): 81 | """Returns a list containing information about all the results 82 | (matches) played by the teams in the given league in the given season. 83 | 84 | :param league_name: The league's name. 85 | :type league_name: str 86 | :param season: The season. 87 | :type season: str or int 88 | :param options: Options to filter the data by, defaults to None. 89 | :param options: dict, optional 90 | :return: A list of the results as seen on Understat's league overview. 91 | :rtype: list 92 | """ 93 | 94 | url = LEAGUE_URL.format(to_league_name(league_name), season) 95 | dates_data = await get_data(self.session, url, "datesData") 96 | results = [r for r in dates_data if r["isResult"]] 97 | 98 | if options: 99 | kwargs = options 100 | 101 | filtered_data = filter_data(results, kwargs) 102 | 103 | return filtered_data 104 | 105 | async def get_league_fixtures( 106 | self, league_name, season, options=None, **kwargs): 107 | """Returns a list containing information about all the upcoming 108 | fixtures of the given league in the given season. 109 | 110 | :param league_name: The league's name. 111 | :type league_name: str 112 | :param season: The season. 113 | :type season: str or int 114 | :param options: Options to filter the data by, defaults to None. 115 | :param options: dict, optional 116 | :return: A list of the fixtures as seen on Understat's league overview. 117 | :rtype: list 118 | """ 119 | 120 | url = LEAGUE_URL.format(to_league_name(league_name), season) 121 | dates_data = await get_data(self.session, url, "datesData") 122 | fixtures = [f for f in dates_data if not f["isResult"]] 123 | 124 | if options: 125 | kwargs = options 126 | 127 | filtered_data = filter_data(fixtures, kwargs) 128 | 129 | return filtered_data 130 | 131 | async def get_league_table(self, league_name, season, with_headers=True, h_a="overall", start_date=None, end_date=None): 132 | """Returns the latest league table of a specified league in a specified year. 133 | 134 | :param league_name: The league's name. 135 | :type league_name: str 136 | :param season: The season. 137 | :type season: str or int 138 | :param with_headers: whether or not to include headers in the returned table. 139 | :type with_headers: bool 140 | :param h_a: whether to return the overall table ("overall"), home table ("home"), or away table ("away"). 141 | :type h_a: str 142 | :param start_date: start date to filter the table by (format: YYYY-MM-DD). 143 | :type start_date: str 144 | :param end_date: end date of the table to filter the table by (format: YYYY-MM-DD). 145 | :type end_date: str 146 | :return: List of lists. 147 | :rtype: list 148 | """ 149 | 150 | url = LEAGUE_URL.format(to_league_name(league_name), season) 151 | stats = await get_data(self.session, url, "teamsData") 152 | 153 | keys = ["wins", "draws", "loses", "scored", "missed", 154 | "pts", "xG", "npxG", "xGA", "npxGA", "npxGD", 155 | "deep", "deep_allowed", "xpts"] 156 | team_ids = [x for x in stats] 157 | 158 | data = [] 159 | for team_id in team_ids: 160 | team_data = [] 161 | season_stats = stats[team_id]["history"] 162 | if start_date is not None or end_date is not None: 163 | season_stats = filter_by_date(season_stats, season, start_date, end_date) 164 | if h_a[0].lower() != "o": 165 | season_stats = filter_data(season_stats, options={"h_a": h_a[0].lower()}) 166 | team_data.append(stats[team_id]["title"]) 167 | team_data.append(len(season_stats)) 168 | team_data.extend([round(sum(x[key] for x in season_stats), 2) for key in keys]) 169 | 170 | passes = sum(x["ppda"]["att"] for x in season_stats) 171 | def_act = sum(x["ppda"]["def"] for x in season_stats) 172 | 173 | o_passes = sum(x["ppda_allowed"]["att"] for x in season_stats) 174 | o_def_act = sum(x["ppda_allowed"]["def"] for x in season_stats) 175 | 176 | # insert PPDA and OPPDA so they match with the positions in the table on the website 177 | team_data.insert(-3, round(0 if def_act == 0 else (passes / def_act), 2)) 178 | team_data.insert(-3, round(0 if o_def_act == 0 else (o_passes / o_def_act), 2)) 179 | 180 | data.append(team_data) 181 | 182 | # sort by pts descending, followed by goal difference descending 183 | data = sorted(data, key=lambda x: (-x[7], x[6] - x[5])) 184 | 185 | if with_headers: 186 | data = [["Team", "M", "W", "D", "L", "G", "GA", "PTS", "xG", 187 | "NPxG", "xGA", "NPxGA", "NPxGD", "PPDA", "OPPDA", 188 | "DC", "ODC", "xPTS"]] + data 189 | 190 | return data 191 | 192 | async def get_player_shots(self, player_id, options=None, **kwargs): 193 | """Returns the player with the given ID's shot data. 194 | 195 | :param player_id: The player's Understat ID. 196 | :type player_id: int or str 197 | :param options: Options to filter the data by, defaults to None. 198 | :param options: dict, optional 199 | :return: List of the player's shot data. 200 | :rtype: list 201 | """ 202 | 203 | url = PLAYER_URL.format(player_id) 204 | shots_data = await get_data(self.session, url, "shotsData") 205 | 206 | if options: 207 | kwargs = options 208 | 209 | filtered_data = filter_data(shots_data, kwargs) 210 | 211 | return filtered_data 212 | 213 | async def get_player_matches(self, player_id, options=None, **kwargs): 214 | """Returns the player with the given ID's matches data. 215 | 216 | :param player_id: The player's Understat ID. 217 | :type player_id: int or str 218 | :param options: Options to filter the data by, defaults to None. 219 | :param options: dict, optional 220 | :return: List of the player's matches data. 221 | :rtype: list 222 | """ 223 | url = PLAYER_URL.format(player_id) 224 | matches_data = await get_data(self.session, url, "matchesData") 225 | 226 | if options: 227 | kwargs = options 228 | 229 | filtered_data = filter_data(matches_data, kwargs) 230 | 231 | return filtered_data 232 | 233 | async def get_player_stats(self, player_id, positions=None): 234 | """Returns the player with the given ID's min / max stats, per 235 | position(s). 236 | 237 | :param player_id: The player's Understat ID. 238 | :type player_id: int or str 239 | :param positions: Positions to filter the data by, defaults to None. 240 | :param positions: list, optional 241 | :return: List of the player's stats per position. 242 | :rtype: list 243 | """ 244 | url = PLAYER_URL.format(player_id) 245 | player_stats = await get_data(self.session, url, "minMaxPlayerStats") 246 | 247 | player_stats = filter_by_positions(player_stats, positions) 248 | 249 | return player_stats 250 | 251 | async def get_player_grouped_stats(self, player_id): 252 | """Returns the player with the given ID's grouped stats (as seen at 253 | the top of a player's page). 254 | 255 | :param player_id: The player's Understat ID. 256 | :type player_id: int or str 257 | :return: Dictionary of the player's grouped stats. 258 | :rtype: dict 259 | """ 260 | url = PLAYER_URL.format(player_id) 261 | player_stats = await get_data(self.session, url, "groupsData") 262 | 263 | return player_stats 264 | 265 | async def get_team_stats(self, team_name, season): 266 | """Returns a team's stats, as seen on their page on Understat, in the 267 | given season. 268 | 269 | :param team_name: A team's name, e.g. Manchester United. 270 | :type team_name: str 271 | :param season: A season / year, e.g. 2018. 272 | :type season: int or str 273 | :return: A dictionary containing a team's stats. 274 | :rtype: dict 275 | """ 276 | 277 | url = TEAM_URL.format(team_name.replace(" ", "_"), season) 278 | team_stats = await get_data(self.session, url, "statisticsData") 279 | 280 | return team_stats 281 | 282 | async def get_team_results( 283 | self, team_name, season, options=None, **kwargs): 284 | """Returns a team's results in the given season. 285 | 286 | :param team_name: A team's name. 287 | :type team_name: str 288 | :param season: The season. 289 | :type season: int or str 290 | :param options: Options to filter the data by, defaults to None. 291 | :param options: dict, optional 292 | :return: List of the team's results in the given season. 293 | :rtype: list 294 | """ 295 | 296 | url = TEAM_URL.format(team_name.replace(" ", "_"), season) 297 | dates_data = await get_data(self.session, url, "datesData") 298 | results = [r for r in dates_data if r["isResult"]] 299 | 300 | if options: 301 | kwargs = options 302 | 303 | filtered_data = filter_data(results, kwargs) 304 | 305 | return filtered_data 306 | 307 | async def get_team_fixtures( 308 | self, team_name, season, options=None, **kwargs): 309 | """Returns a team's upcoming fixtures in the given season. 310 | 311 | :param team_name: A team's name. 312 | :type team_name: str 313 | :param season: The season. 314 | :type season: int or str 315 | :param options: Options to filter the data by, defaults to None. 316 | :param options: dict, optional 317 | :return: List of the team's upcoming fixtures in the given season. 318 | :rtype: list 319 | """ 320 | 321 | url = TEAM_URL.format(team_name.replace(" ", "_"), season) 322 | dates_data = await get_data(self.session, url, "datesData") 323 | fixtures = [f for f in dates_data if not f["isResult"]] 324 | 325 | if options: 326 | kwargs = options 327 | 328 | filtered_data = filter_data(fixtures, kwargs) 329 | 330 | return filtered_data 331 | 332 | async def get_team_players( 333 | self, team_name, season, options=None, **kwargs): 334 | """Returns a team's player statistics in the given season. 335 | 336 | :param team_name: A team's name. 337 | :type team_name: str 338 | :param season: The season. 339 | :type season: int or str 340 | :param options: Options to filter the data by, defaults to None. 341 | :param options: dict, optional 342 | :return: List of the team's players' statistics in the given season. 343 | :rtype: list 344 | """ 345 | 346 | url = TEAM_URL.format(team_name.replace(" ", "_"), season) 347 | players_data = await get_data(self.session, url, "playersData") 348 | 349 | if options: 350 | kwargs = options 351 | 352 | filtered_data = filter_data(players_data, kwargs) 353 | 354 | return filtered_data 355 | 356 | async def get_match_stats(self, match_id): 357 | """Returns a dictionary containing stats from a given match 358 | 359 | :param fixture_id: A match's ID. 360 | :type fixture_id: int 361 | :return: Dictionary containing stats about the match played 362 | :rtype: dict 363 | """ 364 | 365 | url = MATCH_URL.format(match_id) 366 | stats_data = await get_data(self.session, url, "match_info") 367 | 368 | filtered_data = filter_data(stats_data, None) 369 | 370 | return filtered_data 371 | 372 | async def get_match_players(self, match_id, options=None, **kwargs): 373 | """Returns a dictionary containing information about the players who 374 | played in the given match. 375 | 376 | :param fixture_id: A match's ID. 377 | :type fixture_id: int 378 | :param options: Options to filter the data by, defaults to None. 379 | :param options: dict, optional 380 | :return: Dictionary containing information about the players who played 381 | in the match. 382 | :rtype: dict 383 | """ 384 | 385 | url = MATCH_URL.format(match_id) 386 | players_data = await get_data(self.session, url, "rostersData") 387 | 388 | if options: 389 | kwargs = options 390 | 391 | filtered_data = filter_data(players_data, kwargs) 392 | 393 | return filtered_data 394 | 395 | async def get_match_shots(self, match_id, options=None, **kwargs): 396 | """Returns a dictionary containing information about shots taken by 397 | the players in the given match. 398 | 399 | :param fixture_id: A match's ID. 400 | :type fixture_id: int 401 | :param options: Options to filter the data by, defaults to None. 402 | :param options: dict, optional 403 | :return: Dictionary containing information about the players who played 404 | in the match. 405 | :rtype: dict 406 | """ 407 | 408 | url = MATCH_URL.format(match_id) 409 | players_data = await get_data(self.session, url, "shotsData") 410 | 411 | if options: 412 | kwargs = options 413 | 414 | filtered_data = filter_data(players_data, kwargs) 415 | 416 | return filtered_data 417 | -------------------------------------------------------------------------------- /understat/utils.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import json 3 | import re 4 | from datetime import datetime 5 | 6 | from bs4 import BeautifulSoup 7 | 8 | from understat.constants import PATTERN 9 | 10 | 11 | def to_league_name(league_name): 12 | """Maps league name to the league name used by Understat for ease of use. 13 | """ 14 | 15 | league_mapper = { 16 | "epl": "EPL", 17 | "la_liga": "La_liga", 18 | "bundesliga": "Bundesliga", 19 | "serie_a": "Serie_A", 20 | "ligue_1": "Ligue_1", 21 | "rfpl": "RFPL" 22 | } 23 | try: 24 | return league_mapper[league_name] 25 | except KeyError: 26 | return league_name 27 | 28 | 29 | async def fetch(session, url): 30 | async with session.get(url, cookies={"beget": "begetok"}) as response: 31 | return await response.text() 32 | 33 | 34 | def find_match(scripts, pattern): 35 | """Returns the first match found in the given scripts.""" 36 | 37 | for script in scripts: 38 | match = re.search(pattern, script.string) 39 | if match: 40 | break 41 | 42 | return match 43 | 44 | 45 | def decode_data(match): 46 | """Returns data in the match's first group decoded to JSON.""" 47 | 48 | byte_data = codecs.escape_decode(match.group(1)) 49 | json_data = json.loads(byte_data[0].decode("utf-8")) 50 | 51 | return json_data 52 | 53 | async def get_data(session, url, data_type): 54 | """Returns data from the given URL of the given data type.""" 55 | 56 | html = await fetch(session, url) 57 | soup = BeautifulSoup(html, "html.parser") 58 | scripts = soup.find_all("script") 59 | 60 | pattern = re.compile(PATTERN.format(data_type)) 61 | match = find_match(scripts, pattern) 62 | data = decode_data(match) 63 | 64 | return data 65 | 66 | 67 | def filter_data(data, options): 68 | """Filters the data by the given options.""" 69 | if not options: 70 | return data 71 | 72 | return [item for item in data if 73 | all(key in item and options[key] == item[key] 74 | for key in options.keys())] 75 | 76 | 77 | def filter_by_positions(data, positions): 78 | """Filter data by positions.""" 79 | relevant_stats = [] 80 | 81 | for position, stats in data.items(): 82 | if not positions or position in positions: 83 | stats["position"] = position 84 | relevant_stats.append(stats) 85 | 86 | return relevant_stats 87 | 88 | 89 | def filter_by_date(data, season, start, end): 90 | """Filter data by start and end date.""" 91 | 92 | # change strings to datetime if specified, otherwise get full season time span 93 | try: 94 | start = datetime.strptime(start, "%Y-%m-%d") if start is not None else datetime(int(season), 1, 1) 95 | end = datetime.strptime(end, "%Y-%m-%d") if end is not None else datetime(int(season) + 2, 1, 1) 96 | 97 | return list(filter(lambda x: start <= datetime.strptime(x["date"].split()[0], "%Y-%m-%d") <= end, data)) 98 | 99 | except ValueError: 100 | raise ValueError("Invalid date format. Please use YYYY-MM-DD.") 101 | --------------------------------------------------------------------------------