├── docs ├── source │ ├── _static │ │ └── .gitkeep │ ├── _templates │ │ └── .gitkeep │ ├── misc_index.rst │ ├── usage.md │ ├── api_index.rst │ ├── index.rst │ ├── endpoints_index.rst │ ├── conf.py │ ├── glossary.rst │ └── README.md ├── Makefile └── make.bat ├── requirements.in ├── test ├── __init__.py ├── resources │ ├── data │ │ ├── match_matchinfo.json │ │ ├── team_statisticsdata.json │ │ ├── match_shotsdata.json │ │ ├── team_playersdata.json │ │ ├── match_rostersdata.json │ │ ├── team_datesdata.json │ │ ├── player_groupsdata.json │ │ └── match_ajax.json │ └── minimal.html ├── mock_requests.py └── test_api.py ├── .gitattributes ├── pyproject.toml ├── understatapi ├── __init__.py ├── endpoints │ ├── __init__.py │ ├── match.py │ ├── player.py │ ├── league.py │ ├── base.py │ └── team.py ├── parsers │ ├── __init__.py │ ├── league.py │ ├── match.py │ ├── player.py │ ├── base.py │ └── team.py ├── exceptions.py ├── utils.py └── api.py ├── ci ├── install_dependencies.sh ├── make_docs.sh └── run_tests.sh ├── .mypy.ini ├── test_requirements.in ├── cd ├── deploy_package.sh ├── deploy_docs.sh └── make_release.sh ├── .coveragerc ├── docs_requirements.in ├── requirements.txt ├── .gitignore ├── setup.cfg ├── LICENSE ├── test_requirements.txt ├── docs_requirements.txt ├── .circleci └── config.yml ├── README.md └── .pylintrc /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/_templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | """tests""" 2 | 3 | from .mock_requests import mocked_requests_get 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=false 2 | test/resources/** linguist-detectable=false 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /understatapi/__init__.py: -------------------------------------------------------------------------------- 1 | """An API for scraping data from understat.com""" 2 | 3 | from .api import UnderstatClient 4 | 5 | __version__ = "0.7.0" 6 | -------------------------------------------------------------------------------- /ci/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | pip install -r requirements.txt -r test_requirements.txt 3 | pip install -r docs_requirements.txt 4 | 5 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | disallow_untyped_defs = True 4 | disallow_incomplete_defs = True 5 | disallow_any_generics = True 6 | 7 | -------------------------------------------------------------------------------- /test_requirements.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | pylint>=3.0.0 3 | black>=24.0.0 4 | coverage>=7.0.0 5 | mypy>=1.8.0 6 | pytest>=8.0.0 7 | types-requests==2.31.0.6 8 | -------------------------------------------------------------------------------- /cd/deploy_package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | python -m pip install build twine 6 | python -m build 7 | twine upload -r dist/* --repository pypi 8 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | include = 5 | understatapi/* 6 | 7 | omit = 8 | */__init__.py 9 | 10 | [report] 11 | show_missing = True 12 | -------------------------------------------------------------------------------- /ci/make_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ -d "docs/build" ]; then 4 | rm -rf docs/build 5 | fi 6 | sphinx-apidoc --separate -f -o docs/source . ./setup.py ./test 7 | make -C docs/ html 8 | -------------------------------------------------------------------------------- /docs/source/misc_index.rst: -------------------------------------------------------------------------------- 1 | Misc 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | Utils 9 | Exceptions -------------------------------------------------------------------------------- /docs_requirements.in: -------------------------------------------------------------------------------- 1 | sphinx~=5.0.0 2 | sphinx_rtd_theme==0.5.1 3 | recommonmark==0.7.1 4 | m2r2==0.2.7 5 | sphinx-autodoc-typehints==1.11.1 6 | MiniMock==1.2.8 7 | urllib3>=1.26.5 8 | babel>=2.9.1 9 | -------------------------------------------------------------------------------- /docs/source/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | An overview of how understatAPI can be used 3 | 4 | ## Basic Usage 5 | 6 | TODO 7 | 8 | ## Context Manager 9 | 10 | TODO 11 | 12 | ## Player and Match Ids 13 | 14 | TODO 15 | -------------------------------------------------------------------------------- /docs/source/api_index.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | UnderstatClient 9 | Endpoints 10 | Misc 11 | -------------------------------------------------------------------------------- /understatapi/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | """Endpoints""" 2 | 3 | from .base import BaseEndpoint 4 | from .league import LeagueEndpoint 5 | from .player import PlayerEndpoint 6 | from .team import TeamEndpoint 7 | from .match import MatchEndpoint 8 | -------------------------------------------------------------------------------- /understatapi/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | """Parsers for extracting data from html""" 2 | 3 | from .base import BaseParser 4 | from .league import LeagueParser 5 | from .match import MatchParser 6 | from .player import PlayerParser 7 | from .team import TeamParser 8 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | understatAPI Home 2 | ================= 3 | 4 | .. toctree:: 5 | :hidden: 6 | 7 | self 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | Usage 13 | API Referance 14 | Glossary 15 | 16 | .. mdinclude:: README.md 17 | 18 | -------------------------------------------------------------------------------- /cd/deploy_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | mkdir -p /tmp/gh-pages 6 | cp -r docs/build/html /tmp/gh-pages 7 | git checkout gh-pages 8 | git pull 9 | rm -rf * && \ 10 | cp -r /tmp/gh-pages/html/* ./ && \ 11 | rm -rf /tmp/gh-pages && git add . && \ 12 | git commit -m "Updated gh-pages" && \ 13 | git push && \ 14 | git checkout - 15 | 16 | -------------------------------------------------------------------------------- /docs/source/endpoints_index.rst: -------------------------------------------------------------------------------- 1 | Endpoints 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | BaseEndPoint 9 | LeagueEndpoint 10 | MatchEndpoint 11 | PlayerEndpoint 12 | TeamEndpoint -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=requirements.txt requirements.in 6 | # 7 | certifi==2025.11.12 8 | # via requests 9 | charset-normalizer==3.4.4 10 | # via requests 11 | idna==2.10 12 | # via requests 13 | requests==2.32.5 14 | # via -r requirements.in 15 | urllib3==1.26.5 16 | # via requests 17 | -------------------------------------------------------------------------------- /ci/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | CHANGED_FILES=`git diff --name-only --diff-filter=d origin/master | grep -E '\.py$' | tr '\n' ' '` 5 | echo $CHANGED_FILES 6 | if [ -z "$CHANGED_FILES" ] 7 | then 8 | export CHANGED_FILES="understatapi/api.py" 9 | fi 10 | black --check --line-length=79 ${CHANGED_FILES} 11 | pylint ${CHANGED_FILES} 12 | mypy -p understatapi 13 | coverage run -m unittest discover 14 | coverage report --fail-under=90 15 | make -C docs/ doctest 16 | -------------------------------------------------------------------------------- /test/resources/data/match_matchinfo.json: -------------------------------------------------------------------------------- 1 | {"id": "14717", "fid": "1485433", "h": "78", "a": "89", "date": "2021-03-03 20:15:00", "league_id": "1", "season": "2020", "h_goals": "0", "a_goals": "0", "team_h": "Crystal Palace", "team_a": "Manchester United", "h_xg": "0.742397", "a_xg": "0.584016", "h_w": "0.3773", "h_d": "0.3779", "h_l": "0.2448", "league": "EPL", "h_shot": "8", "a_shot": "11", "h_shotOnTarget": "2", "a_shotOnTarget": "1", "h_deep": "3", "a_deep": "8", "a_ppda": "10.7368", "h_ppda": "16.4737"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environments 2 | venv 3 | .venv 4 | .env 5 | .python-version 6 | 7 | # log files 8 | *.log 9 | 10 | # Notebooks 11 | *.ipynb 12 | 13 | # Autogenerated files 14 | *.pyc 15 | __pycache__ 16 | htmlcov 17 | *.coverage 18 | *.rst 19 | !index.rst 20 | !glossary.rst 21 | !api_index.rst 22 | !endpoints_index.rst 23 | !services_index.rst 24 | !misc_index.rst 25 | docs/source/README.md 26 | build 27 | dist 28 | *.egg* 29 | 30 | # vscode files 31 | .vscode/ 32 | 33 | # node 34 | node_modules/ 35 | 36 | # data files 37 | *.csv 38 | *.pkl 39 | *.pickle 40 | *.png 41 | *.json 42 | !test/resources/** 43 | 44 | # Credentials 45 | *.crt 46 | *.key 47 | *.csr 48 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 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 | cp ../README.md source/README.md 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /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 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = understatapi 3 | version = attr: understatapi.__version__ 4 | description = An API for scraping data from understat.com, 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | author = collinb9 8 | author_email = brendan.m.collins@outlook.com 9 | url = https://github.com/collinb9/understatAPI 10 | project_urls = 11 | Documentation = https://collinb9.github.io/understatAPI/ 12 | Source = https://github.com/collinb9/understatAPI 13 | Download = https://pypi.org/project/understatapi/#files 14 | license_files = LICENSE.txt 15 | keywords = 16 | statistics 17 | xG 18 | expected 19 | goals 20 | fpl 21 | fantasy 22 | premier 23 | league 24 | understat 25 | football 26 | web 27 | scraping 28 | scraper 29 | 30 | [options] 31 | install_requires = file: requirements.txt 32 | packages = find: 33 | 34 | [options.packages.find] 35 | exclude = 36 | test* 37 | docs* 38 | -------------------------------------------------------------------------------- /test/resources/minimal.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | xG stats for teams and players from the TOP European leagues 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /understatapi/parsers/league.py: -------------------------------------------------------------------------------- 1 | """League parser""" 2 | 3 | from typing import Dict, Any 4 | from .base import BaseParser 5 | 6 | 7 | class LeagueParser(BaseParser): 8 | """ 9 | Parse a html page from a url of the form 10 | ``https://understat.com/league//`` 11 | """ 12 | 13 | def get_team_data(self, html: str) -> Dict[str, Any]: 14 | """ 15 | Get data for all teams 16 | 17 | :param html: The html string to parse 18 | """ 19 | return self.parse(html=html, query="teamsData") 20 | 21 | def get_match_data(self, html: str) -> Dict[str, Any]: 22 | """ 23 | Get data for all fixtures 24 | 25 | :param html: The html string to parse 26 | """ 27 | return self.parse(html=html, query="datesData") 28 | 29 | def get_player_data(self, html: str) -> Dict[str, Any]: 30 | """ 31 | Get data for all players 32 | 33 | :param html: The html string to parse 34 | """ 35 | return self.parse(html=html, query="playersData") 36 | -------------------------------------------------------------------------------- /understatapi/parsers/match.py: -------------------------------------------------------------------------------- 1 | """Match parser""" 2 | 3 | from typing import Dict, Any 4 | from .base import BaseParser 5 | 6 | 7 | class MatchParser(BaseParser): 8 | """ 9 | Parse a html page from a url of the form 10 | ``https://understat.com/match/`` 11 | """ 12 | 13 | def get_shot_data(self, html: str) -> Dict[str, Any]: 14 | """ 15 | Get shot level data for a match 16 | 17 | :param html: The html string to parse 18 | """ 19 | return self.parse(html=html, query="shotsData") 20 | 21 | def get_roster_data(self, html: str) -> Dict[str, Any]: 22 | """ 23 | Get data about the roster for each team 24 | 25 | :param html: The html string to parse 26 | """ 27 | return self.parse(html=html, query="rostersData") 28 | 29 | def get_match_info(self, html: str) -> Dict[str, Any]: 30 | """ 31 | Get information about the match 32 | 33 | :param html: The html string to parse 34 | """ 35 | return self.parse(html=html, query="match_info") 36 | -------------------------------------------------------------------------------- /understatapi/parsers/player.py: -------------------------------------------------------------------------------- 1 | """Player parser""" 2 | 3 | from typing import Dict, Any 4 | from .base import BaseParser 5 | 6 | 7 | class PlayerParser(BaseParser): 8 | """ 9 | Parse a html page from a url of the form 10 | ``https://understat.com/player/`` 11 | """ 12 | 13 | def get_match_data(self, html: str) -> Dict[str, Any]: 14 | """ 15 | Get match level data for a player 16 | 17 | :param html: The html string to parse 18 | """ 19 | return self.parse(html=html, query="matchesData") 20 | 21 | def get_shot_data(self, html: str) -> Dict[str, Any]: 22 | """ 23 | Get shot level data for a player 24 | 25 | :param html: The html string to parse 26 | """ 27 | return self.parse(html=html, query="shotsData") 28 | 29 | def get_season_data(self, html: str) -> Dict[str, Any]: 30 | """ 31 | Get season level data for a player 32 | 33 | :param html: The html string to parse 34 | """ 35 | return self.parse(html=html, query="groupsData") 36 | -------------------------------------------------------------------------------- /cd/make_release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | version=$1 6 | branch=master 7 | repo=understatapi 8 | owner=collinb9 9 | token=$GITHUB_TOKEN 10 | changeLog=`git log $(git tag -l | grep -v 'b' | tail -n 1)..HEAD --oneline --no-decorate --no-abbrev-commit | sed 's/\r$//'` 11 | 12 | generate_post_data() 13 | { 14 | jq -n \ 15 | --arg version "$version" \ 16 | --arg branch "$branch" \ 17 | --arg body "Commits since last release:\n$changeLog" \ 18 | '{ 19 | "tag_name": $version, 20 | "target_commitish": $branch, 21 | "name": $version, 22 | "body": $body, 23 | "draft": false, 24 | "prerelease": false 25 | }' 26 | } 27 | 28 | # Validate JSON before sending 29 | generate_post_data | jq -e . >/dev/null 30 | 31 | echo "Create release $version for repo: $repo_full_name branch: $branch" 32 | curl -X POST "https://api.github.com/repos/$owner/$repo/releases" \ 33 | -H "Authorization: token $token" \ 34 | -H "owner: $owner" \ 35 | -H "repo: $repo" \ 36 | -H "accept: application/vnd.github+json" \ 37 | --data-binary "$(generate_post_data)" 38 | -------------------------------------------------------------------------------- /understatapi/parsers/base.py: -------------------------------------------------------------------------------- 1 | """Base html parser""" 2 | 3 | from typing import List, Any, Dict 4 | import json 5 | 6 | 7 | class BaseParser: 8 | """Parse a html document and extract relevant data""" 9 | 10 | queries: List[str] 11 | 12 | # def __init__(self, html: str): 13 | # self.html = html 14 | 15 | @staticmethod 16 | def parse(html: str, query: str = "teamsData") -> Dict[str, Any]: 17 | """ 18 | Finds a JSON in the HTML according to a query, and returns the 19 | object corresponding to this JSON. 20 | 21 | :param html: A html document 22 | :param query: A sub-string to look for in the html document 23 | """ 24 | query_index = html.find(query) 25 | # get the start and end of the JSON data string 26 | start = html.find("(", query_index) + 2 27 | end = html.find(")", start) - 1 28 | json_data = html[start:end] 29 | # Clean up the json and return the data 30 | json_data = json_data.encode("utf8").decode("unicode_escape") 31 | data = json.loads(json_data) 32 | return data 33 | -------------------------------------------------------------------------------- /understatapi/parsers/team.py: -------------------------------------------------------------------------------- 1 | """Team parser""" 2 | 3 | from typing import Dict, Any 4 | from .base import BaseParser 5 | 6 | 7 | class TeamParser(BaseParser): 8 | """ 9 | Parse a html page from a url of the form 10 | ``https://understat.com/team//`` 11 | """ 12 | 13 | def get_player_data(self, html: str) -> Dict[str, Any]: 14 | """ 15 | Get data on a per-team basis 16 | 17 | :param html: The html string to parse 18 | """ 19 | return self.parse(html=html, query="playersData") 20 | 21 | def get_match_data(self, html: str) -> Dict[str, Any]: 22 | """ 23 | Get data on a per match level for a given team in a given season 24 | 25 | :param html: The html string to parse 26 | """ 27 | return self.parse(html=html, query="datesData") 28 | 29 | def get_context_data(self, html: str) -> Dict[str, Any]: 30 | """ 31 | Get data based on different contexts in the game 32 | 33 | :param html: The html string to parse 34 | """ 35 | return self.parse(html=html, query="statisticsData") 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brendan Collins 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 | -------------------------------------------------------------------------------- /understatapi/exceptions.py: -------------------------------------------------------------------------------- 1 | """Define custom exceptions""" 2 | 3 | from typing import Union, List 4 | 5 | PrimaryAttribute = Union[List[str], str] 6 | 7 | 8 | class InvalidSeason(Exception): 9 | """Invalid season""" 10 | 11 | def __init__(self, message: str, season: str) -> None: 12 | super().__init__(message) 13 | self.season = season 14 | 15 | 16 | class InvalidPlayer(Exception): 17 | """Invalid player""" 18 | 19 | def __init__(self, message: str, player: str) -> None: 20 | super().__init__(message) 21 | self.player = player 22 | 23 | 24 | class InvalidLeague(Exception): 25 | """Invalid league""" 26 | 27 | def __init__(self, message: str, league: str) -> None: 28 | super().__init__(message) 29 | self.league = league 30 | 31 | 32 | class InvalidTeam(Exception): 33 | """Invalid team""" 34 | 35 | def __init__(self, message: str, team: str) -> None: 36 | super().__init__(message) 37 | self.team = team 38 | 39 | 40 | class InvalidMatch(Exception): 41 | """Invalid match""" 42 | 43 | def __init__(self, message: str, match: str) -> None: 44 | super().__init__(message) 45 | self.match = match 46 | -------------------------------------------------------------------------------- /understatapi/utils.py: -------------------------------------------------------------------------------- 1 | """Helper functions for formatting data""" 2 | 3 | import sys 4 | from typing import List 5 | import inspect 6 | import re 7 | 8 | 9 | def get_all_methods(cls: type) -> List[str]: 10 | """ 11 | Get the names of all methods in a class, excluding 12 | methods decorated with ``@property``, ``@classmethod``, etc 13 | 14 | :param cls: The class to get the methods for 15 | :return: A list of the names of the methods 16 | """ 17 | return [meth[0] for meth in inspect.getmembers(cls, inspect.isfunction)] 18 | 19 | 20 | def get_public_methods(cls: type) -> List[str]: 21 | """ 22 | Get the names of all public methods in a class 23 | 24 | :param cls: The class to get all public methods for 25 | :return: A list of the names of the public methods 26 | """ 27 | methods = get_all_methods(cls) 28 | methods = [meth for meth in methods if not meth.startswith("_")] 29 | return methods 30 | 31 | 32 | def find_endpoints(line: str) -> List[str]: 33 | """ 34 | Find the name of a subclass of 35 | ``~understatapi.endpoints.base.BaseEndpoint`` in a string 36 | 37 | :param line: The string in which to search for the name of a 38 | ``~understatapi.endpoints.base.BaseEndpoint`` object 39 | """ 40 | match = re.findall(r"\w+Endpoint", line) 41 | if match is None: 42 | return [] # pragma: no cover 43 | return match 44 | 45 | 46 | def str_to_class(modulename: str, classname: str) -> type: 47 | """ 48 | Get a class by using its name 49 | """ 50 | return getattr(sys.modules[modulename], classname) 51 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=test_requirements.txt test_requirements.in 6 | # 7 | 8 | astroid==4.0.2 9 | # via pylint 10 | black==25.12.0 11 | # via -r test_requirements.in 12 | click==8.3.1 13 | # via black 14 | coverage==7.13.0 15 | # via -r test_requirements.in 16 | dill==0.4.0 17 | # via pylint 18 | exceptiongroup==1.3.1 19 | # via pytest 20 | iniconfig==2.3.0 21 | # via pytest 22 | isort==5.7.0 23 | # via pylint 24 | librt==0.7.3 25 | # via mypy 26 | mccabe==0.6.1 27 | # via pylint 28 | mypy==1.19.0 29 | # via -r test_requirements.in 30 | mypy-extensions==1.1.0 31 | # via 32 | # black 33 | # mypy 34 | packaging==25.0 35 | # via 36 | # black 37 | # pytest 38 | pathspec==0.9.0 39 | # via 40 | # black 41 | # mypy 42 | platformdirs==2.4.0 43 | # via 44 | # black 45 | # pylint 46 | pluggy==1.6.0 47 | # via pytest 48 | pygments==2.19.2 49 | # via pytest 50 | pylint==4.0.4 51 | # via -r test_requirements.in 52 | pytest==9.0.2 53 | # via -r test_requirements.in 54 | pytokens==0.3.0 55 | # via black 56 | tomli==2.3.0 57 | # via 58 | # black 59 | # mypy 60 | # pylint 61 | # pytest 62 | tomlkit==0.13.3 63 | # via pylint 64 | types-requests==2.31.0.6 65 | # via -r test_requirements.in 66 | types-urllib3==1.26.25.14 67 | # via types-requests 68 | typing-extensions==4.15.0 69 | # via 70 | # astroid 71 | # black 72 | # exceptiongroup 73 | # mypy 74 | -------------------------------------------------------------------------------- /test/mock_requests.py: -------------------------------------------------------------------------------- 1 | """Mock the requests library""" 2 | 3 | import json 4 | from requests.exceptions import HTTPError 5 | 6 | 7 | class MockResponse: 8 | """Mock response from requests.get()""" 9 | 10 | def __init__(self, url=None, status_code=200, reason="OK", **kwargs): 11 | # Accept and ignore extra kwargs like 'headers' that requests.get() accepts 12 | self.url = url 13 | self.status_code = status_code 14 | self.reason = reason 15 | 16 | @property 17 | def content(self): 18 | """Response.content""" 19 | with open(self.url) as file: 20 | content = file.read() 21 | return content 22 | 23 | @property 24 | def text(self): 25 | """Response.content""" 26 | with open(self.url) as file: 27 | text = file.read() 28 | return text 29 | 30 | def json(self): 31 | """Response.json()""" 32 | with open(self.url) as file: 33 | return json.load(file) 34 | 35 | def raise_for_status(self): 36 | """Raises ``HTTPError``, if one occurred.""" 37 | 38 | http_error_msg = "" 39 | reason = self.reason 40 | if 400 <= self.status_code < 500: 41 | http_error_msg = "%s Client Error: %s for url: %s" % ( 42 | self.status_code, 43 | reason, 44 | self.url, 45 | ) 46 | 47 | elif 500 <= self.status_code < 600: 48 | http_error_msg = "%s Server Error: %s for url: %s" % ( 49 | self.status_code, 50 | reason, 51 | self.url, 52 | ) 53 | 54 | if http_error_msg: 55 | raise HTTPError(http_error_msg, response=self) 56 | 57 | 58 | def mocked_requests_get(*args, **kwargs): 59 | """ 60 | Return a MockResponse object 61 | """ 62 | return MockResponse(*args, **kwargs) 63 | -------------------------------------------------------------------------------- /docs_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.10 3 | # by the following command: 4 | # 5 | # pip-compile --no-emit-index-url --output-file=docs_requirements.txt docs_requirements.in 6 | # 7 | alabaster==0.7.12 8 | # via sphinx 9 | babel==2.10.3 10 | # via 11 | # -r docs_requirements.in 12 | # sphinx 13 | certifi==2023.7.22 14 | # via requests 15 | chardet==4.0.0 16 | # via requests 17 | commonmark==0.9.1 18 | # via recommonmark 19 | docutils==0.17 20 | # via 21 | # m2r2 22 | # recommonmark 23 | # sphinx 24 | idna==2.10 25 | # via requests 26 | imagesize==1.2.0 27 | # via sphinx 28 | jinja2==2.11.3 29 | # via sphinx 30 | m2r2==0.2.7 31 | # via -r docs_requirements.in 32 | markupsafe==1.1.1 33 | # via jinja2 34 | minimock==1.2.8 35 | # via -r docs_requirements.in 36 | mistune==0.8.4 37 | # via m2r2 38 | packaging==20.9 39 | # via sphinx 40 | pygments==2.8.1 41 | # via sphinx 42 | pyparsing==2.4.7 43 | # via packaging 44 | pytz==2021.1 45 | # via babel 46 | recommonmark==0.7.1 47 | # via -r docs_requirements.in 48 | requests==2.25.1 49 | # via sphinx 50 | snowballstemmer==2.1.0 51 | # via sphinx 52 | sphinx==5.0.2 53 | # via 54 | # -r docs_requirements.in 55 | # recommonmark 56 | # sphinx-autodoc-typehints 57 | # sphinx-rtd-theme 58 | sphinx-autodoc-typehints==1.11.1 59 | # via -r docs_requirements.in 60 | sphinx-rtd-theme==0.5.1 61 | # via -r docs_requirements.in 62 | sphinxcontrib-applehelp==1.0.2 63 | # via sphinx 64 | sphinxcontrib-devhelp==1.0.2 65 | # via sphinx 66 | sphinxcontrib-htmlhelp==2.1.0 67 | # via sphinx 68 | sphinxcontrib-jsmath==1.0.1 69 | # via sphinx 70 | sphinxcontrib-qthelp==1.0.3 71 | # via sphinx 72 | sphinxcontrib-serializinghtml==2.0.0 73 | # via sphinx 74 | urllib3==1.26.11 75 | # via 76 | # -r docs_requirements.in 77 | # requests 78 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | import os 3 | import sys 4 | 5 | sys.path.insert(0, os.path.abspath(os.path.join("..", "..", "."))) 6 | import understatapi 7 | from datetime import datetime as dt 8 | 9 | project = "understatAPI" 10 | copyright = f"{dt.today().year}, Brendan Collins" 11 | author = "Brendan Collins" 12 | 13 | release = understatapi.__version__ 14 | version = understatapi.__version__ 15 | 16 | source_suffix = [".rst", ".md"] 17 | 18 | extensions = [ 19 | "sphinx.ext.autodoc", 20 | "sphinx.ext.autosummary", 21 | "sphinx.ext.autosectionlabel", 22 | "sphinx.ext.viewcode", 23 | "sphinx.ext.githubpages", 24 | "sphinx.ext.doctest", 25 | "sphinx_rtd_theme", 26 | "m2r2", 27 | "sphinx_autodoc_typehints", 28 | ] 29 | 30 | templates_path = ["_templates"] 31 | 32 | exclude_patterns = [] 33 | 34 | master_doc = "index" 35 | 36 | html_theme = "sphinx_rtd_theme" 37 | html_theme_options = { 38 | "collapse_navigation": True, 39 | "sticky_navigation": True, 40 | "navigation_depth": 2, 41 | "titles_only": False, 42 | } 43 | html_context = { 44 | "github_user_name": "collinb9", 45 | "github_repo_name": "collinb9/understatAPI", 46 | "project_name": "understatAPI", 47 | } 48 | 49 | autodoc_default_options = { 50 | "members": True, 51 | "member-order": "bysource", 52 | "undoc-members": True, 53 | "private-members": True, 54 | "special-members": "__init__", 55 | "show-inheritance": True, 56 | } 57 | 58 | html_static_path = ["_static"] 59 | 60 | autosummary_generate = True 61 | 62 | autosectionlabel_prefix_document = True 63 | 64 | 65 | def skip(app, what, name, obj, would_skip, options): 66 | """ 67 | Define which methods should be skipped in the documentation 68 | """ 69 | if obj.__doc__ is None: 70 | return True 71 | return would_skip 72 | 73 | 74 | def process_docstring(app, what, name, obj, options, lines): 75 | """ 76 | Process docstring before creating docs 77 | """ 78 | for i in range(len(lines)): 79 | if "#pylint" in lines[i]: 80 | lines[i] = "" 81 | 82 | 83 | def setup(app): 84 | app.connect("autodoc-skip-member", skip) 85 | app.connect("autodoc-process-docstring", process_docstring) 86 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | 5 | install_dependencies: 6 | description: Install all dependencies 7 | parameters: 8 | allowed_branch: 9 | default: staging 10 | type: string 11 | steps: 12 | - run: 13 | name: Install dependencies 14 | command: | 15 | python -m venv venv 16 | ./ci/install_dependencies.sh 17 | venv/bin/python -m pip install toml 18 | 19 | jobs: 20 | 21 | test: 22 | docker: 23 | - image: cimg/python:3.10 24 | steps: 25 | - checkout 26 | - install_dependencies 27 | - run: 28 | name: Test 29 | command: | 30 | ./ci/make_docs.sh 31 | ./ci/run_tests.sh 32 | 33 | deploy: 34 | docker: 35 | - image: cimg/python:3.10 36 | steps: 37 | - checkout 38 | - install_dependencies 39 | - run: 40 | name: Setup 41 | command: | 42 | echo $CIRCLE_TAG 43 | echo -e "[pypi]" >> ~/.pypirc 44 | echo -e "username = __token__" >> ~/.pypirc 45 | echo -e "password = $PYPI_TOKEN" >> ~/.pypirc 46 | echo -e "[user]" >> ~/.gitconfig 47 | echo -e "email = $GIT_EMAIL" >> ~/.gitconfig 48 | echo -e "name = $GIT_USERNAME" >> ~/.gitconfig 49 | - add_ssh_keys: 50 | fingerprints: 51 | - "SHA256:M0gQRqs0Xk8VoOZ4klOxGILuKVNfnYhUhkgo8Eefm50" 52 | - run: 53 | name: Deploy package 54 | command: | 55 | echo $CIRCLE_BRANCH 56 | source venv/bin/activate 57 | ./cd/deploy_package.sh 58 | ./cd/make_release.sh $CIRCLE_TAG 59 | - run: 60 | name: Publish documentation 61 | command: | 62 | echo $CIRCLE_BRANCH 63 | source venv/bin/activate 64 | git config --global user.email "$GIT_EMAIL" 65 | git config --global user.name "$GIT_USERNAME" 66 | ./ci/make_docs.sh 67 | ./cd/deploy_docs.sh 68 | 69 | workflows: 70 | version: 2 71 | build: 72 | jobs: 73 | - test 74 | deploy: 75 | jobs: 76 | - deploy: 77 | filters: 78 | tags: 79 | only: /^v.*/ 80 | branches: 81 | ignore: /.*/ 82 | -------------------------------------------------------------------------------- /docs/source/glossary.rst: -------------------------------------------------------------------------------- 1 | Glossary 2 | ======== 3 | A glossary of terms used in column names for the different tables on understat.com, divided by type of tables they can be found in 4 | 5 | .. _Glossary - General: 6 | 7 | .. rubric:: General 8 | 9 | :Team: 10 | Team name 11 | :M: 12 | Matches 13 | :W: 14 | Wins 15 | :D: 16 | Draws 17 | :L: 18 | Losses 19 | :G: 20 | Goals scored 21 | :GA: 22 | Goals against 23 | :PTS: 24 | Points 25 | :xG: 26 | Expected goals 27 | :xGA: 28 | Expected goals against 29 | :xGD: 30 | The difference between xG and xGA 31 | :NPxG: 32 | Expected goals excluding penalties as own goals 33 | :NPxGA: 34 | Expected goals against excluding penalties and own goals 35 | :NPxGD: 36 | The difference between NPxG and NPxGA 37 | :xG90: 38 | xG per 90 minutes 39 | :NPxG90: 40 | NPxG per 90 41 | 42 | .. _Glossary - Team: 43 | 44 | .. rubric:: Team 45 | 46 | :PPDA: 47 | Passes allowed per defensive action in the opposition half 48 | :OPPDA: 49 | Opponent passes allowed per defensive action in the opposition half 50 | :DC: 51 | Passes completed within 20 yards of goal (excluding crosses) 52 | :ODC: 53 | Opponent passes completed within 20 yards of goal (excluding crosses) 54 | :xPTS: 55 | Expected points 56 | 57 | .. _Glossary - Players: 58 | 59 | .. rubric:: Players 60 | 61 | :Player: 62 | Player name 63 | :Pos: 64 | Position 65 | :Apps: 66 | Appearances 67 | :Min: 68 | Minutes 69 | :NPG: 70 | Non penalty goals 71 | :A: 72 | Assists 73 | :xA: 74 | The sum of the xG of shots from a players key passes 75 | :xA90: 76 | Expected assists per 90 77 | :KP90: 78 | Key passes per 90 79 | :xGChain: 80 | The total xG of every posession the player is involved in 81 | :xGBuildop: 82 | The total xG of every posession the player is involved in excluding key passes and shots 83 | :Sh90: 84 | Shots per 90 85 | :xA90: 86 | xA per 90 87 | :xG90 + xA90: 88 | xG90 plus xA90 89 | :NPxG90 + xA90: 90 | NPxg90 plus xA90 91 | :xGChain90: 92 | xGChain per 90 93 | :xGBuildup90: 94 | xGBuildup per 90 95 | 96 | .. _Glossary - Match Context: 97 | 98 | .. rubric:: Match Context 99 | 100 | :Formation: 101 | Formation 102 | :Game state: 103 | Current goal difference 104 | :Timing: 105 | Current time in the match, broken down into 15 minute intervals 106 | :Shot zones: 107 | Zones where shots are taken from 108 | :Attack speed: 109 | Speed og the attack 110 | :Result: 111 | Outcome of a shot 112 | :Sh: 113 | Shots 114 | :ShA: 115 | Shots against 116 | :xG/Sh: 117 | Expected goals per shot 118 | :xGA/Sh: 119 | Expected goals against per shot 120 | -------------------------------------------------------------------------------- /understatapi/endpoints/match.py: -------------------------------------------------------------------------------- 1 | """Match endpoint""" 2 | 3 | from typing import Dict, Any 4 | import requests 5 | from requests.exceptions import HTTPError 6 | from .base import BaseEndpoint 7 | from ..parsers import MatchParser 8 | from ..exceptions import InvalidMatch, PrimaryAttribute 9 | 10 | 11 | class MatchEndpoint(BaseEndpoint): 12 | """ 13 | Use this class to get data from a url of the form 14 | ``https://understat.com/match/`` 15 | 16 | :Example: 17 | 18 | .. testsetup:: 19 | 20 | import requests 21 | from understatapi.endpoints import MatchEndpoint 22 | 23 | .. testcleanup:: 24 | 25 | session.close() 26 | 27 | .. doctest:: 28 | 29 | >>> session = requests.Session() 30 | >>> match_ids = ["123", "456"] 31 | >>> for match in MatchEndpoint(match_ids, session=session): 32 | ... print(match.match) 33 | 123 34 | 456 35 | """ 36 | 37 | parser = MatchParser() 38 | 39 | def __init__(self, match: PrimaryAttribute, session: requests.Session): 40 | """ 41 | :param match: Id of match(es) to get data for 42 | :param session: The current session 43 | """ 44 | self._primary_attr = match 45 | super().__init__(primary_attr=self._primary_attr, session=session) 46 | 47 | @property 48 | def match(self) -> PrimaryAttribute: 49 | """match id""" 50 | return self._primary_attr 51 | 52 | def _get_data(self, **kwargs: Any) -> Dict[str, Any]: 53 | """ 54 | Get data on a per-match basis via AJAX endpoint. 55 | 56 | :param kwargs: Keyword argument to pass to 57 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 58 | :return: Dictionary with keys: rosters, shots, tmpl 59 | """ 60 | if not isinstance(self.match, str): 61 | raise TypeError("``match`` must be a string") 62 | self._check_args() 63 | endpoint = f"getMatchData/{self.match}" 64 | 65 | try: 66 | return self._request_ajax(endpoint, **kwargs) 67 | except HTTPError as err: 68 | raise InvalidMatch( 69 | f"{self.match} is not a valid match", match=self.match 70 | ) from err 71 | 72 | def get_shot_data(self, **kwargs: Any) -> Dict[str, Any]: 73 | """ 74 | Get shot level data for a match 75 | 76 | :param kwargs: Keyword argument to pass to 77 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 78 | """ 79 | data = self._get_data(**kwargs) 80 | return data.get("shots", {}) 81 | 82 | def get_roster_data(self, **kwargs: Any) -> Dict[str, Any]: 83 | """ 84 | Get data about the roster for each team 85 | 86 | :param kwargs: Keyword argument to pass to 87 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 88 | """ 89 | data = self._get_data(**kwargs) 90 | return data.get("rosters", {}) 91 | 92 | def get_match_info(self, **kwargs: Any) -> Dict[str, Any]: 93 | """ 94 | Get information about the match 95 | 96 | :param kwargs: Keyword argument to pass to 97 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 98 | """ 99 | data = self._get_data(**kwargs) 100 | return data.get("tmpl", {}) 101 | -------------------------------------------------------------------------------- /understatapi/endpoints/player.py: -------------------------------------------------------------------------------- 1 | """Player endpoint""" 2 | 3 | from typing import Dict, Any, List 4 | import requests 5 | from requests.exceptions import HTTPError 6 | from .base import BaseEndpoint 7 | from ..parsers import PlayerParser 8 | from ..exceptions import InvalidPlayer, PrimaryAttribute 9 | 10 | 11 | class PlayerEndpoint(BaseEndpoint): 12 | """ 13 | Use this class to get data from a url of the form 14 | ``https://understat.com/player/`` 15 | 16 | :Example: 17 | 18 | .. testsetup:: 19 | 20 | import requests 21 | from understatapi.endpoints import PlayerEndpoint 22 | 23 | .. testcleanup:: 24 | 25 | session.close() 26 | 27 | .. doctest:: 28 | 29 | >>> session = requests.Session() 30 | >>> player_ids = ["000", "111"] 31 | >>> for player in PlayerEndpoint(player_ids, session=session): 32 | ... print(player.player) 33 | 000 34 | 111 35 | 36 | """ 37 | 38 | parser = PlayerParser() 39 | 40 | def __init__(self, player: PrimaryAttribute, session: requests.Session) -> None: 41 | """ 42 | :param player: Id of the player(s) to get data for 43 | :param session: The current session 44 | """ 45 | self._primary_attr = player 46 | super().__init__(primary_attr=self._primary_attr, session=session) 47 | 48 | @property 49 | def player(self) -> PrimaryAttribute: 50 | """player id""" 51 | return self._primary_attr 52 | 53 | def _get_data(self, **kwargs: Any) -> Dict[str, Any]: 54 | """ 55 | Get data on a per-player basis via AJAX endpoint. 56 | 57 | :param kwargs: Keyword argument to pass to 58 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 59 | :return: Dictionary with keys: player, matches, shots, groups, etc. 60 | """ 61 | if not isinstance(self.player, str): 62 | raise TypeError("``player`` must be a string") 63 | self._check_args() 64 | endpoint = f"getPlayerData/{self.player}" 65 | 66 | try: 67 | return self._request_ajax(endpoint, **kwargs) 68 | except HTTPError as err: 69 | raise InvalidPlayer( 70 | f"{self.player} is not a valid player or player id", 71 | player=self.player, 72 | ) from err 73 | 74 | def get_match_data(self, **kwargs: Any) -> List[Dict[str, Any]]: 75 | """ 76 | Get match level data for a player 77 | 78 | :param kwargs: Keyword argument to pass to 79 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 80 | """ 81 | data = self._get_data(**kwargs) 82 | return data.get("matches", []) 83 | 84 | def get_shot_data(self, **kwargs: Any) -> List[Dict[str, Any]]: 85 | """ 86 | Get shot level data for a player 87 | 88 | :param kwargs: Keyword argument to pass to 89 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 90 | """ 91 | data = self._get_data(**kwargs) 92 | return data.get("shots", []) 93 | 94 | def get_season_data(self, **kwargs: Any) -> List[Dict[str, Any]]: 95 | """ 96 | Get season level data for a player 97 | 98 | :param kwargs: Keyword argument to pass to 99 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 100 | """ 101 | data = self._get_data(**kwargs) 102 | return data.get("groups", []) 103 | -------------------------------------------------------------------------------- /understatapi/endpoints/league.py: -------------------------------------------------------------------------------- 1 | """League endpoint""" 2 | 3 | from typing import Dict, Any, List 4 | import requests 5 | from .base import BaseEndpoint 6 | from ..parsers import LeagueParser 7 | from ..exceptions import PrimaryAttribute 8 | 9 | 10 | class LeagueEndpoint(BaseEndpoint): 11 | """#pylint: disable-line-too-long 12 | Endpoint for league data. Use this class to get data from a url of the form 13 | ``https://understat.com/league//`` 14 | 15 | :Example: 16 | 17 | .. testsetup:: 18 | 19 | import requests 20 | from understatapi.endpoints import LeagueEndpoint 21 | 22 | .. testcleanup:: 23 | 24 | session.close() 25 | 26 | .. doctest:: 27 | 28 | >>> session = requests.Session() 29 | >>> leagues = ["EPL", "Bundesliga"] 30 | >>> for league in LeagueEndpoint(leagues, session=session): 31 | ... print(league.league) 32 | EPL 33 | Bundesliga 34 | 35 | """ 36 | 37 | parser = LeagueParser() 38 | 39 | def __init__(self, league: PrimaryAttribute, session: requests.Session): 40 | """ 41 | :param league: Name of the league(s) to get data for, 42 | one of {EPL, La_Liga, Bundesliga, Serie_A, Ligue_1, RFPL} 43 | :param session: The current session 44 | """ 45 | self._primary_attr = league 46 | super().__init__(primary_attr=self._primary_attr, session=session) 47 | 48 | @property 49 | def league(self) -> PrimaryAttribute: 50 | """league name""" 51 | return self._primary_attr 52 | 53 | def _get_data(self, season: str, **kwargs: Any) -> Dict[str, Any]: 54 | """ 55 | Get data on a league-wide basis via AJAX endpoint. 56 | 57 | :param season: Season to get data for 58 | :param kwargs: Keyword argument to pass to 59 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 60 | :return: Dictionary with keys: teams, players, dates 61 | """ 62 | if not isinstance(self.league, str): 63 | raise TypeError("``league`` must be a string") 64 | self._check_args(league=self.league, season=season) 65 | endpoint = f"getLeagueData/{self.league}/{season}" 66 | return self._request_ajax(endpoint, **kwargs) 67 | 68 | def get_team_data(self, season: str, **kwargs: Any) -> Dict[str, Any]: 69 | """ 70 | Get data for all teams in a given league and season 71 | 72 | :param season: Season to get data for 73 | :param kwargs: Keyword argument to pass to 74 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 75 | """ 76 | data = self._get_data(season=season, **kwargs) 77 | return data.get("teams", {}) 78 | 79 | def get_match_data(self, season: str, **kwargs: Any) -> List[Dict[str, Any]]: 80 | """ 81 | Get data for all fixtures in a given league and season. 82 | 83 | :param season: Season to get data for 84 | :param kwargs: Keyword argument to pass to 85 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 86 | """ 87 | data = self._get_data(season=season, **kwargs) 88 | return data.get("dates", []) 89 | 90 | def get_player_data(self, season: str, **kwargs: Any) -> List[Dict[str, Any]]: 91 | """ 92 | Get data for all players in a given league and season 93 | 94 | :param season: Season to get data for 95 | :param kwargs: Keyword argument to pass to 96 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 97 | """ 98 | data = self._get_data(season=season, **kwargs) 99 | return data.get("players", []) 100 | -------------------------------------------------------------------------------- /understatapi/endpoints/base.py: -------------------------------------------------------------------------------- 1 | """Base endpoint""" 2 | 3 | from typing import Sequence, Dict, Any, Optional 4 | import requests 5 | from requests import Response 6 | from ..parsers import BaseParser 7 | from ..exceptions import ( 8 | InvalidLeague, 9 | InvalidSeason, 10 | PrimaryAttribute, 11 | ) 12 | 13 | # Headers required for AJAX requests to Understat 14 | AJAX_HEADERS = { 15 | "X-Requested-With": "XMLHttpRequest", 16 | } 17 | 18 | 19 | class BaseEndpoint: 20 | """ 21 | Base endpoint for understat API 22 | 23 | :attr base_url: str: The base url to use for requests, 24 | ``https://understat.com/`` 25 | :attr leagues: List[str]: The available leagues, ``EPL``, ``La_Liga``, 26 | ``Bundesliga``, optional``Serie_A``, ``Ligue_1``, ``RFPL`` 27 | """ 28 | 29 | base_url = "https://understat.com/" 30 | leagues = ["EPL", "La_Liga", "Bundesliga", "Serie_A", "Ligue_1", "RFPL"] 31 | parser: BaseParser 32 | 33 | def __init__( 34 | self, 35 | primary_attr: PrimaryAttribute, 36 | session: requests.Session, 37 | ) -> None: 38 | """ 39 | :session: requests.Session: The current ``request`` session 40 | """ 41 | self.session = session 42 | self._primary_attr = primary_attr 43 | 44 | def __repr__(self) -> str: 45 | return f"<{self.__class__.__name__}({self._primary_attr!r})>" 46 | 47 | def __len__(self) -> int: 48 | if isinstance(self._primary_attr, str): 49 | return 1 50 | if isinstance(self._primary_attr, Sequence): 51 | return len(self._primary_attr) 52 | raise TypeError("Primary attribute is not a sequence or string") 53 | 54 | def __getitem__(self, index: int) -> "BaseEndpoint": 55 | if index >= len(self): 56 | raise IndexError 57 | if isinstance(self._primary_attr, str): 58 | return self.__class__(self._primary_attr, session=self.session) 59 | return self.__class__(self._primary_attr[index], session=self.session) 60 | 61 | def _check_args( 62 | self, league: Optional[str] = None, season: Optional[str] = None 63 | ) -> None: 64 | """Handle invalid arguments""" 65 | if league is not None and league not in self.leagues: 66 | raise InvalidLeague(f"{league}is not a valid league", league=league) 67 | if season is not None and int(season) < 2014: 68 | raise InvalidSeason(f"{season} is not a valid season", season=season) 69 | 70 | def _request_url(self, *args: Any, **kwargs: Any) -> Response: 71 | """ 72 | Use the requests module to send a HTTP request to a url, and check 73 | that this request worked. 74 | 75 | :param args: Arguments to pass to ``requests.get()`` 76 | :param kwargs: Keyword arguments to pass to ``requests.get()`` 77 | """ 78 | res = self.session.get(*args, **kwargs) 79 | res.raise_for_status() 80 | return res 81 | 82 | def _request_ajax(self, endpoint: str, **kwargs: Any) -> Dict[str, Any]: 83 | """ 84 | Make an AJAX request to Understat's internal API endpoints. 85 | 86 | Understat loads data dynamically via AJAX calls. This method 87 | handles the required headers and returns parsed JSON data. 88 | 89 | :param endpoint: The AJAX endpoint path (e.g., 'getLeagueData/EPL/2024') 90 | :param kwargs: Additional keyword arguments to pass to ``requests.get()`` 91 | :return: Parsed JSON response as a dictionary 92 | """ 93 | url = self.base_url + endpoint 94 | headers: Dict[str, str] = kwargs.pop("headers", {}) 95 | headers.update(AJAX_HEADERS) 96 | res = self.session.get(url, headers=headers, **kwargs) 97 | res.raise_for_status() 98 | return res.json() 99 | -------------------------------------------------------------------------------- /understatapi/endpoints/team.py: -------------------------------------------------------------------------------- 1 | """Team endpoint""" 2 | 3 | from typing import Dict, Any, List 4 | import requests 5 | from requests.exceptions import HTTPError 6 | from .base import BaseEndpoint 7 | from ..parsers import TeamParser 8 | from ..exceptions import InvalidTeam, PrimaryAttribute 9 | 10 | 11 | class TeamEndpoint(BaseEndpoint): 12 | """ 13 | Use this class to get data from a url of the form 14 | ``https://understat.com/team//`` 15 | 16 | :Example: 17 | 18 | .. testsetup:: 19 | 20 | import requests 21 | from understatapi.endpoints import TeamEndpoint 22 | 23 | .. testcleanup:: 24 | 25 | session.close() 26 | 27 | .. doctest:: 28 | 29 | >>> session = requests.Session() 30 | >>> team_names = ["Manchester_United", "Liverpool"] 31 | >>> for team in TeamEndpoint(team_names, session=session): 32 | ... print(team.team) 33 | Manchester_United 34 | Liverpool 35 | 36 | """ 37 | 38 | parser = TeamParser() 39 | 40 | def __init__(self, team: PrimaryAttribute, session: requests.Session) -> None: 41 | """ 42 | :param team: Name of the team(s) to get data for 43 | :param session: The current session 44 | """ 45 | self._primary_attr = team 46 | super().__init__(primary_attr=self._primary_attr, session=session) 47 | 48 | @property 49 | def team(self) -> PrimaryAttribute: 50 | """team name""" 51 | return self._primary_attr 52 | 53 | def _get_data(self, season: str, **kwargs: Any) -> Dict[str, Any]: 54 | """ 55 | Get data on a per-team basis via AJAX endpoint. 56 | 57 | :param season: Season to get data for 58 | :param kwargs: Keyword argument to pass to 59 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 60 | :return: Dictionary with keys: dates, players, statistics 61 | """ 62 | if not isinstance(self.team, str): 63 | raise TypeError("``team`` must be a string") 64 | self._check_args() 65 | endpoint = f"getTeamData/{self.team}/{season}" 66 | 67 | try: 68 | return self._request_ajax(endpoint, **kwargs) 69 | except HTTPError as err: 70 | raise InvalidTeam( 71 | f"{self.team} is not a valid team", team=self.team 72 | ) from err 73 | 74 | def get_player_data(self, season: str, **kwargs: Any) -> List[Dict[str, Any]]: 75 | """ 76 | Get data for all players on a given team in a given season 77 | 78 | :param season: Season to get data for 79 | :param kwargs: Keyword argument to pass to 80 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 81 | """ 82 | data = self._get_data(season=season, **kwargs) 83 | return data.get("players", []) 84 | 85 | def get_match_data(self, season: str, **kwargs: Any) -> List[Dict[str, Any]]: 86 | """ 87 | Get data on a per match level for a given team in a given season 88 | 89 | :param season: Season to get data for 90 | :param kwargs: Keyword argument to pass to 91 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 92 | """ 93 | data = self._get_data(season=season, **kwargs) 94 | return data.get("dates", []) 95 | 96 | def get_context_data( 97 | self, 98 | season: str, 99 | **kwargs: Any, 100 | ) -> Dict[str, Any]: 101 | """ 102 | Get data based on different contexts in the game 103 | 104 | :param season: Season to get data for 105 | :param kwargs: Keyword argument to pass to 106 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_ajax` 107 | """ 108 | data = self._get_data(season=season, **kwargs) 109 | return data.get("statistics", {}) 110 | -------------------------------------------------------------------------------- /test/resources/data/team_statisticsdata.json: -------------------------------------------------------------------------------- 1 | {"situation": {"OpenPlay": {"shots": 296, "goals": 40, "xG": 37.06387163233012, "against": {"shots": 253, "goals": 18, "xG": 24.169011445716023}}, "FromCorner": {"shots": 73, "goals": 5, "xG": 5.12554657459259, "against": {"shots": 46, "goals": 6, "xG": 3.4720450434833765}}, "DirectFreekick": {"shots": 15, "goals": 0, "xG": 0.9057546127587557, "against": {"shots": 13, "goals": 1, "xG": 0.8798475153744221}}, "SetPiece": {"shots": 13, "goals": 3, "xG": 2.0923739448189735, "against": {"shots": 20, "goals": 3, "xG": 2.8743203384801745}}, "Penalty": {"shots": 9, "goals": 8, "xG": 6.850482761859894, "against": {"shots": 4, "goals": 4, "xG": 3.0446385741233826}}}, "formation": {"4-2-3-1": {"stat": "4-2-3-1", "time": 2345, "shots": 351, "goals": 51, "xG": 46.046491388231516, "against": {"shots": 292, "goals": 28, "xG": 29.969899957999587}}, "4-1-2-1-2": {"stat": "4-1-2-1-2", "time": 281, "shots": 38, "goals": 2, "xG": 3.4509430108591914, "against": {"shots": 31, "goals": 2, "xG": 3.871929860673845}}, "4-3-1-2": {"stat": "4-3-1-2", "time": 95, "shots": 15, "goals": 3, "xG": 2.5066683925688267, "against": {"shots": 10, "goals": 2, "xG": 0.4987832382321358}}, "4-2-2-2": {"stat": "4-2-2-2", "time": 11, "shots": 2, "goals": 0, "xG": 0.03392673470079899, "against": {"shots": 1, "goals": 0, "xG": 0.02308344841003418}}, "4-4-2": {"stat": "4-4-2", "time": 7, "shots": 0, "goals": 0, "xG": 0, "against": {"shots": 2, "goals": 0, "xG": 0.0761664118617773}}}, "gameState": {"Goal diff 0": {"stat": "Goal diff 0", "time": 1621, "shots": 212, "goals": 24, "xG": 24.27265651896596, "against": {"shots": 167, "goals": 13, "xG": 16.585439119488}}, "Goal diff +1": {"stat": "Goal diff +1", "time": 430, "shots": 59, "goals": 9, "xG": 9.178283012472093, "against": {"shots": 80, "goals": 8, "xG": 8.280326046980917}}, "Goal diff -1": {"stat": "Goal diff -1", "time": 318, "shots": 61, "goals": 9, "xG": 5.2798657808452845, "against": {"shots": 35, "goals": 4, "xG": 3.9664257364347577}}, "Goal diff > +1": {"stat": "Goal diff > +1", "time": 273, "shots": 64, "goals": 12, "xG": 12.130523779429495, "against": {"shots": 39, "goals": 4, "xG": 3.7812048410996795}}, "Goal diff < -1": {"stat": "Goal diff < -1", "time": 97, "shots": 10, "goals": 2, "xG": 1.1767004346475005, "against": {"shots": 15, "goals": 3, "xG": 1.8264671731740236}}}, "timing": {"1-15": {"stat": "1-15", "shots": 45, "goals": 4, "xG": 5.221434570848942, "against": {"shots": 48, "goals": 7, "xG": 5.304950061254203}}, "16-30": {"stat": "16-30", "shots": 68, "goals": 10, "xG": 6.971111190505326, "against": {"shots": 59, "goals": 5, "xG": 3.966503909789026}}, "31-45": {"stat": "31-45", "shots": 63, "goals": 9, "xG": 8.017648584209383, "against": {"shots": 44, "goals": 6, "xG": 5.190296335145831}}, "46-60": {"stat": "46-60", "shots": 72, "goals": 8, "xG": 10.524363692849874, "against": {"shots": 63, "goals": 4, "xG": 6.51651868969202}}, "61-75": {"stat": "61-75", "shots": 77, "goals": 12, "xG": 10.413152324967086, "against": {"shots": 44, "goals": 4, "xG": 4.653240266256034}}, "76+": {"stat": "76+", "shots": 81, "goals": 13, "xG": 10.890319162979722, "against": {"shots": 78, "goals": 6, "xG": 8.808353655040264}}}, "shotZone": {"ownGoals": {"stat": "ownGoals", "shots": 3, "goals": 3, "xG": 3, "against": {"shots": 2, "goals": 2, "xG": 2}}, "shotOboxTotal": {"stat": "shotOboxTotal", "shots": 160, "goals": 7, "xG": 5.110826983116567, "against": {"shots": 120, "goals": 3, "xG": 3.7954597854986787}}, "shotPenaltyArea": {"stat": "shotPenaltyArea", "shots": 223, "goals": 39, "xG": 35.760384446009994, "against": {"shots": 186, "goals": 17, "xG": 20.65961684472859}}, "shotSixYardBox": {"stat": "shotSixYardBox", "shots": 20, "goals": 7, "xG": 8.166818097233772, "against": {"shots": 28, "goals": 10, "xG": 7.984786286950111}}}, "attackSpeed": {"Normal": {"stat": "Normal", "shots": 238, "goals": 34, "xG": 28.652238664217293, "against": {"shots": 203, "goals": 14, "xG": 18.807163101620972}}, "Standard": {"stat": "Standard", "shots": 110, "goals": 16, "xG": 14.974157894030213, "against": {"shots": 83, "goals": 14, "xG": 10.270851471461356}}, "Slow": {"stat": "Slow", "shots": 38, "goals": 2, "xG": 3.8474159631878138, "against": {"shots": 38, "goals": 4, "xG": 4.375127009116113}}, "Fast": {"stat": "Fast", "shots": 20, "goals": 4, "xG": 4.564217004925013, "against": {"shots": 12, "goals": 0, "xG": 0.9867213349789381}}}, "result": {"MissedShots": {"shots": 115, "goals": 0, "xG": 11.371283490210772, "against": {"shots": 124, "goals": 0, "xG": 9.530040805228055}}, "SavedShot": {"shots": 107, "goals": 0, "xG": 12.708629734814167, "against": {"shots": 73, "goals": 0, "xG": 6.267106755636632}}, "Goal": {"shots": 56, "goals": 56, "xG": 20.739717919379473, "against": {"shots": 32, "goals": 32, "xG": 11.719112562015653}}, "BlockedShot": {"shots": 125, "goals": 0, "xG": 6.706955146975815, "against": {"shots": 95, "goals": 0, "xG": 5.772894547320902}}, "ShotOnPost": {"shots": 3, "goals": 0, "xG": 0.5114432349801064, "against": {"shots": 12, "goals": 0, "xG": 1.1507082469761372}}}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/collinb9/understatAPI.svg?branch=master)](https://travis-ci.com/collinb9/understatAPI) 2 | ![PyPI](https://img.shields.io/pypi/v/understatapi) 3 | ![PyPI - License](https://img.shields.io/pypi/l/understatapi) 4 | 5 | # understatAPI 6 | 7 | This is a python API for scraping data from [understat.com](https://understat.com/). Understat is a website with football data for 6 european leagues for every season since 2014/15 season. The leagues available are the Premier League, La Liga, Ligue 1, Serie A, Bundesliga and the Russian Premier League. 8 | 9 | --- 10 | 11 | **NOTE** 12 | 13 | I am not affiliated with understat.com in any way 14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | To install the package run 20 | 21 | ```bash 22 | pip install understatapi 23 | ``` 24 | 25 | If you would like to use the package with the latest development changes you can clone this repo and install the package 26 | 27 | ```bash 28 | git clone git@github.com:collinb9/understatAPI understatAPI 29 | cd understatAPI 30 | python -m pip install . 31 | ``` 32 | ## Getting started 33 | 34 | The API contains endpoints which reflect the structure of the understat website. Below is a table showing the different endpoints and the pages on understat.com to which they correspond 35 | 36 | | Endpoint | Webpage | 37 | | ---------------------- | ------------------------------------------------- | 38 | | UnderstatClient.league | `https://understat.com/league/` | 39 | | UnderstatClient.team | `https://understat.com/team//` | 40 | | UnderstatClient.player | `https://understat.com/player/` | 41 | | UnderstatClient.match | `https://understat.com/match/` | 42 | 43 | Every method in the public API corresponds to one of the tables visible on the understat page for the relevant endpoint. 44 | Each method returns JSON with the relevant data. Below are some examples of how to use the API. Note how the `league()` and `team()` methods can accept the names of leagues and teams respectively, but `player()` and `match()` must receive an id number. 45 | 46 | ```python 47 | from understatapi import UnderstatClient 48 | 49 | understat = UnderstatClient() 50 | # get data for every player playing in the Premier League in 2019/20 51 | league_player_data = understat.league(league="EPL").get_player_data(season="2019") 52 | # Get the name and id of one of the player 53 | player_id, player_name = league_player_data[0]["id"], league_player_data[0]["player_name"] 54 | # Get data for every shot this player has taken in a league match (for all seasons) 55 | player_shot_data = understat.player(player=player_id).get_shot_data() 56 | ``` 57 | 58 | ```python 59 | from understatapi import UnderstatClient 60 | 61 | understat = UnderstatClient() 62 | # get data for every league match involving Manchester United in 2019/20 63 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 64 | # get the id for the first match of the season 65 | match_id = team_match_data[0]["id"] 66 | # get the rosters for the both teams in that match 67 | roster_data = understat.match(match=match_id).get_roster_data() 68 | ``` 69 | 70 | You can also use the `UnderstatClient` class as a context manager which closes the session after it has been used, and also has some improved error handling. This is the recommended way to interact with the API. 71 | 72 | ```python 73 | from understatapi import UnderstatClient 74 | 75 | with UnderstatClient() as understat: 76 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 77 | ``` 78 | 79 | For a full API reference, see [the documentation](https://collinb9.github.io/understatAPI/) 80 | 81 | ## Contributing 82 | 83 | If you find any bugs in the code or have any feature requests, please make an issue and I'll try to address it as soon as possible. If you would like to implement the changes yourself you can make a pull request 84 | 85 | - Clone the repo `git clone git@github.com:collinb9/understatAPI` 86 | - Create a branch to work off `git checkout -b descriptive_branch_name` 87 | - Make and commit your changes 88 | - Push your changes `git push` 89 | - Come back to the repo on github, and click on Pull requests -> New pull request 90 | 91 | Before a pull request can be merged the code will have to pass a number of checks that are run using CircleCI. These checks are 92 | 93 | - Check that the code has been formatted using [black](https://github.com/psf/black) 94 | - Lint the code using [pylint](https://github.com/PyCQA/pylint) 95 | - Check type annotations using [mypy](https://github.com/python/mypy) 96 | - Run the unit tests and check that they have 100% coverage 97 | 98 | These checks are in place to ensure a consistent style and quality across the code. To check if the changes you have made will pass these tests run 99 | 100 | ```bash 101 | pip install -r requirements.txt 102 | pip install -r test_requirments.txt 103 | pip install -r docs_requirments.txt 104 | chmod +x ci/run_tests.sh 105 | ci/run_tests.sh 106 | ``` 107 | 108 | Don't let these tests deter you from making a pull request. Make the changes to introduce the new functionality/bug fix and then I will be happy to help get the code to a stage where it passes the tests. 109 | 110 | ## Versioning 111 | 112 | The versioning for this project follows the [semantic versioning](https://semver.org/) conventions. 113 | 114 | ## TODO 115 | 116 | - Add asynchronous support 117 | -------------------------------------------------------------------------------- /docs/source/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/collinb9/understatAPI.svg?branch=master)](https://travis-ci.com/collinb9/understatAPI) 2 | ![PyPI](https://img.shields.io/pypi/v/understatapi) 3 | ![PyPI - License](https://img.shields.io/pypi/l/understatapi) 4 | 5 | # understatAPI 6 | 7 | This is a python API for scraping data from [understat.com](https://understat.com/). Understat is a website with football data for 6 european leagues for every season since 2014/15 season. The leagues available are the Premier League, La Liga, Ligue 1, Serie A, Bundesliga and the Russian Premier League. 8 | 9 | --- 10 | 11 | **NOTE** 12 | 13 | I am not affiliated with understat.com in any way 14 | 15 | --- 16 | 17 | ## Installation 18 | 19 | To install the package run 20 | 21 | ```bash 22 | pip install understatapi 23 | ``` 24 | 25 | If you would like to use the package with the latest development changes you can clone this repo and install the package 26 | 27 | ```bash 28 | git clone git@github.com:collinb9/understatAPI understatAPI 29 | cd understatAPI 30 | python -m pip install . 31 | ``` 32 | ## Getting started 33 | 34 | The API contains endpoints which reflect the structure of the understat website. Below is a table showing the different endpoints and the pages on understat.com to which they correspond 35 | 36 | | Endpoint | Webpage | 37 | | ---------------------- | ------------------------------------------------- | 38 | | UnderstatClient.league | `https://understat.com/league/` | 39 | | UnderstatClient.team | `https://understat.com/team//` | 40 | | UnderstatClient.player | `https://understat.com/player/` | 41 | | UnderstatClient.match | `https://understat.com/match/` | 42 | 43 | Every method in the public API corresponds to one of the tables visible on the understat page for the relevant endpoint. 44 | Each method returns JSON with the relevant data. Below are some examples of how to use the API. Note how the `league()` and `team()` methods can accept the names of leagues and teams respectively, but `player()` and `match()` must receive an id number. 45 | 46 | ```python 47 | from understatapi import UnderstatClient 48 | 49 | understat = UnderstatClient() 50 | # get data for every player playing in the Premier League in 2019/20 51 | league_player_data = understat.league(league="EPL").get_player_data(season="2019") 52 | # Get the name and id of one of the player 53 | player_id, player_name = league_player_data[0]["id"], league_player_data[0]["player_name"] 54 | # Get data for every shot this player has taken in a league match (for all seasons) 55 | player_shot_data = understat.player(player=player_id).get_shot_data() 56 | ``` 57 | 58 | ```python 59 | from understatapi import UnderstatClient 60 | 61 | understat = UnderstatClient() 62 | # get data for every league match involving Manchester United in 2019/20 63 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 64 | # get the id for the first match of the season 65 | match_id = team_match_data[0]["id"] 66 | # get the rosters for the both teams in that match 67 | roster_data = understat.match(match=match_id).get_roster_data() 68 | ``` 69 | 70 | You can also use the `UnderstatClient` class as a context manager which closes the session after it has been used, and also has some improved error handling. This is the recommended way to interact with the API. 71 | 72 | ```python 73 | from understatapi import UnderstatClient 74 | 75 | with UnderstatClient() as understat: 76 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 77 | ``` 78 | 79 | For a full API reference, see [the documentation](https://collinb9.github.io/understatAPI/) 80 | 81 | ## Contributing 82 | 83 | If you find any bugs in the code or have any feature requests, please make an issue and I'll try to address it as soon as possible. If you would like to implement the changes yourself you can make a pull request 84 | 85 | - Clone the repo `git clone git@github.com:collinb9/understatAPI` 86 | - Create a branch to work off `git checkout -b descriptive_branch_name` 87 | - Make and commit your changes 88 | - Push your changes `git push` 89 | - Come back to the repo on github, and click on Pull requests -> New pull request 90 | 91 | Before a pull request can be merged the code will have to pass a number of checks that are run using CircleCI. These checks are 92 | 93 | - Check that the code has been formatted using [black](https://github.com/psf/black) 94 | - Lint the code using [pylint](https://github.com/PyCQA/pylint) 95 | - Check type annotations using [mypy](https://github.com/python/mypy) 96 | - Run the unit tests and check that they have 100% coverage 97 | 98 | These checks are in place to ensure a consistent style and quality across the code. To check if the changes you have made will pass these tests run 99 | 100 | ```bash 101 | pip install -r requirements.txt 102 | pip install -r test_requirments.txt 103 | pip install -r docs_requirments.txt 104 | chmod +x ci/run_tests.sh 105 | ci/run_tests.sh 106 | ``` 107 | 108 | Don't let these tests deter you from making a pull request. Make the changes to introduce the new functionality/bug fix and then I will be happy to help get the code to a stage where it passes the tests. 109 | 110 | ## Versioning 111 | 112 | The versioning for this project follows the [semantic versioning](https://semver.org/) conventions. 113 | 114 | ## TODO 115 | 116 | - Add asynchronous support 117 | -------------------------------------------------------------------------------- /understatapi/api.py: -------------------------------------------------------------------------------- 1 | """understatAPI client""" 2 | 3 | from types import TracebackType 4 | import requests 5 | from .utils import get_public_methods, str_to_class, find_endpoints 6 | from .endpoints import ( 7 | LeagueEndpoint, 8 | PlayerEndpoint, 9 | TeamEndpoint, 10 | MatchEndpoint, 11 | ) 12 | from .exceptions import PrimaryAttribute 13 | 14 | 15 | class UnderstatClient: 16 | """#pylint: disable=line-too-long 17 | API client for understat 18 | 19 | The main interface for interacting with understatAPI. Exposes 20 | each of the entrypoints, maintains a consistent 21 | session and handles errors 22 | 23 | :Example: 24 | 25 | .. code-block:: 26 | 27 | from understatapi import UnderstatClient 28 | 29 | with UnderstatClient() as understat: 30 | league_player_data = understat.league(league="EPL").get_player_data(season="2019") 31 | player_shot_data = understat.player(player="2371").get_shot_data() 32 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 33 | roster_data = understat.match(match="14711").get_roster_data() 34 | 35 | Using the context manager gives some more verbose error handling 36 | 37 | .. testsetup:: 38 | 39 | from understatapi import UnderstatClient 40 | 41 | .. doctest:: 42 | 43 | >>> team="" 44 | >>> with UnderstatClient() as understat: 45 | ... understat.team(team).get_bad_data() # doctest: +SKIP 46 | Traceback (most recent call last) 47 | File "", line 2, in 48 | File "understatapi/api.py", line 59, in __exit__ 49 | raise AttributeError( 50 | AttributeError: 'TeamEndpoint' object has no attribute 'get_bad_data' 51 | Its public methods are ['get_context_data', 'get_match_data', 'get_player_data'] 52 | 53 | """ 54 | 55 | def __init__(self) -> None: 56 | self.session = requests.Session() 57 | 58 | def __enter__(self) -> "UnderstatClient": 59 | return self 60 | 61 | def __exit__( 62 | self, 63 | exception_type: type, 64 | exception_value: BaseException, 65 | traceback: TracebackType, 66 | ) -> None: 67 | if exception_type is AttributeError: 68 | endpoint = find_endpoints(str(exception_value)) 69 | endpoint_obj = str_to_class(__name__, endpoint[0]) 70 | public_methods = get_public_methods(endpoint_obj) 71 | raise AttributeError( 72 | str(exception_value) + f"\nIts public methods are {public_methods}" 73 | ) 74 | self.session.close() 75 | 76 | def league(self, league: PrimaryAttribute) -> LeagueEndpoint: 77 | """ 78 | Endpoint for league data. Use this function to get data from a 79 | url of the form ``https://understat.com/league//`` 80 | 81 | :param league: Name of the league(s) to get data for, 82 | one of {EPL, La_Liga, Bundesliga, Serie_A, Ligue_1, RFPL} 83 | :rtype: :py:class:`~understatapi.endpoints.league.LeagueEndpoint` 84 | 85 | :Example: 86 | 87 | .. testsetup:: 88 | 89 | from understatapi import UnderstatClient 90 | 91 | .. doctest:: 92 | 93 | >>> leagues = ["EPL", "Bundesliga"] 94 | >>> with UnderstatClient() as understat: 95 | ... for league in understat.league(leagues): 96 | ... print(league.league) 97 | EPL 98 | Bundesliga 99 | 100 | """ 101 | return LeagueEndpoint(league=league, session=self.session) 102 | 103 | def player(self, player: PrimaryAttribute) -> PlayerEndpoint: 104 | """ 105 | Endpoint for player data. Use this function to get data from a 106 | url of the form ``https://understat.com/player//`` 107 | 108 | :param player: Id of the player(s) to get data for 109 | :rtype: :py:class:`~understatapi.endpoints.player.PlayerEndpoint` 110 | 111 | :Example: 112 | 113 | .. testsetup:: 114 | 115 | from understatapi import UnderstatClient 116 | 117 | .. doctest:: 118 | 119 | >>> player_ids = ["000", "111"] 120 | >>> with UnderstatClient() as understat: 121 | ... for player in understat.player(player_ids): 122 | ... print(player.player) 123 | 000 124 | 111 125 | 126 | """ 127 | return PlayerEndpoint(player=player, session=self.session) 128 | 129 | def team(self, team: PrimaryAttribute) -> TeamEndpoint: 130 | """ 131 | Endpoint for team data. Use this function to get data from a 132 | url of the form ``https://understat.com/team//`` 133 | 134 | :param team: Name of the team(s) to get data for 135 | :rtype: :py:class:`~understatapi.endpoints.team.TeamEndpoint` 136 | 137 | :Example: 138 | 139 | .. testsetup:: 140 | 141 | from understatapi import UnderstatClient 142 | 143 | .. doctest:: 144 | 145 | >>> team_names = ["Manchester_United", "Liverpool"] 146 | >>> with UnderstatClient() as understat: 147 | ... for team in understat.team(team_names): 148 | ... print(team.team) 149 | Manchester_United 150 | Liverpool 151 | 152 | """ 153 | return TeamEndpoint(team=team, session=self.session) 154 | 155 | def match(self, match: PrimaryAttribute) -> MatchEndpoint: 156 | """ 157 | Endpoint for match data. Use this function to get data from a 158 | url of the form ``https://understat.com/match/`` 159 | 160 | :param match: Id of match(es) to get data for 161 | :rtype: :class:`~understatapi.endpoints.match.MatchEndpoint` 162 | 163 | :Example: 164 | 165 | .. testsetup:: 166 | 167 | from understatapi import UnderstatClient 168 | 169 | .. doctest:: 170 | 171 | >>> match_ids = ["123", "456"] 172 | >>> with UnderstatClient() as understat: 173 | ... for match in understat.match(match_ids): 174 | ... print(match.match) 175 | 123 176 | 456 177 | 178 | """ 179 | return MatchEndpoint( 180 | match=match, 181 | session=self.session, 182 | ) 183 | -------------------------------------------------------------------------------- /test/resources/data/match_shotsdata.json: -------------------------------------------------------------------------------- 1 | {"h": [{"id": "408200", "minute": "6", "result": "MissedShots", "X": "0.8830000305175781", "Y": "0.5579999923706055", "xG": "0.08583834767341614", "player": "Christian Benteke", "h_a": "h", "player_id": "606", "situation": "OpenPlay", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Andros Townsend", "lastAction": "Cross"}, {"id": "408201", "minute": "7", "result": "BlockedShot", "X": "0.759000015258789", "Y": "0.5170000076293946", "xG": "0.01916843093931675", "player": "Andros Townsend", "h_a": "h", "player_id": "775", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Christian Benteke", "lastAction": "HeadPass"}, {"id": "408205", "minute": "13", "result": "BlockedShot", "X": "0.8190000152587891", "Y": "0.524000015258789", "xG": "0.05942648649215698", "player": "Andros Townsend", "h_a": "h", "player_id": "775", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": null, "lastAction": "BallRecovery"}, {"id": "408210", "minute": "49", "result": "MissedShots", "X": "0.91", "Y": "0.54", "xG": "0.07911469042301178", "player": "Christian Benteke", "h_a": "h", "player_id": "606", "situation": "FromCorner", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "James McCarthy", "lastAction": "Cross"}, {"id": "408211", "minute": "50", "result": "SavedShot", "X": "0.865", "Y": "0.26399999618530273", "xG": "0.032427556812763214", "player": "Jordan Ayew", "h_a": "h", "player_id": "672", "situation": "OpenPlay", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Christian Benteke", "lastAction": "Pass"}, {"id": "408212", "minute": "58", "result": "BlockedShot", "X": "0.7609999847412109", "Y": "0.5159999847412109", "xG": "0.07418951392173767", "player": "Luka Milivojevic", "h_a": "h", "player_id": "5549", "situation": "DirectFreekick", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": null, "lastAction": "Standard"}, {"id": "408213", "minute": "58", "result": "MissedShots", "X": "0.8430000305175781", "Y": "0.4229999923706055", "xG": "0.05620395392179489", "player": "Andros Townsend", "h_a": "h", "player_id": "775", "situation": "SetPiece", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": null, "lastAction": "None"}, {"id": "408218", "minute": "89", "result": "SavedShot", "X": "0.9159999847412109", "Y": "0.6020000076293945", "xG": "0.336028516292572", "player": "Patrick van Aanholt", "h_a": "h", "player_id": "730", "situation": "OpenPlay", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Luka Milivojevic", "lastAction": "Pass"}], "a": [{"id": "408202", "minute": "12", "result": "SavedShot", "X": "0.7780000305175782", "Y": "0.7090000152587891", "xG": "0.01633571647107601", "player": "Nemanja Matic", "h_a": "a", "player_id": "697", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Bruno Fernandes", "lastAction": "Pass"}, {"id": "408203", "minute": "12", "result": "BlockedShot", "X": "0.9019999694824219", "Y": "0.49700000762939456", "xG": "0.02284710295498371", "player": "Harry Maguire", "h_a": "a", "player_id": "1687", "situation": "FromCorner", "season": "2020", "shotType": "Head", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Bruno Fernandes", "lastAction": "Aerial"}, {"id": "408204", "minute": "13", "result": "MissedShots", "X": "0.96", "Y": "0.495", "xG": "0.1537684053182602", "player": "Edinson Cavani", "h_a": "a", "player_id": "3294", "situation": "FromCorner", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Marcus Rashford", "lastAction": "Rebound"}, {"id": "408206", "minute": "15", "result": "MissedShots", "X": "0.865", "Y": "0.5609999847412109", "xG": "0.10002944618463516", "player": "Marcus Rashford", "h_a": "a", "player_id": "556", "situation": "OpenPlay", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Luke Shaw", "lastAction": "Pass"}, {"id": "408207", "minute": "17", "result": "BlockedShot", "X": "0.7269999694824218", "Y": "0.5129999923706055", "xG": "0.016564758494496346", "player": "Fred", "h_a": "a", "player_id": "6817", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": null, "lastAction": "BallTouch"}, {"id": "408208", "minute": "22", "result": "BlockedShot", "X": "0.8190000152587891", "Y": "0.500999984741211", "xG": "0.07781993597745895", "player": "Mason Greenwood", "h_a": "a", "player_id": "7490", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Edinson Cavani", "lastAction": "LayOff"}, {"id": "408209", "minute": "26", "result": "MissedShots", "X": "0.875", "Y": "0.4159999847412109", "xG": "0.024585524573922157", "player": "Edinson Cavani", "h_a": "a", "player_id": "3294", "situation": "OpenPlay", "season": "2020", "shotType": "Head", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Marcus Rashford", "lastAction": "Cross"}, {"id": "408214", "minute": "68", "result": "BlockedShot", "X": "0.7580000305175781", "Y": "0.6940000152587891", "xG": "0.017387786880135536", "player": "Nemanja Matic", "h_a": "a", "player_id": "697", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Fred", "lastAction": "Pass"}, {"id": "408215", "minute": "77", "result": "MissedShots", "X": "0.889000015258789", "Y": "0.5379999923706055", "xG": "0.06543221324682236", "player": "Daniel James", "h_a": "a", "player_id": "5595", "situation": "OpenPlay", "season": "2020", "shotType": "Head", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Luke Shaw", "lastAction": "Cross"}, {"id": "408216", "minute": "80", "result": "MissedShots", "X": "0.8190000152587891", "Y": "0.45799999237060546", "xG": "0.061181697994470596", "player": "Mason Greenwood", "h_a": "a", "player_id": "7490", "situation": "OpenPlay", "season": "2020", "shotType": "LeftFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Bruno Fernandes", "lastAction": "Pass"}, {"id": "408217", "minute": "87", "result": "MissedShots", "X": "0.845", "Y": "0.6179999923706054", "xG": "0.031576987355947495", "player": "Luke Shaw", "h_a": "a", "player_id": "1006", "situation": "OpenPlay", "season": "2020", "shotType": "RightFoot", "match_id": "14717", "h_team": "Crystal Palace", "a_team": "Manchester United", "h_goals": "0", "a_goals": "0", "date": "2021-03-03 20:15:00", "player_assisted": "Bruno Fernandes", "lastAction": "HeadPass"}]} -------------------------------------------------------------------------------- /test/resources/data/team_playersdata.json: -------------------------------------------------------------------------------- 1 | [{"id": "1228", "player_name": "Bruno Fernandes", "games": "29", "time": "2479", "goals": "16", "xG": "13.099921450950205", "assists": "10", "xA": "9.646174143999815", "shots": "90", "key_passes": "82", "yellow_cards": "5", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "8", "npxG": "6.249438648112118", "xGChain": "19.98861371539533", "xGBuildup": "8.240112544968724"}, {"id": "556", "player_name": "Marcus Rashford", "games": "29", "time": "2390", "goals": "9", "xG": "8.376784265041351", "assists": "7", "xA": "2.9866093453019857", "shots": "63", "key_passes": "34", "yellow_cards": "4", "red_cards": "0", "position": "F M S", "team_title": "Manchester United", "npg": "9", "npxG": "8.376784265041351", "xGChain": "16.367887154221535", "xGBuildup": "7.064654899761081"}, {"id": "3294", "player_name": "Edinson Cavani", "games": "18", "time": "910", "goals": "6", "xG": "6.466721750795841", "assists": "2", "xA": "1.6402411442250013", "shots": "26", "key_passes": "8", "yellow_cards": "1", "red_cards": "0", "position": "F S", "team_title": "Manchester United", "npg": "6", "npxG": "6.466721750795841", "xGChain": "8.781574584543705", "xGBuildup": "1.9556357935070992"}, {"id": "553", "player_name": "Anthony Martial", "games": "22", "time": "1494", "goals": "4", "xG": "7.405651165172458", "assists": "3", "xA": "2.6198029601946473", "shots": "42", "key_passes": "17", "yellow_cards": "0", "red_cards": "1", "position": "F M S", "team_title": "Manchester United", "npg": "4", "npxG": "7.405651165172458", "xGChain": "11.814813017845154", "xGBuildup": "3.3195053301751614"}, {"id": "5560", "player_name": "Scott McTominay", "games": "25", "time": "1604", "goals": "4", "xG": "1.1963342912495136", "assists": "1", "xA": "2.050936982035637", "shots": "19", "key_passes": "15", "yellow_cards": "1", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "4", "npxG": "1.1963342912495136", "xGChain": "9.169829338788986", "xGBuildup": "6.660057496279478"}, {"id": "1740", "player_name": "Paul Pogba", "games": "19", "time": "1344", "goals": "3", "xG": "1.6847801934927702", "assists": "0", "xA": "0.7464981349185109", "shots": "20", "key_passes": "13", "yellow_cards": "3", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "3", "npxG": "1.6847801934927702", "xGChain": "7.229092352092266", "xGBuildup": "5.390020430088043"}, {"id": "5595", "player_name": "Daniel James", "games": "12", "time": "737", "goals": "3", "xG": "1.9432968124747276", "assists": "0", "xA": "0.3018530663102865", "shots": "16", "key_passes": "6", "yellow_cards": "3", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "3", "npxG": "1.9432968124747276", "xGChain": "3.674423024058342", "xGBuildup": "1.5651227254420519"}, {"id": "1687", "player_name": "Harry Maguire", "games": "29", "time": "2610", "goals": "2", "xG": "1.2747255498543382", "assists": "1", "xA": "0.2676918674260378", "shots": "31", "key_passes": "6", "yellow_cards": "8", "red_cards": "0", "position": "D", "team_title": "Manchester United", "npg": "2", "npxG": "1.2747255498543382", "xGChain": "11.035578865557909", "xGBuildup": "10.950173202902079"}, {"id": "5584", "player_name": "Aaron Wan-Bissaka", "games": "27", "time": "2430", "goals": "2", "xG": "0.8909827638417482", "assists": "2", "xA": "1.855541504919529", "shots": "6", "key_passes": "20", "yellow_cards": "2", "red_cards": "0", "position": "D", "team_title": "Manchester United", "npg": "2", "npxG": "0.8909827638417482", "xGChain": "9.927728943526745", "xGBuildup": "7.866563767194748"}, {"id": "1006", "player_name": "Luke Shaw", "games": "25", "time": "2029", "goals": "1", "xG": "0.5771848578006029", "assists": "5", "xA": "5.394548369571567", "shots": "8", "key_passes": "52", "yellow_cards": "6", "red_cards": "0", "position": "D S", "team_title": "Manchester United", "npg": "1", "npxG": "0.5771848578006029", "xGChain": "9.244300354272127", "xGBuildup": "6.846996210515499"}, {"id": "6080", "player_name": "Victor Lindel\u00f6f", "games": "22", "time": "1957", "goals": "1", "xG": "0.7988894507288933", "assists": "1", "xA": "0.2897103577852249", "shots": "4", "key_passes": "5", "yellow_cards": "0", "red_cards": "0", "position": "D", "team_title": "Manchester United", "npg": "1", "npxG": "0.7988894507288933", "xGChain": "7.179988000541925", "xGBuildup": "7.031541469506919"}, {"id": "7490", "player_name": "Mason Greenwood", "games": "23", "time": "1300", "goals": "1", "xG": "3.335772570222616", "assists": "1", "xA": "1.740884579718113", "shots": "40", "key_passes": "10", "yellow_cards": "1", "red_cards": "0", "position": "F M S", "team_title": "Manchester United", "npg": "1", "npxG": "3.335772570222616", "xGChain": "8.200904758647084", "xGBuildup": "3.971968460828066"}, {"id": "8821", "player_name": "Donny van de Beek", "games": "13", "time": "292", "goals": "1", "xG": "0.28271791338920593", "assists": "0", "xA": "0.12390778213739395", "shots": "1", "key_passes": "2", "yellow_cards": "1", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "1", "npxG": "0.28271791338920593", "xGChain": "2.161629047244787", "xGBuildup": "1.7550033256411552"}, {"id": "546", "player_name": "David de Gea", "games": "24", "time": "2118", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "0", "red_cards": "0", "position": "GK", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "2.067058579996228", "xGBuildup": "2.067058579996228"}, {"id": "549", "player_name": "Timothy Fosu-Mensah", "games": "1", "time": "85", "goals": "0", "xG": "0.015449298545718193", "assists": "0", "xA": "0.30515241622924805", "shots": "1", "key_passes": "2", "yellow_cards": "1", "red_cards": "0", "position": "D", "team_title": "Manchester United", "npg": "0", "npxG": "0.015449298545718193", "xGChain": "0.4019051194190979", "xGBuildup": "0.3608218729496002"}, {"id": "554", "player_name": "Juan Mata", "games": "7", "time": "341", "goals": "0", "xG": "0.3115340732038021", "assists": "2", "xA": "0.722595326602459", "shots": "4", "key_passes": "8", "yellow_cards": "0", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "0", "npxG": "0.3115340732038021", "xGChain": "2.1740669272840023", "xGBuildup": "1.3244589436799288"}, {"id": "573", "player_name": "Odion Ighalo", "games": "1", "time": "5", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "0", "red_cards": "0", "position": "S", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "0", "xGBuildup": "0"}, {"id": "697", "player_name": "Nemanja Matic", "games": "15", "time": "914", "goals": "0", "xG": "0.1647858265787363", "assists": "0", "xA": "0.6243858942762017", "shots": "6", "key_passes": "7", "yellow_cards": "2", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "0", "npxG": "0.1647858265787363", "xGChain": "4.9117074981331825", "xGBuildup": "4.28893250413239"}, {"id": "934", "player_name": "Axel Tuanzebe", "games": "6", "time": "131", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "2", "red_cards": "0", "position": "D S", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "0.6552485078573227", "xGBuildup": "0.6552485078573227"}, {"id": "1739", "player_name": "Eric Bailly", "games": "8", "time": "635", "goals": "0", "xG": "0.04878591373562813", "assists": "0", "xA": "0", "shots": "1", "key_passes": "0", "yellow_cards": "2", "red_cards": "0", "position": "D S", "team_title": "Manchester United", "npg": "0", "npxG": "0.04878591373562813", "xGChain": "1.023825764656067", "xGBuildup": "1.023825764656067"}, {"id": "1828", "player_name": "Alex Telles", "games": "7", "time": "516", "goals": "0", "xG": "0.12450907565653324", "assists": "2", "xA": "0.686748668551445", "shots": "4", "key_passes": "10", "yellow_cards": "0", "red_cards": "0", "position": "D S", "team_title": "Manchester United", "npg": "0", "npxG": "0.12450907565653324", "xGChain": "2.7918532490730286", "xGBuildup": "2.7112308740615845"}, {"id": "6817", "player_name": "Fred", "games": "23", "time": "1840", "goals": "0", "xG": "1.0392025168985128", "assists": "0", "xA": "1.8865783978253603", "shots": "21", "key_passes": "19", "yellow_cards": "4", "red_cards": "0", "position": "M S", "team_title": "Manchester United", "npg": "0", "npxG": "1.0392025168985128", "xGChain": "10.218200903385878", "xGBuildup": "8.164208553731441"}, {"id": "7702", "player_name": "Dean Henderson", "games": "6", "time": "492", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "2", "red_cards": "0", "position": "GK S", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "0.5076637826859951", "xGBuildup": "0.5076637826859951"}, {"id": "8075", "player_name": "Brandon Williams", "games": "2", "time": "5", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "0", "red_cards": "0", "position": "S", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "0", "xGBuildup": "0"}, {"id": "9359", "player_name": "Shola Shoretire", "games": "1", "time": "1", "goals": "0", "xG": "0", "assists": "0", "xA": "0", "shots": "0", "key_passes": "0", "yellow_cards": "0", "red_cards": "0", "position": "S", "team_title": "Manchester United", "npg": "0", "npxG": "0", "xGChain": "0", "xGBuildup": "0"}] -------------------------------------------------------------------------------- /test/resources/data/match_rostersdata.json: -------------------------------------------------------------------------------- 1 | {"h": {"454138": {"id": "454138", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "2190", "team_id": "78", "position": "GK", "player": "Vicente Guaita", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "1"}, "454139": {"id": "454139", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "510", "team_id": "78", "position": "DR", "player": "Joel Ward", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "2"}, "454140": {"id": "454140", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "532", "team_id": "78", "position": "DC", "player": "Cheikhou Kouyat\u00e9", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "3"}, "454141": {"id": "454141", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "699", "team_id": "78", "position": "DC", "player": "Gary Cahill", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "3"}, "454142": {"id": "454142", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.336028516292572", "time": "90", "player_id": "730", "team_id": "78", "position": "DL", "player": "Patrick van Aanholt", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.336028516292572", "xGBuildup": "0", "positionOrder": "4"}, "454143": {"id": "454143", "goals": "0", "own_goals": "0", "shots": "3", "xG": "0.13479886949062347", "time": "90", "player_id": "775", "team_id": "78", "position": "MR", "player": "Andros Townsend", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "1", "assists": "0", "xA": "0.08583834767341614", "xGChain": "0.5004618167877197", "xGBuildup": "0.42186686396598816", "positionOrder": "8"}, "454145": {"id": "454145", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.07418951392173767", "time": "90", "player_id": "5549", "team_id": "78", "position": "MC", "player": "Luka Milivojevic", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "1", "assists": "0", "xA": "0.336028516292572", "xGChain": "0.336028516292572", "xGBuildup": "0.336028516292572", "positionOrder": "9"}, "454144": {"id": "454144", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "62", "player_id": "589", "team_id": "78", "position": "MC", "player": "James McCarthy", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "454150", "roster_out": "0", "key_passes": "1", "assists": "0", "xA": "0.07911469042301178", "xGChain": "0", "xGBuildup": "0", "positionOrder": "9"}, "454146": {"id": "454146", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "84", "player_id": "8706", "team_id": "78", "position": "ML", "player": "Eberechi Eze", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "454149", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "10"}, "454148": {"id": "454148", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.032427556812763214", "time": "90", "player_id": "672", "team_id": "78", "position": "FW", "player": "Jordan Ayew", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.45429444313049316", "xGBuildup": "0.42186686396598816", "positionOrder": "15"}, "454147": {"id": "454147", "goals": "0", "own_goals": "0", "shots": "2", "xG": "0.16495303809642792", "time": "90", "player_id": "606", "team_id": "78", "position": "FW", "player": "Christian Benteke", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "2", "assists": "0", "xA": "0.051595985889434814", "xGChain": "0.473462849855423", "xGBuildup": "0.336028516292572", "positionOrder": "15"}, "454149": {"id": "454149", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "6", "player_id": "757", "team_id": "78", "position": "Sub", "player": "Jeffrey Schlupp", "h_a": "h", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "454146", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0", "xGBuildup": "0", "positionOrder": "17"}, "454150": {"id": "454150", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "28", "player_id": "6027", "team_id": "78", "position": "Sub", "player": "Jairo Riedewald", "h_a": "h", "yellow_card": "1", "red_card": "0", "roster_in": "0", "roster_out": "454144", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.336028516292572", "xGBuildup": "0.336028516292572", "positionOrder": "17"}}, "a": {"454151": {"id": "454151", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "7702", "team_id": "89", "position": "GK", "player": "Dean Henderson", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.031576987355947495", "xGBuildup": "0.031576987355947495", "positionOrder": "1"}, "454152": {"id": "454152", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "5584", "team_id": "89", "position": "DR", "player": "Aaron Wan-Bissaka", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.29431986808776855", "xGBuildup": "0.29431986808776855", "positionOrder": "2"}, "454154": {"id": "454154", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "1739", "team_id": "89", "position": "DC", "player": "Eric Bailly", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.31683188676834106", "xGBuildup": "0.31683188676834106", "positionOrder": "3"}, "454153": {"id": "454153", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.02284710295498371", "time": "90", "player_id": "1687", "team_id": "89", "position": "DC", "player": "Harry Maguire", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.17357975244522095", "xGBuildup": "0.17357975244522095", "positionOrder": "3"}, "454155": {"id": "454155", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.031576987355947495", "time": "90", "player_id": "1006", "team_id": "89", "position": "DL", "player": "Luke Shaw", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "2", "assists": "0", "xA": "0.16546165943145752", "xGChain": "0.3331676125526428", "xGBuildup": "0.26773539185523987", "positionOrder": "4"}, "454157": {"id": "454157", "goals": "0", "own_goals": "0", "shots": "2", "xG": "0.03372350335121155", "time": "90", "player_id": "697", "team_id": "89", "position": "DMC", "player": "Nemanja Matic", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.3331676125526428", "xGBuildup": "0.31577983498573303", "positionOrder": "7"}, "454156": {"id": "454156", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.016564758494496346", "time": "74", "player_id": "6817", "team_id": "89", "position": "DMC", "player": "Fred", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "454162", "roster_out": "0", "key_passes": "1", "assists": "0", "xA": "0.017387786880135536", "xGChain": "0.13635800778865814", "xGBuildup": "0.10240545868873596", "positionOrder": "7"}, "454158": {"id": "454158", "goals": "0", "own_goals": "0", "shots": "2", "xG": "0.13900163769721985", "time": "90", "player_id": "7490", "team_id": "89", "position": "AMR", "player": "Mason Greenwood", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.1727251410484314", "xGBuildup": "0.03372350335121155", "positionOrder": "11"}, "454159": {"id": "454159", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "90", "player_id": "1228", "team_id": "89", "position": "AMC", "player": "Bruno Fernandes", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "4", "assists": "0", "xA": "0.13194149732589722", "xGChain": "0.31652936339378357", "xGBuildup": "0.2390119582414627", "positionOrder": "12"}, "454160": {"id": "454160", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.10002944618463516", "time": "90", "player_id": "556", "team_id": "89", "position": "AML", "player": "Marcus Rashford", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "0", "key_passes": "2", "assists": "0", "xA": "0.1783539205789566", "xGChain": "0.15833847224712372", "xGBuildup": "0.058309026062488556", "positionOrder": "13"}, "454161": {"id": "454161", "goals": "0", "own_goals": "0", "shots": "2", "xG": "0.1783539205789566", "time": "76", "player_id": "3294", "team_id": "89", "position": "FW", "player": "Edinson Cavani", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "454163", "roster_out": "0", "key_passes": "1", "assists": "0", "xA": "0.07781993597745895", "xGChain": "0.10240545868873596", "xGBuildup": "0", "positionOrder": "15"}, "454163": {"id": "454163", "goals": "0", "own_goals": "0", "shots": "1", "xG": "0.06543221324682236", "time": "14", "player_id": "5595", "team_id": "89", "position": "Sub", "player": "Daniel James", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "454161", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.15819089114665985", "xGBuildup": "0.15819089114665985", "positionOrder": "17"}, "454162": {"id": "454162", "goals": "0", "own_goals": "0", "shots": "0", "xG": "0", "time": "16", "player_id": "5560", "team_id": "89", "position": "Sub", "player": "Scott McTominay", "h_a": "a", "yellow_card": "0", "red_card": "0", "roster_in": "0", "roster_out": "454156", "key_passes": "0", "assists": "0", "xA": "0", "xGChain": "0.09700919687747955", "xGBuildup": "0.09700919687747955", "positionOrder": "17"}}} -------------------------------------------------------------------------------- /test/resources/data/team_datesdata.json: -------------------------------------------------------------------------------- 1 | [{"id": "14098", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "78", "title": "Crystal Palace", "short_title": "CRY"}, "goals": {"h": "1", "a": "3"}, "xG": {"h": "1.09737", "a": "1.91456"}, "datetime": "2020-09-19 16:30:00", "forecast": {"w": 0.21451946658065246, "d": 0.22133392682042802, "l": 0.564146604420384}, "result": "l"}, {"id": "14106", "isResult": true, "side": "a", "h": {"id": "220", "title": "Brighton", "short_title": "BRI"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "2", "a": "3"}, "xG": {"h": "2.97659", "a": "1.57583"}, "datetime": "2020-09-26 11:30:00", "forecast": {"w": 0.66096217645782, "d": 0.15895234861098081, "l": 0.18008486565339443}, "result": "w"}, {"id": "14471", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "82", "title": "Tottenham", "short_title": "TOT"}, "goals": {"h": "1", "a": "6"}, "xG": {"h": "0.873435", "a": "3.3017"}, "datetime": "2020-10-04 15:30:00", "forecast": {"w": 0.0627841003430905, "d": 0.10398721011194013, "l": 0.8332265545026284}, "result": "l"}, {"id": "14481", "isResult": true, "side": "a", "h": {"id": "86", "title": "Newcastle United", "short_title": "NEW"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "1", "a": "4"}, "xG": {"h": "0.867496", "a": "2.21729"}, "datetime": "2020-10-17 19:00:00", "forecast": {"w": 0.13325121478925717, "d": 0.1862443683311705, "l": 0.6805044020191591}, "result": "w"}, {"id": "14491", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "80", "title": "Chelsea", "short_title": "CHE"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "0.648427", "a": "0.219906"}, "datetime": "2020-10-24 16:30:00", "forecast": {"w": 0.4078690853907332, "d": 0.48165700939610445, "l": 0.11047390521316129}, "result": "d"}, {"id": "14500", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "83", "title": "Arsenal", "short_title": "ARS"}, "goals": {"h": "0", "a": "1"}, "xG": {"h": "0.394113", "a": "0.998072"}, "datetime": "2020-11-01 16:30:00", "forecast": {"w": 0.14356691856324408, "d": 0.35633667162390653, "l": 0.5000964098125575}, "result": "l"}, {"id": "14509", "isResult": true, "side": "a", "h": {"id": "72", "title": "Everton", "short_title": "EVE"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "1", "a": "3"}, "xG": {"h": "0.376464", "a": "1.59642"}, "datetime": "2020-11-07 12:30:00", "forecast": {"w": 0.08329464010851619, "d": 0.2360539528097624, "l": 0.6806514068899713}, "result": "w"}, {"id": "14520", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "76", "title": "West Bromwich Albion", "short_title": "WBA"}, "goals": {"h": "1", "a": "0"}, "xG": {"h": "2.42994", "a": "0.438646"}, "datetime": "2020-11-21 20:00:00", "forecast": {"w": 0.8149384312413306, "d": 0.1354686706591009, "l": 0.04959284991843597}, "result": "w"}, {"id": "14534", "isResult": true, "side": "a", "h": {"id": "74", "title": "Southampton", "short_title": "SOU"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "2", "a": "3"}, "xG": {"h": "0.498783", "a": "2.47547"}, "datetime": "2020-11-29 14:00:00", "forecast": {"w": 0.05582293255626284, "d": 0.13652044114596074, "l": 0.8076565652794164}, "result": "w"}, {"id": "14544", "isResult": true, "side": "a", "h": {"id": "81", "title": "West Ham", "short_title": "WHU"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "1", "a": "3"}, "xG": {"h": "2.53006", "a": "1.78708"}, "datetime": "2020-12-05 17:30:00", "forecast": {"w": 0.542161063608623, "d": 0.18772802144015974, "l": 0.2701108336220972}, "result": "w"}, {"id": "14549", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "88", "title": "Manchester City", "short_title": "MCI"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "0.591617", "a": "1.28434"}, "datetime": "2020-12-12 17:30:00", "forecast": {"w": 0.17140280342046546, "d": 0.2936938639079481, "l": 0.5349033326617674}, "result": "d"}, {"id": "14560", "isResult": true, "side": "a", "h": {"id": "238", "title": "Sheffield United", "short_title": "SHE"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "2", "a": "3"}, "xG": {"h": "1.23525", "a": "1.78667"}, "datetime": "2020-12-17 20:00:00", "forecast": {"w": 0.26441755352506724, "d": 0.23225445592770402, "l": 0.5033279896720257}, "result": "w"}, {"id": "14570", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "245", "title": "Leeds", "short_title": "LED"}, "goals": {"h": "6", "a": "2"}, "xG": {"h": "4.77743", "a": "1.63467"}, "datetime": "2020-12-20 16:30:00", "forecast": {"w": 0.856996601813555, "d": 0.07596984071086851, "l": 0.06689344746982284}, "result": "w"}, {"id": "14579", "isResult": true, "side": "a", "h": {"id": "75", "title": "Leicester", "short_title": "LEI"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "2", "a": "2"}, "xG": {"h": "1.0396", "a": "2.08423"}, "datetime": "2020-12-26 12:30:00", "forecast": {"w": 0.1808992729317085, "d": 0.20519918388634734, "l": 0.6139015365352054}, "result": "d"}, {"id": "14592", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "229", "title": "Wolverhampton Wanderers", "short_title": "WOL"}, "goals": {"h": "1", "a": "0"}, "xG": {"h": "1.48207", "a": "0.411046"}, "datetime": "2020-12-29 20:00:00", "forecast": {"w": 0.642164819856352, "d": 0.2573039388915537, "l": 0.1005312411821372}, "result": "w"}, {"id": "14596", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "71", "title": "Aston Villa", "short_title": "AVL"}, "goals": {"h": "2", "a": "1"}, "xG": {"h": "2.46473", "a": "1.56434"}, "datetime": "2021-01-01 20:00:00", "forecast": {"w": 0.5742766132689795, "d": 0.1888353314768562, "l": 0.23688799737021607}, "result": "w"}, {"id": "14088", "isResult": true, "side": "a", "h": {"id": "92", "title": "Burnley", "short_title": "BUR"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "1"}, "xG": {"h": "0.648582", "a": "1.23996"}, "datetime": "2021-01-12 22:15:00", "forecast": {"w": 0.19436777826982385, "d": 0.2997257725659128, "l": 0.5059064491582237}, "result": "w"}, {"id": "14620", "isResult": true, "side": "a", "h": {"id": "87", "title": "Liverpool", "short_title": "LIV"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "1.20205", "a": "1.18929"}, "datetime": "2021-01-17 16:30:00", "forecast": {"w": 0.36449615611620356, "d": 0.27720718226484475, "l": 0.35829666161163864}, "result": "d"}, {"id": "14607", "isResult": true, "side": "a", "h": {"id": "228", "title": "Fulham", "short_title": "FLH"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "1", "a": "2"}, "xG": {"h": "0.753669", "a": "1.68401"}, "datetime": "2021-01-20 20:15:00", "forecast": {"w": 0.16481932436067429, "d": 0.2388013972233927, "l": 0.5963792780221339}, "result": "w"}, {"id": "14628", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "238", "title": "Sheffield United", "short_title": "SHE"}, "goals": {"h": "1", "a": "2"}, "xG": {"h": "0.9931", "a": "0.689548"}, "datetime": "2021-01-27 20:15:00", "forecast": {"w": 0.4164305808016704, "d": 0.33669333960931014, "l": 0.24687607958874555}, "result": "l"}, {"id": "14635", "isResult": true, "side": "a", "h": {"id": "83", "title": "Arsenal", "short_title": "ARS"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "0.747166", "a": "1.51332"}, "datetime": "2021-01-30 17:30:00", "forecast": {"w": 0.18439183922681335, "d": 0.2600660747788027, "l": 0.5555420859014581}, "result": "d"}, {"id": "14651", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "74", "title": "Southampton", "short_title": "SOU"}, "goals": {"h": "9", "a": "0"}, "xG": {"h": "5.0325", "a": "0.507893"}, "datetime": "2021-02-02 20:15:00", "forecast": {"w": 0.9700794855075544, "d": 0.022510945619181322, "l": 0.007167528022898979}, "result": "w"}, {"id": "14660", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "72", "title": "Everton", "short_title": "EVE"}, "goals": {"h": "3", "a": "3"}, "xG": {"h": "1.71769", "a": "1.5571"}, "datetime": "2021-02-06 20:00:00", "forecast": {"w": 0.4186728083511068, "d": 0.23048828366092938, "l": 0.35083890733750217}, "result": "d"}, {"id": "14673", "isResult": true, "side": "a", "h": {"id": "76", "title": "West Bromwich Albion", "short_title": "WBA"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "1", "a": "1"}, "xG": {"h": "0.945964", "a": "0.423885"}, "datetime": "2021-02-14 14:00:00", "forecast": {"w": 0.47243822983658834, "d": 0.3667349338432797, "l": 0.16082683631999464}, "result": "d"}, {"id": "14681", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "86", "title": "Newcastle United", "short_title": "NEW"}, "goals": {"h": "3", "a": "1"}, "xG": {"h": "1.7256", "a": "0.341778"}, "datetime": "2021-02-21 19:00:00", "forecast": {"w": 0.719794634255231, "d": 0.21288311507878657, "l": 0.06732225011974248}, "result": "w"}, {"id": "14685", "isResult": true, "side": "a", "h": {"id": "80", "title": "Chelsea", "short_title": "CHE"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "1.17294", "a": "0.35954"}, "datetime": "2021-02-28 16:30:00", "forecast": {"w": 0.5695527936860393, "d": 0.3171560768109821, "l": 0.11329112950018488}, "result": "d"}, {"id": "14717", "isResult": true, "side": "a", "h": {"id": "78", "title": "Crystal Palace", "short_title": "CRY"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "0"}, "xG": {"h": "0.742397", "a": "0.584016"}, "datetime": "2021-03-03 20:15:00", "forecast": {"w": 0.3516996388866015, "d": 0.393601137514086, "l": 0.25469922359930813}, "result": "d"}, {"id": "14700", "isResult": true, "side": "a", "h": {"id": "88", "title": "Manchester City", "short_title": "MCI"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": "0", "a": "2"}, "xG": {"h": "1.27561", "a": "2.11411"}, "datetime": "2021-03-07 16:30:00", "forecast": {"w": 0.22594153532419387, "d": 0.20777932647194736, "l": 0.5662791301921102}, "result": "w"}, {"id": "14711", "isResult": true, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "81", "title": "West Ham", "short_title": "WHU"}, "goals": {"h": "1", "a": "0"}, "xG": {"h": "1.70128", "a": "0.463737"}, "datetime": "2021-03-14 19:15:00", "forecast": {"w": 0.6795496769251961, "d": 0.22477876380875295, "l": 0.09567155881445273}, "result": "w"}, {"id": "14731", "isResult": false, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "220", "title": "Brighton", "short_title": "BRI"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-04-02 18:00:00"}, {"id": "14742", "isResult": false, "side": "a", "h": {"id": "82", "title": "Tottenham", "short_title": "TOT"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-04-09 18:00:00"}, {"id": "14751", "isResult": false, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "92", "title": "Burnley", "short_title": "BUR"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-04-16 18:00:00"}, {"id": "14758", "isResult": false, "side": "a", "h": {"id": "245", "title": "Leeds", "short_title": "LED"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-04-23 18:00:00"}, {"id": "14770", "isResult": false, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "87", "title": "Liverpool", "short_title": "LIV"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-04-30 18:00:00"}, {"id": "14776", "isResult": false, "side": "a", "h": {"id": "71", "title": "Aston Villa", "short_title": "AVL"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-05-07 18:00:00"}, {"id": "14788", "isResult": false, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "75", "title": "Leicester", "short_title": "LEI"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-05-10 23:00:00"}, {"id": "14800", "isResult": false, "side": "h", "h": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "a": {"id": "228", "title": "Fulham", "short_title": "FLH"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-05-14 18:00:00"}, {"id": "14814", "isResult": false, "side": "a", "h": {"id": "229", "title": "Wolverhampton Wanderers", "short_title": "WOL"}, "a": {"id": "89", "title": "Manchester United", "short_title": "MUN"}, "goals": {"h": null, "a": null}, "xG": {"h": null, "a": null}, "datetime": "2021-05-22 19:00:00"}] -------------------------------------------------------------------------------- /test/test_api.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unused-argument 2 | # pylint: disable=invalid-name 3 | # pylint: disable=too-many-instance-attributes 4 | # pylint: disable=undefined-loop-variable 5 | """Test understatapi""" 6 | from typing import Dict 7 | import unittest 8 | from unittest.mock import patch 9 | import json 10 | from test import mocked_requests_get 11 | import requests 12 | from understatapi import UnderstatClient 13 | from understatapi.exceptions import ( 14 | InvalidMatch, 15 | InvalidPlayer, 16 | InvalidTeam, 17 | InvalidLeague, 18 | InvalidSeason, 19 | ) 20 | 21 | 22 | def read_json(path: str) -> Dict: 23 | """Read json data""" 24 | with open(path, "r", encoding="utf-8") as fh: 25 | data = json.load(fh) 26 | return data 27 | 28 | 29 | class EndpointBaseTestCase(unittest.TestCase): 30 | """Base class for all endpoint ``unittest.TestCase``` classes""" 31 | 32 | def setUp(self): 33 | self.understat = UnderstatClient() 34 | self.match_id = "dummy_match" 35 | self.match = self.understat.match(self.match_id) 36 | self.league_name = "EPL" 37 | self.league = self.understat.league(self.league_name) 38 | self.player_id = "dummy_player" 39 | self.player = self.understat.player(self.player_id) 40 | self.team_name = "dummy_team" 41 | self.team = self.understat.team(self.team_name) 42 | 43 | def tearDown(self): 44 | self.understat.session.close() 45 | 46 | 47 | @patch.object(requests.Session, "get") 48 | class TestEndpointsResponse(EndpointBaseTestCase): 49 | """Test that endpoints return the expected output""" 50 | 51 | def test_match_get_shot_data(self, mock_get): 52 | """test ``match.get_shot_data()``""" 53 | mock_get.return_value = mocked_requests_get( 54 | "test/resources/data/match_ajax.json" 55 | ) 56 | data = self.match.get_shot_data() 57 | data_path = "test/resources/data/match_shotsdata.json" 58 | expected_data = read_json(data_path) 59 | self.assertDictEqual(expected_data, data) 60 | 61 | def test_match_get_roster_data(self, mock_get): 62 | """test ``match.get_roster_data()``""" 63 | mock_get.return_value = mocked_requests_get( 64 | "test/resources/data/match_ajax.json" 65 | ) 66 | data = self.match.get_roster_data() 67 | data_path = "test/resources/data/match_rostersdata.json" 68 | expected_data = read_json(data_path) 69 | self.assertDictEqual(expected_data, data) 70 | 71 | def test_match_get_match_info(self, mock_get): 72 | """test ``match.get_match_info()``""" 73 | mock_get.return_value = mocked_requests_get( 74 | "test/resources/data/match_ajax.json" 75 | ) 76 | data = self.match.get_match_info() 77 | data_path = "test/resources/data/match_matchinfo.json" 78 | expected_data = read_json(data_path) 79 | self.assertDictEqual(expected_data, data) 80 | 81 | def test_player_get_match_data(self, mock_get): 82 | """test ``player.get_match_data()``""" 83 | mock_get.return_value = mocked_requests_get( 84 | "test/resources/data/player_ajax.json" 85 | ) 86 | data = self.player.get_match_data() 87 | data_path = "test/resources/data/player_matchesdata.json" 88 | expected_data = read_json(data_path) 89 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 90 | with self.subTest(record=i): 91 | self.assertDictEqual(record, expected_record) 92 | 93 | def test_get_shot_data_return_value(self, mock_get): 94 | """test ``player.get_shot_data()``""" 95 | mock_get.return_value = mocked_requests_get( 96 | "test/resources/data/player_ajax.json" 97 | ) 98 | data = self.player.get_shot_data() 99 | data_path = "test/resources/data/player_shotsdata.json" 100 | expected_data = read_json(data_path) 101 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 102 | with self.subTest(record=i): 103 | self.assertDictEqual(record, expected_record) 104 | 105 | def test_player_get_season_data(self, mock_get): 106 | """test ``player.get_season_data()``""" 107 | mock_get.return_value = mocked_requests_get( 108 | "test/resources/data/player_ajax.json" 109 | ) 110 | data = self.player.get_season_data() 111 | data_path = "test/resources/data/player_groupsdata.json" 112 | expected_data = read_json(data_path) 113 | self.assertDictEqual(data, expected_data) 114 | 115 | def test_team_get_player_data(self, mock_get): 116 | """test ``team.get_match_data()``""" 117 | mock_get.return_value = mocked_requests_get( 118 | "test/resources/data/team_ajax.json" 119 | ) 120 | data = self.team.get_player_data(season="2019") 121 | data_path = "test/resources/data/team_playersdata.json" 122 | expected_data = read_json(data_path) 123 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 124 | with self.subTest(record=i): 125 | self.assertDictEqual(record, expected_record) 126 | 127 | def test_team_get_match_data(self, mock_get): 128 | """test ``team.get_match_data()``""" 129 | mock_get.return_value = mocked_requests_get( 130 | "test/resources/data/team_ajax.json" 131 | ) 132 | data = self.team.get_match_data(season="2019") 133 | data_path = "test/resources/data/team_datesdata.json" 134 | expected_data = read_json(data_path) 135 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 136 | with self.subTest(record=i): 137 | self.assertDictEqual(record, expected_record) 138 | 139 | def test_team_get_context_data(self, mock_get): 140 | """test ``team.get_context_data()``""" 141 | mock_get.return_value = mocked_requests_get( 142 | "test/resources/data/team_ajax.json" 143 | ) 144 | data = self.team.get_context_data(season="2019") 145 | data_path = "test/resources/data/team_statisticsdata.json" 146 | expected_data = read_json(data_path) 147 | self.assertDictEqual(data, expected_data) 148 | 149 | def test_league_get_team_data(self, mock_get): 150 | """test ``league.get_team_data()``""" 151 | mock_get.return_value = mocked_requests_get( 152 | "test/resources/data/league_ajax.json" 153 | ) 154 | data = self.league.get_team_data(season="2019") 155 | data_path = "test/resources/data/league_teamsdata.json" 156 | expected_data = read_json(data_path) 157 | self.assertDictEqual(data, expected_data) 158 | 159 | def test_league_get_match_data(self, mock_get): 160 | """test ``league.get_match_data()``""" 161 | mock_get.return_value = mocked_requests_get( 162 | "test/resources/data/league_ajax.json" 163 | ) 164 | data = self.league.get_match_data(season="2019") 165 | data_path = "test/resources/data/league_datesdata.json" 166 | expected_data = read_json(data_path) 167 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 168 | with self.subTest(record=i): 169 | self.assertDictEqual(record, expected_record) 170 | 171 | def test_league_get_player_data(self, mock_get): 172 | """test ``league.get_player_data()``""" 173 | mock_get.return_value = mocked_requests_get( 174 | "test/resources/data/league_ajax.json" 175 | ) 176 | data = self.league.get_player_data(season="2019") 177 | data_path = "test/resources/data/league_playersdata.json" 178 | expected_data = read_json(data_path) 179 | for i, (record, expected_record) in enumerate(zip(data, expected_data)): 180 | with self.subTest(record=i): 181 | self.assertDictEqual(record, expected_record) 182 | 183 | 184 | @patch.object(requests.Session, "get", side_effect=mocked_requests_get) 185 | class TestEndpointErrors(EndpointBaseTestCase): 186 | """Test the conditions under which exceptions are expected""" 187 | 188 | def test_match_get_data_bad_player(self, mock_get): 189 | """test that ``match._get_data()`` raises an InvalidMatch error""" 190 | with self.assertRaises(InvalidMatch): 191 | self.match.get_shot_data(status_code=404) 192 | 193 | def test_match_get_data_type_error(self, mock_get): 194 | """ 195 | test that ``mathc.get_data()`` raises a TypeError 196 | when ``match`` is not a string 197 | """ 198 | match = self.understat.match(match=None) 199 | with self.assertRaises(TypeError): 200 | _ = match.get_shot_data() 201 | 202 | def test_get_data_bad_player(self, mock_get): 203 | """test that ``player._get_data()`` raises an InvalidPlayer error""" 204 | with self.assertRaises(InvalidPlayer): 205 | self.player.get_shot_data(status_code=404) 206 | 207 | def test_player_get_data_type_error(self, mock_get): 208 | """ 209 | test that ``player._get_data()`` raises a TypeError 210 | when ``player`` is not a string 211 | """ 212 | player = self.understat.player(None) 213 | with self.assertRaises(TypeError): 214 | _ = player.get_shot_data() 215 | 216 | def test_team_get_data_bad_team(self, mock_get): 217 | """test that ``team._get_data()`` raises an InvalidTeam error""" 218 | team = self.understat.team(self.team_name) 219 | with self.assertRaises(InvalidTeam): 220 | _ = team.get_match_data(season="2019", status_code=404) 221 | 222 | def test_team_get_data_type_error(self, mock_get): 223 | """ 224 | test that ``team._get_data()`` raises a TypeError 225 | when ``team`` is not a string 226 | """ 227 | team = self.understat.team(None) 228 | with self.assertRaises(TypeError): 229 | _ = team.get_match_data(season="") 230 | 231 | def test_league_get_data_bad_team(self, mock_get): 232 | """test that ``league._get_data()`` raises an InvalidLeague error""" 233 | league = self.understat.league("dummy_team") 234 | with self.assertRaises(InvalidLeague): 235 | _ = league.get_match_data(season="2019", status_code=404) 236 | 237 | def test_league_get_data_type_error(self, mock_get): 238 | """ 239 | test that ``league._get_data()`` raises a TypeError 240 | when ``league`` is not a string 241 | """ 242 | league = self.understat.league(None) 243 | with self.assertRaises(TypeError): 244 | _ = league.get_match_data(season="2019") 245 | 246 | def test_invalid_season(self, mock_get): 247 | """ 248 | Test that an error is raised when you try to get data for a 249 | season before 2014 250 | """ 251 | with self.assertRaises(InvalidSeason): 252 | _ = self.league.get_match_data(season="2013") 253 | 254 | def test_error_handling_method(self, mock_get): 255 | # pylint: disable=no-member 256 | """ 257 | test the error handling works as expected when a method is called 258 | that does not belong to the given endpoint 259 | """ 260 | with self.assertRaises(AttributeError) as err: 261 | with UnderstatClient() as understat: 262 | understat.team("").get_bad_data() 263 | self.assertEqual( 264 | str(err), 265 | "'TeamEndpoint' object has no attribute 'get_bad_data'\n" 266 | "Its public methods are ['get_context_data', " 267 | "'get_match_data', get_player_data']", 268 | ) 269 | 270 | 271 | class TestEndpointDunder(EndpointBaseTestCase): 272 | """Tests for the dunder methods in the endpoint class""" 273 | 274 | def test_league(self): 275 | """test ``league()``""" 276 | self.assertEqual( 277 | repr(self.understat.league(league="EPL")), 278 | "", 279 | ) 280 | 281 | def test_player(self): 282 | """test ``player()``""" 283 | self.assertEqual( 284 | repr(self.understat.player(player="1234")), 285 | "", 286 | ) 287 | 288 | def test_team(self): 289 | """test ``team()``""" 290 | self.assertEqual( 291 | repr(self.understat.team(team="Manchester_United")), 292 | "", 293 | ) 294 | 295 | def test_match(self): 296 | """test ``match()``""" 297 | self.assertEqual( 298 | repr(self.understat.match(match="1234")), "" 299 | ) 300 | 301 | def test_iteration(self): 302 | """Test iterating over players""" 303 | player_names = ["player_1", "player_2"] 304 | self.player._primary_attr = player_names 305 | for player, player_name in zip(self.player, player_names): 306 | with self.subTest(player=player_name): 307 | self.assertEqual(player.player, player_name) 308 | 309 | def test_len_one(self): 310 | """Test len() when there is only one player""" 311 | self.assertEqual(1, len(self.player)) 312 | 313 | def test_len_error(self): 314 | """Test len() errors out when passed a non-sequence""" 315 | self.player._primary_attr = None 316 | with self.assertRaises(TypeError): 317 | self.assertEqual(1, len(self.player)) 318 | 319 | def test_getitem_one(self): 320 | """Test getitem() when there is only one player""" 321 | self.assertEqual(self.player[0].player, self.player.player) 322 | 323 | def test_context_manager(self): 324 | """ 325 | Test that the client behaves as a context manager as expected 326 | """ 327 | try: 328 | with UnderstatClient(): 329 | pass 330 | except Exception: # pylint: disable=broad-except 331 | self.fail() 332 | 333 | 334 | if __name__ == "__main__": 335 | unittest.main() 336 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist= 7 | 8 | # Specify a score threshold to be exceeded before program exits with error. 9 | fail-under=10.0 10 | 11 | # Add files or directories to the blacklist. They should be base names, not 12 | # paths. 13 | ignore=CVS 14 | 15 | # Add files or directories matching the regex patterns to the blacklist. The 16 | # regex matches against base names, not paths. 17 | ignore-patterns= 18 | 19 | # Python code to execute, usually for sys.path manipulation such as 20 | # pygtk.require(). 21 | #init-hook= 22 | 23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 24 | # number of processors available to use. 25 | jobs=0 26 | 27 | # Control the amount of potential inferred values when inferring a single 28 | # object. This can help the performance when dealing with large functions or 29 | # complex, nested conditions. 30 | limit-inference-results=100 31 | 32 | # List of plugins (as comma separated values of python module names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=no 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=print-statement, 64 | parameter-unpacking, 65 | unpacking-in-except, 66 | old-raise-syntax, 67 | backtick, 68 | long-suffix, 69 | old-ne-operator, 70 | old-octal-literal, 71 | import-star-module-level, 72 | non-ascii-bytes-literal, 73 | raw-checker-failed, 74 | bad-inline-option, 75 | locally-disabled, 76 | file-ignored, 77 | suppressed-message, 78 | useless-suppression, 79 | deprecated-pragma, 80 | use-symbolic-message-instead, 81 | apply-builtin, 82 | basestring-builtin, 83 | buffer-builtin, 84 | cmp-builtin, 85 | coerce-builtin, 86 | execfile-builtin, 87 | file-builtin, 88 | long-builtin, 89 | raw_input-builtin, 90 | reduce-builtin, 91 | standarderror-builtin, 92 | unicode-builtin, 93 | xrange-builtin, 94 | coerce-method, 95 | delslice-method, 96 | getslice-method, 97 | setslice-method, 98 | no-absolute-import, 99 | old-division, 100 | dict-iter-method, 101 | dict-view-method, 102 | next-method-called, 103 | metaclass-assignment, 104 | indexing-exception, 105 | raising-string, 106 | reload-builtin, 107 | oct-method, 108 | hex-method, 109 | nonzero-method, 110 | cmp-method, 111 | input-builtin, 112 | round-builtin, 113 | intern-builtin, 114 | unichr-builtin, 115 | map-builtin-not-iterating, 116 | zip-builtin-not-iterating, 117 | range-builtin-not-iterating, 118 | filter-builtin-not-iterating, 119 | using-cmp-argument, 120 | eq-without-hash, 121 | div-method, 122 | idiv-method, 123 | rdiv-method, 124 | exception-message-attribute, 125 | invalid-str-codec, 126 | sys-max-int, 127 | bad-python3-import, 128 | deprecated-string-function, 129 | deprecated-str-translate-call, 130 | deprecated-itertools-function, 131 | deprecated-types-field, 132 | next-method-defined, 133 | dict-items-not-iterating, 134 | dict-keys-not-iterating, 135 | dict-values-not-iterating, 136 | deprecated-operator-function, 137 | deprecated-urllib-function, 138 | xreadlines-attribute, 139 | deprecated-sys-function, 140 | exception-escape, 141 | comprehension-escape, 142 | protected-access, 143 | super-init-not-called, 144 | too-few-public-methods 145 | 146 | # Enable the message, report, category or checker with the given id(s). You can 147 | # either give multiple identifier separated by comma (,) or put this option 148 | # multiple time (only on the command line, not in the configuration file where 149 | # it should appear only once). See also the "--disable" option for examples. 150 | enable=c-extension-no-member 151 | 152 | 153 | [REPORTS] 154 | 155 | # Python expression which should return a score less than or equal to 10. You 156 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 157 | # which contain the number of messages in each category, as well as 'statement' 158 | # which is the total number of statements analyzed. This score is used by the 159 | # global evaluation report (RP0004). 160 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 161 | 162 | # Template used to display messages. This is a python new-style format string 163 | # used to format the message information. See doc for all details. 164 | #msg-template= 165 | 166 | # Set the output format. Available formats are text, parseable, colorized, json 167 | # and msvs (visual studio). You can also give a reporter class, e.g. 168 | # mypackage.mymodule.MyReporterClass. 169 | output-format=text 170 | 171 | # Tells whether to display a full report or only the messages. 172 | reports=no 173 | 174 | # Activate the evaluation score. 175 | score=yes 176 | 177 | 178 | [REFACTORING] 179 | 180 | # Maximum number of nested blocks for function / method body 181 | max-nested-blocks=5 182 | 183 | # Complete name of functions that never returns. When checking for 184 | # inconsistent-return-statements if a never returning function is called then 185 | # it will be considered as an explicit return statement and no message will be 186 | # printed. 187 | never-returning-functions=sys.exit 188 | 189 | 190 | [STRING] 191 | 192 | # This flag controls whether inconsistent-quotes generates a warning when the 193 | # character used as a quote delimiter is used inconsistently within a module. 194 | check-quote-consistency=yes 195 | 196 | # This flag controls whether the implicit-str-concat should generate a warning 197 | # on implicit string concatenation in sequences defined over several lines. 198 | check-str-concat-over-line-jumps=no 199 | 200 | 201 | [MISCELLANEOUS] 202 | 203 | # List of note tags to take in consideration, separated by a comma. 204 | notes=FIXME, 205 | XXX, 206 | TODO 207 | 208 | # Regular expression of note tags to take in consideration. 209 | #notes-rgx= 210 | 211 | 212 | [SPELLING] 213 | 214 | # Limits count of emitted suggestions for spelling mistakes. 215 | max-spelling-suggestions=50 216 | 217 | # Spelling dictionary name. Available dictionaries: none. To make it work, 218 | # install the python-enchant package. 219 | spelling-dict= 220 | 221 | # List of comma separated words that should not be checked. 222 | spelling-ignore-words= 223 | init, 224 | understat, 225 | nan, 226 | int, 227 | str, 228 | url, 229 | EPL, 230 | Bundesliga, 231 | kwargs, 232 | args, 233 | teamsData, 234 | datesData, 235 | playersData, 236 | shotsData, 237 | DataFrame, 238 | dataframe, 239 | dataframes, 240 | html, 241 | json, 242 | dtypes 243 | 244 | 245 | # A path to a file that contains the private dictionary; one word per line. 246 | spelling-private-dict-file= 247 | 248 | # Tells whether to store unknown words to the private dictionary (see the 249 | # --spelling-private-dict-file option) instead of raising a message. 250 | spelling-store-unknown-words=no 251 | 252 | 253 | [BASIC] 254 | 255 | # Naming style matching correct argument names. 256 | argument-naming-style=snake_case 257 | 258 | # Regular expression matching correct argument names. Overrides argument- 259 | # naming-style. 260 | #argument-rgx= 261 | 262 | # Naming style matching correct attribute names. 263 | attr-naming-style=snake_case 264 | 265 | # Regular expression matching correct attribute names. Overrides attr-naming- 266 | # style. 267 | #attr-rgx= 268 | 269 | # Bad variable names which should always be refused, separated by a comma. 270 | bad-names=foo, 271 | bar, 272 | baz, 273 | toto, 274 | tutu, 275 | tata 276 | 277 | # Bad variable names regexes, separated by a comma. If names match any regex, 278 | # they will always be refused 279 | bad-names-rgxs= 280 | 281 | # Naming style matching correct class attribute names. 282 | class-attribute-naming-style=any 283 | 284 | # Regular expression matching correct class attribute names. Overrides class- 285 | # attribute-naming-style. 286 | #class-attribute-rgx= 287 | 288 | # Naming style matching correct class names. 289 | class-naming-style=PascalCase 290 | 291 | # Regular expression matching correct class names. Overrides class-naming- 292 | # style. 293 | #class-rgx= 294 | 295 | # Naming style matching correct constant names. 296 | const-naming-style=UPPER_CASE 297 | 298 | # Regular expression matching correct constant names. Overrides const-naming- 299 | # style. 300 | #const-rgx= 301 | 302 | # Minimum line length for functions/classes that require docstrings, shorter 303 | # ones are exempt. 304 | docstring-min-length=-1 305 | 306 | # Naming style matching correct function names. 307 | function-naming-style=snake_case 308 | 309 | # Regular expression matching correct function names. Overrides function- 310 | # naming-style. 311 | #function-rgx= 312 | 313 | # Good variable names which should always be accepted, separated by a comma. 314 | good-names=i, 315 | j, 316 | k, 317 | ex, 318 | Run, 319 | _ 320 | 321 | # Good variable names regexes, separated by a comma. If names match any regex, 322 | # they will always be accepted 323 | good-names-rgxs= 324 | 325 | # Include a hint for the correct naming format with invalid-name. 326 | include-naming-hint=no 327 | 328 | # Naming style matching correct inline iteration names. 329 | inlinevar-naming-style=any 330 | 331 | # Regular expression matching correct inline iteration names. Overrides 332 | # inlinevar-naming-style. 333 | #inlinevar-rgx= 334 | 335 | # Naming style matching correct method names. 336 | method-naming-style=snake_case 337 | 338 | # Regular expression matching correct method names. Overrides method-naming- 339 | # style. 340 | #method-rgx= 341 | 342 | # Naming style matching correct module names. 343 | module-naming-style=snake_case 344 | 345 | # Regular expression matching correct module names. Overrides module-naming- 346 | # style. 347 | #module-rgx= 348 | 349 | # Colon-delimited sets of names that determine each other's naming style when 350 | # the name regexes allow several styles. 351 | name-group= 352 | 353 | # Regular expression which should only match function or class names that do 354 | # not require a docstring. 355 | no-docstring-rgx=^_ 356 | 357 | # List of decorators that produce properties, such as abc.abstractproperty. Add 358 | # to this list to register other decorators that produce valid properties. 359 | # These decorators are taken in consideration only for invalid-name. 360 | property-classes=abc.abstractproperty 361 | 362 | # Naming style matching correct variable names. 363 | variable-naming-style=snake_case 364 | 365 | # Regular expression matching correct variable names. Overrides variable- 366 | # naming-style. 367 | #variable-rgx= 368 | 369 | 370 | [LOGGING] 371 | 372 | # The type of string formatting that logging methods do. `old` means using % 373 | # formatting, `new` is for `{}` formatting. 374 | logging-format-style=old 375 | 376 | # Logging modules to check that the string format arguments are in logging 377 | # function parameter format. 378 | logging-modules=logging 379 | 380 | 381 | [VARIABLES] 382 | 383 | # List of additional names supposed to be defined in builtins. Remember that 384 | # you should avoid defining new builtins when possible. 385 | additional-builtins= 386 | 387 | # Tells whether unused global variables should be treated as a violation. 388 | allow-global-unused-variables=yes 389 | 390 | # List of strings which can identify a callback function by name. A callback 391 | # name must start or end with one of those strings. 392 | callbacks=cb_, 393 | _cb 394 | 395 | # A regular expression matching the name of dummy variables (i.e. expected to 396 | # not be used). 397 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 398 | 399 | # Argument names that match this expression will be ignored. Default to name 400 | # with leading underscore. 401 | ignored-argument-names=_.*|^ignored_|^unused_ 402 | 403 | # Tells whether we should check for unused import in __init__ files. 404 | init-import=no 405 | 406 | # List of qualified module names which can have objects that can redefine 407 | # builtins. 408 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 409 | 410 | 411 | [TYPECHECK] 412 | 413 | # List of decorators that produce context managers, such as 414 | # contextlib.contextmanager. Add to this list to register other decorators that 415 | # produce valid context managers. 416 | contextmanager-decorators=contextlib.contextmanager 417 | 418 | # List of members which are set dynamically and missed by pylint inference 419 | # system, and so shouldn't trigger E1101 when accessed. Python regular 420 | # expressions are accepted. 421 | generated-members= 422 | 423 | # Tells whether missing members accessed in mixin class should be ignored. A 424 | # mixin class is detected if its name ends with "mixin" (case insensitive). 425 | ignore-mixin-members=yes 426 | 427 | # Tells whether to warn about missing members when the owner of the attribute 428 | # is inferred to be None. 429 | ignore-none=yes 430 | 431 | # This flag controls whether pylint should warn about no-member and similar 432 | # checks whenever an opaque object is returned when inferring. The inference 433 | # can return multiple potential results while evaluating a Python object, but 434 | # some branches might not be evaluated, which results in partial inference. In 435 | # that case, it might be useful to still emit no-member and other checks for 436 | # the rest of the inferred objects. 437 | ignore-on-opaque-inference=yes 438 | 439 | # List of class names for which member attributes should not be checked (useful 440 | # for classes with dynamically set attributes). This supports the use of 441 | # qualified names. 442 | ignored-classes=optparse.Values,thread._local,_thread._local 443 | 444 | # List of module names for which member attributes should not be checked 445 | # (useful for modules/projects where namespaces are manipulated during runtime 446 | # and thus existing member attributes cannot be deduced by static analysis). It 447 | # supports qualified module names, as well as Unix pattern matching. 448 | ignored-modules= 449 | 450 | # Show a hint with possible names when a member name was not found. The aspect 451 | # of finding the hint is based on edit distance. 452 | missing-member-hint=yes 453 | 454 | # The minimum edit distance a name should have in order to be considered a 455 | # similar match for a missing member name. 456 | missing-member-hint-distance=1 457 | 458 | # The total number of similar names that should be taken in consideration when 459 | # showing a hint for a missing member. 460 | missing-member-max-choices=1 461 | 462 | # List of decorators that change the signature of a decorated function. 463 | signature-mutators= 464 | 465 | 466 | [FORMAT] 467 | 468 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 469 | expected-line-ending-format= 470 | 471 | # Regexp for a line that is allowed to be longer than the limit. 472 | ignore-long-lines=^\s*(# )??$ 473 | 474 | # Number of spaces of indent required inside a hanging or continued line. 475 | indent-after-paren=4 476 | 477 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 478 | # tab). 479 | indent-string=' ' 480 | 481 | # Maximum number of characters on a single line. 482 | max-line-length=79 483 | 484 | # Maximum number of lines in a module. 485 | max-module-lines=1000 486 | 487 | # Allow the body of a class to be on the same line as the declaration if body 488 | # contains single statement. 489 | single-line-class-stmt=no 490 | 491 | # Allow the body of an if to be on the same line as the test if there is no 492 | # else. 493 | single-line-if-stmt=no 494 | 495 | 496 | [SIMILARITIES] 497 | 498 | # Ignore comments when computing similarities. 499 | ignore-comments=yes 500 | 501 | # Ignore docstrings when computing similarities. 502 | ignore-docstrings=yes 503 | 504 | # Ignore imports when computing similarities. 505 | ignore-imports=no 506 | 507 | # Minimum lines number of a similarity. 508 | min-similarity-lines=6 509 | 510 | ignore-signatures=yes 511 | 512 | 513 | [DESIGN] 514 | 515 | # Maximum number of arguments for function / method. 516 | max-args=5 517 | 518 | # Maximum number of attributes for a class (see R0902). 519 | max-attributes=7 520 | 521 | # Maximum number of boolean expressions in an if statement (see R0916). 522 | max-bool-expr=5 523 | 524 | # Maximum number of branch for function / method body. 525 | max-branches=12 526 | 527 | # Maximum number of locals for function / method body. 528 | max-locals=15 529 | 530 | # Maximum number of parents for a class (see R0901). 531 | max-parents=7 532 | 533 | # Maximum number of public methods for a class (see R0904). 534 | max-public-methods=20 535 | 536 | # Maximum number of return / yield for function / method body. 537 | max-returns=6 538 | 539 | # Maximum number of statements in function / method body. 540 | max-statements=50 541 | 542 | # Minimum number of public methods for a class (see R0903). 543 | min-public-methods=2 544 | 545 | 546 | [IMPORTS] 547 | 548 | # List of modules that can be imported at any level, not just the top level 549 | # one. 550 | allow-any-import-level= 551 | 552 | # Allow wildcard imports from modules that define __all__. 553 | allow-wildcard-with-all=no 554 | 555 | # Analyse import fallback blocks. This can be used to support both Python 2 and 556 | # 3 compatible code, which means that the block might have code that exists 557 | # only in one or another interpreter, leading to false positives when analysed. 558 | analyse-fallback-blocks=no 559 | 560 | # Deprecated modules which should not be used, separated by a comma. 561 | deprecated-modules=optparse,tkinter.tix 562 | 563 | # Create a graph of external dependencies in the given file (report RP0402 must 564 | # not be disabled). 565 | ext-import-graph= 566 | 567 | # Create a graph of every (i.e. internal and external) dependencies in the 568 | # given file (report RP0402 must not be disabled). 569 | import-graph= 570 | 571 | # Create a graph of internal dependencies in the given file (report RP0402 must 572 | # not be disabled). 573 | int-import-graph= 574 | 575 | # Force import order to recognize a module as part of the standard 576 | # compatibility libraries. 577 | known-standard-library= 578 | 579 | # Force import order to recognize a module as part of a third party library. 580 | known-third-party=enchant 581 | 582 | # Couples of modules and preferred modules, separated by a comma. 583 | preferred-modules= 584 | 585 | 586 | [CLASSES] 587 | 588 | # Warn about protected attribute access inside special methods 589 | check-protected-access-in-special-methods=no 590 | 591 | # List of method names used to declare (i.e. assign) instance attributes. 592 | defining-attr-methods=__init__, 593 | __new__, 594 | setUp, 595 | __post_init__ 596 | 597 | # List of member names, which should be excluded from the protected access 598 | # warning. 599 | exclude-protected=_asdict, 600 | _fields, 601 | _replace, 602 | _source, 603 | _make 604 | 605 | # List of valid names for the first argument in a class method. 606 | valid-classmethod-first-arg=cls 607 | 608 | # List of valid names for the first argument in a metaclass class method. 609 | valid-metaclass-classmethod-first-arg=cls 610 | 611 | 612 | [EXCEPTIONS] 613 | 614 | # Exceptions that will emit a warning when being caught. Defaults to 615 | # "BaseException, Exception". 616 | overgeneral-exceptions=BaseException, 617 | Exception 618 | -------------------------------------------------------------------------------- /test/resources/data/player_groupsdata.json: -------------------------------------------------------------------------------- 1 | {"season": [{"position": "FW", "games": "25", "goals": "16", "shots": "96", "time": "2197", "xG": "14.41612608358264", "assists": "13", "xA": "6.7384555246680975", "key_passes": "39", "season": "2020", "team": "Tottenham", "yellow": "1", "red": "0", "npg": "13", "npxG": "12.132619481533766", "xGChain": "17.977360193617642", "xGBuildup": "3.6040820917114615"}, {"position": "FW", "games": "29", "goals": "18", "shots": "82", "time": "2595", "xG": "13.297065950930119", "assists": "2", "xA": "3.1170063093304634", "key_passes": "27", "season": "2019", "team": "Tottenham", "yellow": "4", "red": "0", "npg": "16", "npxG": "11.77476505190134", "xGChain": "16.854615883901715", "xGBuildup": "3.0513013089075685"}, {"position": "FW", "games": "28", "goals": "17", "shots": "102", "time": "2437", "xG": "16.12239446118474", "assists": "4", "xA": "4.562663219869137", "key_passes": "30", "season": "2018", "team": "Tottenham", "yellow": "5", "red": "0", "npg": "13", "npxG": "13.077755857259035", "xGChain": "18.838230532594025", "xGBuildup": "4.841164272278547"}, {"position": "FW", "games": "37", "goals": "30", "shots": "183", "time": "3094", "xG": "26.859890587627888", "assists": "2", "xA": "3.8204412199556828", "key_passes": "34", "season": "2017", "team": "Tottenham", "yellow": "5", "red": "0", "npg": "28", "npxG": "24.576384104788303", "xGChain": "28.51526607386768", "xGBuildup": "7.9616343677043915"}, {"position": "FW", "games": "30", "goals": "29", "shots": "110", "time": "2556", "xG": "19.82009919732809", "assists": "7", "xA": "5.5538915153592825", "key_passes": "41", "season": "2016", "team": "Tottenham", "yellow": "3", "red": "0", "npg": "24", "npxG": "15.253085978329182", "xGChain": "21.94719305820763", "xGBuildup": "4.12599990144372"}, {"position": "FW", "games": "38", "goals": "25", "shots": "159", "time": "3382", "xG": "22.732073578983545", "assists": "1", "xA": "3.088511780835688", "key_passes": "44", "season": "2015", "team": "Tottenham", "yellow": "5", "red": "0", "npg": "20", "npxG": "18.926266126334667", "xGChain": "26.939671490341425", "xGBuildup": "8.189033068716526"}, {"position": "Sub", "games": "34", "goals": "21", "shots": "112", "time": "2589", "xG": "17.15729223564267", "assists": "4", "xA": "3.922500966116786", "key_passes": "27", "season": "2014", "team": "Tottenham", "yellow": "4", "red": "0", "npg": "19", "npxG": "14.873822528868914", "xGChain": "16.488438992761075", "xGBuildup": "5.549698735587299"}], "position": {"2020": {"FW": {"position": "FW", "games": "25", "goals": "16", "shots": "96", "time": "2197", "xG": "14.41612608358264", "assists": "13", "xA": "6.7384555246680975", "key_passes": "39", "season": "2020", "yellow": "1", "red": "0", "npg": "13", "npxG": "12.132619481533766", "xGChain": "17.977360193617642", "xGBuildup": "3.6040820917114615"}}, "2019": {"FW": {"position": "FW", "games": "29", "goals": "18", "shots": "82", "time": "2595", "xG": "13.297065950930119", "assists": "2", "xA": "3.1170063093304634", "key_passes": "27", "season": "2019", "yellow": "4", "red": "0", "npg": "16", "npxG": "11.77476505190134", "xGChain": "16.854615883901715", "xGBuildup": "3.0513013089075685"}}, "2018": {"FW": {"position": "FW", "games": "27", "goals": "17", "shots": "102", "time": "2422", "xG": "16.12239446118474", "assists": "4", "xA": "4.562663219869137", "key_passes": "30", "season": "2018", "yellow": "5", "red": "0", "npg": "13", "npxG": "13.077755857259035", "xGChain": "18.838230532594025", "xGBuildup": "4.841164272278547"}, "Sub": {"position": "Sub", "games": "1", "goals": "0", "shots": "0", "time": "15", "xG": "0", "assists": "0", "xA": "0", "key_passes": "0", "season": "2018", "yellow": "0", "red": "0", "npg": "0", "npxG": "0", "xGChain": "0", "xGBuildup": "0"}}, "2017": {"FW": {"position": "FW", "games": "35", "goals": "30", "shots": "182", "time": "3059", "xG": "26.756999373435974", "assists": "2", "xA": "3.6687282361090183", "key_passes": "33", "season": "2017", "yellow": "5", "red": "0", "npg": "28", "npxG": "24.47349289059639", "xGChain": "28.26066188327968", "xGBuildup": "7.9616343677043915"}, "Sub": {"position": "Sub", "games": "2", "goals": "0", "shots": "1", "time": "35", "xG": "0.1028912141919136", "assists": "0", "xA": "0.15171298384666443", "key_passes": "1", "season": "2017", "yellow": "0", "red": "0", "npg": "0", "npxG": "0.1028912141919136", "xGChain": "0.25460419058799744", "xGBuildup": "0"}}, "2016": {"FW": {"position": "FW", "games": "28", "goals": "29", "shots": "102", "time": "2438", "xG": "19.444850981235504", "assists": "6", "xA": "4.543751752004027", "key_passes": "38", "season": "2016", "yellow": "3", "red": "0", "npg": "24", "npxG": "14.877837762236595", "xGChain": "20.910808416083455", "xGBuildup": "3.674430940300226"}, "AMC": {"position": "AMC", "games": "1", "goals": "0", "shots": "6", "time": "90", "xG": "0.2572745382785797", "assists": "1", "xA": "0.7144922018051147", "key_passes": "2", "season": "2016", "yellow": "0", "red": "0", "npg": "0", "npxG": "0.2572745382785797", "xGChain": "0.532966673374176", "xGBuildup": "0.3248686194419861"}, "Sub": {"position": "Sub", "games": "1", "goals": "0", "shots": "2", "time": "28", "xG": "0.1179736778140068", "assists": "0", "xA": "0.2956475615501404", "key_passes": "1", "season": "2016", "yellow": "0", "red": "0", "npg": "0", "npxG": "0.1179736778140068", "xGChain": "0.50341796875", "xGBuildup": "0.12670034170150757"}}, "2015": {"FW": {"position": "FW", "games": "38", "goals": "25", "shots": "159", "time": "3382", "xG": "22.732073578983545", "assists": "1", "xA": "3.088511780835688", "key_passes": "44", "season": "2015", "yellow": "5", "red": "0", "npg": "20", "npxG": "18.926266126334667", "xGChain": "26.939671490341425", "xGBuildup": "8.189033068716526"}}, "2014": {"FW": {"position": "FW", "games": "24", "goals": "19", "shots": "91", "time": "2140", "xG": "14.968920174986124", "assists": "3", "xA": "3.20852561108768", "key_passes": "24", "season": "2014", "yellow": "4", "red": "0", "npg": "17", "npxG": "12.685450468212366", "xGChain": "13.147061884403229", "xGBuildup": "3.7572909630835056"}, "AMC": {"position": "AMC", "games": "4", "goals": "1", "shots": "17", "time": "360", "xG": "1.5515516996383667", "assists": "0", "xA": "0.04617445915937424", "key_passes": "1", "season": "2014", "yellow": "0", "red": "0", "npg": "1", "npxG": "1.5515516996383667", "xGChain": "2.1431012749671936", "xGBuildup": "1.6835775077342987"}, "Sub": {"position": "Sub", "games": "6", "goals": "1", "shots": "4", "time": "89", "xG": "0.6368203610181808", "assists": "1", "xA": "0.6678008958697319", "key_passes": "2", "season": "2014", "yellow": "0", "red": "0", "npg": "1", "npxG": "0.6368203610181808", "xGChain": "1.1982758333906531", "xGBuildup": "0.10883026476949453"}}}, "situation": {"2014": {"OpenPlay": {"situation": "OpenPlay", "season": "2014", "goals": "11", "shots": "91", "xG": "9.696403390727937", "assists": "3", "key_passes": "26", "xA": "3.053647884167731", "npg": "11", "npxG": "9.696403390727937", "time": 2589}, "FromCorner": {"situation": "FromCorner", "season": "2014", "goals": "3", "shots": "7", "xG": "2.044639505445957", "assists": "0", "key_passes": "0", "xA": "0", "npg": "3", "npxG": "2.044639505445957", "time": 2589}, "SetPiece": {"situation": "SetPiece", "season": "2014", "goals": "4", "shots": "7", "xG": "2.8599609546363354", "assists": "1", "key_passes": "1", "xA": "0.8688530921936035", "npg": "4", "npxG": "2.8599609546363354", "time": 2589}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2014", "goals": "1", "shots": "4", "xG": "0.27281852811574936", "assists": "0", "key_passes": "0", "xA": "0", "npg": "1", "npxG": "0.27281852811574936", "time": 2589}, "Penalty": {"situation": "Penalty", "season": "2014", "goals": "2", "shots": "3", "xG": "2.2834697365760803", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 2589}}, "2015": {"OpenPlay": {"situation": "OpenPlay", "season": "2015", "goals": "17", "shots": "135", "xG": "17.020271045155823", "assists": "1", "key_passes": "40", "xA": "2.907301559112966", "npg": "17", "npxG": "17.020271045155823", "time": 3382}, "FromCorner": {"situation": "FromCorner", "season": "2015", "goals": "1", "shots": "11", "xG": "1.1922366442158818", "assists": "0", "key_passes": "4", "xA": "0.18121023289859295", "npg": "1", "npxG": "1.1922366442158818", "time": 3382}, "SetPiece": {"situation": "SetPiece", "season": "2015", "goals": "2", "shots": "6", "xG": "0.6342423260211945", "assists": "0", "key_passes": "0", "xA": "0", "npg": "2", "npxG": "0.6342423260211945", "time": 3382}, "Penalty": {"situation": "Penalty", "season": "2015", "goals": "5", "shots": "5", "xG": "3.805807411670685", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 3382}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2015", "goals": "0", "shots": "2", "xG": "0.07951611280441284", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.07951611280441284", "time": 3382}}, "2016": {"OpenPlay": {"situation": "OpenPlay", "season": "2016", "goals": "20", "shots": "85", "xG": "13.462294988334179", "assists": "6", "key_passes": "39", "xA": "4.78057935833931", "npg": "20", "npxG": "13.462294988334179", "time": 2556}, "FromCorner": {"situation": "FromCorner", "season": "2016", "goals": "2", "shots": "9", "xG": "0.7177510261535645", "assists": "1", "key_passes": "2", "xA": "0.7733122408390045", "npg": "2", "npxG": "0.7177510261535645", "time": 2556}, "Penalty": {"situation": "Penalty", "season": "2016", "goals": "5", "shots": "6", "xG": "4.5670130252838135", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 2556}, "SetPiece": {"situation": "SetPiece", "season": "2016", "goals": "2", "shots": "6", "xG": "0.8500569742172956", "assists": "0", "key_passes": "0", "xA": "0", "npg": "2", "npxG": "0.8500569742172956", "time": 2556}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2016", "goals": "0", "shots": "4", "xG": "0.22298306226730347", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.22298306226730347", "time": 2556}}, "2017": {"OpenPlay": {"situation": "OpenPlay", "season": "2017", "goals": "21", "shots": "141", "xG": "19.3939512912184", "assists": "2", "key_passes": "30", "xA": "3.348975677974522", "npg": "21", "npxG": "19.3939512912184", "time": 3094}, "FromCorner": {"situation": "FromCorner", "season": "2017", "goals": "4", "shots": "21", "xG": "1.8646575910970569", "assists": "0", "key_passes": "3", "xA": "0.47146556340157986", "npg": "4", "npxG": "1.8646575910970569", "time": 3094}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2017", "goals": "0", "shots": "10", "xG": "0.6165531277656555", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.6165531277656555", "time": 3094}, "SetPiece": {"situation": "SetPiece", "season": "2017", "goals": "3", "shots": "8", "xG": "2.7012223917990923", "assists": "0", "key_passes": "0", "xA": "0", "npg": "3", "npxG": "2.7012223917990923", "time": 3094}, "Penalty": {"situation": "Penalty", "season": "2017", "goals": "2", "shots": "3", "xG": "2.2835065126419067", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 3094}}, "2018": {"OpenPlay": {"situation": "OpenPlay", "season": "2018", "goals": "10", "shots": "79", "xG": "10.803446734324098", "assists": "4", "key_passes": "30", "xA": "4.56266326457262", "npg": "10", "npxG": "10.803446734324098", "time": 2437}, "FromCorner": {"situation": "FromCorner", "season": "2018", "goals": "2", "shots": "11", "xG": "1.3955602012574673", "assists": "0", "key_passes": "0", "xA": "0", "npg": "2", "npxG": "1.3955602012574673", "time": 2437}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2018", "goals": "0", "shots": "5", "xG": "0.2873593121767044", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.2873593121767044", "time": 2437}, "Penalty": {"situation": "Penalty", "season": "2018", "goals": "4", "shots": "4", "xG": "3.0446385741233826", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 2437}, "SetPiece": {"situation": "SetPiece", "season": "2018", "goals": "1", "shots": "3", "xG": "0.5913897342979908", "assists": "0", "key_passes": "0", "xA": "0", "npg": "1", "npxG": "0.5913897342979908", "time": 2437}}, "2019": {"OpenPlay": {"situation": "OpenPlay", "season": "2019", "goals": "16", "shots": "69", "xG": "11.244249852374196", "assists": "2", "key_passes": "27", "xA": "3.1170063111931086", "npg": "16", "npxG": "11.244249852374196", "time": 2595}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2019", "goals": "0", "shots": "7", "xG": "0.3404918201267719", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.3404918201267719", "time": 2595}, "FromCorner": {"situation": "FromCorner", "season": "2019", "goals": "0", "shots": "2", "xG": "0.07589231804013252", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.07589231804013252", "time": 2595}, "Penalty": {"situation": "Penalty", "season": "2019", "goals": "2", "shots": "2", "xG": "1.522300899028778", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 2595}, "SetPiece": {"situation": "SetPiece", "season": "2019", "goals": "0", "shots": "2", "xG": "0.11413120105862617", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.11413120105862617", "time": 2595}}, "2020": {"OpenPlay": {"situation": "OpenPlay", "season": "2020", "goals": "13", "shots": "65", "xG": "8.919568072538823", "assists": "11", "key_passes": "37", "xA": "6.056653283536434", "npg": "13", "npxG": "8.919568072538823", "time": 2197}, "FromCorner": {"situation": "FromCorner", "season": "2020", "goals": "0", "shots": "11", "xG": "1.5539829265326262", "assists": "1", "key_passes": "1", "xA": "0.3875686228275299", "npg": "0", "npxG": "1.5539829265326262", "time": 2197}, "SetPiece": {"situation": "SetPiece", "season": "2020", "goals": "0", "shots": "9", "xG": "1.2040405832231045", "assists": "1", "key_passes": "1", "xA": "0.29423364996910095", "npg": "0", "npxG": "1.2040405832231045", "time": 2197}, "DirectFreekick": {"situation": "DirectFreekick", "season": "2020", "goals": "0", "shots": "8", "xG": "0.45502782240509987", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0.45502782240509987", "time": 2197}, "Penalty": {"situation": "Penalty", "season": "2020", "goals": "3", "shots": "3", "xG": "2.2835065126419067", "assists": "0", "key_passes": "0", "xA": "0", "npg": "0", "npxG": "0", "time": 2197}}}, "shotZones": {"2014": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2014", "goals": "2", "shots": "46", "xG": "1.3829279532656074", "assists": "0", "key_passes": "14", "xA": "0.3664685832336545", "npg": "2", "npxG": "1.3829279532656074"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2014", "goals": "14", "shots": "56", "xG": "10.270193297415972", "assists": "3", "key_passes": "11", "xA": "2.3536476232111454", "npg": "12", "npxG": "7.986723560839891"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2014", "goals": "5", "shots": "10", "xG": "5.50417086482048", "assists": "1", "key_passes": "2", "xA": "1.2023847699165344", "npg": "5", "npxG": "5.50417086482048"}}, "2015": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2015", "goals": "2", "shots": "54", "xG": "1.4278511172160506", "assists": "1", "key_passes": "20", "xA": "0.5922065665945411", "npg": "2", "npxG": "1.4278511172160506"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2015", "goals": "21", "shots": "91", "xG": "16.728700577281415", "assists": "0", "key_passes": "22", "xA": "2.22392369620502", "npg": "16", "npxG": "12.92289316561073"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2015", "goals": "2", "shots": "14", "xG": "4.575521845370531", "assists": "0", "key_passes": "2", "xA": "0.272381529211998", "npg": "2", "npxG": "4.575521845370531"}}, "2016": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2016", "goals": "5", "shots": "42", "xG": "1.5037434920668602", "assists": "2", "key_passes": "16", "xA": "0.5867257043719292", "npg": "5", "npxG": "1.5037434920668602"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2016", "goals": "18", "shots": "54", "xG": "12.070566887035966", "assists": "2", "key_passes": "22", "xA": "3.140030153095722", "npg": "13", "npxG": "7.503553861752152"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2016", "goals": "6", "shots": "14", "xG": "6.24578869715333", "assists": "3", "key_passes": "3", "xA": "1.8271357417106628", "npg": "6", "npxG": "6.24578869715333"}}, "2017": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2017", "goals": "3", "shots": "62", "xG": "2.2881215419620275", "assists": "0", "key_passes": "12", "xA": "0.427053096704185", "npg": "3", "npxG": "2.2881215419620275"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2017", "goals": "19", "shots": "103", "xG": "18.350982722826302", "assists": "2", "key_passes": "20", "xA": "2.967952720820904", "npg": "17", "npxG": "16.067476210184395"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2017", "goals": "8", "shots": "18", "xG": "6.220786649733782", "assists": "0", "key_passes": "1", "xA": "0.4254354238510132", "npg": "8", "npxG": "6.220786649733782"}}, "2018": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2018", "goals": "2", "shots": "32", "xG": "1.3173850532621145", "assists": "0", "key_passes": "12", "xA": "0.39095161855220795", "npg": "2", "npxG": "1.3173850532621145"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2018", "goals": "11", "shots": "63", "xG": "12.240042183548212", "assists": "3", "key_passes": "15", "xA": "3.1233376041054726", "npg": "7", "npxG": "9.19540360942483"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2018", "goals": "4", "shots": "7", "xG": "2.564967319369316", "assists": "1", "key_passes": "3", "xA": "1.0483740419149399", "npg": "4", "npxG": "2.564967319369316"}}, "2019": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2019", "goals": "3", "shots": "31", "xG": "1.6080237291753292", "assists": "0", "key_passes": "7", "xA": "0.21812928281724453", "npg": "3", "npxG": "1.6080237291753292"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2019", "goals": "12", "shots": "45", "xG": "8.398831652477384", "assists": "2", "key_passes": "19", "xA": "2.746767196804285", "npg": "10", "npxG": "6.8765307534486055"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2019", "goals": "3", "shots": "6", "xG": "3.290210708975792", "assists": "0", "key_passes": "1", "xA": "0.15210983157157898", "npg": "3", "npxG": "3.290210708975792"}}, "2020": {"shotOboxTotal": {"shotZones": "shotOboxTotal", "season": "2020", "goals": "4", "shots": "37", "xG": "1.4803340598009527", "assists": "1", "key_passes": "13", "xA": "0.7370217144489288", "npg": "4", "npxG": "1.4803340598009527"}, "shotPenaltyArea": {"shotZones": "shotPenaltyArea", "season": "2020", "goals": "9", "shots": "48", "xG": "8.935012713074684", "assists": "7", "key_passes": "19", "xA": "3.150884471833706", "npg": "6", "npxG": "6.651506200432777"}, "shotSixYardBox": {"shotZones": "shotSixYardBox", "season": "2020", "goals": "3", "shots": "11", "xG": "4.000779144465923", "assists": "5", "key_passes": "7", "xA": "2.8505493700504303", "npg": "3", "npxG": "4.000779144465923"}}}, "shotTypes": {"2014": {"RightFoot": {"shotTypes": "RightFoot", "season": "2014", "goals": "11", "shots": "72", "xG": "8.629085346125066", "assists": "1", "key_passes": "11", "xA": "0.5302978483960032", "npg": "9", "npxG": "6.345615609548986"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2014", "goals": "5", "shots": "23", "xG": "3.931983718648553", "assists": "1", "key_passes": "13", "xA": "2.2030401919037104", "npg": "5", "npxG": "3.931983718648553"}, "Head": {"shotTypes": "Head", "season": "2014", "goals": "5", "shots": "17", "xG": "4.59622305072844", "assists": "0", "key_passes": "1", "xA": "0.05027111992239952", "npg": "5", "npxG": "4.59622305072844"}, "OtherBodyPart": {"shotTypes": "OtherBodyPart", "season": "2014", "goals": "0", "shots": "0", "xG": "0", "assists": "2", "key_passes": "2", "xA": "1.1388918161392212", "npg": "0", "npxG": "0"}}, "2015": {"RightFoot": {"shotTypes": "RightFoot", "season": "2015", "goals": "19", "shots": "116", "xG": "16.307426773943007", "assists": "0", "key_passes": "25", "xA": "1.4455587146803737", "npg": "14", "npxG": "12.501619362272322"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2015", "goals": "5", "shots": "30", "xG": "4.995699690654874", "assists": "1", "key_passes": "17", "xA": "1.262615466490388", "npg": "5", "npxG": "4.995699690654874"}, "Head": {"shotTypes": "Head", "season": "2015", "goals": "1", "shots": "13", "xG": "1.4289470752701163", "assists": "0", "key_passes": "2", "xA": "0.3803376108407974", "npg": "1", "npxG": "1.4289470752701163"}}, "2016": {"RightFoot": {"shotTypes": "RightFoot", "season": "2016", "goals": "20", "shots": "71", "xG": "13.053499516099691", "assists": "4", "key_passes": "27", "xA": "3.032530754804611", "npg": "15", "npxG": "8.486486490815878"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2016", "goals": "7", "shots": "26", "xG": "4.085558691993356", "assists": "2", "key_passes": "13", "xA": "1.8366305530071259", "npg": "7", "npxG": "4.085558691993356"}, "Head": {"shotTypes": "Head", "season": "2016", "goals": "2", "shots": "13", "xG": "2.681040868163109", "assists": "1", "key_passes": "1", "xA": "0.6847302913665771", "npg": "2", "npxG": "2.681040868163109"}}, "2017": {"RightFoot": {"shotTypes": "RightFoot", "season": "2017", "goals": "13", "shots": "112", "xG": "14.405678367242217", "assists": "0", "key_passes": "21", "xA": "2.5106828706339", "npg": "11", "npxG": "12.12217185460031"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2017", "goals": "10", "shots": "42", "xG": "6.856423366814852", "assists": "1", "key_passes": "11", "xA": "1.1015855725854635", "npg": "10", "npxG": "6.856423366814852"}, "Head": {"shotTypes": "Head", "season": "2017", "goals": "6", "shots": "27", "xG": "4.359961790032685", "assists": "1", "key_passes": "1", "xA": "0.20817279815673828", "npg": "6", "npxG": "4.359961790032685"}, "OtherBodyPart": {"shotTypes": "OtherBodyPart", "season": "2017", "goals": "1", "shots": "2", "xG": "1.2378273904323578", "assists": "0", "key_passes": "0", "xA": "0", "npg": "1", "npxG": "1.2378273904323578"}}, "2018": {"RightFoot": {"shotTypes": "RightFoot", "season": "2018", "goals": "12", "shots": "61", "xG": "10.190295937471092", "assists": "4", "key_passes": "25", "xA": "3.793409189209342", "npg": "8", "npxG": "7.145657363347709"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2018", "goals": "3", "shots": "24", "xG": "3.9130195705220103", "assists": "0", "key_passes": "5", "xA": "0.7692540753632784", "npg": "3", "npxG": "3.9130195705220103"}, "Head": {"shotTypes": "Head", "season": "2018", "goals": "2", "shots": "17", "xG": "2.0190790481865406", "assists": "0", "key_passes": "0", "xA": "0", "npg": "2", "npxG": "2.0190790481865406"}}, "2019": {"RightFoot": {"shotTypes": "RightFoot", "season": "2019", "goals": "12", "shots": "53", "xG": "7.881941772997379", "assists": "1", "key_passes": "18", "xA": "1.953562581911683", "npg": "10", "npxG": "6.359640873968601"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2019", "goals": "2", "shots": "18", "xG": "2.619428213685751", "assists": "1", "key_passes": "8", "xA": "1.0802360326051712", "npg": "2", "npxG": "2.619428213685751"}, "Head": {"shotTypes": "Head", "season": "2019", "goals": "4", "shots": "11", "xG": "2.7956961039453745", "assists": "0", "key_passes": "1", "xA": "0.08320769667625427", "npg": "4", "npxG": "2.7956961039453745"}}, "2020": {"RightFoot": {"shotTypes": "RightFoot", "season": "2020", "goals": "10", "shots": "52", "xG": "7.514500542078167", "assists": "5", "key_passes": "21", "xA": "2.350218325853348", "npg": "7", "npxG": "5.2309940294362605"}, "Head": {"shotTypes": "Head", "season": "2020", "goals": "4", "shots": "23", "xG": "4.789859354496002", "assists": "2", "key_passes": "2", "xA": "0.5389167815446854", "npg": "4", "npxG": "4.789859354496002"}, "LeftFoot": {"shotTypes": "LeftFoot", "season": "2020", "goals": "2", "shots": "21", "xG": "2.1117660207673907", "assists": "6", "key_passes": "16", "xA": "3.849320448935032", "npg": "2", "npxG": "2.1117660207673907"}}}} -------------------------------------------------------------------------------- /test/resources/data/match_ajax.json: -------------------------------------------------------------------------------- 1 | { 2 | "rosters": { 3 | "h": { 4 | "454138": { 5 | "id": "454138", 6 | "goals": "0", 7 | "own_goals": "0", 8 | "shots": "0", 9 | "xG": "0", 10 | "time": "90", 11 | "player_id": "2190", 12 | "team_id": "78", 13 | "position": "GK", 14 | "player": "Vicente Guaita", 15 | "h_a": "h", 16 | "yellow_card": "0", 17 | "red_card": "0", 18 | "roster_in": "0", 19 | "roster_out": "0", 20 | "key_passes": "0", 21 | "assists": "0", 22 | "xA": "0", 23 | "xGChain": "0", 24 | "xGBuildup": "0", 25 | "positionOrder": "1" 26 | }, 27 | "454139": { 28 | "id": "454139", 29 | "goals": "0", 30 | "own_goals": "0", 31 | "shots": "0", 32 | "xG": "0", 33 | "time": "90", 34 | "player_id": "510", 35 | "team_id": "78", 36 | "position": "DR", 37 | "player": "Joel Ward", 38 | "h_a": "h", 39 | "yellow_card": "0", 40 | "red_card": "0", 41 | "roster_in": "0", 42 | "roster_out": "0", 43 | "key_passes": "0", 44 | "assists": "0", 45 | "xA": "0", 46 | "xGChain": "0", 47 | "xGBuildup": "0", 48 | "positionOrder": "2" 49 | }, 50 | "454140": { 51 | "id": "454140", 52 | "goals": "0", 53 | "own_goals": "0", 54 | "shots": "0", 55 | "xG": "0", 56 | "time": "90", 57 | "player_id": "532", 58 | "team_id": "78", 59 | "position": "DC", 60 | "player": "Cheikhou Kouyat\u00e9", 61 | "h_a": "h", 62 | "yellow_card": "0", 63 | "red_card": "0", 64 | "roster_in": "0", 65 | "roster_out": "0", 66 | "key_passes": "0", 67 | "assists": "0", 68 | "xA": "0", 69 | "xGChain": "0", 70 | "xGBuildup": "0", 71 | "positionOrder": "3" 72 | }, 73 | "454141": { 74 | "id": "454141", 75 | "goals": "0", 76 | "own_goals": "0", 77 | "shots": "0", 78 | "xG": "0", 79 | "time": "90", 80 | "player_id": "699", 81 | "team_id": "78", 82 | "position": "DC", 83 | "player": "Gary Cahill", 84 | "h_a": "h", 85 | "yellow_card": "0", 86 | "red_card": "0", 87 | "roster_in": "0", 88 | "roster_out": "0", 89 | "key_passes": "0", 90 | "assists": "0", 91 | "xA": "0", 92 | "xGChain": "0", 93 | "xGBuildup": "0", 94 | "positionOrder": "3" 95 | }, 96 | "454142": { 97 | "id": "454142", 98 | "goals": "0", 99 | "own_goals": "0", 100 | "shots": "1", 101 | "xG": "0.336028516292572", 102 | "time": "90", 103 | "player_id": "730", 104 | "team_id": "78", 105 | "position": "DL", 106 | "player": "Patrick van Aanholt", 107 | "h_a": "h", 108 | "yellow_card": "0", 109 | "red_card": "0", 110 | "roster_in": "0", 111 | "roster_out": "0", 112 | "key_passes": "0", 113 | "assists": "0", 114 | "xA": "0", 115 | "xGChain": "0.336028516292572", 116 | "xGBuildup": "0", 117 | "positionOrder": "4" 118 | }, 119 | "454143": { 120 | "id": "454143", 121 | "goals": "0", 122 | "own_goals": "0", 123 | "shots": "3", 124 | "xG": "0.13479886949062347", 125 | "time": "90", 126 | "player_id": "775", 127 | "team_id": "78", 128 | "position": "MR", 129 | "player": "Andros Townsend", 130 | "h_a": "h", 131 | "yellow_card": "0", 132 | "red_card": "0", 133 | "roster_in": "0", 134 | "roster_out": "0", 135 | "key_passes": "1", 136 | "assists": "0", 137 | "xA": "0.08583834767341614", 138 | "xGChain": "0.5004618167877197", 139 | "xGBuildup": "0.42186686396598816", 140 | "positionOrder": "8" 141 | }, 142 | "454145": { 143 | "id": "454145", 144 | "goals": "0", 145 | "own_goals": "0", 146 | "shots": "1", 147 | "xG": "0.07418951392173767", 148 | "time": "90", 149 | "player_id": "5549", 150 | "team_id": "78", 151 | "position": "MC", 152 | "player": "Luka Milivojevic", 153 | "h_a": "h", 154 | "yellow_card": "0", 155 | "red_card": "0", 156 | "roster_in": "0", 157 | "roster_out": "0", 158 | "key_passes": "1", 159 | "assists": "0", 160 | "xA": "0.336028516292572", 161 | "xGChain": "0.336028516292572", 162 | "xGBuildup": "0.336028516292572", 163 | "positionOrder": "9" 164 | }, 165 | "454144": { 166 | "id": "454144", 167 | "goals": "0", 168 | "own_goals": "0", 169 | "shots": "0", 170 | "xG": "0", 171 | "time": "62", 172 | "player_id": "589", 173 | "team_id": "78", 174 | "position": "MC", 175 | "player": "James McCarthy", 176 | "h_a": "h", 177 | "yellow_card": "0", 178 | "red_card": "0", 179 | "roster_in": "454150", 180 | "roster_out": "0", 181 | "key_passes": "1", 182 | "assists": "0", 183 | "xA": "0.07911469042301178", 184 | "xGChain": "0", 185 | "xGBuildup": "0", 186 | "positionOrder": "9" 187 | }, 188 | "454146": { 189 | "id": "454146", 190 | "goals": "0", 191 | "own_goals": "0", 192 | "shots": "0", 193 | "xG": "0", 194 | "time": "84", 195 | "player_id": "8706", 196 | "team_id": "78", 197 | "position": "ML", 198 | "player": "Eberechi Eze", 199 | "h_a": "h", 200 | "yellow_card": "0", 201 | "red_card": "0", 202 | "roster_in": "454149", 203 | "roster_out": "0", 204 | "key_passes": "0", 205 | "assists": "0", 206 | "xA": "0", 207 | "xGChain": "0", 208 | "xGBuildup": "0", 209 | "positionOrder": "10" 210 | }, 211 | "454148": { 212 | "id": "454148", 213 | "goals": "0", 214 | "own_goals": "0", 215 | "shots": "1", 216 | "xG": "0.032427556812763214", 217 | "time": "90", 218 | "player_id": "672", 219 | "team_id": "78", 220 | "position": "FW", 221 | "player": "Jordan Ayew", 222 | "h_a": "h", 223 | "yellow_card": "0", 224 | "red_card": "0", 225 | "roster_in": "0", 226 | "roster_out": "0", 227 | "key_passes": "0", 228 | "assists": "0", 229 | "xA": "0", 230 | "xGChain": "0.45429444313049316", 231 | "xGBuildup": "0.42186686396598816", 232 | "positionOrder": "15" 233 | }, 234 | "454147": { 235 | "id": "454147", 236 | "goals": "0", 237 | "own_goals": "0", 238 | "shots": "2", 239 | "xG": "0.16495303809642792", 240 | "time": "90", 241 | "player_id": "606", 242 | "team_id": "78", 243 | "position": "FW", 244 | "player": "Christian Benteke", 245 | "h_a": "h", 246 | "yellow_card": "0", 247 | "red_card": "0", 248 | "roster_in": "0", 249 | "roster_out": "0", 250 | "key_passes": "2", 251 | "assists": "0", 252 | "xA": "0.051595985889434814", 253 | "xGChain": "0.473462849855423", 254 | "xGBuildup": "0.336028516292572", 255 | "positionOrder": "15" 256 | }, 257 | "454149": { 258 | "id": "454149", 259 | "goals": "0", 260 | "own_goals": "0", 261 | "shots": "0", 262 | "xG": "0", 263 | "time": "6", 264 | "player_id": "757", 265 | "team_id": "78", 266 | "position": "Sub", 267 | "player": "Jeffrey Schlupp", 268 | "h_a": "h", 269 | "yellow_card": "0", 270 | "red_card": "0", 271 | "roster_in": "0", 272 | "roster_out": "454146", 273 | "key_passes": "0", 274 | "assists": "0", 275 | "xA": "0", 276 | "xGChain": "0", 277 | "xGBuildup": "0", 278 | "positionOrder": "17" 279 | }, 280 | "454150": { 281 | "id": "454150", 282 | "goals": "0", 283 | "own_goals": "0", 284 | "shots": "0", 285 | "xG": "0", 286 | "time": "28", 287 | "player_id": "6027", 288 | "team_id": "78", 289 | "position": "Sub", 290 | "player": "Jairo Riedewald", 291 | "h_a": "h", 292 | "yellow_card": "1", 293 | "red_card": "0", 294 | "roster_in": "0", 295 | "roster_out": "454144", 296 | "key_passes": "0", 297 | "assists": "0", 298 | "xA": "0", 299 | "xGChain": "0.336028516292572", 300 | "xGBuildup": "0.336028516292572", 301 | "positionOrder": "17" 302 | } 303 | }, 304 | "a": { 305 | "454151": { 306 | "id": "454151", 307 | "goals": "0", 308 | "own_goals": "0", 309 | "shots": "0", 310 | "xG": "0", 311 | "time": "90", 312 | "player_id": "7702", 313 | "team_id": "89", 314 | "position": "GK", 315 | "player": "Dean Henderson", 316 | "h_a": "a", 317 | "yellow_card": "0", 318 | "red_card": "0", 319 | "roster_in": "0", 320 | "roster_out": "0", 321 | "key_passes": "0", 322 | "assists": "0", 323 | "xA": "0", 324 | "xGChain": "0.031576987355947495", 325 | "xGBuildup": "0.031576987355947495", 326 | "positionOrder": "1" 327 | }, 328 | "454152": { 329 | "id": "454152", 330 | "goals": "0", 331 | "own_goals": "0", 332 | "shots": "0", 333 | "xG": "0", 334 | "time": "90", 335 | "player_id": "5584", 336 | "team_id": "89", 337 | "position": "DR", 338 | "player": "Aaron Wan-Bissaka", 339 | "h_a": "a", 340 | "yellow_card": "0", 341 | "red_card": "0", 342 | "roster_in": "0", 343 | "roster_out": "0", 344 | "key_passes": "0", 345 | "assists": "0", 346 | "xA": "0", 347 | "xGChain": "0.29431986808776855", 348 | "xGBuildup": "0.29431986808776855", 349 | "positionOrder": "2" 350 | }, 351 | "454154": { 352 | "id": "454154", 353 | "goals": "0", 354 | "own_goals": "0", 355 | "shots": "0", 356 | "xG": "0", 357 | "time": "90", 358 | "player_id": "1739", 359 | "team_id": "89", 360 | "position": "DC", 361 | "player": "Eric Bailly", 362 | "h_a": "a", 363 | "yellow_card": "0", 364 | "red_card": "0", 365 | "roster_in": "0", 366 | "roster_out": "0", 367 | "key_passes": "0", 368 | "assists": "0", 369 | "xA": "0", 370 | "xGChain": "0.31683188676834106", 371 | "xGBuildup": "0.31683188676834106", 372 | "positionOrder": "3" 373 | }, 374 | "454153": { 375 | "id": "454153", 376 | "goals": "0", 377 | "own_goals": "0", 378 | "shots": "1", 379 | "xG": "0.02284710295498371", 380 | "time": "90", 381 | "player_id": "1687", 382 | "team_id": "89", 383 | "position": "DC", 384 | "player": "Harry Maguire", 385 | "h_a": "a", 386 | "yellow_card": "0", 387 | "red_card": "0", 388 | "roster_in": "0", 389 | "roster_out": "0", 390 | "key_passes": "0", 391 | "assists": "0", 392 | "xA": "0", 393 | "xGChain": "0.17357975244522095", 394 | "xGBuildup": "0.17357975244522095", 395 | "positionOrder": "3" 396 | }, 397 | "454155": { 398 | "id": "454155", 399 | "goals": "0", 400 | "own_goals": "0", 401 | "shots": "1", 402 | "xG": "0.031576987355947495", 403 | "time": "90", 404 | "player_id": "1006", 405 | "team_id": "89", 406 | "position": "DL", 407 | "player": "Luke Shaw", 408 | "h_a": "a", 409 | "yellow_card": "0", 410 | "red_card": "0", 411 | "roster_in": "0", 412 | "roster_out": "0", 413 | "key_passes": "2", 414 | "assists": "0", 415 | "xA": "0.16546165943145752", 416 | "xGChain": "0.3331676125526428", 417 | "xGBuildup": "0.26773539185523987", 418 | "positionOrder": "4" 419 | }, 420 | "454157": { 421 | "id": "454157", 422 | "goals": "0", 423 | "own_goals": "0", 424 | "shots": "2", 425 | "xG": "0.03372350335121155", 426 | "time": "90", 427 | "player_id": "697", 428 | "team_id": "89", 429 | "position": "DMC", 430 | "player": "Nemanja Matic", 431 | "h_a": "a", 432 | "yellow_card": "0", 433 | "red_card": "0", 434 | "roster_in": "0", 435 | "roster_out": "0", 436 | "key_passes": "0", 437 | "assists": "0", 438 | "xA": "0", 439 | "xGChain": "0.3331676125526428", 440 | "xGBuildup": "0.31577983498573303", 441 | "positionOrder": "7" 442 | }, 443 | "454156": { 444 | "id": "454156", 445 | "goals": "0", 446 | "own_goals": "0", 447 | "shots": "1", 448 | "xG": "0.016564758494496346", 449 | "time": "74", 450 | "player_id": "6817", 451 | "team_id": "89", 452 | "position": "DMC", 453 | "player": "Fred", 454 | "h_a": "a", 455 | "yellow_card": "0", 456 | "red_card": "0", 457 | "roster_in": "454162", 458 | "roster_out": "0", 459 | "key_passes": "1", 460 | "assists": "0", 461 | "xA": "0.017387786880135536", 462 | "xGChain": "0.13635800778865814", 463 | "xGBuildup": "0.10240545868873596", 464 | "positionOrder": "7" 465 | }, 466 | "454158": { 467 | "id": "454158", 468 | "goals": "0", 469 | "own_goals": "0", 470 | "shots": "2", 471 | "xG": "0.13900163769721985", 472 | "time": "90", 473 | "player_id": "7490", 474 | "team_id": "89", 475 | "position": "AMR", 476 | "player": "Mason Greenwood", 477 | "h_a": "a", 478 | "yellow_card": "0", 479 | "red_card": "0", 480 | "roster_in": "0", 481 | "roster_out": "0", 482 | "key_passes": "0", 483 | "assists": "0", 484 | "xA": "0", 485 | "xGChain": "0.1727251410484314", 486 | "xGBuildup": "0.03372350335121155", 487 | "positionOrder": "11" 488 | }, 489 | "454159": { 490 | "id": "454159", 491 | "goals": "0", 492 | "own_goals": "0", 493 | "shots": "0", 494 | "xG": "0", 495 | "time": "90", 496 | "player_id": "1228", 497 | "team_id": "89", 498 | "position": "AMC", 499 | "player": "Bruno Fernandes", 500 | "h_a": "a", 501 | "yellow_card": "0", 502 | "red_card": "0", 503 | "roster_in": "0", 504 | "roster_out": "0", 505 | "key_passes": "4", 506 | "assists": "0", 507 | "xA": "0.13194149732589722", 508 | "xGChain": "0.31652936339378357", 509 | "xGBuildup": "0.2390119582414627", 510 | "positionOrder": "12" 511 | }, 512 | "454160": { 513 | "id": "454160", 514 | "goals": "0", 515 | "own_goals": "0", 516 | "shots": "1", 517 | "xG": "0.10002944618463516", 518 | "time": "90", 519 | "player_id": "556", 520 | "team_id": "89", 521 | "position": "AML", 522 | "player": "Marcus Rashford", 523 | "h_a": "a", 524 | "yellow_card": "0", 525 | "red_card": "0", 526 | "roster_in": "0", 527 | "roster_out": "0", 528 | "key_passes": "2", 529 | "assists": "0", 530 | "xA": "0.1783539205789566", 531 | "xGChain": "0.15833847224712372", 532 | "xGBuildup": "0.058309026062488556", 533 | "positionOrder": "13" 534 | }, 535 | "454161": { 536 | "id": "454161", 537 | "goals": "0", 538 | "own_goals": "0", 539 | "shots": "2", 540 | "xG": "0.1783539205789566", 541 | "time": "76", 542 | "player_id": "3294", 543 | "team_id": "89", 544 | "position": "FW", 545 | "player": "Edinson Cavani", 546 | "h_a": "a", 547 | "yellow_card": "0", 548 | "red_card": "0", 549 | "roster_in": "454163", 550 | "roster_out": "0", 551 | "key_passes": "1", 552 | "assists": "0", 553 | "xA": "0.07781993597745895", 554 | "xGChain": "0.10240545868873596", 555 | "xGBuildup": "0", 556 | "positionOrder": "15" 557 | }, 558 | "454163": { 559 | "id": "454163", 560 | "goals": "0", 561 | "own_goals": "0", 562 | "shots": "1", 563 | "xG": "0.06543221324682236", 564 | "time": "14", 565 | "player_id": "5595", 566 | "team_id": "89", 567 | "position": "Sub", 568 | "player": "Daniel James", 569 | "h_a": "a", 570 | "yellow_card": "0", 571 | "red_card": "0", 572 | "roster_in": "0", 573 | "roster_out": "454161", 574 | "key_passes": "0", 575 | "assists": "0", 576 | "xA": "0", 577 | "xGChain": "0.15819089114665985", 578 | "xGBuildup": "0.15819089114665985", 579 | "positionOrder": "17" 580 | }, 581 | "454162": { 582 | "id": "454162", 583 | "goals": "0", 584 | "own_goals": "0", 585 | "shots": "0", 586 | "xG": "0", 587 | "time": "16", 588 | "player_id": "5560", 589 | "team_id": "89", 590 | "position": "Sub", 591 | "player": "Scott McTominay", 592 | "h_a": "a", 593 | "yellow_card": "0", 594 | "red_card": "0", 595 | "roster_in": "0", 596 | "roster_out": "454156", 597 | "key_passes": "0", 598 | "assists": "0", 599 | "xA": "0", 600 | "xGChain": "0.09700919687747955", 601 | "xGBuildup": "0.09700919687747955", 602 | "positionOrder": "17" 603 | } 604 | } 605 | }, 606 | "shots": { 607 | "h": [ 608 | { 609 | "id": "408200", 610 | "minute": "6", 611 | "result": "MissedShots", 612 | "X": "0.8830000305175781", 613 | "Y": "0.5579999923706055", 614 | "xG": "0.08583834767341614", 615 | "player": "Christian Benteke", 616 | "h_a": "h", 617 | "player_id": "606", 618 | "situation": "OpenPlay", 619 | "season": "2020", 620 | "shotType": "RightFoot", 621 | "match_id": "14717", 622 | "h_team": "Crystal Palace", 623 | "a_team": "Manchester United", 624 | "h_goals": "0", 625 | "a_goals": "0", 626 | "date": "2021-03-03 20:15:00", 627 | "player_assisted": "Andros Townsend", 628 | "lastAction": "Cross" 629 | }, 630 | { 631 | "id": "408201", 632 | "minute": "7", 633 | "result": "BlockedShot", 634 | "X": "0.759000015258789", 635 | "Y": "0.5170000076293946", 636 | "xG": "0.01916843093931675", 637 | "player": "Andros Townsend", 638 | "h_a": "h", 639 | "player_id": "775", 640 | "situation": "OpenPlay", 641 | "season": "2020", 642 | "shotType": "LeftFoot", 643 | "match_id": "14717", 644 | "h_team": "Crystal Palace", 645 | "a_team": "Manchester United", 646 | "h_goals": "0", 647 | "a_goals": "0", 648 | "date": "2021-03-03 20:15:00", 649 | "player_assisted": "Christian Benteke", 650 | "lastAction": "HeadPass" 651 | }, 652 | { 653 | "id": "408205", 654 | "minute": "13", 655 | "result": "BlockedShot", 656 | "X": "0.8190000152587891", 657 | "Y": "0.524000015258789", 658 | "xG": "0.05942648649215698", 659 | "player": "Andros Townsend", 660 | "h_a": "h", 661 | "player_id": "775", 662 | "situation": "OpenPlay", 663 | "season": "2020", 664 | "shotType": "LeftFoot", 665 | "match_id": "14717", 666 | "h_team": "Crystal Palace", 667 | "a_team": "Manchester United", 668 | "h_goals": "0", 669 | "a_goals": "0", 670 | "date": "2021-03-03 20:15:00", 671 | "player_assisted": null, 672 | "lastAction": "BallRecovery" 673 | }, 674 | { 675 | "id": "408210", 676 | "minute": "49", 677 | "result": "MissedShots", 678 | "X": "0.91", 679 | "Y": "0.54", 680 | "xG": "0.07911469042301178", 681 | "player": "Christian Benteke", 682 | "h_a": "h", 683 | "player_id": "606", 684 | "situation": "FromCorner", 685 | "season": "2020", 686 | "shotType": "RightFoot", 687 | "match_id": "14717", 688 | "h_team": "Crystal Palace", 689 | "a_team": "Manchester United", 690 | "h_goals": "0", 691 | "a_goals": "0", 692 | "date": "2021-03-03 20:15:00", 693 | "player_assisted": "James McCarthy", 694 | "lastAction": "Cross" 695 | }, 696 | { 697 | "id": "408211", 698 | "minute": "50", 699 | "result": "SavedShot", 700 | "X": "0.865", 701 | "Y": "0.26399999618530273", 702 | "xG": "0.032427556812763214", 703 | "player": "Jordan Ayew", 704 | "h_a": "h", 705 | "player_id": "672", 706 | "situation": "OpenPlay", 707 | "season": "2020", 708 | "shotType": "RightFoot", 709 | "match_id": "14717", 710 | "h_team": "Crystal Palace", 711 | "a_team": "Manchester United", 712 | "h_goals": "0", 713 | "a_goals": "0", 714 | "date": "2021-03-03 20:15:00", 715 | "player_assisted": "Christian Benteke", 716 | "lastAction": "Pass" 717 | }, 718 | { 719 | "id": "408212", 720 | "minute": "58", 721 | "result": "BlockedShot", 722 | "X": "0.7609999847412109", 723 | "Y": "0.5159999847412109", 724 | "xG": "0.07418951392173767", 725 | "player": "Luka Milivojevic", 726 | "h_a": "h", 727 | "player_id": "5549", 728 | "situation": "DirectFreekick", 729 | "season": "2020", 730 | "shotType": "RightFoot", 731 | "match_id": "14717", 732 | "h_team": "Crystal Palace", 733 | "a_team": "Manchester United", 734 | "h_goals": "0", 735 | "a_goals": "0", 736 | "date": "2021-03-03 20:15:00", 737 | "player_assisted": null, 738 | "lastAction": "Standard" 739 | }, 740 | { 741 | "id": "408213", 742 | "minute": "58", 743 | "result": "MissedShots", 744 | "X": "0.8430000305175781", 745 | "Y": "0.4229999923706055", 746 | "xG": "0.05620395392179489", 747 | "player": "Andros Townsend", 748 | "h_a": "h", 749 | "player_id": "775", 750 | "situation": "SetPiece", 751 | "season": "2020", 752 | "shotType": "LeftFoot", 753 | "match_id": "14717", 754 | "h_team": "Crystal Palace", 755 | "a_team": "Manchester United", 756 | "h_goals": "0", 757 | "a_goals": "0", 758 | "date": "2021-03-03 20:15:00", 759 | "player_assisted": null, 760 | "lastAction": "None" 761 | }, 762 | { 763 | "id": "408218", 764 | "minute": "89", 765 | "result": "SavedShot", 766 | "X": "0.9159999847412109", 767 | "Y": "0.6020000076293945", 768 | "xG": "0.336028516292572", 769 | "player": "Patrick van Aanholt", 770 | "h_a": "h", 771 | "player_id": "730", 772 | "situation": "OpenPlay", 773 | "season": "2020", 774 | "shotType": "RightFoot", 775 | "match_id": "14717", 776 | "h_team": "Crystal Palace", 777 | "a_team": "Manchester United", 778 | "h_goals": "0", 779 | "a_goals": "0", 780 | "date": "2021-03-03 20:15:00", 781 | "player_assisted": "Luka Milivojevic", 782 | "lastAction": "Pass" 783 | } 784 | ], 785 | "a": [ 786 | { 787 | "id": "408202", 788 | "minute": "12", 789 | "result": "SavedShot", 790 | "X": "0.7780000305175782", 791 | "Y": "0.7090000152587891", 792 | "xG": "0.01633571647107601", 793 | "player": "Nemanja Matic", 794 | "h_a": "a", 795 | "player_id": "697", 796 | "situation": "OpenPlay", 797 | "season": "2020", 798 | "shotType": "LeftFoot", 799 | "match_id": "14717", 800 | "h_team": "Crystal Palace", 801 | "a_team": "Manchester United", 802 | "h_goals": "0", 803 | "a_goals": "0", 804 | "date": "2021-03-03 20:15:00", 805 | "player_assisted": "Bruno Fernandes", 806 | "lastAction": "Pass" 807 | }, 808 | { 809 | "id": "408203", 810 | "minute": "12", 811 | "result": "BlockedShot", 812 | "X": "0.9019999694824219", 813 | "Y": "0.49700000762939456", 814 | "xG": "0.02284710295498371", 815 | "player": "Harry Maguire", 816 | "h_a": "a", 817 | "player_id": "1687", 818 | "situation": "FromCorner", 819 | "season": "2020", 820 | "shotType": "Head", 821 | "match_id": "14717", 822 | "h_team": "Crystal Palace", 823 | "a_team": "Manchester United", 824 | "h_goals": "0", 825 | "a_goals": "0", 826 | "date": "2021-03-03 20:15:00", 827 | "player_assisted": "Bruno Fernandes", 828 | "lastAction": "Aerial" 829 | }, 830 | { 831 | "id": "408204", 832 | "minute": "13", 833 | "result": "MissedShots", 834 | "X": "0.96", 835 | "Y": "0.495", 836 | "xG": "0.1537684053182602", 837 | "player": "Edinson Cavani", 838 | "h_a": "a", 839 | "player_id": "3294", 840 | "situation": "FromCorner", 841 | "season": "2020", 842 | "shotType": "RightFoot", 843 | "match_id": "14717", 844 | "h_team": "Crystal Palace", 845 | "a_team": "Manchester United", 846 | "h_goals": "0", 847 | "a_goals": "0", 848 | "date": "2021-03-03 20:15:00", 849 | "player_assisted": "Marcus Rashford", 850 | "lastAction": "Rebound" 851 | }, 852 | { 853 | "id": "408206", 854 | "minute": "15", 855 | "result": "MissedShots", 856 | "X": "0.865", 857 | "Y": "0.5609999847412109", 858 | "xG": "0.10002944618463516", 859 | "player": "Marcus Rashford", 860 | "h_a": "a", 861 | "player_id": "556", 862 | "situation": "OpenPlay", 863 | "season": "2020", 864 | "shotType": "RightFoot", 865 | "match_id": "14717", 866 | "h_team": "Crystal Palace", 867 | "a_team": "Manchester United", 868 | "h_goals": "0", 869 | "a_goals": "0", 870 | "date": "2021-03-03 20:15:00", 871 | "player_assisted": "Luke Shaw", 872 | "lastAction": "Pass" 873 | }, 874 | { 875 | "id": "408207", 876 | "minute": "17", 877 | "result": "BlockedShot", 878 | "X": "0.7269999694824218", 879 | "Y": "0.5129999923706055", 880 | "xG": "0.016564758494496346", 881 | "player": "Fred", 882 | "h_a": "a", 883 | "player_id": "6817", 884 | "situation": "OpenPlay", 885 | "season": "2020", 886 | "shotType": "LeftFoot", 887 | "match_id": "14717", 888 | "h_team": "Crystal Palace", 889 | "a_team": "Manchester United", 890 | "h_goals": "0", 891 | "a_goals": "0", 892 | "date": "2021-03-03 20:15:00", 893 | "player_assisted": null, 894 | "lastAction": "BallTouch" 895 | }, 896 | { 897 | "id": "408208", 898 | "minute": "22", 899 | "result": "BlockedShot", 900 | "X": "0.8190000152587891", 901 | "Y": "0.500999984741211", 902 | "xG": "0.07781993597745895", 903 | "player": "Mason Greenwood", 904 | "h_a": "a", 905 | "player_id": "7490", 906 | "situation": "OpenPlay", 907 | "season": "2020", 908 | "shotType": "LeftFoot", 909 | "match_id": "14717", 910 | "h_team": "Crystal Palace", 911 | "a_team": "Manchester United", 912 | "h_goals": "0", 913 | "a_goals": "0", 914 | "date": "2021-03-03 20:15:00", 915 | "player_assisted": "Edinson Cavani", 916 | "lastAction": "LayOff" 917 | }, 918 | { 919 | "id": "408209", 920 | "minute": "26", 921 | "result": "MissedShots", 922 | "X": "0.875", 923 | "Y": "0.4159999847412109", 924 | "xG": "0.024585524573922157", 925 | "player": "Edinson Cavani", 926 | "h_a": "a", 927 | "player_id": "3294", 928 | "situation": "OpenPlay", 929 | "season": "2020", 930 | "shotType": "Head", 931 | "match_id": "14717", 932 | "h_team": "Crystal Palace", 933 | "a_team": "Manchester United", 934 | "h_goals": "0", 935 | "a_goals": "0", 936 | "date": "2021-03-03 20:15:00", 937 | "player_assisted": "Marcus Rashford", 938 | "lastAction": "Cross" 939 | }, 940 | { 941 | "id": "408214", 942 | "minute": "68", 943 | "result": "BlockedShot", 944 | "X": "0.7580000305175781", 945 | "Y": "0.6940000152587891", 946 | "xG": "0.017387786880135536", 947 | "player": "Nemanja Matic", 948 | "h_a": "a", 949 | "player_id": "697", 950 | "situation": "OpenPlay", 951 | "season": "2020", 952 | "shotType": "LeftFoot", 953 | "match_id": "14717", 954 | "h_team": "Crystal Palace", 955 | "a_team": "Manchester United", 956 | "h_goals": "0", 957 | "a_goals": "0", 958 | "date": "2021-03-03 20:15:00", 959 | "player_assisted": "Fred", 960 | "lastAction": "Pass" 961 | }, 962 | { 963 | "id": "408215", 964 | "minute": "77", 965 | "result": "MissedShots", 966 | "X": "0.889000015258789", 967 | "Y": "0.5379999923706055", 968 | "xG": "0.06543221324682236", 969 | "player": "Daniel James", 970 | "h_a": "a", 971 | "player_id": "5595", 972 | "situation": "OpenPlay", 973 | "season": "2020", 974 | "shotType": "Head", 975 | "match_id": "14717", 976 | "h_team": "Crystal Palace", 977 | "a_team": "Manchester United", 978 | "h_goals": "0", 979 | "a_goals": "0", 980 | "date": "2021-03-03 20:15:00", 981 | "player_assisted": "Luke Shaw", 982 | "lastAction": "Cross" 983 | }, 984 | { 985 | "id": "408216", 986 | "minute": "80", 987 | "result": "MissedShots", 988 | "X": "0.8190000152587891", 989 | "Y": "0.45799999237060546", 990 | "xG": "0.061181697994470596", 991 | "player": "Mason Greenwood", 992 | "h_a": "a", 993 | "player_id": "7490", 994 | "situation": "OpenPlay", 995 | "season": "2020", 996 | "shotType": "LeftFoot", 997 | "match_id": "14717", 998 | "h_team": "Crystal Palace", 999 | "a_team": "Manchester United", 1000 | "h_goals": "0", 1001 | "a_goals": "0", 1002 | "date": "2021-03-03 20:15:00", 1003 | "player_assisted": "Bruno Fernandes", 1004 | "lastAction": "Pass" 1005 | }, 1006 | { 1007 | "id": "408217", 1008 | "minute": "87", 1009 | "result": "MissedShots", 1010 | "X": "0.845", 1011 | "Y": "0.6179999923706054", 1012 | "xG": "0.031576987355947495", 1013 | "player": "Luke Shaw", 1014 | "h_a": "a", 1015 | "player_id": "1006", 1016 | "situation": "OpenPlay", 1017 | "season": "2020", 1018 | "shotType": "RightFoot", 1019 | "match_id": "14717", 1020 | "h_team": "Crystal Palace", 1021 | "a_team": "Manchester United", 1022 | "h_goals": "0", 1023 | "a_goals": "0", 1024 | "date": "2021-03-03 20:15:00", 1025 | "player_assisted": "Bruno Fernandes", 1026 | "lastAction": "HeadPass" 1027 | } 1028 | ] 1029 | }, 1030 | "tmpl": { 1031 | "id": "14717", 1032 | "fid": "1485433", 1033 | "h": "78", 1034 | "a": "89", 1035 | "date": "2021-03-03 20:15:00", 1036 | "league_id": "1", 1037 | "season": "2020", 1038 | "h_goals": "0", 1039 | "a_goals": "0", 1040 | "team_h": "Crystal Palace", 1041 | "team_a": "Manchester United", 1042 | "h_xg": "0.742397", 1043 | "a_xg": "0.584016", 1044 | "h_w": "0.3773", 1045 | "h_d": "0.3779", 1046 | "h_l": "0.2448", 1047 | "league": "EPL", 1048 | "h_shot": "8", 1049 | "a_shot": "11", 1050 | "h_shotOnTarget": "2", 1051 | "a_shotOnTarget": "1", 1052 | "h_deep": "3", 1053 | "a_deep": "8", 1054 | "a_ppda": "10.7368", 1055 | "h_ppda": "16.4737" 1056 | } 1057 | } --------------------------------------------------------------------------------