vlrdevapi 2.3.0__tar.gz → 2.4.1__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.4.1/.nvmrc +1 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.readthedocs.yaml +3 -2
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CHANGELOG.md +125 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/PKG-INFO +5 -5
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/pyproject.toml +11 -11
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/scripts/check_mdx_examples.py +31 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/scripts/download_fixtures.py +149 -4
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/__init__.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_base.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/models.py +4 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/parser.py +49 -4
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/common.py +32 -7
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/common.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/_common_parsing.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/parser.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/parser.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/parser.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/parser.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/_utils.py +57 -3
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/namespace.py +24 -8
- vlrdevapi-2.4.1/src/vlrdevapi/_series/economy/parser.py +169 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/models.py +29 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/namespace.py +3 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/parser.py +130 -47
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/parser.py +54 -16
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/namespace.py +18 -6
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/parser.py +7 -2
- vlrdevapi-2.4.1/src/vlrdevapi/_series/vods/parser.py +50 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/namespace.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/parser.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/parser.py +2 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/pagination.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/team_enrichment.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/team_parsing.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/__init__.py +14 -0
- vlrdevapi-2.4.1/src/vlrdevapi/commons/regions.py +242 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/timezone.py +1 -1
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/fetcher.py +1 -1
- vlrdevapi-2.4.1/uv.lock +1165 -0
- vlrdevapi-2.3.0/src/vlrdevapi/_series/economy/parser.py +0 -139
- vlrdevapi-2.3.0/src/vlrdevapi/_series/vods/parser.py +0 -30
- vlrdevapi-2.3.0/uv.lock +0 -1049
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.gitattributes +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/cleanup-caches.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/docs-check.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/publish.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/test.yml +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.gitignore +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.python-version +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CODE_OF_CONDUCT.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CONTRIBUTING.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/LICENCE +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/README.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/SECURITY.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/SUPPORT.md +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/__init__.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_cache.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_client.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/_common.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/match_namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/match_namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/models.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/namespace.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/namespace.pyi +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/__init__.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/paths.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/countries.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/datetime.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/mappings.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/prizes.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/exceptions.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/py.typed +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/validators.py +0 -0
- {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/wrangler.jsonc +0 -0
vlrdevapi-2.4.1/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
26.10.0
|
|
@@ -6,10 +6,11 @@ build:
|
|
|
6
6
|
python: latest
|
|
7
7
|
jobs:
|
|
8
8
|
install:
|
|
9
|
-
- pip install
|
|
9
|
+
- pip install uv
|
|
10
|
+
- uv sync --group dev
|
|
10
11
|
build:
|
|
11
12
|
html:
|
|
12
|
-
- cd docs-py && zensical build
|
|
13
|
+
- cd docs-py && uv run zensical build
|
|
13
14
|
post_build:
|
|
14
15
|
- mkdir -p $READTHEDOCS_OUTPUT/html/
|
|
15
16
|
- cp --recursive docs-py/site/* $READTHEDOCS_OUTPUT/html/
|
|
@@ -5,6 +5,129 @@ 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.1] - 2026-10-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Series players/stats parsing** — newer vlr.gg overview tables add a
|
|
13
|
+
dedicated `.mod-agents` cell before the stat columns. Stat cells are now
|
|
14
|
+
mapped dynamically around that column, and agents are read from the new
|
|
15
|
+
cell. This fixes `SideStats` validation errors and misaligned values
|
|
16
|
+
(rating, ACS, KDA, KAST, ADR, HS%, first kills/deaths).
|
|
17
|
+
- **Series economy parsing** — the round-by-round economy grid now uses the
|
|
18
|
+
newer `.vlr-rounds.ecr` markup (`.ecr-bank`, `.ecc-c`, `.ecc-sq`). Banks,
|
|
19
|
+
spends, buy types, and round winners are read from it, with the legacy
|
|
20
|
+
`.wf-table-inset.mod-econ` table kept as a fallback.
|
|
21
|
+
- **Series VODs parsing** — VODs are now read from the newer `.sm-vods`
|
|
22
|
+
module (`.sm-vod-name` labels, `a.sm-ext` links), keeping the legacy
|
|
23
|
+
`.match-vods` container as a fallback.
|
|
24
|
+
- **Series game map names** — the game header now renders the duration as a
|
|
25
|
+
sibling of the map name. The map name is read from `.map-name`, so the
|
|
26
|
+
duration is no longer concatenated (e.g. `Pearl` instead of `Pearl35:48`).
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Series economy team matching** — team/abbreviation resolution is now
|
|
31
|
+
case-insensitive, so team IDs resolve for pages that render short tags.
|
|
32
|
+
- **HTML parsing backend** — parsing now uses selectolax's Lexbor engine
|
|
33
|
+
(`selectolax.lexbor`) instead of the discontinued Modest engine
|
|
34
|
+
(`selectolax.parser`), which selectolax removed in 1.0. This is an internal
|
|
35
|
+
change: the public API, CSS selectors, and parsed output are unchanged.
|
|
36
|
+
- **Dependencies** — raised the `selectolax` minimum to `>=1.0.0` and moved
|
|
37
|
+
`pydantic` to the 2.14 series.
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
- **Changelog** — this release is mirrored across `CHANGELOG.md`, the
|
|
42
|
+
ReadTheDocs changelog (`docs-py/docs/changelog.md`), and the official
|
|
43
|
+
site changelog (`official-docs/app/(home)/changelog/page.tsx`).
|
|
44
|
+
|
|
45
|
+
## [2.4.0] - 2026-08-19
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **Bo1 series support** — single-map series now resolve games from
|
|
50
|
+
`.vm-stats-game` containers when nav tabs are absent. `resolve_game_id`
|
|
51
|
+
raises `ParsingError` instead of silently returning wrong data on
|
|
52
|
+
out-of-range or empty game lists.
|
|
53
|
+
- **Event region resolution** — `_parse_breadcrumb` resolves `subregion=`
|
|
54
|
+
breadcrumb links (e.g. "Japan", "North America") to their parent region
|
|
55
|
+
via `commons.regions.resolve_subregion_to_region`. A new
|
|
56
|
+
`EventRegion.subregion` field preserves the original subregion name
|
|
57
|
+
(e.g. `"Japan"`); `None` for `region=` links.
|
|
58
|
+
- **Event country-to-region fallback** — when no breadcrumb region is
|
|
59
|
+
present, the `region_location` country flag is resolved via
|
|
60
|
+
`resolve_country_to_region` to infer the parent region.
|
|
61
|
+
- **Event region deduplication** — duplicate region entries (from both
|
|
62
|
+
`region=` and `subregion=` breadcrumbs for the same parent region)
|
|
63
|
+
are collapsed, preferring the entry that carries a subregion name.
|
|
64
|
+
- **`commons.regions` module** — new shared utilities for region
|
|
65
|
+
resolution: `VALID_REGIONS`, `SUBREGION_TO_REGION` (17 VCL/GC
|
|
66
|
+
subregions), `COUNTRY_TO_REGION` (~100+ countries),
|
|
67
|
+
`resolve_subregion_to_region`, and `resolve_country_to_region`.
|
|
68
|
+
- **`commons.__init__` re-exports** — `resolve_subregion_to_region`,
|
|
69
|
+
`resolve_country_to_region`, `VALID_REGIONS`, `SUBREGION_TO_REGION`,
|
|
70
|
+
and `COUNTRY_TO_REGION` are now re-exported from `vlrdevapi.commons`.
|
|
71
|
+
- **Series forfeit detection** — new `ForfeitInfo` model on
|
|
72
|
+
`SeriesInfo.forfeit` exposes `forfeited`, `team`, `team_id`, and
|
|
73
|
+
`reason` fields. Parser detects `"forfeited by TEAM"` in the
|
|
74
|
+
`match-header-vs-note` element and maps the team name to the
|
|
75
|
+
corresponding `team1`/`team2` for ID resolution. Note-level
|
|
76
|
+
forfeit text (e.g. "Blind Esports Forfeit Map 2") is also parsed.
|
|
77
|
+
- **Series match notes** — new `SeriesInfo.notes` list captures
|
|
78
|
+
non-veto `match-header-note` text such as technical pause warnings
|
|
79
|
+
and lobby remake notices.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- **Series economy enrichment** — team matching now tries abbreviation
|
|
84
|
+
names from `.ovw-player-tag` elements first (e.g. "SEN"), then falls
|
|
85
|
+
back to full team names. This fixes mismatches where the economy tab
|
|
86
|
+
shows a different abbreviation than the overview header.
|
|
87
|
+
- **Series rounds enrichment** — same abbreviation-first matching
|
|
88
|
+
strategy as economy.
|
|
89
|
+
- **Series info veto parsing** — Bo1 veto fallback now handles the
|
|
90
|
+
"bans + decider" pattern (e.g. `"Ban1 remains"`) in addition to
|
|
91
|
+
pick/ban. The PICK badge is excluded from map names via
|
|
92
|
+
`span.text(deep=False)`.
|
|
93
|
+
- **Series info notes parsing** — the parser now iterates all
|
|
94
|
+
`match-header-note` elements instead of only the first. This
|
|
95
|
+
correctly separates forfeit reasons, informational notes, and
|
|
96
|
+
map veto text when multiple notes are present.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- **Economy bank/spend parsing** — `int()` calls on `.rnd-sq` `title`
|
|
101
|
+
attributes and `.rnd-num` text are wrapped in `try/except ValueError`;
|
|
102
|
+
malformed values default to `0` instead of crashing the parser.
|
|
103
|
+
- **Event region from location country** — events that only have a
|
|
104
|
+
"Location" label with a flag (no separate "Region" label) now resolve
|
|
105
|
+
the region from `location.country` via `resolve_country_to_region`.
|
|
106
|
+
Previously the country-to-region fallback only checked `region_location`.
|
|
107
|
+
- **Phantom pagination in matches** — terminal pages no longer report
|
|
108
|
+
`has_next_page = True` (from DaviAlcanfor PR #50).
|
|
109
|
+
|
|
110
|
+
### Documentation
|
|
111
|
+
|
|
112
|
+
- **Event info reference** — `EventRegion` fields table now documents
|
|
113
|
+
the `subregion` field. Region example demonstrates subregion access.
|
|
114
|
+
`regions` field description explains deduplication and International
|
|
115
|
+
collapse.
|
|
116
|
+
- **Series info reference** — `ForfeitInfo` fields table documents the
|
|
117
|
+
new `forfeit` nested model. `notes` field description explains the
|
|
118
|
+
distinction between forfeit reasons and informational notes.
|
|
119
|
+
- **Series economy reference** — docstrings reference correct model
|
|
120
|
+
types (`RoundEconomyData`, `bank_team1`).
|
|
121
|
+
- **Series rounds reference** — docstrings reference correct model
|
|
122
|
+
types (`RoundData`, `winner_team_name`).
|
|
123
|
+
- **`commons.regions` module** — docstrings describe all mapping
|
|
124
|
+
dictionaries and resolution functions.
|
|
125
|
+
- **Project structure** — contributing guide updated with actual
|
|
126
|
+
underscore-prefixed directory names and new modules (`_news/`,
|
|
127
|
+
`_utils/`, `commons/regions.py`).
|
|
128
|
+
- **Module docstrings** — added to `economy/parser.py`,
|
|
129
|
+
`rounds/parser.py`, and `commons/__init__.py`.
|
|
130
|
+
|
|
8
131
|
## [2.3.0] - 2026-08-11
|
|
9
132
|
|
|
10
133
|
### Added
|
|
@@ -193,6 +316,8 @@ The API surface, module structure, and type system are all new.
|
|
|
193
316
|
- `aiohttp` and `asyncio` dependencies.
|
|
194
317
|
- v1.x match/event/team parsing modules.
|
|
195
318
|
|
|
319
|
+
[2.4.1]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.4.0...v2.4.1
|
|
320
|
+
[2.4.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.3.0...v2.4.0
|
|
196
321
|
[2.3.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.2.0...v2.3.0
|
|
197
322
|
[2.2.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.1.0...v2.2.0
|
|
198
323
|
[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.1
|
|
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
|
|
@@ -38,11 +38,11 @@ Classifier: Topic :: Games/Entertainment
|
|
|
38
38
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
39
39
|
Requires-Python: >=3.11
|
|
40
40
|
Requires-Dist: httpx>=0.28.1
|
|
41
|
-
Requires-Dist: pydantic>=2.
|
|
42
|
-
Requires-Dist: selectolax>=0.
|
|
41
|
+
Requires-Dist: pydantic>=2.14.1
|
|
42
|
+
Requires-Dist: selectolax>=1.0.0
|
|
43
43
|
Provides-Extra: test
|
|
44
44
|
Requires-Dist: pytest-cov>=7.1.0; extra == 'test'
|
|
45
|
-
Requires-Dist: pytest>=
|
|
45
|
+
Requires-Dist: pytest>=9.1.1; extra == 'test'
|
|
46
46
|
Requires-Dist: respx>=0.23.1; extra == 'test'
|
|
47
47
|
Description-Content-Type: text/markdown
|
|
48
48
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vlrdevapi"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.1"
|
|
8
8
|
description = "A Python library to scrape data from vlr.gg"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -22,8 +22,8 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"httpx>=0.28.1",
|
|
25
|
-
"pydantic>=2.
|
|
26
|
-
"selectolax>=0.
|
|
25
|
+
"pydantic>=2.14.1",
|
|
26
|
+
"selectolax>=1.0.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -33,22 +33,22 @@ Documentation = "https://vlrdevapi.pages.dev/docs"
|
|
|
33
33
|
|
|
34
34
|
[project.optional-dependencies]
|
|
35
35
|
test = [
|
|
36
|
-
"pytest>=
|
|
36
|
+
"pytest>=9.1.1",
|
|
37
37
|
"respx>=0.23.1",
|
|
38
38
|
"pytest-cov>=7.1.0",
|
|
39
39
|
]
|
|
40
40
|
|
|
41
41
|
[dependency-groups]
|
|
42
42
|
dev = [
|
|
43
|
-
"ruff>=0.
|
|
44
|
-
"pytest>=
|
|
45
|
-
"tzdata>=2026.
|
|
46
|
-
"ty>=0.0.
|
|
43
|
+
"ruff>=0.17.1",
|
|
44
|
+
"pytest>=9.1.1",
|
|
45
|
+
"tzdata>=2026.5",
|
|
46
|
+
"ty>=0.0.89",
|
|
47
47
|
"respx>=0.23.1",
|
|
48
48
|
"pytest-cov>=7.1.0",
|
|
49
|
-
"zensical>=0.0.
|
|
50
|
-
"mkdocstrings>=0.
|
|
51
|
-
"mkdocstrings-python>=
|
|
49
|
+
"zensical>=0.0.71",
|
|
50
|
+
"mkdocstrings>=1.0.7",
|
|
51
|
+
"mkdocstrings-python>=2.0.9",
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
[tool.pytest.ini_options]
|
|
@@ -6,6 +6,12 @@ executes examples. An ``httpx.Client.get`` interceptor de-duplicates
|
|
|
6
6
|
fetches within a run and can replay validation offline against HTML
|
|
7
7
|
fixtures saved under ``.cache/mdx-html/`` (gitignored).
|
|
8
8
|
|
|
9
|
+
Blocks may carry two markers (both stripped from rendered docs by the
|
|
10
|
+
remark plugin in ``official-docs/source.config.ts``):
|
|
11
|
+
|
|
12
|
+
``# doc-check: skip`` never execute (non-runnable demo code)
|
|
13
|
+
``# doc-check: allow-empty`` execute, but empty output is acceptable
|
|
14
|
+
|
|
9
15
|
CI/CD usage:
|
|
10
16
|
python scripts/check_mdx_examples.py # warm + offline replay
|
|
11
17
|
python scripts/check_mdx_examples.py --skip-warm # offline replay only
|
|
@@ -401,6 +407,26 @@ def has_skip_marker(code: str) -> bool:
|
|
|
401
407
|
return any(line.strip().startswith("# doc-check: skip") for line in code.splitlines())
|
|
402
408
|
|
|
403
409
|
|
|
410
|
+
def has_allow_empty_marker(code: str) -> bool:
|
|
411
|
+
"""Blocks may opt out of the empty-output rule with `# doc-check: allow-empty`.
|
|
412
|
+
|
|
413
|
+
Use for examples that filter live data and can legitimately match nothing
|
|
414
|
+
on a given day. The block still runs and still fails on exceptions or
|
|
415
|
+
timeouts; only the "no output" requirement is relaxed.
|
|
416
|
+
"""
|
|
417
|
+
return any(
|
|
418
|
+
line.strip().startswith("# doc-check: allow-empty")
|
|
419
|
+
for line in code.splitlines()
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def apply_allow_empty(code: str, run_ok: bool, run_msg: str) -> tuple[bool, str]:
|
|
424
|
+
"""Relax an empty-output failure for blocks marked `# doc-check: allow-empty`."""
|
|
425
|
+
if not run_ok and has_allow_empty_marker(code) and "no output" in run_msg.lower():
|
|
426
|
+
return True, "(no output - allowed by marker)"
|
|
427
|
+
return run_ok, run_msg
|
|
428
|
+
|
|
429
|
+
|
|
404
430
|
async def execute_blocks(
|
|
405
431
|
results: list[Result],
|
|
406
432
|
timeout: float,
|
|
@@ -433,6 +459,11 @@ async def execute_blocks(
|
|
|
433
459
|
else:
|
|
434
460
|
run_ok, run_msg, dur = await exec_sync(code, timeout)
|
|
435
461
|
|
|
462
|
+
# Blocks marked `# doc-check: allow-empty` may legitimately print
|
|
463
|
+
# nothing (e.g. a filtered loop over live data that matched no rows).
|
|
464
|
+
# They still fail on exceptions and timeouts.
|
|
465
|
+
run_ok, run_msg = apply_allow_empty(code, run_ok, run_msg)
|
|
466
|
+
|
|
436
467
|
if record_results:
|
|
437
468
|
results[i] = Result(
|
|
438
469
|
r.file, r.code, r.syntax_ok, r.syntax_error, r.imports_ok,
|
|
@@ -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.lexbor import LexborHTMLParser as 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
|
|
|
@@ -4,7 +4,8 @@ from datetime import date, datetime
|
|
|
4
4
|
|
|
5
5
|
from vlrdevapi.commons.datetime import date_to_utc_datetime
|
|
6
6
|
|
|
7
|
-
from selectolax.
|
|
7
|
+
from selectolax.lexbor import LexborHTMLParser as HTMLParser
|
|
8
|
+
from selectolax.lexbor import LexborNode as Node
|
|
8
9
|
|
|
9
10
|
from vlrdevapi._event.info.models import (
|
|
10
11
|
EventInfo,
|
|
@@ -16,12 +17,25 @@ from vlrdevapi._event.info.models import (
|
|
|
16
17
|
)
|
|
17
18
|
from vlrdevapi.commons.countries import get_country_name
|
|
18
19
|
from vlrdevapi.commons.prizes import parse_prize_amount
|
|
20
|
+
from vlrdevapi.commons.regions import resolve_country_to_region, resolve_subregion_to_region
|
|
19
21
|
import contextlib
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
|
|
23
25
|
"""Parse event info from the event page HTML.
|
|
24
26
|
|
|
27
|
+
After basic parsing the region list is post-processed:
|
|
28
|
+
|
|
29
|
+
1. Duplicates (same region name from ``region=`` and ``subregion=``
|
|
30
|
+
breadcrumbs) are collapsed; the entry carrying a ``subregion``
|
|
31
|
+
value is kept.
|
|
32
|
+
2. Two or more distinct regions collapse to a single
|
|
33
|
+
``EventRegion(name="International")``.
|
|
34
|
+
3. When no breadcrumb region was found, the ``region_location``
|
|
35
|
+
country is tried first; if absent, ``location.country`` is
|
|
36
|
+
tried. Either is resolved via ``resolve_country_to_region``
|
|
37
|
+
to infer the parent region.
|
|
38
|
+
|
|
25
39
|
Args:
|
|
26
40
|
html: Parsed HTML from the event page.
|
|
27
41
|
event_id: Unique event identifier on vlr.gg.
|
|
@@ -42,6 +56,28 @@ def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
|
|
|
42
56
|
_parse_title(header, event)
|
|
43
57
|
_parse_desc_items(header, event)
|
|
44
58
|
|
|
59
|
+
seen: dict[str, EventRegion] = {}
|
|
60
|
+
for r in event.regions:
|
|
61
|
+
if r.name not in seen:
|
|
62
|
+
seen[r.name] = r
|
|
63
|
+
elif r.subregion and not seen[r.name].subregion:
|
|
64
|
+
seen[r.name] = r
|
|
65
|
+
event.regions = list(seen.values())
|
|
66
|
+
|
|
67
|
+
if len(event.regions) > 1:
|
|
68
|
+
event.regions = [EventRegion(name="International")]
|
|
69
|
+
|
|
70
|
+
if not event.regions:
|
|
71
|
+
country = None
|
|
72
|
+
if event.region_location and event.region_location.country:
|
|
73
|
+
country = event.region_location.country
|
|
74
|
+
elif event.location and event.location.country:
|
|
75
|
+
country = event.location.country
|
|
76
|
+
if country:
|
|
77
|
+
parent = resolve_country_to_region(country)
|
|
78
|
+
if parent:
|
|
79
|
+
event.regions = [EventRegion(name=parent)]
|
|
80
|
+
|
|
45
81
|
return event
|
|
46
82
|
|
|
47
83
|
|
|
@@ -62,7 +98,14 @@ def _parse_image(header: Node, event: EventInfo) -> None:
|
|
|
62
98
|
|
|
63
99
|
|
|
64
100
|
def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
|
|
65
|
-
"""Parse breadcrumb links (series, stage, regions) from the header.
|
|
101
|
+
"""Parse breadcrumb links (series, stage, regions/subregions) from the header.
|
|
102
|
+
|
|
103
|
+
Region resolution priority:
|
|
104
|
+
|
|
105
|
+
1. ``region=`` links are stored directly.
|
|
106
|
+
2. ``subregion=`` links are resolved to their parent region via
|
|
107
|
+
``resolve_subregion_to_region``; the original subregion name is
|
|
108
|
+
preserved in ``EventRegion.subregion``.
|
|
66
109
|
|
|
67
110
|
Args:
|
|
68
111
|
header: The event-header root node.
|
|
@@ -93,10 +136,12 @@ def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
|
|
|
93
136
|
|
|
94
137
|
if "stage=" in href:
|
|
95
138
|
event.stage = EventStageTag(name=name, href=href)
|
|
139
|
+
elif "subregion=" in href:
|
|
140
|
+
parent = resolve_subregion_to_region(name)
|
|
141
|
+
if parent:
|
|
142
|
+
event.regions.append(EventRegion(name=parent, subregion=name, href=href))
|
|
96
143
|
elif "region=" in href:
|
|
97
144
|
event.regions.append(EventRegion(name=name, href=href))
|
|
98
|
-
else:
|
|
99
|
-
event.regions.append(EventRegion(name=name, href=href))
|
|
100
145
|
|
|
101
146
|
|
|
102
147
|
def _parse_title(header: Node, event: EventInfo) -> None:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
|
-
from selectolax.
|
|
4
|
+
from selectolax.lexbor import LexborHTMLParser as HTMLParser
|
|
5
|
+
from selectolax.lexbor import LexborNode as Node
|
|
5
6
|
|
|
6
7
|
from vlrdevapi._event.info.models import EventPrize
|
|
7
8
|
from vlrdevapi._event.list.models import (
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
from datetime import date, tzinfo
|
|
3
3
|
from zoneinfo import ZoneInfo
|
|
4
4
|
|
|
5
|
-
from selectolax.
|
|
5
|
+
from selectolax.lexbor import LexborHTMLParser as HTMLParser
|
|
6
|
+
from selectolax.lexbor import LexborNode as Node
|
|
6
7
|
|
|
7
8
|
from vlrdevapi._event.matches.models import (
|
|
8
9
|
EventMatch,
|
|
@@ -4,7 +4,8 @@ import re
|
|
|
4
4
|
from datetime import date, datetime
|
|
5
5
|
from urllib.parse import parse_qs, urlparse
|
|
6
6
|
|
|
7
|
-
from selectolax.
|
|
7
|
+
from selectolax.lexbor import LexborHTMLParser as HTMLParser
|
|
8
|
+
from selectolax.lexbor import LexborNode as Node
|
|
8
9
|
|
|
9
10
|
from vlrdevapi._event.stages.models import EventStage, EventStages
|
|
10
11
|
from vlrdevapi.exceptions import DataNotFoundError
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Event standings parser."""
|
|
2
2
|
|
|
3
|
-
from selectolax.
|
|
3
|
+
from selectolax.lexbor import LexborHTMLParser as HTMLParser
|
|
4
|
+
from selectolax.lexbor import LexborNode as Node
|
|
4
5
|
|
|
5
6
|
from vlrdevapi._event.standings.models import StandingEntry, TeamStanding
|
|
6
7
|
from vlrdevapi.commons.prizes import parse_prize_amount
|