├── .circleci └── config.yml ├── .coveragerc ├── .gitattributes ├── .gitignore ├── .mypy.ini ├── .pylintrc ├── LICENSE ├── README.md ├── cd ├── deploy_docs.sh ├── deploy_package.sh └── make_release.sh ├── ci ├── install_dependencies.sh ├── make_docs.sh └── run_tests.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── README.md │ ├── _static │ └── .gitkeep │ ├── _templates │ └── .gitkeep │ ├── api_index.rst │ ├── conf.py │ ├── endpoints_index.rst │ ├── glossary.rst │ ├── index.rst │ ├── misc_index.rst │ └── usage.md ├── docs_requirements.in ├── docs_requirements.txt ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── setup.cfg ├── test ├── __init__.py ├── mock_requests.py ├── resources │ ├── data │ │ ├── league_datesdata.json │ │ ├── league_playersdata.json │ │ ├── league_teamsdata.json │ │ ├── match_matchinfo.json │ │ ├── match_rostersdata.json │ │ ├── match_shotsdata.json │ │ ├── player_groupsdata.json │ │ ├── player_matchesdata.json │ │ ├── player_shotsdata.json │ │ ├── team_datesdata.json │ │ ├── team_playersdata.json │ │ └── team_statisticsdata.json │ ├── home.html │ ├── league_epl.html │ ├── match.html │ ├── minimal.html │ ├── player.html │ └── team.html └── test_api.py ├── test_requirements.in ├── test_requirements.txt └── understatapi ├── __init__.py ├── api.py ├── endpoints ├── __init__.py ├── base.py ├── league.py ├── match.py ├── player.py └── team.py ├── exceptions.py ├── parsers ├── __init__.py ├── base.py ├── league.py ├── match.py ├── player.py └── team.py └── utils.py /.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.9 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.9 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 | - "79:f3:4e:44:f3:b9:7c:1e:95:f9:eb:ee:4b:c9:de:cf" 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html linguist-detectable=false 2 | test/resources/** linguist-detectable=false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Environments 2 | venv 3 | .env 4 | .python-version 5 | 6 | # log files 7 | *.log 8 | 9 | # Notebooks 10 | *.ipynb 11 | 12 | # Autogenerated files 13 | *.pyc 14 | __pycache__ 15 | htmlcov 16 | *.coverage 17 | *.rst 18 | !index.rst 19 | !glossary.rst 20 | !api_index.rst 21 | !endpoints_index.rst 22 | !services_index.rst 23 | !misc_index.rst 24 | docs/source/README.md 25 | build 26 | dist 27 | *.egg* 28 | 29 | # vscode files 30 | .vscode/ 31 | 32 | # node 33 | node_modules/ 34 | 35 | # data files 36 | *.csv 37 | *.pkl 38 | *.pickle 39 | *.png 40 | *.json 41 | !test/resources/** 42 | 43 | # Credentials 44 | *.crt 45 | *.key 46 | *.csr 47 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cd/deploy_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | generate_circleci_config() 6 | { 7 | cat < .circleci/config.yml \\ 26 | rm -rf /tmp/gh-pages && git add . && \ 27 | git commit -m "Updated gh-pages" && \ 28 | git push && \ 29 | git checkout - 30 | 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 11 | generate_post_data() 12 | { 13 | cat <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 | -------------------------------------------------------------------------------- /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 setup.py install 31 | ``` 32 | ## Getting started 33 | 34 | --- 35 | 36 | **NOTE** 37 | 38 | This package is in early stages of development and the API is likely to change 39 | 40 | --- 41 | 42 | 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 43 | 44 | | Endpoint | Webpage | 45 | | ---------------------- | ------------------------------------------------- | 46 | | UnderstatClient.league | `https://understat.com/league/` | 47 | | UnderstatClient.team | `https://understat.com/team//` | 48 | | UnderstatClient.player | `https://understat.com/player/` | 49 | | UnderstatClient.match | `https://understat.com/match/` | 50 | 51 | Every mwthod in the public API corresponds to one of the tables visible on the understat page for the relevant endpoint. 52 | 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. 53 | 54 | ```python 55 | from understatapi import UnderstatClient 56 | 57 | understat = UnderstatClient() 58 | # get data for every player playing in the Premier League in 2019/20 59 | league_player_data = understat.league(league="EPL").get_player_data(season="2019") 60 | # Get the name and id of one of the player 61 | player_id, player_name = league_player_data[0]["id"], league_player_data[0]["player_name"] 62 | # Get data for every shot this player has taken in a league match (for all seasons) 63 | player_shot_data = understat.player(player=player_id).get_shot_data() 64 | ``` 65 | 66 | ```python 67 | from understatapi import UnderstatClient 68 | 69 | understat = UnderstatClient() 70 | # get data for every league match involving Manchester United in 2019/20 71 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 72 | # get the id for the first match of the season 73 | match_id = match_data[0]["id"] 74 | # get the rosters for the both teams in that match 75 | roster_data = understat.match(match=match_id).get_roster_data() 76 | ``` 77 | 78 | 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. 79 | 80 | ```python 81 | from understatapi import UnderstatClient 82 | 83 | with UnderstatClient() as understat: 84 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 85 | ``` 86 | 87 | For a full API reference, see [the documentation](https://collinb9.github.io/understatAPI/) 88 | 89 | ## Contributing 90 | 91 | 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 92 | 93 | - Clone the repo `git clone git@github.com:collinb9/understatAPI` 94 | - Create a branch to work off `git checkout -b descriptive_branch_name` 95 | - Make and commit your changes 96 | - Push your changes `git push` 97 | - Come back to the repo on github, and click on Pull requests -> New pull request 98 | 99 | 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 100 | 101 | - Check that the code has been formatted using [black](https://github.com/psf/black) 102 | - Lint the code using [pylint](https://github.com/PyCQA/pylint) 103 | - Check type annotations using [mypy](https://github.com/python/mypy) 104 | - Run the unit tests and check that they have 100% coverage 105 | 106 | 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 107 | 108 | ```bash 109 | pip install -r requirements.txt 110 | pip install -r test_requirments.txt 111 | pip install -r docs_requirments.txt 112 | chmod +x ci/run_tests.sh 113 | ci/run_tests.sh 114 | ``` 115 | 116 | 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. 117 | 118 | ## Versioning 119 | 120 | The versioning for this project follows the [semantic versioning](https://semver.org/) conventions. 121 | 122 | ## TODO 123 | 124 | - Add asynchronous support 125 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collinb9/understatAPI/df412e28da98c2c59fcc48f975d19f209261f730/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/collinb9/understatAPI/df412e28da98c2c59fcc48f975d19f209261f730/docs/source/_templates/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/source/misc_index.rst: -------------------------------------------------------------------------------- 1 | Misc 2 | ==== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | Utils 9 | Exceptions -------------------------------------------------------------------------------- /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_requirements.in: -------------------------------------------------------------------------------- 1 | sphinx==3.5.2 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_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile 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==3.5.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==1.0.3 67 | # via sphinx 68 | sphinxcontrib-jsmath==1.0.1 69 | # via sphinx 70 | sphinxcontrib-qthelp==1.0.3 71 | # via sphinx 72 | sphinxcontrib-serializinghtml==1.1.4 73 | # via sphinx 74 | urllib3==1.26.11 75 | # via 76 | # -r docs_requirements.in 77 | # requests 78 | 79 | # The following packages are considered to be unsafe in a requirements file: 80 | # setuptools 81 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | requests==2.25.1 2 | urllib3==1.26.5 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | certifi>=2021.10.8 8 | # via requests 9 | chardet==4.0.0 10 | # via requests 11 | idna==2.10 12 | # via requests 13 | requests>=2.25.1 14 | # via -r requirements.in 15 | urllib3==1.26.5 16 | # via 17 | # -r requirements.in 18 | # requests 19 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- 1 | """ tests """ 2 | from .mock_requests import mocked_requests_get 3 | -------------------------------------------------------------------------------- /test/mock_requests.py: -------------------------------------------------------------------------------- 1 | """ Mock the requests library """ 2 | from requests.exceptions import HTTPError 3 | 4 | 5 | class MockResponse: 6 | """ Mock response from requests.get() """ 7 | 8 | def __init__(self, url, status_code=200, reason="OK"): 9 | self.url = url 10 | self.status_code = status_code 11 | self.reason = reason 12 | self.url = url 13 | 14 | @property 15 | def content(self): 16 | """ Response.content """ 17 | with open(self.url) as file: 18 | content = file.read() 19 | return content 20 | 21 | @property 22 | def text(self): 23 | """ Response.content """ 24 | with open(self.url) as file: 25 | text = file.read() 26 | return text 27 | 28 | def raise_for_status(self): 29 | """Raises ``HTTPError``, if one occurred.""" 30 | 31 | http_error_msg = "" 32 | reason = self.reason 33 | if 400 <= self.status_code < 500: 34 | http_error_msg = u"%s Client Error: %s for url: %s" % ( 35 | self.status_code, 36 | reason, 37 | self.url, 38 | ) 39 | 40 | elif 500 <= self.status_code < 600: 41 | http_error_msg = u"%s Server Error: %s for url: %s" % ( 42 | self.status_code, 43 | reason, 44 | self.url, 45 | ) 46 | 47 | if http_error_msg: 48 | raise HTTPError(http_error_msg, response=self) 49 | 50 | 51 | def mocked_requests_get(*args, **kwargs): 52 | """ 53 | Return a MockResponse object 54 | """ 55 | return MockResponse(*args, **kwargs) 56 | -------------------------------------------------------------------------------- /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"} -------------------------------------------------------------------------------- /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/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/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/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/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/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}}}} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/match.html" 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/match.html" 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/match.html" 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/player.html" 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( 90 | zip(data, expected_data) 91 | ): 92 | with self.subTest(record=i): 93 | self.assertDictEqual(record, expected_record) 94 | 95 | def test_get_shot_data_return_value(self, mock_get): 96 | """test ``player.get_shot_data()``""" 97 | mock_get.return_value = mocked_requests_get( 98 | "test/resources/player.html" 99 | ) 100 | data = self.player.get_shot_data() 101 | data_path = "test/resources/data/player_shotsdata.json" 102 | expected_data = read_json(data_path) 103 | for i, (record, expected_record) in enumerate( 104 | zip(data, expected_data) 105 | ): 106 | with self.subTest(record=i): 107 | self.assertDictEqual(record, expected_record) 108 | 109 | def test_player_get_season_data(self, mock_get): 110 | """test ``player.get_season_data()``""" 111 | mock_get.return_value = mocked_requests_get( 112 | "test/resources/player.html" 113 | ) 114 | data = self.player.get_season_data() 115 | data_path = "test/resources/data/player_groupsdata.json" 116 | expected_data = read_json(data_path) 117 | self.assertDictEqual(data, expected_data) 118 | 119 | def test_team_get_player_data(self, mock_get): 120 | """test ``team.get_match_data()``""" 121 | mock_get.return_value = mocked_requests_get("test/resources/team.html") 122 | data = self.team.get_player_data(season="2019") 123 | data_path = "test/resources/data/team_playersdata.json" 124 | expected_data = read_json(data_path) 125 | for i, (record, expected_record) in enumerate( 126 | zip(data, expected_data) 127 | ): 128 | with self.subTest(record=i): 129 | self.assertDictEqual(record, expected_record) 130 | 131 | def test_team_get_match_data(self, mock_get): 132 | """test ``team.get_match_data()``""" 133 | mock_get.return_value = mocked_requests_get("test/resources/team.html") 134 | data = self.team.get_match_data(season="2019") 135 | data_path = "test/resources/data/team_datesdata.json" 136 | expected_data = read_json(data_path) 137 | for i, (record, expected_record) in enumerate( 138 | zip(data, expected_data) 139 | ): 140 | with self.subTest(record=i): 141 | self.assertDictEqual(record, expected_record) 142 | 143 | def test_team_get_context_data(self, mock_get): 144 | """test ``team.get_context_data()``""" 145 | mock_get.return_value = mocked_requests_get("test/resources/team.html") 146 | data = self.team.get_context_data(season="2019") 147 | data_path = "test/resources/data/team_statisticsdata.json" 148 | expected_data = read_json(data_path) 149 | self.assertDictEqual(data, expected_data) 150 | 151 | def test_league_get_team_data(self, mock_get): 152 | """test ``league.get_team_data()``""" 153 | mock_get.return_value = mocked_requests_get( 154 | "test/resources/league_epl.html" 155 | ) 156 | data = self.league.get_team_data(season="2019") 157 | data_path = "test/resources/data/league_teamsdata.json" 158 | expected_data = read_json(data_path) 159 | self.assertDictEqual(data, expected_data) 160 | 161 | def test_league_get_match_data(self, mock_get): 162 | """test ``league.get_match_data()``""" 163 | mock_get.return_value = mocked_requests_get( 164 | "test/resources/league_epl.html" 165 | ) 166 | data = self.league.get_match_data(season="2019") 167 | data_path = "test/resources/data/league_datesdata.json" 168 | expected_data = read_json(data_path) 169 | for i, (record, expected_record) in enumerate( 170 | zip(data, expected_data) 171 | ): 172 | with self.subTest(record=i): 173 | self.assertDictEqual(record, expected_record) 174 | 175 | def test_league_get_player_data(self, mock_get): 176 | """test ``league.get_player_data()``""" 177 | mock_get.return_value = mocked_requests_get( 178 | "test/resources/league_epl.html" 179 | ) 180 | data = self.league.get_player_data(season="2019") 181 | data_path = "test/resources/data/league_playersdata.json" 182 | expected_data = read_json(data_path) 183 | for i, (record, expected_record) in enumerate( 184 | zip(data, expected_data) 185 | ): 186 | with self.subTest(record=i): 187 | self.assertDictEqual(record, expected_record) 188 | 189 | 190 | @patch.object(requests.Session, "get", side_effect=mocked_requests_get) 191 | class TestEndpointErrors(EndpointBaseTestCase): 192 | """Test the conditions under which exceptions are expected""" 193 | 194 | def test_match_get_data_bad_player(self, mock_get): 195 | """test that ``match._get_data()`` raises an InvalidMatch error""" 196 | with self.assertRaises(InvalidMatch): 197 | self.match.get_shot_data(status_code=404) 198 | 199 | def test_match_get_data_type_error(self, mock_get): 200 | """ 201 | test that ``mathc.get_data()`` raises a TypeError 202 | when ``match`` is not a string 203 | """ 204 | match = self.understat.match(match=None) 205 | with self.assertRaises(TypeError): 206 | _ = match.get_shot_data() 207 | 208 | def test_get_data_bad_player(self, mock_get): 209 | """test that ``player._get_data()`` raises an InvalidPlayer error""" 210 | with self.assertRaises(InvalidPlayer): 211 | self.player.get_shot_data(status_code=404) 212 | 213 | def test_player_get_data_type_error(self, mock_get): 214 | """ 215 | test that ``player._get_data()`` raises a TypeError 216 | when ``player`` is not a string 217 | """ 218 | player = self.understat.player(None) 219 | with self.assertRaises(TypeError): 220 | _ = player.get_shot_data() 221 | 222 | def test_team_get_data_bad_team(self, mock_get): 223 | """test that ``team._get_data()`` raises an InvalidTeam error""" 224 | team = self.understat.team(self.team_name) 225 | with self.assertRaises(InvalidTeam): 226 | _ = team.get_match_data(season="2019", status_code=404) 227 | 228 | def test_team_get_data_type_error(self, mock_get): 229 | """ 230 | test that ``team._get_data()`` raises a TypeError 231 | when ``team`` is not a string 232 | """ 233 | team = self.understat.team(None) 234 | with self.assertRaises(TypeError): 235 | _ = team.get_match_data(season="") 236 | 237 | def test_league_get_data_bad_team(self, mock_get): 238 | """test that ``league._get_data()`` raises an InvalidLeague error""" 239 | league = self.understat.league("dummy_team") 240 | with self.assertRaises(InvalidLeague): 241 | _ = league.get_match_data(season="2019", status_code=404) 242 | 243 | def test_league_get_data_type_error(self, mock_get): 244 | """ 245 | test that ``league._get_data()`` raises a TypeError 246 | when ``league`` is not a string 247 | """ 248 | league = self.understat.league(None) 249 | with self.assertRaises(TypeError): 250 | _ = league.get_match_data(season="2019") 251 | 252 | def test_invalid_season(self, mock_get): 253 | """ 254 | Test that an error is raised when you try to get data for a 255 | season before 2014 256 | """ 257 | with self.assertRaises(InvalidSeason): 258 | _ = self.league.get_match_data(season="2013") 259 | 260 | def test_error_handling_method(self, mock_get): 261 | # pylint: disable=no-member 262 | """ 263 | test the error handling works as expected when a method is called 264 | that does not belong to the given endpoint 265 | """ 266 | with self.assertRaises(AttributeError) as err: 267 | with UnderstatClient() as understat: 268 | understat.team("").get_bad_data() 269 | self.assertEqual( 270 | str(err), 271 | "'TeamEndpoint' object has no attribute 'get_bad_data'\n" 272 | "Its public methods are ['get_context_data', " 273 | "'get_match_data', get_player_data']", 274 | ) 275 | 276 | 277 | class TestEndpointDunder(EndpointBaseTestCase): 278 | """Tests for the dunder methods in the endpoint class""" 279 | 280 | def test_league(self): 281 | """test ``league()``""" 282 | self.assertEqual( 283 | repr(self.understat.league(league="EPL")), 284 | "", 285 | ) 286 | 287 | def test_player(self): 288 | """test ``player()``""" 289 | self.assertEqual( 290 | repr(self.understat.player(player="1234")), 291 | "", 292 | ) 293 | 294 | def test_team(self): 295 | """test ``team()``""" 296 | self.assertEqual( 297 | repr(self.understat.team(team="Manchester_United")), 298 | "", 299 | ) 300 | 301 | def test_match(self): 302 | """test ``match()``""" 303 | self.assertEqual( 304 | repr(self.understat.match(match="1234")), "" 305 | ) 306 | 307 | def test_iteration(self): 308 | """Test iterating over players""" 309 | player_names = ["player_1", "player_2"] 310 | self.player._primary_attr = player_names 311 | for player, player_name in zip(self.player, player_names): 312 | with self.subTest(player=player_name): 313 | self.assertEqual(player.player, player_name) 314 | 315 | def test_len_one(self): 316 | """Test len() when there is only one player""" 317 | self.assertEqual(1, len(self.player)) 318 | 319 | def test_len_error(self): 320 | """Test len() errors out when passed a non-sequence""" 321 | self.player._primary_attr = None 322 | with self.assertRaises(TypeError): 323 | self.assertEqual(1, len(self.player)) 324 | 325 | def test_getitem_one(self): 326 | """Test getitem() when there is only one player""" 327 | self.assertEqual(self.player[0].player, self.player.player) 328 | 329 | def test_context_manager(self): 330 | """ 331 | Test that the client behaves as a context manager as expected 332 | """ 333 | try: 334 | with UnderstatClient(): 335 | pass 336 | except Exception: # pylint: disable=broad-except 337 | self.fail() 338 | 339 | 340 | if __name__ == "__main__": 341 | unittest.main() 342 | -------------------------------------------------------------------------------- /test_requirements.in: -------------------------------------------------------------------------------- 1 | -c requirements.txt 2 | pylint==2.11.1 3 | black==21.10b0 4 | coverage==6.0.1 5 | mypy==0.812 6 | pyenchant==3.2.0 7 | lxml>=4.9.1 8 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with python 3.9 3 | # To update, run: 4 | # 5 | # pip-compile test_requirements.in 6 | # 7 | astroid==2.8.2 8 | # via pylint 9 | black==21.10b0 10 | # via -r test_requirements.in 11 | click==7.1.2 12 | # via black 13 | coverage==6.0.1 14 | # via -r test_requirements.in 15 | isort==5.7.0 16 | # via pylint 17 | lazy-object-proxy==1.5.2 18 | # via astroid 19 | lxml==4.9.1 20 | # via -r test_requirements.in 21 | mccabe==0.6.1 22 | # via pylint 23 | mypy==0.812 24 | # via -r test_requirements.in 25 | mypy-extensions==0.4.3 26 | # via 27 | # black 28 | # mypy 29 | pathspec==0.9.0 30 | # via black 31 | platformdirs==2.4.0 32 | # via 33 | # black 34 | # pylint 35 | pyenchant==3.2.0 36 | # via -r test_requirements.in 37 | pylint==2.11.1 38 | # via -r test_requirements.in 39 | regex==2020.11.13 40 | # via black 41 | toml==0.10.2 42 | # via pylint 43 | tomli==1.2.3 44 | # via black 45 | typed-ast==1.4.2 46 | # via mypy 47 | typing-extensions==3.10.0.2 48 | # via 49 | # astroid 50 | # black 51 | # mypy 52 | # pylint 53 | wrapt==1.12.1 54 | # via astroid 55 | 56 | # The following packages are considered to be unsafe in a requirements file: 57 | # setuptools 58 | -------------------------------------------------------------------------------- /understatapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ An API for scraping data from understat.com """ 2 | from .api import UnderstatClient 3 | 4 | __version__ = "0.6.1" 5 | -------------------------------------------------------------------------------- /understatapi/api.py: -------------------------------------------------------------------------------- 1 | """ understatAPI client """ 2 | from types import TracebackType 3 | import requests 4 | from .utils import get_public_methods, str_to_class, find_endpoints 5 | from .endpoints import ( 6 | LeagueEndpoint, 7 | PlayerEndpoint, 8 | TeamEndpoint, 9 | MatchEndpoint, 10 | ) 11 | from .exceptions import PrimaryAttribute 12 | 13 | 14 | class UnderstatClient: 15 | """#pylint: disable=line-too-long 16 | API client for understat 17 | 18 | The main interface for interacting with understatAPI. Exposes 19 | each of the entrypoints, maintains a consistent 20 | session and handles errors 21 | 22 | :Example: 23 | 24 | .. code-block:: 25 | 26 | from understatapi import UnderstatClient 27 | 28 | with UnderstatClient() as understat: 29 | league_player_data = understat.league(league="EPL").get_player_data(season="2019") 30 | player_shot_data = understat.player(player="2371").get_shot_data() 31 | team_match_data = understat.team(team="Manchester_United").get_match_data(season="2019") 32 | roster_data = understat.match(match="14711").get_roster_data() 33 | 34 | Using the context manager gives some more verbose error handling 35 | 36 | .. testsetup:: 37 | 38 | from understatapi import UnderstatClient 39 | 40 | .. doctest:: 41 | 42 | >>> team="" 43 | >>> with UnderstatClient() as understat: 44 | ... understat.team(team).get_bad_data() # doctest: +SKIP 45 | Traceback (most recent call last) 46 | File "", line 2, in 47 | File "understatapi/api.py", line 59, in __exit__ 48 | raise AttributeError( 49 | AttributeError: 'TeamEndpoint' object has no attribute 'get_bad_data' 50 | Its public methods are ['get_context_data', 'get_match_data', 'get_player_data'] 51 | 52 | """ 53 | 54 | def __init__(self) -> None: 55 | self.session = requests.Session() 56 | 57 | def __enter__(self) -> "UnderstatClient": 58 | return self 59 | 60 | def __exit__( 61 | self, 62 | exception_type: type, 63 | exception_value: BaseException, 64 | traceback: TracebackType, 65 | ) -> None: 66 | if exception_type is AttributeError: 67 | endpoint = find_endpoints(str(exception_value)) 68 | endpoint_obj = str_to_class(__name__, endpoint[0]) 69 | public_methods = get_public_methods(endpoint_obj) 70 | raise AttributeError( 71 | str(exception_value) 72 | + 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 | -------------------------------------------------------------------------------- /understatapi/endpoints/__init__.py: -------------------------------------------------------------------------------- 1 | """ Endpoints """ 2 | from .base import BaseEndpoint 3 | from .league import LeagueEndpoint 4 | from .player import PlayerEndpoint 5 | from .team import TeamEndpoint 6 | from .match import MatchEndpoint 7 | -------------------------------------------------------------------------------- /understatapi/endpoints/base.py: -------------------------------------------------------------------------------- 1 | """ Base endpoint """ 2 | from typing import Sequence 3 | import requests 4 | from requests import Response 5 | from ..parsers import BaseParser 6 | from ..exceptions import ( 7 | InvalidLeague, 8 | InvalidSeason, 9 | PrimaryAttribute, 10 | ) 11 | 12 | 13 | class BaseEndpoint: 14 | """ 15 | Base endpoint for understat API 16 | 17 | :attr base_url: str: The base url to use for requests, 18 | ``https://understat.com/`` 19 | :attr leagues: List[str]: The available leagues, ``EPL``, ``La_Liga``, 20 | ``Bundesliga``, optional``Serie_A``, ``Ligue_1``, ``RFPL`` 21 | """ 22 | 23 | base_url = "https://understat.com/" 24 | leagues = ["EPL", "La_Liga", "Bundesliga", "Serie_A", "Ligue_1", "RFPL"] 25 | parser: BaseParser 26 | 27 | def __init__( 28 | self, 29 | primary_attr: PrimaryAttribute, 30 | session: requests.Session, 31 | ) -> None: 32 | """ 33 | :session: requests.Session: The current ``request`` session 34 | """ 35 | self.session = session 36 | self._primary_attr = primary_attr 37 | 38 | def __repr__(self) -> str: 39 | return f"<{self.__class__.__name__}({self._primary_attr!r})>" 40 | 41 | def __len__(self) -> int: 42 | if isinstance(self._primary_attr, str): 43 | return 1 44 | if isinstance(self._primary_attr, Sequence): 45 | return len(self._primary_attr) 46 | raise TypeError("Primary attribute is not a sequence or string") 47 | 48 | def __getitem__(self, index: int) -> "BaseEndpoint": 49 | if index >= len(self): 50 | raise IndexError 51 | if isinstance(self._primary_attr, str): 52 | return self.__class__(self._primary_attr, session=self.session) 53 | return self.__class__(self._primary_attr[index], session=self.session) 54 | 55 | def _check_args(self, league: str = None, season: str = None) -> None: 56 | """ Handle invalid arguments """ 57 | if league is not None and league not in self.leagues: 58 | raise InvalidLeague( 59 | f"{league}is not a valid league", league=league 60 | ) 61 | if season is not None and int(season) < 2014: 62 | raise InvalidSeason( 63 | f"{season} is not a valid season", season=season 64 | ) 65 | 66 | def _request_url(self, *args: str, **kwargs: str) -> Response: 67 | """ 68 | Use the requests module to send a HTTP request to a url, and check 69 | that this request worked. 70 | 71 | :param args: Arguments to pass to ``requests.get()`` 72 | :param kwargs: Keyword arguments to pass to ``requests.get()`` 73 | """ 74 | res = self.session.get(*args, **kwargs) 75 | res.raise_for_status() 76 | return res 77 | -------------------------------------------------------------------------------- /understatapi/endpoints/league.py: -------------------------------------------------------------------------------- 1 | """ League endpoint """ 2 | from typing import Dict, Any 3 | import requests 4 | from .base import BaseEndpoint 5 | from ..parsers import LeagueParser 6 | from ..exceptions import PrimaryAttribute 7 | 8 | 9 | class LeagueEndpoint(BaseEndpoint): 10 | """#pylint: disable-line-too-long 11 | Endpoint for league data. Use this class to get data from a url of the form 12 | ``https://understat.com/league//`` 13 | 14 | :Example: 15 | 16 | .. testsetup:: 17 | 18 | import requests 19 | from understatapi.endpoints import LeagueEndpoint 20 | 21 | .. testcleanup:: 22 | 23 | session.close() 24 | 25 | .. doctest:: 26 | 27 | >>> session = requests.Session() 28 | >>> leagues = ["EPL", "Bundesliga"] 29 | >>> for league in LeagueEndpoint(leagues, session=session): 30 | ... print(league.league) 31 | EPL 32 | Bundesliga 33 | 34 | """ 35 | 36 | parser = LeagueParser() 37 | 38 | def __init__(self, league: PrimaryAttribute, session: requests.Session): 39 | """ 40 | :param league: Name of the league(s) to get data for, 41 | one of {EPL, La_Liga, Bundesliga, Serie_A, Ligue_1, RFPL} 42 | :param session: The current session 43 | """ 44 | self._primary_attr = league 45 | super().__init__(primary_attr=self._primary_attr, session=session) 46 | 47 | @property 48 | def league(self) -> PrimaryAttribute: 49 | """ league name """ 50 | return self._primary_attr 51 | 52 | def _get_data(self, season: str, **kwargs: str) -> requests.Response: 53 | """ 54 | Get data on a league-wide basis 55 | 56 | :param season: Season to get data for 57 | :param kwargs: Keyword argument to pass to 58 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_url` 59 | """ 60 | if not isinstance(self.league, str): 61 | raise TypeError("``league`` must be a string") 62 | self._check_args(league=self.league, season=season) 63 | url = self.base_url + "league/" + self.league + "/" + season 64 | 65 | response = self._request_url(url=url, **kwargs) 66 | 67 | return response 68 | 69 | def get_team_data(self, season: str, **kwargs: str) -> Dict[str, Any]: 70 | """ 71 | Get data for all teams in a given league and season 72 | 73 | :param season: Season to get data for 74 | :param kwargs: Keyword argument to pass to 75 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 76 | """ 77 | res = self._get_data(season=season, **kwargs) 78 | data = self.parser.get_team_data(html=res.text) 79 | return data 80 | 81 | def get_match_data(self, season: str, **kwargs: str) -> Dict[str, Any]: 82 | """ 83 | Get data for all fixtures in a given league and season. 84 | 85 | :param season: Season to get data for 86 | :param kwargs: Keyword argument to pass to 87 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 88 | """ 89 | res = self._get_data(season=season, **kwargs) 90 | data = self.parser.get_match_data(html=res.text) 91 | return data 92 | 93 | def get_player_data(self, season: str, **kwargs: str) -> Dict[str, Any]: 94 | """ 95 | Get data for all players in a given league and season 96 | 97 | :param season: Season to get data for 98 | :param kwargs: Keyword argument to pass to 99 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response()` 100 | """ 101 | res = self._get_data(season=season, **kwargs) 102 | data = self.parser.get_player_data(html=res.text) 103 | return data 104 | -------------------------------------------------------------------------------- /understatapi/endpoints/match.py: -------------------------------------------------------------------------------- 1 | """ Match endpoint """ 2 | from typing import Dict, Any 3 | import requests 4 | from requests.exceptions import HTTPError 5 | from .base import BaseEndpoint 6 | from ..parsers import MatchParser 7 | from ..exceptions import InvalidMatch, PrimaryAttribute 8 | 9 | 10 | class MatchEndpoint(BaseEndpoint): 11 | """ 12 | Use this class to get data from a url of the form 13 | ``https://understat.com/match/`` 14 | 15 | :Example: 16 | 17 | .. testsetup:: 18 | 19 | import requests 20 | from understatapi.endpoints import MatchEndpoint 21 | 22 | .. testcleanup:: 23 | 24 | session.close() 25 | 26 | .. doctest:: 27 | 28 | >>> session = requests.Session() 29 | >>> match_ids = ["123", "456"] 30 | >>> for match in MatchEndpoint(match_ids, session=session): 31 | ... print(match.match) 32 | 123 33 | 456 34 | """ 35 | 36 | parser = MatchParser() 37 | 38 | def __init__(self, match: PrimaryAttribute, session: requests.Session): 39 | """ 40 | :param match: Id of match(es) to get data for 41 | :param session: The current session 42 | """ 43 | self._primary_attr = match 44 | super().__init__(primary_attr=self._primary_attr, session=session) 45 | 46 | @property 47 | def match(self) -> PrimaryAttribute: 48 | """ match id """ 49 | return self._primary_attr 50 | 51 | def _get_data(self, **kwargs: str) -> requests.Response: 52 | """ 53 | Get data on a per-match basis 54 | 55 | :param kwargs: Keyword argument to pass to 56 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_url` 57 | """ 58 | if not isinstance(self.match, str): 59 | raise TypeError("``match`` must be a string") 60 | self._check_args() 61 | url = self.base_url + "match/" + self.match 62 | 63 | try: 64 | response = self._request_url(url=url, **kwargs) 65 | except HTTPError as err: 66 | raise InvalidMatch( 67 | f"{self.match} is not a valid match", match=self.match 68 | ) from err 69 | 70 | return response 71 | 72 | def get_shot_data(self, **kwargs: str) -> 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._get_response` 78 | """ 79 | res = self._get_data(**kwargs) 80 | data = self.parser.get_shot_data(html=res.text) 81 | return data 82 | 83 | def get_roster_data(self, **kwargs: str) -> Dict[str, Any]: 84 | """ 85 | Get data about the roster for each team 86 | 87 | :param kwargs: Keyword argument to pass to 88 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 89 | """ 90 | res = self._get_data(**kwargs) 91 | data = self.parser.get_roster_data(html=res.text) 92 | return data 93 | 94 | def get_match_info(self, **kwargs: str) -> Dict[str, Any]: 95 | """ 96 | Get information about the match 97 | 98 | :param kwargs: Keyword argument to pass to 99 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 100 | """ 101 | res = self._get_data(**kwargs) 102 | data = self.parser.get_match_info(html=res.text) 103 | return data 104 | -------------------------------------------------------------------------------- /understatapi/endpoints/player.py: -------------------------------------------------------------------------------- 1 | """ Player endpoint """ 2 | from typing import Dict, Any 3 | import requests 4 | from requests.exceptions import HTTPError 5 | from .base import BaseEndpoint 6 | from ..parsers import PlayerParser 7 | from ..exceptions import InvalidPlayer, PrimaryAttribute 8 | 9 | 10 | class PlayerEndpoint(BaseEndpoint): 11 | """ 12 | Use this class to get data from a url of the form 13 | ``https://understat.com/player/`` 14 | 15 | :Example: 16 | 17 | .. testsetup:: 18 | 19 | import requests 20 | from understatapi.endpoints import PlayerEndpoint 21 | 22 | .. testcleanup:: 23 | 24 | session.close() 25 | 26 | .. doctest:: 27 | 28 | >>> session = requests.Session() 29 | >>> player_ids = ["000", "111"] 30 | >>> for player in PlayerEndpoint(player_ids, session=session): 31 | ... print(player.player) 32 | 000 33 | 111 34 | 35 | """ 36 | 37 | parser = PlayerParser() 38 | 39 | def __init__( 40 | self, player: PrimaryAttribute, session: requests.Session 41 | ) -> None: 42 | """ 43 | :param player: Id of the player(s) to get data for 44 | :param session: The current session 45 | """ 46 | self._primary_attr = player 47 | super().__init__(primary_attr=self._primary_attr, session=session) 48 | 49 | @property 50 | def player(self) -> PrimaryAttribute: 51 | """ player id """ 52 | return self._primary_attr 53 | 54 | def _get_data(self, **kwargs: str) -> requests.Response: 55 | """ 56 | Get data on a per-player basis 57 | 58 | :param query: Identifies the type of data to get, 59 | one of {matchesData, shotsData, groupsData} 60 | :param kwargs: Keyword argument to pass to 61 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 62 | """ 63 | if not isinstance(self.player, str): 64 | raise TypeError("``player`` must be a string") 65 | self._check_args() 66 | url = self.base_url + "player/" + self.player 67 | 68 | try: 69 | response = self._request_url(url=url, **kwargs) 70 | except HTTPError as err: 71 | raise InvalidPlayer( 72 | f"{self.player} is not a valid player or player id", 73 | player=self.player, 74 | ) from err 75 | 76 | return response 77 | 78 | def get_match_data(self, **kwargs: str) -> Dict[str, Any]: 79 | """ 80 | Get match level data for a player 81 | 82 | :param kwargs: Keyword argument to pass to 83 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 84 | """ 85 | res = self._get_data(**kwargs) 86 | data = self.parser.get_match_data(html=res.text) 87 | return data 88 | 89 | def get_shot_data(self, **kwargs: str) -> Dict[str, Any]: 90 | """ 91 | Get shot level data for a player 92 | 93 | :param kwargs: Keyword argument to pass to 94 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 95 | """ 96 | res = self._get_data(**kwargs) 97 | data = self.parser.get_shot_data(html=res.text) 98 | return data 99 | 100 | def get_season_data(self, **kwargs: str) -> Dict[str, Any]: 101 | """ 102 | Get season level data for a player 103 | 104 | :param kwargs: Keyword argument to pass to 105 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 106 | """ 107 | res = self._get_data(**kwargs) 108 | data = self.parser.get_season_data(html=res.text) 109 | return data 110 | -------------------------------------------------------------------------------- /understatapi/endpoints/team.py: -------------------------------------------------------------------------------- 1 | """ Team endpoint """ 2 | from typing import Dict, Any 3 | import requests 4 | from requests.exceptions import HTTPError 5 | from .base import BaseEndpoint 6 | from ..parsers import TeamParser 7 | from ..exceptions import InvalidTeam, PrimaryAttribute 8 | 9 | 10 | class TeamEndpoint(BaseEndpoint): 11 | """ 12 | Use this class to get data from a url of the form 13 | ``https://understat.com/team//`` 14 | 15 | :Example: 16 | 17 | .. testsetup:: 18 | 19 | import requests 20 | from understatapi.endpoints import TeamEndpoint 21 | 22 | .. testcleanup:: 23 | 24 | session.close() 25 | 26 | .. doctest:: 27 | 28 | >>> session = requests.Session() 29 | >>> team_names = ["Manchester_United", "Liverpool"] 30 | >>> for team in TeamEndpoint(team_names, session=session): 31 | ... print(team.team) 32 | Manchester_United 33 | Liverpool 34 | 35 | """ 36 | 37 | parser = TeamParser() 38 | 39 | def __init__( 40 | self, team: PrimaryAttribute, session: requests.Session 41 | ) -> None: 42 | """ 43 | :param team: Name of the team(s) to get data for 44 | :param session: The current session 45 | """ 46 | self._primary_attr = team 47 | super().__init__(primary_attr=self._primary_attr, session=session) 48 | 49 | @property 50 | def team(self) -> PrimaryAttribute: 51 | """ team name """ 52 | return self._primary_attr 53 | 54 | def _get_data(self, season: str, **kwargs: str) -> requests.Response: 55 | """ 56 | Get data on a per-team basis 57 | 58 | :param season: Season to get data for 59 | :param kwargs: Keyword argument to pass to 60 | :meth:`understatapi.endpoints.base.BaseEndpoint._request_url` 61 | """ 62 | if not isinstance(self.team, str): 63 | raise TypeError("``team`` must be a string") 64 | self._check_args() 65 | url = self.base_url + "team/" + self.team + "/" + season 66 | 67 | try: 68 | response = self._request_url(url=url, **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 | return response 75 | 76 | def get_player_data(self, season: str, **kwargs: str) -> Dict[str, Any]: 77 | """ 78 | Get data for all players on a given team in a given season 79 | 80 | :param season: Season to get data for 81 | :param kwargs: Keyword argument to pass to 82 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 83 | """ 84 | res = self._get_data(season=season, **kwargs) 85 | data = self.parser.get_player_data(html=res.text) 86 | return data 87 | 88 | def get_match_data(self, season: str, **kwargs: str) -> Dict[str, Any]: 89 | """ 90 | Get data on a per match level for a given team in a given season 91 | 92 | :param season: Season to get data for 93 | :param kwargs: Keyword argument to pass to 94 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 95 | """ 96 | res = self._get_data(season=season, **kwargs) 97 | data = self.parser.get_match_data(html=res.text) 98 | return data 99 | 100 | def get_context_data( 101 | self, 102 | season: str, 103 | **kwargs: str, 104 | ) -> Dict[str, Any]: 105 | """ 106 | Get data based on different contexts in the game 107 | 108 | :param season: Season to get data for 109 | :param kwargs: Keyword argument to pass to 110 | :meth:`understatapi.endpoints.base.BaseEndpoint._get_response` 111 | """ 112 | res = self._get_data(season=season, **kwargs) 113 | data = self.parser.get_context_data(html=res.text) 114 | return data 115 | -------------------------------------------------------------------------------- /understatapi/exceptions.py: -------------------------------------------------------------------------------- 1 | """ Define custom exceptions """ 2 | from typing import Union, List 3 | 4 | PrimaryAttribute = Union[List[str], str] 5 | 6 | 7 | class InvalidSeason(Exception): 8 | """ Invalid season """ 9 | 10 | def __init__(self, message: str, season: str) -> None: 11 | super().__init__(message) 12 | self.season = season 13 | 14 | 15 | class InvalidPlayer(Exception): 16 | """ Invalid player """ 17 | 18 | def __init__(self, message: str, player: str) -> None: 19 | super().__init__(message) 20 | self.player = player 21 | 22 | 23 | class InvalidLeague(Exception): 24 | """ Invalid league """ 25 | 26 | def __init__(self, message: str, league: str) -> None: 27 | super().__init__(message) 28 | self.league = league 29 | 30 | 31 | class InvalidTeam(Exception): 32 | """ Invalid team """ 33 | 34 | def __init__(self, message: str, team: str) -> None: 35 | super().__init__(message) 36 | self.team = team 37 | 38 | 39 | class InvalidMatch(Exception): 40 | """ Invalid match """ 41 | 42 | def __init__(self, message: str, match: str) -> None: 43 | super().__init__(message) 44 | self.match = match 45 | -------------------------------------------------------------------------------- /understatapi/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | """Parsers for extracting data from html""" 2 | from .base import BaseParser 3 | from .league import LeagueParser 4 | from .match import MatchParser 5 | from .player import PlayerParser 6 | from .team import TeamParser 7 | -------------------------------------------------------------------------------- /understatapi/parsers/base.py: -------------------------------------------------------------------------------- 1 | """Base html parser""" 2 | from typing import List, Any, Dict 3 | import json 4 | 5 | 6 | class BaseParser: 7 | """Parse a html document and extract relevant data""" 8 | 9 | queries: List[str] 10 | 11 | # def __init__(self, html: str): 12 | # self.html = html 13 | 14 | @staticmethod 15 | def parse(html: str, query: str = "teamsData") -> Dict[str, Any]: 16 | """ 17 | Finds a JSON in the HTML according to a query, and returns the 18 | object corresponding to this JSON. 19 | 20 | :param html: A html document 21 | :param query: A sub-string to look for in the html document 22 | """ 23 | query_index = html.find(query) 24 | # get the start and end of the JSON data string 25 | start = html.find("(", query_index) + 2 26 | end = html.find(")", start) - 1 27 | json_data = html[start:end] 28 | # Clean up the json and return the data 29 | json_data = json_data.encode("utf8").decode("unicode_escape") 30 | data = json.loads(json_data) 31 | return data 32 | -------------------------------------------------------------------------------- /understatapi/parsers/league.py: -------------------------------------------------------------------------------- 1 | """ League parser """ 2 | from typing import Dict, Any 3 | from .base import BaseParser 4 | 5 | 6 | class LeagueParser(BaseParser): 7 | """ 8 | Parse a html page from a url of the form 9 | ``https://understat.com/league//`` 10 | """ 11 | 12 | def get_team_data(self, html: str) -> Dict[str, Any]: 13 | """ 14 | Get data for all teams 15 | 16 | :param html: The html string to parse 17 | """ 18 | return self.parse(html=html, query="teamsData") 19 | 20 | def get_match_data(self, html: str) -> Dict[str, Any]: 21 | """ 22 | Get data for all fixtures 23 | 24 | :param html: The html string to parse 25 | """ 26 | return self.parse(html=html, query="datesData") 27 | 28 | def get_player_data(self, html: str) -> Dict[str, Any]: 29 | """ 30 | Get data for all players 31 | 32 | :param html: The html string to parse 33 | """ 34 | return self.parse(html=html, query="playersData") 35 | -------------------------------------------------------------------------------- /understatapi/parsers/match.py: -------------------------------------------------------------------------------- 1 | """ Match parser """ 2 | from typing import Dict, Any 3 | from .base import BaseParser 4 | 5 | 6 | class MatchParser(BaseParser): 7 | """ 8 | Parse a html page from a url of the form 9 | ``https://understat.com/match/`` 10 | """ 11 | 12 | def get_shot_data(self, html: str) -> Dict[str, Any]: 13 | """ 14 | Get shot level data for a match 15 | 16 | :param html: The html string to parse 17 | """ 18 | return self.parse(html=html, query="shotsData") 19 | 20 | def get_roster_data(self, html: str) -> Dict[str, Any]: 21 | """ 22 | Get data about the roster for each team 23 | 24 | :param html: The html string to parse 25 | """ 26 | return self.parse(html=html, query="rostersData") 27 | 28 | def get_match_info(self, html: str) -> Dict[str, Any]: 29 | """ 30 | Get information about the match 31 | 32 | :param html: The html string to parse 33 | """ 34 | return self.parse(html=html, query="match_info") 35 | -------------------------------------------------------------------------------- /understatapi/parsers/player.py: -------------------------------------------------------------------------------- 1 | """ Player parser """ 2 | from typing import Dict, Any 3 | from .base import BaseParser 4 | 5 | 6 | class PlayerParser(BaseParser): 7 | """ 8 | Parse a html page from a url of the form 9 | ``https://understat.com/player/`` 10 | """ 11 | 12 | def get_match_data(self, html: str) -> Dict[str, Any]: 13 | """ 14 | Get match level data for a player 15 | 16 | :param html: The html string to parse 17 | """ 18 | return self.parse(html=html, query="matchesData") 19 | 20 | def get_shot_data(self, html: str) -> Dict[str, Any]: 21 | """ 22 | Get shot level data for a player 23 | 24 | :param html: The html string to parse 25 | """ 26 | return self.parse(html=html, query="shotsData") 27 | 28 | def get_season_data(self, html: str) -> Dict[str, Any]: 29 | """ 30 | Get season level data for a player 31 | 32 | :param html: The html string to parse 33 | """ 34 | return self.parse(html=html, query="groupsData") 35 | -------------------------------------------------------------------------------- /understatapi/parsers/team.py: -------------------------------------------------------------------------------- 1 | """ Team parser """ 2 | from typing import Dict, Any 3 | from .base import BaseParser 4 | 5 | 6 | class TeamParser(BaseParser): 7 | """ 8 | Parse a html page from a url of the form 9 | ``https://understat.com/team//`` 10 | """ 11 | 12 | def get_player_data(self, html: str) -> Dict[str, Any]: 13 | """ 14 | Get data on a per-team basis 15 | 16 | :param html: The html string to parse 17 | """ 18 | return self.parse(html=html, query="playersData") 19 | 20 | def get_match_data(self, html: str) -> Dict[str, Any]: 21 | """ 22 | Get data on a per match level for a given team in a given season 23 | 24 | :param html: The html string to parse 25 | """ 26 | return self.parse(html=html, query="datesData") 27 | 28 | def get_context_data(self, html: str) -> Dict[str, Any]: 29 | """ 30 | Get data based on different contexts in the game 31 | 32 | :param html: The html string to parse 33 | """ 34 | return self.parse(html=html, query="statisticsData") 35 | -------------------------------------------------------------------------------- /understatapi/utils.py: -------------------------------------------------------------------------------- 1 | """ Helper functions for formatting data """ 2 | import sys 3 | from typing import List 4 | import inspect 5 | import re 6 | 7 | 8 | def get_all_methods(cls: type) -> List[str]: 9 | """ 10 | Get the names of all methods in a class, excluding 11 | methods decorated with ``@property``, ``@classmethod``, etc 12 | 13 | :param cls: The class to get the methods for 14 | :return: A list of the names of the methods 15 | """ 16 | return [meth[0] for meth in inspect.getmembers(cls, inspect.isfunction)] 17 | 18 | 19 | def get_public_methods(cls: type) -> List[str]: 20 | """ 21 | Get the names of all public methods in a class 22 | 23 | :param cls: The class to get all public methods for 24 | :return: A list of the names of the public methods 25 | """ 26 | methods = get_all_methods(cls) 27 | methods = [meth for meth in methods if not meth.startswith("_")] 28 | return methods 29 | 30 | 31 | def find_endpoints(line: str) -> List[str]: 32 | """ 33 | Find the name of a subclass of 34 | ``~understatapi.endpoints.base.BaseEndpoint`` in a string 35 | 36 | :param line: The string in which to search for the name of a 37 | ``~understatapi.endpoints.base.BaseEndpoint`` object 38 | """ 39 | match = re.findall(r"\w+Endpoint", line) 40 | if match is None: 41 | return [] # pragma: no cover 42 | return match 43 | 44 | 45 | def str_to_class(modulename: str, classname: str) -> type: 46 | """ 47 | Get a class by using its name 48 | """ 49 | return getattr(sys.modules[modulename], classname) 50 | --------------------------------------------------------------------------------