vlrdevapi 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CHANGELOG.md +87 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/PKG-INFO +2 -2
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/pyproject.toml +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/scripts/download_fixtures.py +149 -4
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/__init__.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/models.py +4 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/parser.py +47 -3
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/common.py +30 -6
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/_utils.py +56 -2
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/namespace.py +20 -7
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/parser.py +7 -2
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/models.py +29 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/namespace.py +3 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/parser.py +120 -46
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/namespace.py +17 -5
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/parser.py +6 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/__init__.py +14 -0
- vlrdevapi-2.4.0/src/vlrdevapi/commons/regions.py +242 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/uv.lock +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.gitattributes +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/cleanup-caches.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/docs-check.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/publish.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/test.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.gitignore +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.python-version +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.readthedocs.yaml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CODE_OF_CONDUCT.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CONTRIBUTING.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/LICENCE +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/README.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/SECURITY.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/SUPPORT.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/scripts/check_mdx_examples.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/__init__.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_base.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_cache.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_client.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/_common.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/common.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/_common_parsing.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/match_namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/match_namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/parser.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/pagination.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/paths.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/team_enrichment.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/team_parsing.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/countries.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/datetime.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/mappings.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/prizes.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/timezone.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/exceptions.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/fetcher.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/py.typed +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/validators.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/wrangler.jsonc +0 -0
|
@@ -5,6 +5,92 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [2.4.0] - 2026-08-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Bo1 series support** — single-map series now resolve games from
|
|
13
|
+
`.vm-stats-game` containers when nav tabs are absent. `resolve_game_id`
|
|
14
|
+
raises `ParsingError` instead of silently returning wrong data on
|
|
15
|
+
out-of-range or empty game lists.
|
|
16
|
+
- **Event region resolution** — `_parse_breadcrumb` resolves `subregion=`
|
|
17
|
+
breadcrumb links (e.g. "Japan", "North America") to their parent region
|
|
18
|
+
via `commons.regions.resolve_subregion_to_region`. A new
|
|
19
|
+
`EventRegion.subregion` field preserves the original subregion name
|
|
20
|
+
(e.g. `"Japan"`); `None` for `region=` links.
|
|
21
|
+
- **Event country-to-region fallback** — when no breadcrumb region is
|
|
22
|
+
present, the `region_location` country flag is resolved via
|
|
23
|
+
`resolve_country_to_region` to infer the parent region.
|
|
24
|
+
- **Event region deduplication** — duplicate region entries (from both
|
|
25
|
+
`region=` and `subregion=` breadcrumbs for the same parent region)
|
|
26
|
+
are collapsed, preferring the entry that carries a subregion name.
|
|
27
|
+
- **`commons.regions` module** — new shared utilities for region
|
|
28
|
+
resolution: `VALID_REGIONS`, `SUBREGION_TO_REGION` (17 VCL/GC
|
|
29
|
+
subregions), `COUNTRY_TO_REGION` (~100+ countries),
|
|
30
|
+
`resolve_subregion_to_region`, and `resolve_country_to_region`.
|
|
31
|
+
- **`commons.__init__` re-exports** — `resolve_subregion_to_region`,
|
|
32
|
+
`resolve_country_to_region`, `VALID_REGIONS`, `SUBREGION_TO_REGION`,
|
|
33
|
+
and `COUNTRY_TO_REGION` are now re-exported from `vlrdevapi.commons`.
|
|
34
|
+
- **Series forfeit detection** — new `ForfeitInfo` model on
|
|
35
|
+
`SeriesInfo.forfeit` exposes `forfeited`, `team`, `team_id`, and
|
|
36
|
+
`reason` fields. Parser detects `"forfeited by TEAM"` in the
|
|
37
|
+
`match-header-vs-note` element and maps the team name to the
|
|
38
|
+
corresponding `team1`/`team2` for ID resolution. Note-level
|
|
39
|
+
forfeit text (e.g. "Blind Esports Forfeit Map 2") is also parsed.
|
|
40
|
+
- **Series match notes** — new `SeriesInfo.notes` list captures
|
|
41
|
+
non-veto `match-header-note` text such as technical pause warnings
|
|
42
|
+
and lobby remake notices.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **Series economy enrichment** — team matching now tries abbreviation
|
|
47
|
+
names from `.ovw-player-tag` elements first (e.g. "SEN"), then falls
|
|
48
|
+
back to full team names. This fixes mismatches where the economy tab
|
|
49
|
+
shows a different abbreviation than the overview header.
|
|
50
|
+
- **Series rounds enrichment** — same abbreviation-first matching
|
|
51
|
+
strategy as economy.
|
|
52
|
+
- **Series info veto parsing** — Bo1 veto fallback now handles the
|
|
53
|
+
"bans + decider" pattern (e.g. `"Ban1 remains"`) in addition to
|
|
54
|
+
pick/ban. The PICK badge is excluded from map names via
|
|
55
|
+
`span.text(deep=False)`.
|
|
56
|
+
- **Series info notes parsing** — the parser now iterates all
|
|
57
|
+
`match-header-note` elements instead of only the first. This
|
|
58
|
+
correctly separates forfeit reasons, informational notes, and
|
|
59
|
+
map veto text when multiple notes are present.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **Economy bank/spend parsing** — `int()` calls on `.rnd-sq` `title`
|
|
64
|
+
attributes and `.rnd-num` text are wrapped in `try/except ValueError`;
|
|
65
|
+
malformed values default to `0` instead of crashing the parser.
|
|
66
|
+
- **Event region from location country** — events that only have a
|
|
67
|
+
"Location" label with a flag (no separate "Region" label) now resolve
|
|
68
|
+
the region from `location.country` via `resolve_country_to_region`.
|
|
69
|
+
Previously the country-to-region fallback only checked `region_location`.
|
|
70
|
+
- **Phantom pagination in matches** — terminal pages no longer report
|
|
71
|
+
`has_next_page = True` (from DaviAlcanfor PR #50).
|
|
72
|
+
|
|
73
|
+
### Documentation
|
|
74
|
+
|
|
75
|
+
- **Event info reference** — `EventRegion` fields table now documents
|
|
76
|
+
the `subregion` field. Region example demonstrates subregion access.
|
|
77
|
+
`regions` field description explains deduplication and International
|
|
78
|
+
collapse.
|
|
79
|
+
- **Series info reference** — `ForfeitInfo` fields table documents the
|
|
80
|
+
new `forfeit` nested model. `notes` field description explains the
|
|
81
|
+
distinction between forfeit reasons and informational notes.
|
|
82
|
+
- **Series economy reference** — docstrings reference correct model
|
|
83
|
+
types (`RoundEconomyData`, `bank_team1`).
|
|
84
|
+
- **Series rounds reference** — docstrings reference correct model
|
|
85
|
+
types (`RoundData`, `winner_team_name`).
|
|
86
|
+
- **`commons.regions` module** — docstrings describe all mapping
|
|
87
|
+
dictionaries and resolution functions.
|
|
88
|
+
- **Project structure** — contributing guide updated with actual
|
|
89
|
+
underscore-prefixed directory names and new modules (`_news/`,
|
|
90
|
+
`_utils/`, `commons/regions.py`).
|
|
91
|
+
- **Module docstrings** — added to `economy/parser.py`,
|
|
92
|
+
`rounds/parser.py`, and `commons/__init__.py`.
|
|
93
|
+
|
|
8
94
|
## [2.3.0] - 2026-08-11
|
|
9
95
|
|
|
10
96
|
### Added
|
|
@@ -193,6 +279,7 @@ The API surface, module structure, and type system are all new.
|
|
|
193
279
|
- `aiohttp` and `asyncio` dependencies.
|
|
194
280
|
- v1.x match/event/team parsing modules.
|
|
195
281
|
|
|
282
|
+
[2.4.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.3.0...v2.4.0
|
|
196
283
|
[2.3.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.2.0...v2.3.0
|
|
197
284
|
[2.2.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.1.0...v2.2.0
|
|
198
285
|
[2.1.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.0.1...v2.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: vlrdevapi
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: A Python library to scrape data from vlr.gg
|
|
5
5
|
Project-URL: Homepage, https://vlrdevapi.pages.dev/
|
|
6
6
|
Project-URL: Repository, https://github.com/Vanshbordia/vlrdevapi
|
|
@@ -33,8 +33,7 @@ ROUTES += [
|
|
|
33
33
|
ROUTES += [
|
|
34
34
|
("/news", "news/news.html", {}),
|
|
35
35
|
("/news/?page=2", "news/news_page2.html", {}),
|
|
36
|
-
("/news/?page=
|
|
37
|
-
("/news/?page=176", "news/news_page176.html", {}),
|
|
36
|
+
("/news/?page=300", "news/news_page300.html", {}),
|
|
38
37
|
("/734100", "news/article_734100.html", {}),
|
|
39
38
|
("/720033/americas-recent-victors-start-strong-in-week-1", "news/article_720033.html", {}),
|
|
40
39
|
]
|
|
@@ -110,6 +109,9 @@ ADDITIONAL_EVENTS = [
|
|
|
110
109
|
(2949, "gameon-productivity-and-technology-tournament-2026"),
|
|
111
110
|
(2847, "challengers-2026-japan-split-1"),
|
|
112
111
|
(58, "100t-x-cashapp-gamers-for-equality"),
|
|
112
|
+
(2763, "tesfed-turkish-cup-2025"),
|
|
113
|
+
(2544, "morocco-gaming-expo-2025"),
|
|
114
|
+
(3074, "raidiant-academy-2026"),
|
|
113
115
|
]
|
|
114
116
|
|
|
115
117
|
for _eid, _eslug in ADDITIONAL_EVENTS:
|
|
@@ -128,8 +130,8 @@ ROUTES += [
|
|
|
128
130
|
|
|
129
131
|
# ── Event standings pages (for standings filtering tests) ──────
|
|
130
132
|
ROUTES += [
|
|
131
|
-
("/event/2283", "event/
|
|
132
|
-
("/event/2682", "event/
|
|
133
|
+
("/event/2283/valorant-champions-2025", "event/2283_valorant-champions-2025/overview.html", {}),
|
|
134
|
+
("/event/2682/vct-2026-americas-kickoff", "event/2682_vct-2026-americas-kickoff/overview.html", {}),
|
|
133
135
|
]
|
|
134
136
|
|
|
135
137
|
# ── Event matches pages ─────────────────────────────────────────
|
|
@@ -321,10 +323,153 @@ ROUTES += [
|
|
|
321
323
|
]
|
|
322
324
|
|
|
323
325
|
|
|
326
|
+
# ── Series 704037 Bo1 (single-map game, no nav tabs, no veto) ─
|
|
327
|
+
SERIES_ID_BO1 = 704037
|
|
328
|
+
SERIES_SLUG_BO1 = "mushoku-vs-fallen-angels-game-changers-2026-emea-stage-2-promotion-relegation-r1"
|
|
329
|
+
GAME_ID_BO1 = 274641
|
|
330
|
+
|
|
331
|
+
ROUTES += [
|
|
332
|
+
(
|
|
333
|
+
f"/{SERIES_ID_BO1}/{SERIES_SLUG_BO1}",
|
|
334
|
+
f"series/{SERIES_ID_BO1}/overview.html",
|
|
335
|
+
{},
|
|
336
|
+
),
|
|
337
|
+
(
|
|
338
|
+
f"/{SERIES_ID_BO1}?game={GAME_ID_BO1}&tab=overview",
|
|
339
|
+
f"series/{SERIES_ID_BO1}/game_{GAME_ID_BO1}_overview.html",
|
|
340
|
+
{},
|
|
341
|
+
),
|
|
342
|
+
(
|
|
343
|
+
f"/{SERIES_ID_BO1}?game={GAME_ID_BO1}&tab=performance",
|
|
344
|
+
f"series/{SERIES_ID_BO1}/game_{GAME_ID_BO1}_performance.html",
|
|
345
|
+
{},
|
|
346
|
+
),
|
|
347
|
+
(
|
|
348
|
+
f"/{SERIES_ID_BO1}?game={GAME_ID_BO1}&tab=economy",
|
|
349
|
+
f"series/{SERIES_ID_BO1}/game_{GAME_ID_BO1}_economy.html",
|
|
350
|
+
{},
|
|
351
|
+
),
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
# ── Series 64819 Bo1 (single-map, bans + decider veto) ────────
|
|
355
|
+
SERIES_ID_BO1_VETO = 64819
|
|
356
|
+
GAME_ID_BO1_VETO = 65212
|
|
357
|
+
|
|
358
|
+
ROUTES += [
|
|
359
|
+
(
|
|
360
|
+
f"/{SERIES_ID_BO1_VETO}",
|
|
361
|
+
f"series/{SERIES_ID_BO1_VETO}/overview.html",
|
|
362
|
+
{},
|
|
363
|
+
),
|
|
364
|
+
(
|
|
365
|
+
f"/{SERIES_ID_BO1_VETO}?game={GAME_ID_BO1_VETO}&tab=overview",
|
|
366
|
+
f"series/{SERIES_ID_BO1_VETO}/game_{GAME_ID_BO1_VETO}_overview.html",
|
|
367
|
+
{},
|
|
368
|
+
),
|
|
369
|
+
(
|
|
370
|
+
f"/{SERIES_ID_BO1_VETO}?game={GAME_ID_BO1_VETO}&tab=performance",
|
|
371
|
+
f"series/{SERIES_ID_BO1_VETO}/game_{GAME_ID_BO1_VETO}_performance.html",
|
|
372
|
+
{},
|
|
373
|
+
),
|
|
374
|
+
(
|
|
375
|
+
f"/{SERIES_ID_BO1_VETO}?game={GAME_ID_BO1_VETO}&tab=economy",
|
|
376
|
+
f"series/{SERIES_ID_BO1_VETO}/game_{GAME_ID_BO1_VETO}_economy.html",
|
|
377
|
+
{},
|
|
378
|
+
),
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
# ── Series 30788 Bo1 (single-map, pick veto) ──────────────────
|
|
382
|
+
SERIES_ID_BO1_PICK = 30788
|
|
383
|
+
GAME_ID_BO1_PICK = 47971
|
|
384
|
+
|
|
385
|
+
ROUTES += [
|
|
386
|
+
(
|
|
387
|
+
f"/{SERIES_ID_BO1_PICK}",
|
|
388
|
+
f"series/{SERIES_ID_BO1_PICK}/overview.html",
|
|
389
|
+
{},
|
|
390
|
+
),
|
|
391
|
+
(
|
|
392
|
+
f"/{SERIES_ID_BO1_PICK}?game={GAME_ID_BO1_PICK}&tab=overview",
|
|
393
|
+
f"series/{SERIES_ID_BO1_PICK}/game_{GAME_ID_BO1_PICK}_overview.html",
|
|
394
|
+
{},
|
|
395
|
+
),
|
|
396
|
+
(
|
|
397
|
+
f"/{SERIES_ID_BO1_PICK}?game={GAME_ID_BO1_PICK}&tab=performance",
|
|
398
|
+
f"series/{SERIES_ID_BO1_PICK}/game_{GAME_ID_BO1_PICK}_performance.html",
|
|
399
|
+
{},
|
|
400
|
+
),
|
|
401
|
+
(
|
|
402
|
+
f"/{SERIES_ID_BO1_PICK}?game={GAME_ID_BO1_PICK}&tab=economy",
|
|
403
|
+
f"series/{SERIES_ID_BO1_PICK}/game_{GAME_ID_BO1_PICK}_economy.html",
|
|
404
|
+
{},
|
|
405
|
+
),
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
# ── Series: forfeit and notes parsing ───────────────────────
|
|
409
|
+
SERIES_FORFEIT_NOTES = [
|
|
410
|
+
(477343, "shopify-rebellion-gold-vs-yfp-x-game-changers-2025-north-america-stage-1-r3"),
|
|
411
|
+
(67357, "t1-vs-tsm-champions-tour-north-america-stage-1-challengers-lr4"),
|
|
412
|
+
(155313, "blind-esports-vs-onlyfriends-nsg-yuddha-phase-2-all-india-championship-playoffs-lr1"),
|
|
413
|
+
(459856, "team-liquid-vs-karmine-corp-champions-tour-2025-emea-stage-1-w5"),
|
|
414
|
+
(715117, "shopify-rebellion-black-vs-2game-esports-vcl-26-americas-last-chance-qualifier-main-event-gf"),
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
for _sid, _sslug in SERIES_FORFEIT_NOTES:
|
|
418
|
+
ROUTES.append((f"/{_sid}/{_sslug}", f"series/{_sid}/overview.html", {}))
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _parse_last_page(html: str) -> int | None:
|
|
422
|
+
"""Parse the highest page number from vlr.gg pagination controls."""
|
|
423
|
+
from selectolax.parser import HTMLParser
|
|
424
|
+
|
|
425
|
+
doc = HTMLParser(html)
|
|
426
|
+
pagination = doc.css_first("div.action-container-pages")
|
|
427
|
+
if pagination is None:
|
|
428
|
+
return None
|
|
429
|
+
max_page = 0
|
|
430
|
+
for el in pagination.css("a.btn.mod-page, span.btn.mod-page"):
|
|
431
|
+
try:
|
|
432
|
+
num = int(el.text(strip=True))
|
|
433
|
+
if num > max_page:
|
|
434
|
+
max_page = num
|
|
435
|
+
except ValueError:
|
|
436
|
+
continue
|
|
437
|
+
return max_page if max_page > 0 else None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
async def _download_news_last_page(client: httpx.AsyncClient) -> None:
|
|
441
|
+
"""Dynamically determine and download the last news page."""
|
|
442
|
+
out = FIXTURES_DIR / "news" / "news_last_page.html"
|
|
443
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
if out.exists():
|
|
445
|
+
print("SKIP (exists) news/news_last_page.html")
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
print("GET /news (to find last page number)")
|
|
449
|
+
try:
|
|
450
|
+
resp = await client.get("/news")
|
|
451
|
+
resp.raise_for_status()
|
|
452
|
+
last_page = _parse_last_page(resp.text)
|
|
453
|
+
if last_page is None:
|
|
454
|
+
print(" WARNING: could not parse last page number")
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
last_path = f"/news/?page={last_page}"
|
|
458
|
+
print(f"GET {BASE_URL}{last_path}")
|
|
459
|
+
resp2 = await client.get(last_path)
|
|
460
|
+
resp2.raise_for_status()
|
|
461
|
+
out.write_text(resp2.text, encoding="utf-8")
|
|
462
|
+
print(f" -> {out.relative_to(FIXTURES_DIR.parent)}")
|
|
463
|
+
except (httpx.HTTPStatusError, httpx.RequestError) as e:
|
|
464
|
+
print(f" ERROR {e}")
|
|
465
|
+
|
|
466
|
+
|
|
324
467
|
async def download_all():
|
|
325
468
|
async with httpx.AsyncClient(
|
|
326
469
|
base_url=BASE_URL, headers=HEADERS, follow_redirects=True, timeout=30
|
|
327
470
|
) as client:
|
|
471
|
+
await _download_news_last_page(client)
|
|
472
|
+
|
|
328
473
|
total = len(ROUTES)
|
|
329
474
|
for i, (path, filename, cookies) in enumerate(ROUTES, 1):
|
|
330
475
|
out = FIXTURES_DIR / filename
|
|
@@ -63,6 +63,10 @@ class EventRegion(BaseModel):
|
|
|
63
63
|
default="",
|
|
64
64
|
description="Region name (e.g., 'EMEA', 'Pacific', 'Americas', 'China', 'International')",
|
|
65
65
|
)
|
|
66
|
+
subregion: str | None = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description="Subregion name if parsed from a subregion= breadcrumb (e.g., 'Japan', 'North America'); None for region= links",
|
|
69
|
+
)
|
|
66
70
|
href: str = Field(default="", description="Relative path (e.g., '/vct/?region=27')")
|
|
67
71
|
|
|
68
72
|
|
|
@@ -16,12 +16,25 @@ from vlrdevapi._event.info.models import (
|
|
|
16
16
|
)
|
|
17
17
|
from vlrdevapi.commons.countries import get_country_name
|
|
18
18
|
from vlrdevapi.commons.prizes import parse_prize_amount
|
|
19
|
+
from vlrdevapi.commons.regions import resolve_country_to_region, resolve_subregion_to_region
|
|
19
20
|
import contextlib
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
|
|
23
24
|
"""Parse event info from the event page HTML.
|
|
24
25
|
|
|
26
|
+
After basic parsing the region list is post-processed:
|
|
27
|
+
|
|
28
|
+
1. Duplicates (same region name from ``region=`` and ``subregion=``
|
|
29
|
+
breadcrumbs) are collapsed; the entry carrying a ``subregion``
|
|
30
|
+
value is kept.
|
|
31
|
+
2. Two or more distinct regions collapse to a single
|
|
32
|
+
``EventRegion(name="International")``.
|
|
33
|
+
3. When no breadcrumb region was found, the ``region_location``
|
|
34
|
+
country is tried first; if absent, ``location.country`` is
|
|
35
|
+
tried. Either is resolved via ``resolve_country_to_region``
|
|
36
|
+
to infer the parent region.
|
|
37
|
+
|
|
25
38
|
Args:
|
|
26
39
|
html: Parsed HTML from the event page.
|
|
27
40
|
event_id: Unique event identifier on vlr.gg.
|
|
@@ -42,6 +55,28 @@ def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
|
|
|
42
55
|
_parse_title(header, event)
|
|
43
56
|
_parse_desc_items(header, event)
|
|
44
57
|
|
|
58
|
+
seen: dict[str, EventRegion] = {}
|
|
59
|
+
for r in event.regions:
|
|
60
|
+
if r.name not in seen:
|
|
61
|
+
seen[r.name] = r
|
|
62
|
+
elif r.subregion and not seen[r.name].subregion:
|
|
63
|
+
seen[r.name] = r
|
|
64
|
+
event.regions = list(seen.values())
|
|
65
|
+
|
|
66
|
+
if len(event.regions) > 1:
|
|
67
|
+
event.regions = [EventRegion(name="International")]
|
|
68
|
+
|
|
69
|
+
if not event.regions:
|
|
70
|
+
country = None
|
|
71
|
+
if event.region_location and event.region_location.country:
|
|
72
|
+
country = event.region_location.country
|
|
73
|
+
elif event.location and event.location.country:
|
|
74
|
+
country = event.location.country
|
|
75
|
+
if country:
|
|
76
|
+
parent = resolve_country_to_region(country)
|
|
77
|
+
if parent:
|
|
78
|
+
event.regions = [EventRegion(name=parent)]
|
|
79
|
+
|
|
45
80
|
return event
|
|
46
81
|
|
|
47
82
|
|
|
@@ -62,7 +97,14 @@ def _parse_image(header: Node, event: EventInfo) -> None:
|
|
|
62
97
|
|
|
63
98
|
|
|
64
99
|
def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
|
|
65
|
-
"""Parse breadcrumb links (series, stage, regions) from the header.
|
|
100
|
+
"""Parse breadcrumb links (series, stage, regions/subregions) from the header.
|
|
101
|
+
|
|
102
|
+
Region resolution priority:
|
|
103
|
+
|
|
104
|
+
1. ``region=`` links are stored directly.
|
|
105
|
+
2. ``subregion=`` links are resolved to their parent region via
|
|
106
|
+
``resolve_subregion_to_region``; the original subregion name is
|
|
107
|
+
preserved in ``EventRegion.subregion``.
|
|
66
108
|
|
|
67
109
|
Args:
|
|
68
110
|
header: The event-header root node.
|
|
@@ -93,10 +135,12 @@ def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
|
|
|
93
135
|
|
|
94
136
|
if "stage=" in href:
|
|
95
137
|
event.stage = EventStageTag(name=name, href=href)
|
|
138
|
+
elif "subregion=" in href:
|
|
139
|
+
parent = resolve_subregion_to_region(name)
|
|
140
|
+
if parent:
|
|
141
|
+
event.regions.append(EventRegion(name=parent, subregion=name, href=href))
|
|
96
142
|
elif "region=" in href:
|
|
97
143
|
event.regions.append(EventRegion(name=name, href=href))
|
|
98
|
-
else:
|
|
99
|
-
event.regions.append(EventRegion(name=name, href=href))
|
|
100
144
|
|
|
101
145
|
|
|
102
146
|
def _parse_title(header: Node, event: EventInfo) -> None:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Shared utilities for match enrichment and parsing across live/upcoming/completed modules."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import logging
|
|
5
|
+
import re
|
|
4
6
|
from collections.abc import Callable
|
|
5
7
|
from datetime import date, datetime, tzinfo
|
|
6
8
|
from typing import Any, Protocol
|
|
@@ -55,20 +57,42 @@ def parse_date_header(text: str) -> date | None:
|
|
|
55
57
|
|
|
56
58
|
|
|
57
59
|
def check_pagination(html: HTMLParser) -> bool:
|
|
58
|
-
"""Check whether
|
|
60
|
+
"""Check whether a page after the current one exists.
|
|
61
|
+
|
|
62
|
+
The active page is rendered as a ``mod-active`` span while other pages
|
|
63
|
+
are anchor links. A terminal page still links back to earlier pages, so
|
|
64
|
+
the mere presence of page links does not imply a next page. Compare the
|
|
65
|
+
active page number against the highest page number offered instead.
|
|
59
66
|
|
|
60
67
|
Args:
|
|
61
68
|
html: Parsed HTML document.
|
|
62
69
|
|
|
63
70
|
Returns:
|
|
64
|
-
``True`` if
|
|
71
|
+
``True`` if a page after the active one exists, ``False`` otherwise.
|
|
65
72
|
|
|
66
73
|
"""
|
|
67
74
|
pagination_el = html.css_first("div.action-container-pages")
|
|
68
|
-
if pagination_el:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
if pagination_el is None:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
current_page = 1
|
|
79
|
+
active = pagination_el.css_first(".btn.mod-page.mod-active")
|
|
80
|
+
if active:
|
|
81
|
+
with contextlib.suppress(ValueError):
|
|
82
|
+
current_page = int(active.text(strip=True))
|
|
83
|
+
|
|
84
|
+
max_page = current_page
|
|
85
|
+
for btn in pagination_el.css(".btn.mod-page"):
|
|
86
|
+
text = btn.text(strip=True)
|
|
87
|
+
try:
|
|
88
|
+
max_page = max(max_page, int(text))
|
|
89
|
+
except ValueError:
|
|
90
|
+
href = btn.attributes.get("href") or ""
|
|
91
|
+
page_match = re.search(r"page=(\d+)", href)
|
|
92
|
+
if page_match:
|
|
93
|
+
max_page = max(max_page, int(page_match.group(1)))
|
|
94
|
+
|
|
95
|
+
return current_page < max_page
|
|
72
96
|
|
|
73
97
|
|
|
74
98
|
# ---------------------------------------------------------------------------
|
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
from selectolax.parser import HTMLParser
|
|
4
4
|
|
|
5
|
+
from vlrdevapi.exceptions import ParsingError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _extract_team_abbreviations(html: HTMLParser) -> tuple[str, str]:
|
|
9
|
+
"""Extract short team names from ``.ovw-player-tag`` in overview tables.
|
|
10
|
+
|
|
11
|
+
The series overview page renders two ``.ovw-table`` elements per
|
|
12
|
+
section (current-map and series), one for each team. The first
|
|
13
|
+
table always belongs to **team 1** and the second to **team 2**.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
html: Parsed series overview page.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
``(team1_abbreviation, team2_abbreviation)`` — empty strings when
|
|
20
|
+
the expected structure is missing.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
tables = html.css("div.ovw-table")
|
|
24
|
+
if len(tables) < 2:
|
|
25
|
+
return "", ""
|
|
26
|
+
t1_el = tables[0].css_first(".ovw-player-tag")
|
|
27
|
+
t2_el = tables[1].css_first(".ovw-player-tag")
|
|
28
|
+
return (t1_el.text(strip=True) if t1_el else ""), (t2_el.text(strip=True) if t2_el else "")
|
|
29
|
+
|
|
5
30
|
|
|
6
31
|
def _ordered_game_ids(html: HTMLParser) -> list[str]:
|
|
7
32
|
"""Return the real VLR game IDs in map order from the series nav tabs.
|
|
@@ -10,6 +35,10 @@ def _ordered_game_ids(html: HTMLParser) -> list[str]:
|
|
|
10
35
|
"All Maps" item (``mod-all``), followed by each map in play order.
|
|
11
36
|
Each item carries the real VLR ``data-game-id``.
|
|
12
37
|
|
|
38
|
+
For single-map (Bo1) series, VLR omits the nav tabs entirely.
|
|
39
|
+
In that case the IDs are discovered from the ``.vm-stats-game``
|
|
40
|
+
content containers instead.
|
|
41
|
+
|
|
13
42
|
Args:
|
|
14
43
|
html: Parsed series stats page (overview, performance, or economy).
|
|
15
44
|
|
|
@@ -24,6 +53,15 @@ def _ordered_game_ids(html: HTMLParser) -> list[str]:
|
|
|
24
53
|
gid = (el.attributes.get("data-game-id") or "").strip()
|
|
25
54
|
if gid:
|
|
26
55
|
ids.append(gid)
|
|
56
|
+
|
|
57
|
+
if ids:
|
|
58
|
+
return ids
|
|
59
|
+
|
|
60
|
+
for el in html.css(".vm-stats-game[data-game-id]"):
|
|
61
|
+
gid = (el.attributes.get("data-game-id") or "").strip()
|
|
62
|
+
if gid and gid != "all":
|
|
63
|
+
ids.append(gid)
|
|
64
|
+
|
|
27
65
|
return ids
|
|
28
66
|
|
|
29
67
|
|
|
@@ -34,9 +72,11 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
|
|
|
34
72
|
|
|
35
73
|
- ``"all"``: aggregate across the whole series (returned unchanged).
|
|
36
74
|
- A 1-based game number (e.g. ``1``): the first map played in the
|
|
37
|
-
series, resolved from the stats nav tabs.
|
|
75
|
+
series, resolved from the stats nav tabs or game containers.
|
|
38
76
|
- A real VLR game ID (e.g. ``"233478"``): passed through unchanged.
|
|
39
77
|
|
|
78
|
+
Raises ``ParsingError`` when the identifier cannot be resolved.
|
|
79
|
+
|
|
40
80
|
Args:
|
|
41
81
|
html: Parsed series stats page containing the game nav tabs.
|
|
42
82
|
game_id: User-supplied game identifier.
|
|
@@ -44,6 +84,11 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
|
|
|
44
84
|
Returns:
|
|
45
85
|
str: Real VLR game ID (or ``"all"``) to use for selectors/URLs.
|
|
46
86
|
|
|
87
|
+
Raises:
|
|
88
|
+
ParsingError: If ``game_id`` is a positional index that exceeds
|
|
89
|
+
the number of games found, or if no games can be discovered
|
|
90
|
+
at all.
|
|
91
|
+
|
|
47
92
|
"""
|
|
48
93
|
as_str = str(game_id).strip()
|
|
49
94
|
if as_str.lower() == "all":
|
|
@@ -61,7 +106,16 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
|
|
|
61
106
|
if 1 <= index <= len(real_ids):
|
|
62
107
|
return real_ids[index - 1]
|
|
63
108
|
|
|
64
|
-
|
|
109
|
+
if not real_ids:
|
|
110
|
+
raise ParsingError(
|
|
111
|
+
f"Cannot resolve game_id={game_id!r}: no games found on the page. "
|
|
112
|
+
"The series page may have an unexpected structure."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
raise ParsingError(
|
|
116
|
+
f"Cannot resolve game_id={game_id!r}: index out of range. "
|
|
117
|
+
f"Series has {len(real_ids)} game(s) (IDs: {real_ids})."
|
|
118
|
+
)
|
|
65
119
|
|
|
66
120
|
|
|
67
121
|
class PlayerMap:
|
|
@@ -7,6 +7,7 @@ import httpx
|
|
|
7
7
|
from selectolax.parser import HTMLParser
|
|
8
8
|
|
|
9
9
|
from vlrdevapi._base import SyncNamespace
|
|
10
|
+
from vlrdevapi._series._utils import _extract_team_abbreviations
|
|
10
11
|
from vlrdevapi._series.economy.models import EconomyData
|
|
11
12
|
from vlrdevapi._series.economy.parser import parse_economy_data
|
|
12
13
|
from vlrdevapi._series.info.parser import parse_series_info
|
|
@@ -46,7 +47,7 @@ class SeriesEconomyNamespace:
|
|
|
46
47
|
|
|
47
48
|
Returns:
|
|
48
49
|
EconomyData: Economy data including ``rounds`` (list of
|
|
49
|
-
``
|
|
50
|
+
``RoundEconomyData`` with purchases, remaining credits, and
|
|
50
51
|
team spending per round), ``team1`` and ``team2`` names,
|
|
51
52
|
and enriched team IDs.
|
|
52
53
|
|
|
@@ -60,8 +61,8 @@ class SeriesEconomyNamespace:
|
|
|
60
61
|
|
|
61
62
|
Examples:
|
|
62
63
|
>>> econ = vlrdevapi.series.economy(series_id=12345, game_id=1)
|
|
63
|
-
>>> econ.rounds[0].
|
|
64
|
-
24000
|
|
64
|
+
>>> econ.rounds[0].bank_team1
|
|
65
|
+
24000.0
|
|
65
66
|
>>> econ.rounds[0].winner.name
|
|
66
67
|
'FNATIC'
|
|
67
68
|
|
|
@@ -77,20 +78,32 @@ class SeriesEconomyNamespace:
|
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
def _enrich_economy(result: EconomyData, html_series: HTMLParser) -> EconomyData:
|
|
80
|
-
"""Enrich economy data with team IDs from the series info.
|
|
81
|
+
"""Enrich economy data with team IDs and round winners from the series info.
|
|
82
|
+
|
|
83
|
+
Team matching first tries abbreviation-level names extracted from
|
|
84
|
+
``.ovw-player-tag`` elements (e.g. "SEN"), then falls back to full
|
|
85
|
+
team names from the series header. Round winners are resolved from
|
|
86
|
+
the stored ``_temp_winner`` tag set during parsing.
|
|
81
87
|
|
|
82
88
|
Args:
|
|
83
89
|
result: The EconomyData to enrich.
|
|
84
90
|
html_series: HTML of the series page for team info parsing.
|
|
85
91
|
|
|
86
92
|
Returns:
|
|
87
|
-
EconomyData: Enriched economy data with team IDs.
|
|
93
|
+
EconomyData: Enriched economy data with team IDs and round winners.
|
|
88
94
|
|
|
89
95
|
"""
|
|
90
96
|
series_info = parse_series_info(html_series)
|
|
91
|
-
|
|
97
|
+
team1_abbr, team2_abbr = _extract_team_abbreviations(html_series)
|
|
98
|
+
|
|
99
|
+
if team1_abbr == result.team1:
|
|
100
|
+
result.team1_id = series_info.team1.id
|
|
101
|
+
elif result.team1 and series_info.team1.name == result.team1:
|
|
92
102
|
result.team1_id = series_info.team1.id
|
|
93
|
-
|
|
103
|
+
|
|
104
|
+
if team2_abbr == result.team2:
|
|
105
|
+
result.team2_id = series_info.team2.id
|
|
106
|
+
elif result.team2 and series_info.team2.name == result.team2:
|
|
94
107
|
result.team2_id = series_info.team2.id
|
|
95
108
|
|
|
96
109
|
for round_data in result.rounds:
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Series economy parser — extracts per-round bank and spend data."""
|
|
2
|
+
|
|
1
3
|
from selectolax.parser import HTMLParser
|
|
2
4
|
|
|
3
5
|
from vlrdevapi._series._utils import resolve_game_id
|
|
@@ -101,8 +103,11 @@ def parse_economy_data(html: HTMLParser, game_id: int | str = "all") -> EconomyD
|
|
|
101
103
|
sq1 = rnd_sqs[0] # team1
|
|
102
104
|
sq2 = rnd_sqs[1] # team2
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
try:
|
|
107
|
+
spent_team1 = int(sq1.attributes.get("title") or "0")
|
|
108
|
+
spent_team2 = int(sq2.attributes.get("title") or "0")
|
|
109
|
+
except ValueError:
|
|
110
|
+
continue
|
|
106
111
|
|
|
107
112
|
buy_type_team1 = _get_buy_type(sq1.text(strip=True))
|
|
108
113
|
buy_type_team2 = _get_buy_type(sq2.text(strip=True))
|