vlrdevapi 2.1.0__tar.gz → 2.2.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.1.0 → vlrdevapi-2.2.0}/.gitattributes +5 -1
- vlrdevapi-2.2.0/.github/workflows/cleanup-caches.yml +31 -0
- vlrdevapi-2.2.0/.github/workflows/docs-check.yml +102 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.gitignore +2 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/CHANGELOG.md +19 -1
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/PKG-INFO +1 -1
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/pyproject.toml +1 -1
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/scripts/check_mdx_examples.py +287 -46
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/__init__.py +1 -1
- vlrdevapi-2.2.0/src/vlrdevapi/_series/_utils.py +134 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/economy/namespace.py +3 -2
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/economy/parser.py +10 -3
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/match_namespace.py +4 -2
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/performance/models.py +3 -3
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/performance/namespace.py +12 -6
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/performance/parser.py +47 -19
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/players/namespace.py +2 -1
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/players/parser.py +5 -3
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/rounds/namespace.py +3 -2
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/rounds/parser.py +10 -2
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/uv.lock +1 -1
- vlrdevapi-2.1.0/.github/workflows/docs-check.yml +0 -29
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.github/workflows/publish.yml +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.github/workflows/test.yml +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.python-version +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/.readthedocs.yaml +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/CONTRIBUTING.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/LICENCE +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/README.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/SECURITY.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/SUPPORT.md +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/scripts/download_fixtures.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/__init__.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_base.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_cache.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_client.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/_common.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/info/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/info/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/info/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/info/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/info/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/list/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/list/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/list/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/list/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/list/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/matches/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/matches/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/matches/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/matches/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/matches/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/stages/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/stages/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/stages/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/stages/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/stages/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/standings/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/standings/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/standings/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/standings/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/standings/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/teams/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/teams/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/teams/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/teams/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_event/teams/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/common.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/completed/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/completed/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/completed/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/completed/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/completed/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/live/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/live/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/live/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/live/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/live/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/upcoming/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/upcoming/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/upcoming/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/upcoming/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_matches/upcoming/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/_common_parsing.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/agents/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/agents/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/agents/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/agents/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/agents/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/info/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/info/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/info/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/info/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/info/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/matches/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/matches/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/matches/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/matches/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/matches/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/profile/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/profile/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/profile/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/profile/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/profile/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/teams/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/teams/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/teams/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/teams/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_player/teams/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/economy/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/economy/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/economy/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/info/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/info/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/info/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/info/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/info/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/match_namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/performance/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/performance/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/players/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/players/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/players/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/rounds/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/rounds/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/rounds/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/vods/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/vods/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/vods/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/vods/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_series/vods/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/completed_matches/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/completed_matches/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/completed_matches/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/completed_matches/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/completed_matches/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/info/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/info/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/info/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/info/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/info/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/placements/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/placements/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/placements/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/placements/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/placements/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/roster/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/roster/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/roster/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/roster/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/roster/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/stats/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/stats/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/stats/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/stats/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/stats/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/transactions/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/transactions/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/transactions/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/transactions/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/transactions/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/upcoming_matches/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/upcoming_matches/models.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/upcoming_matches/namespace.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/upcoming_matches/namespace.pyi +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_team/upcoming_matches/parser.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_utils/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_utils/pagination.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_utils/paths.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_utils/team_enrichment.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/_utils/team_parsing.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/__init__.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/countries.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/datetime.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/mappings.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/prizes.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/commons/timezone.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/exceptions.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/fetcher.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/py.typed +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/src/vlrdevapi/validators.py +0 -0
- {vlrdevapi-2.1.0 → vlrdevapi-2.2.0}/wrangler.jsonc +0 -0
|
@@ -4,4 +4,8 @@ docs-py/** linguist-vendored
|
|
|
4
4
|
|
|
5
5
|
# Exclude test fixtures
|
|
6
6
|
tests/test_html/** linguist-vendored
|
|
7
|
-
tests/test_html/** linguist-detectable=false
|
|
7
|
+
tests/test_html/** linguist-detectable=false
|
|
8
|
+
|
|
9
|
+
# Normalize workflow YAML line endings
|
|
10
|
+
*.yml text eol=lf
|
|
11
|
+
*.yaml text eol=lf
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Cleanup docs-check caches on closed PRs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types:
|
|
6
|
+
- closed
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
actions: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
cleanup:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Delete PR caches
|
|
16
|
+
env:
|
|
17
|
+
GH_TOKEN: ${{ github.token }}
|
|
18
|
+
GH_REPO: ${{ github.repository }}
|
|
19
|
+
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
|
|
20
|
+
run: |
|
|
21
|
+
echo "Fetching list of cache keys"
|
|
22
|
+
cacheKeysForPR=$(gh cache list --ref $BRANCH --limit 100 --json id --jq '.[].id')
|
|
23
|
+
|
|
24
|
+
## Setting this to not fail the workflow while deleting cache keys.
|
|
25
|
+
set +e
|
|
26
|
+
echo "Deleting caches..."
|
|
27
|
+
for cacheKey in $cacheKeysForPR
|
|
28
|
+
do
|
|
29
|
+
gh cache delete $cacheKey
|
|
30
|
+
done
|
|
31
|
+
echo "Done"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
name: Docs Check
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- 'official-docs/**'
|
|
7
|
+
- 'docs-py/**'
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
paths:
|
|
11
|
+
- 'official-docs/**'
|
|
12
|
+
- 'docs-py/**'
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
check:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
|
|
20
|
+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b
|
|
21
|
+
with:
|
|
22
|
+
python-version: '3.12'
|
|
23
|
+
|
|
24
|
+
- name: Install package
|
|
25
|
+
run: uv sync --group dev
|
|
26
|
+
|
|
27
|
+
# main always validates live against fresh vlr.gg data; never uses the
|
|
28
|
+
# cache. Branch/PR runs reuse fixtures cached by earlier runs in the
|
|
29
|
+
# same PR (within a 2h TTL), falling back to live fetches for anything
|
|
30
|
+
# missing, so the check is correct even with a partially-stale cache.
|
|
31
|
+
- name: Run MDX examples check (main, live)
|
|
32
|
+
if: github.ref_name == 'main'
|
|
33
|
+
run: uv run scripts/check_mdx_examples.py --live
|
|
34
|
+
|
|
35
|
+
# actions/cache v6.1.0, pinned to a reviewed commit SHA.
|
|
36
|
+
- name: Restore fixture cache (branch)
|
|
37
|
+
id: mdx-fixtures
|
|
38
|
+
if: github.ref_name != 'main'
|
|
39
|
+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
|
|
40
|
+
with:
|
|
41
|
+
path: fixtures.tar.gz
|
|
42
|
+
key: mdx-fixtures-${{ github.ref_name }}-run-${{ github.run_id }}
|
|
43
|
+
restore-keys: |
|
|
44
|
+
mdx-fixtures-${{ github.ref_name }}-
|
|
45
|
+
|
|
46
|
+
- name: Unpack fixtures (branch)
|
|
47
|
+
if: github.ref_name != 'main'
|
|
48
|
+
run: |
|
|
49
|
+
if [ -f fixtures.tar.gz ]; then
|
|
50
|
+
mkdir -p .cache
|
|
51
|
+
tar -C .cache -xzf fixtures.tar.gz
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
- name: Check fixture cache age (branch, 2h TTL)
|
|
55
|
+
id: cache-age
|
|
56
|
+
if: github.ref_name != 'main'
|
|
57
|
+
run: |
|
|
58
|
+
matched="${{ steps.mdx-fixtures.outputs.cache-matched-key }}"
|
|
59
|
+
now=$(date +%s)
|
|
60
|
+
if [ -n "$matched" ]; then
|
|
61
|
+
epoch="${matched##*-}"
|
|
62
|
+
age=$(( now - epoch ))
|
|
63
|
+
else
|
|
64
|
+
age=999999
|
|
65
|
+
fi
|
|
66
|
+
if [ "$age" -gt 7200 ]; then
|
|
67
|
+
rm -rf .cache/mdx-html
|
|
68
|
+
rm -f fixtures.tar.gz
|
|
69
|
+
mkdir -p .cache/mdx-html
|
|
70
|
+
echo "fresh=false" >> "$GITHUB_OUTPUT"
|
|
71
|
+
else
|
|
72
|
+
echo "fresh=true" >> "$GITHUB_OUTPUT"
|
|
73
|
+
fi
|
|
74
|
+
echo "now=$now" >> "$GITHUB_OUTPUT"
|
|
75
|
+
|
|
76
|
+
- name: Run MDX examples check (branch)
|
|
77
|
+
if: github.ref_name != 'main'
|
|
78
|
+
run: |
|
|
79
|
+
if [ "${{ steps.cache-age.outputs.fresh }}" == "true" ]; then
|
|
80
|
+
uv run scripts/check_mdx_examples.py --skip-warm
|
|
81
|
+
else
|
|
82
|
+
uv run scripts/check_mdx_examples.py
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# always(): a run that fails validation still saved its warm fixtures,
|
|
86
|
+
# so the next fix commit restores and validates fast instead of warming.
|
|
87
|
+
- name: Package fixtures (branch)
|
|
88
|
+
if: always() && github.ref_name != 'main' && steps.cache-age.outputs.fresh == 'false'
|
|
89
|
+
run: |
|
|
90
|
+
mkdir -p .cache/mdx-html
|
|
91
|
+
if [ -n "$(ls -A .cache/mdx-html)" ]; then
|
|
92
|
+
tar -C .cache -czf fixtures.tar.gz mdx-html
|
|
93
|
+
else
|
|
94
|
+
rm -f fixtures.tar.gz
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
- name: Save fixture cache (branch)
|
|
98
|
+
if: always() && github.ref_name != 'main' && steps.cache-age.outputs.fresh == 'false'
|
|
99
|
+
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
|
|
100
|
+
with:
|
|
101
|
+
path: fixtures.tar.gz
|
|
102
|
+
key: mdx-fixtures-${{ github.ref_name }}-${{ steps.cache-age.outputs.now }}
|
|
@@ -5,7 +5,7 @@ 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
|
-
## [
|
|
8
|
+
## [2.2.0] - 2026-08-11
|
|
9
9
|
|
|
10
10
|
### Changed
|
|
11
11
|
|
|
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
14
14
|
sentinel year of `2019` instead of the current year. Any parsed date with
|
|
15
15
|
`year < 2020` means the source omitted the year, so guard with
|
|
16
16
|
`event.start_date.year < 2020` before relying on it.
|
|
17
|
+
- **Series performance field renames (breaking)** — `AdvStatsEntry` fields
|
|
18
|
+
were renamed to descriptive names: `econ` → `economy`, `pl` → `plants`,
|
|
19
|
+
and `de` → `defuses`. Update any code referencing the old field names.
|
|
20
|
+
The series performance reference docs were updated to match.
|
|
17
21
|
|
|
18
22
|
### Fixed
|
|
19
23
|
|
|
@@ -22,6 +26,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
22
26
|
change behavior in Python 3.15). All such parses now supply an explicit
|
|
23
27
|
year derived from context (reference date, page-header year, or the
|
|
24
28
|
sentinel above).
|
|
29
|
+
- **Series per-game stats by game number** — `players()`, `rounds()`,
|
|
30
|
+
`performance()`, and `economy()` game identifiers can now be a 1-based
|
|
31
|
+
game number within the series, resolved to the real VLR game ID from the
|
|
32
|
+
page's game nav tabs. A real VLR game ID is still accepted unchanged;
|
|
33
|
+
previously passing a game number produced empty results.
|
|
34
|
+
- **Series economy and rounds scope** — economy and round-by-round data are
|
|
35
|
+
now parsed from the selected game's section only. The economy parser
|
|
36
|
+
previously collected tables from the whole page, so a per-game request
|
|
37
|
+
could include data from other maps.
|
|
38
|
+
- **Series performance player IDs** — the performance tab renders player
|
|
39
|
+
cells without links, leaving player IDs as `0`. IDs are now recovered from
|
|
40
|
+
the series overview tab (keyed by team abbreviation and player name) and
|
|
41
|
+
attached to kill-matrix entries, advanced stats, and notable-round victims.
|
|
25
42
|
|
|
26
43
|
## [2.1.0] - 2026-08-07
|
|
27
44
|
|
|
@@ -125,6 +142,7 @@ The API surface, module structure, and type system are all new.
|
|
|
125
142
|
- `aiohttp` and `asyncio` dependencies.
|
|
126
143
|
- v1.x match/event/team parsing modules.
|
|
127
144
|
|
|
145
|
+
[2.2.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.1.0...v2.2.0
|
|
128
146
|
[2.1.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.0.1...v2.1.0
|
|
129
147
|
[2.0.1]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.0.0...v2.0.1
|
|
130
148
|
[2.0.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v1.6.2...v2.0.0
|
|
@@ -2,31 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
Extracts every Python code block from official-docs/content/ (docs,
|
|
4
4
|
blog, guides, etc.), checks syntax validity, import resolution, then
|
|
5
|
-
executes examples
|
|
5
|
+
executes examples. An ``httpx.Client.get`` interceptor de-duplicates
|
|
6
|
+
fetches within a run and can replay validation offline against HTML
|
|
7
|
+
fixtures saved under ``.cache/mdx-html/`` (gitignored).
|
|
6
8
|
|
|
7
9
|
CI/CD usage:
|
|
8
|
-
python scripts/check_mdx_examples.py #
|
|
9
|
-
python scripts/check_mdx_examples.py --
|
|
10
|
+
python scripts/check_mdx_examples.py # warm + offline replay
|
|
11
|
+
python scripts/check_mdx_examples.py --skip-warm # offline replay only
|
|
12
|
+
python scripts/check_mdx_examples.py --live # single live pass
|
|
13
|
+
python scripts/check_mdx_examples.py --timeout 60 # custom per-example timeout
|
|
10
14
|
"""
|
|
11
15
|
|
|
12
16
|
from __future__ import annotations
|
|
13
17
|
|
|
14
18
|
import ast
|
|
15
19
|
import asyncio
|
|
20
|
+
import hashlib
|
|
16
21
|
import importlib
|
|
17
22
|
import io
|
|
18
23
|
import os
|
|
19
24
|
import re
|
|
20
25
|
import sys
|
|
21
26
|
import textwrap
|
|
27
|
+
import threading
|
|
22
28
|
import time
|
|
23
29
|
import traceback
|
|
24
|
-
from contextlib import
|
|
30
|
+
from contextlib import redirect_stderr, redirect_stdout
|
|
25
31
|
from pathlib import Path
|
|
26
32
|
from typing import NamedTuple
|
|
27
33
|
|
|
34
|
+
import httpx
|
|
35
|
+
|
|
36
|
+
from vlrdevapi.fetcher import DEFAULT_RATE_LIMIT, RateLimiter
|
|
37
|
+
|
|
28
38
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
39
|
DOCS_DIR = REPO_ROOT / "official-docs" / "content"
|
|
40
|
+
DEFAULT_FIXTURES_DIR = REPO_ROOT / ".cache" / "mdx-html"
|
|
41
|
+
|
|
42
|
+
DYNAMIC_PATHS = frozenset({"/matches", "/matches/results", "/events"})
|
|
30
43
|
|
|
31
44
|
|
|
32
45
|
class Result(NamedTuple):
|
|
@@ -41,6 +54,142 @@ class Result(NamedTuple):
|
|
|
41
54
|
duration: float
|
|
42
55
|
|
|
43
56
|
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# HTTP cache (dedup + offline replay)
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
class HTTPCache:
|
|
62
|
+
"""Thread-safe HTTP response cache that intercepts ``httpx.Client.get``.
|
|
63
|
+
|
|
64
|
+
Patched at the class level so every library request — sync, parallel
|
|
65
|
+
worker threads, enrichment, and the dark-mode team info double fetch —
|
|
66
|
+
funnels through it (all go via ``fetcher.fetch_sync`` ->
|
|
67
|
+
``client.get``). Keys are sha256 of the resolved URL + sorted headers,
|
|
68
|
+
so the same path fetched with a different ``Cookie`` header (dark mode)
|
|
69
|
+
gets a distinct cache entry.
|
|
70
|
+
|
|
71
|
+
Modes:
|
|
72
|
+
record: disk hit -> reuse (no network); miss -> live fetch + cache.
|
|
73
|
+
replay: hit -> reuse; miss -> live fallback (no disk writes).
|
|
74
|
+
live: in-memory only; miss -> live fetch + memory cache.
|
|
75
|
+
|
|
76
|
+
Time-varying listing pages (``/matches``, ``/matches/results``,
|
|
77
|
+
``/events``) are treated as dynamic: always fetched live, deduplicated
|
|
78
|
+
in memory only, and never persisted to disk. Serving a stale cached
|
|
79
|
+
fixture for these could silently validate examples against outdated
|
|
80
|
+
match data.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, fixtures_dir: Path | None, mode: str) -> None:
|
|
84
|
+
self.fixtures_dir = fixtures_dir
|
|
85
|
+
self.mode = mode
|
|
86
|
+
self._lock = threading.Lock()
|
|
87
|
+
self._mem: dict[str, str] = {}
|
|
88
|
+
self._orig_get = None
|
|
89
|
+
self.total_requests = 0
|
|
90
|
+
self.live_fetches = 0
|
|
91
|
+
self.hits_mem = 0
|
|
92
|
+
self.hits_disk = 0
|
|
93
|
+
self.misses = 0
|
|
94
|
+
self.live_fallbacks = 0
|
|
95
|
+
self.unique_pages: set[str] = set()
|
|
96
|
+
|
|
97
|
+
def install(self) -> None:
|
|
98
|
+
"""Patch ``httpx.Client.get`` at the class level."""
|
|
99
|
+
if self._orig_get is None:
|
|
100
|
+
self._orig_get = httpx.Client.get
|
|
101
|
+
cache = self
|
|
102
|
+
orig_get = self._orig_get
|
|
103
|
+
|
|
104
|
+
def _intercept(client: httpx.Client, url: str, **kwargs) -> httpx.Response:
|
|
105
|
+
return cache._handle(client, url, orig_get, **kwargs)
|
|
106
|
+
|
|
107
|
+
httpx.Client.get = _intercept # type: ignore[method-assign]
|
|
108
|
+
|
|
109
|
+
def restore(self) -> None:
|
|
110
|
+
"""Restore the original ``httpx.Client.get``."""
|
|
111
|
+
if self._orig_get is not None:
|
|
112
|
+
httpx.Client.get = self._orig_get # type: ignore[method-assign]
|
|
113
|
+
self._orig_get = None
|
|
114
|
+
|
|
115
|
+
def _key(self, request: httpx.Request, headers: dict[str, str] | None) -> str:
|
|
116
|
+
header_items = sorted((headers or {}).items())
|
|
117
|
+
payload = f"{request.url}|{header_items!r}".encode()
|
|
118
|
+
return hashlib.sha256(payload).hexdigest()
|
|
119
|
+
|
|
120
|
+
def _lookup(self, key: str, allow_disk: bool = True) -> str | None:
|
|
121
|
+
with self._lock:
|
|
122
|
+
html = self._mem.get(key)
|
|
123
|
+
if html is not None:
|
|
124
|
+
self.hits_mem += 1
|
|
125
|
+
return html
|
|
126
|
+
if allow_disk and self.fixtures_dir is not None:
|
|
127
|
+
fixture = self.fixtures_dir / f"{key}.html"
|
|
128
|
+
if fixture.exists():
|
|
129
|
+
try:
|
|
130
|
+
html = fixture.read_text(encoding="utf-8")
|
|
131
|
+
except OSError:
|
|
132
|
+
html = None
|
|
133
|
+
if html is not None:
|
|
134
|
+
self._mem[key] = html
|
|
135
|
+
self.hits_disk += 1
|
|
136
|
+
return html
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def _store(self, key: str, html: str, to_disk: bool = True) -> None:
|
|
140
|
+
with self._lock:
|
|
141
|
+
self._mem[key] = html
|
|
142
|
+
if to_disk and self.mode == "record" and self.fixtures_dir is not None:
|
|
143
|
+
try:
|
|
144
|
+
self.fixtures_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
fixture = self.fixtures_dir / f"{key}.html"
|
|
146
|
+
if not fixture.exists():
|
|
147
|
+
fixture.write_text(html, encoding="utf-8")
|
|
148
|
+
except OSError:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def _throttle(self) -> None:
|
|
152
|
+
if DEFAULT_RATE_LIMIT > 0:
|
|
153
|
+
time.sleep(1.0 / DEFAULT_RATE_LIMIT)
|
|
154
|
+
|
|
155
|
+
def _handle(
|
|
156
|
+
self,
|
|
157
|
+
client: httpx.Client,
|
|
158
|
+
url: str,
|
|
159
|
+
orig_get,
|
|
160
|
+
**kwargs,
|
|
161
|
+
) -> httpx.Response:
|
|
162
|
+
self.total_requests += 1
|
|
163
|
+
headers = kwargs.get("headers")
|
|
164
|
+
request = client.build_request("GET", url, headers=headers)
|
|
165
|
+
key = self._key(request, headers)
|
|
166
|
+
self.unique_pages.add(key)
|
|
167
|
+
dynamic = request.url.path in DYNAMIC_PATHS
|
|
168
|
+
|
|
169
|
+
cached = self._lookup(key, allow_disk=not dynamic)
|
|
170
|
+
if cached is not None:
|
|
171
|
+
return httpx.Response(200, text=cached, request=request)
|
|
172
|
+
|
|
173
|
+
self.misses += 1
|
|
174
|
+
if self.mode == "replay":
|
|
175
|
+
self.live_fallbacks += 1
|
|
176
|
+
self._throttle()
|
|
177
|
+
self.live_fetches += 1
|
|
178
|
+
resp = orig_get(client, url, **kwargs)
|
|
179
|
+
if resp.status_code == 200 and self.mode in ("record", "live"):
|
|
180
|
+
self._store(key, resp.text, to_disk=not dynamic)
|
|
181
|
+
return resp
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RunStats:
|
|
185
|
+
"""Durations for each phase, reported in the summary footer."""
|
|
186
|
+
|
|
187
|
+
def __init__(self) -> None:
|
|
188
|
+
self.phase1 = 0.0
|
|
189
|
+
self.warm = 0.0
|
|
190
|
+
self.replay = 0.0
|
|
191
|
+
|
|
192
|
+
|
|
44
193
|
# ---------------------------------------------------------------------------
|
|
45
194
|
# Extractors
|
|
46
195
|
# ---------------------------------------------------------------------------
|
|
@@ -96,10 +245,7 @@ def is_signature_block(code: str) -> bool:
|
|
|
96
245
|
|
|
97
246
|
# Last line: ")" or ") -> ReturnType"
|
|
98
247
|
last = lines[-1].strip()
|
|
99
|
-
|
|
100
|
-
return False
|
|
101
|
-
|
|
102
|
-
return True
|
|
248
|
+
return bool(re.match(r"^\s*\)\s*(->\s*\S+)?\s*$", last))
|
|
103
249
|
|
|
104
250
|
|
|
105
251
|
def normalize_code(code: str) -> str:
|
|
@@ -190,7 +336,7 @@ async def exec_async(code: str, timeout: float) -> tuple[bool, str, float]:
|
|
|
190
336
|
wrapped = "async def __example():\n" + textwrap.indent(code, " ")
|
|
191
337
|
|
|
192
338
|
compiled = compile(wrapped, "<doc_example>", "exec")
|
|
193
|
-
exec(compiled, namespace)
|
|
339
|
+
exec(compiled, namespace) # noqa: S102
|
|
194
340
|
out = io.StringIO()
|
|
195
341
|
err = io.StringIO()
|
|
196
342
|
with redirect_stdout(out), redirect_stderr(err):
|
|
@@ -205,9 +351,9 @@ async def exec_async(code: str, timeout: float) -> tuple[bool, str, float]:
|
|
|
205
351
|
output += "\n[stderr]\n" + err.getvalue().strip()
|
|
206
352
|
return True, output, elapsed
|
|
207
353
|
|
|
208
|
-
except
|
|
354
|
+
except TimeoutError:
|
|
209
355
|
return False, f"TIMEOUT after {time.monotonic() - start:.1f}s", time.monotonic() - start
|
|
210
|
-
except Exception as e:
|
|
356
|
+
except Exception as e: # noqa: BLE001
|
|
211
357
|
tb = traceback.format_exc()
|
|
212
358
|
return False, f"{type(e).__name__}: {e}\n{tb}", time.monotonic() - start
|
|
213
359
|
|
|
@@ -229,7 +375,7 @@ async def exec_sync(code: str, timeout: float) -> tuple[bool, str, float]:
|
|
|
229
375
|
out = io.StringIO()
|
|
230
376
|
err = io.StringIO()
|
|
231
377
|
with redirect_stdout(out), redirect_stderr(err):
|
|
232
|
-
exec(compiled, namespace)
|
|
378
|
+
exec(compiled, namespace) # noqa: S102
|
|
233
379
|
return out.getvalue().strip() or "(no output)"
|
|
234
380
|
|
|
235
381
|
loop = asyncio.get_running_loop()
|
|
@@ -241,9 +387,9 @@ async def exec_sync(code: str, timeout: float) -> tuple[bool, str, float]:
|
|
|
241
387
|
if not ok:
|
|
242
388
|
return False, msg, time.monotonic() - start
|
|
243
389
|
return True, output, time.monotonic() - start
|
|
244
|
-
except
|
|
390
|
+
except TimeoutError:
|
|
245
391
|
return False, f"TIMEOUT after {time.monotonic() - start:.1f}s", time.monotonic() - start
|
|
246
|
-
except Exception as e:
|
|
392
|
+
except Exception as e: # noqa: BLE001
|
|
247
393
|
return False, f"{type(e).__name__}: {e}", time.monotonic() - start
|
|
248
394
|
|
|
249
395
|
|
|
@@ -255,12 +401,67 @@ def has_skip_marker(code: str) -> bool:
|
|
|
255
401
|
return any(line.strip().startswith("# doc-check: skip") for line in code.splitlines())
|
|
256
402
|
|
|
257
403
|
|
|
258
|
-
async def
|
|
404
|
+
async def execute_blocks(
|
|
405
|
+
results: list[Result],
|
|
406
|
+
timeout: float,
|
|
407
|
+
cache: HTTPCache,
|
|
408
|
+
label: str,
|
|
409
|
+
*,
|
|
410
|
+
record_results: bool,
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Execute every runnable block against the current cache mode.
|
|
413
|
+
|
|
414
|
+
In warm mode (``record_results=False``) execution is best-effort: the
|
|
415
|
+
point is to populate the cache/fixtures and de-duplicate fetches, so
|
|
416
|
+
failures do not affect the run. In replay/live mode (``record_results=True``)
|
|
417
|
+
results are recorded and validated.
|
|
418
|
+
"""
|
|
419
|
+
print(f"=== Phase: {label} ===")
|
|
420
|
+
for i, r in enumerate(results):
|
|
421
|
+
code = r.code
|
|
422
|
+
|
|
423
|
+
# Skip non-runnable blocks
|
|
424
|
+
if not is_runnable(code):
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Skip user-marked blocks
|
|
428
|
+
if has_skip_marker(code):
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
if is_async(code):
|
|
432
|
+
run_ok, run_msg, dur = await exec_async(code, timeout)
|
|
433
|
+
else:
|
|
434
|
+
run_ok, run_msg, dur = await exec_sync(code, timeout)
|
|
435
|
+
|
|
436
|
+
if record_results:
|
|
437
|
+
results[i] = Result(
|
|
438
|
+
r.file, r.code, r.syntax_ok, r.syntax_error, r.imports_ok,
|
|
439
|
+
r.import_errors, run_ok, run_msg, dur,
|
|
440
|
+
)
|
|
441
|
+
tag = "OK" if run_ok else "FAIL"
|
|
442
|
+
else:
|
|
443
|
+
tag = "warm-ok" if run_ok else "warm-err"
|
|
444
|
+
|
|
445
|
+
print(f" {tag} ({dur:.1f}s) {r.file}")
|
|
446
|
+
if record_results:
|
|
447
|
+
msg_short = run_msg[:200].replace("\n", "\\n")
|
|
448
|
+
print(f" -> {msg_short}")
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
async def run_all(
|
|
452
|
+
timeout: float,
|
|
453
|
+
cache: HTTPCache,
|
|
454
|
+
*,
|
|
455
|
+
skip_warm: bool,
|
|
456
|
+
live: bool,
|
|
457
|
+
) -> tuple[list[Result], RunStats]:
|
|
458
|
+
stats = RunStats()
|
|
259
459
|
results: list[Result] = []
|
|
260
460
|
|
|
261
461
|
print("=== Phase 1: Syntax & Import Check ===")
|
|
462
|
+
phase1_start = time.monotonic()
|
|
262
463
|
for path in sorted(all_doc_files()):
|
|
263
|
-
with open(path, encoding="utf-8") as f:
|
|
464
|
+
with open(path, encoding="utf-8") as f: # noqa: ASYNC230
|
|
264
465
|
text = f.read()
|
|
265
466
|
blocks = extract_blocks(text)
|
|
266
467
|
rel = os.path.relpath(path, REPO_ROOT)
|
|
@@ -297,37 +498,39 @@ async def run_all(timeout: float) -> list[Result]:
|
|
|
297
498
|
results.append(Result(rel, code, True, "", True, [], None, "", 0.0))
|
|
298
499
|
print(f" OK {rel}")
|
|
299
500
|
|
|
501
|
+
stats.phase1 = time.monotonic() - phase1_start
|
|
300
502
|
print(f"\n >> Phase 1 complete: all {len(results)} block(s) OK\n")
|
|
301
503
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
504
|
+
if live:
|
|
505
|
+
cache.mode = "live"
|
|
506
|
+
print(f"=== Live execution ({timeout}s timeout per block, in-memory dedup) ===")
|
|
507
|
+
replay_start = time.monotonic()
|
|
508
|
+
await execute_blocks(results, timeout, cache, "Live (validated)", record_results=True)
|
|
509
|
+
stats.replay = time.monotonic() - replay_start
|
|
510
|
+
return results, stats
|
|
511
|
+
|
|
512
|
+
if not skip_warm:
|
|
513
|
+
cache.mode = "record"
|
|
514
|
+
warm_start = time.monotonic()
|
|
515
|
+
await execute_blocks(results, timeout, cache, "Warm (record fixtures)", record_results=False)
|
|
516
|
+
stats.warm = time.monotonic() - warm_start
|
|
517
|
+
print(f"\n >> Warm phase complete: fixtures ready in {cache.fixtures_dir}\n")
|
|
518
|
+
|
|
519
|
+
cache.mode = "replay"
|
|
520
|
+
print(f"=== Replay execution ({timeout}s timeout per block, offline against fixtures) ===")
|
|
521
|
+
orig_acquire = RateLimiter.acquire
|
|
522
|
+
RateLimiter.acquire = lambda self: None # type: ignore[method-assign]
|
|
523
|
+
try:
|
|
524
|
+
replay_start = time.monotonic()
|
|
525
|
+
await execute_blocks(results, timeout, cache, "Replay (validated)", record_results=True)
|
|
526
|
+
stats.replay = time.monotonic() - replay_start
|
|
527
|
+
finally:
|
|
528
|
+
RateLimiter.acquire = orig_acquire # type: ignore[method-assign]
|
|
326
529
|
|
|
327
|
-
return results
|
|
530
|
+
return results, stats
|
|
328
531
|
|
|
329
532
|
|
|
330
|
-
def print_report(results: list[Result]) -> int:
|
|
533
|
+
def print_report(results: list[Result], cache: HTTPCache | None = None, stats: RunStats | None = None) -> int:
|
|
331
534
|
run_attempts = [r for r in results if r.run_ok is not None]
|
|
332
535
|
run_oks = sum(1 for r in run_attempts if r.run_ok is True)
|
|
333
536
|
run_fails = [r for r in results if r.run_ok is False]
|
|
@@ -340,6 +543,23 @@ def print_report(results: list[Result]) -> int:
|
|
|
340
543
|
print(f" EMPTY OUTPUT FAILURES: {no_output} (prints were silently skipped)")
|
|
341
544
|
print(f"{'='*60}")
|
|
342
545
|
|
|
546
|
+
if cache is not None:
|
|
547
|
+
saved = cache.total_requests - cache.live_fetches
|
|
548
|
+
print("\nHTTP CACHE STATS:")
|
|
549
|
+
print(f" Requests intercepted: {cache.total_requests}")
|
|
550
|
+
print(f" Unique pages touched: {len(cache.unique_pages)}")
|
|
551
|
+
print(f" Live fetches: {cache.live_fetches} (dedup saved {max(saved, 0)})")
|
|
552
|
+
print(f" Cache hits: {cache.hits_mem} mem / {cache.hits_disk} disk")
|
|
553
|
+
print(f" Cache misses: {cache.misses}")
|
|
554
|
+
if cache.live_fallbacks:
|
|
555
|
+
print(f" Live fallbacks: {cache.live_fallbacks}")
|
|
556
|
+
if stats is not None:
|
|
557
|
+
print("\nPHASE DURATIONS:")
|
|
558
|
+
print(f" Phase 1 (static): {stats.phase1:.1f}s")
|
|
559
|
+
if stats.warm:
|
|
560
|
+
print(f" Warm (record): {stats.warm:.1f}s")
|
|
561
|
+
print(f" Replay/validate: {stats.replay:.1f}s")
|
|
562
|
+
|
|
343
563
|
if run_fails:
|
|
344
564
|
print(f"\nRUNTIME FAILURES ({len(run_fails)}):")
|
|
345
565
|
for r in run_fails:
|
|
@@ -371,8 +591,26 @@ def main():
|
|
|
371
591
|
parser = argparse.ArgumentParser(description="Check MDX doc examples")
|
|
372
592
|
parser.add_argument("--timeout", type=float, default=60.0, help="Per-example timeout (s)")
|
|
373
593
|
parser.add_argument("--log", type=str, default="", help="Path to save verbose output log")
|
|
594
|
+
parser.add_argument(
|
|
595
|
+
"--fixtures-dir", type=str, default=str(DEFAULT_FIXTURES_DIR),
|
|
596
|
+
help="Directory for cached HTML fixtures (default: .cache/mdx-html)",
|
|
597
|
+
)
|
|
598
|
+
parser.add_argument(
|
|
599
|
+
"--skip-warm", action="store_true",
|
|
600
|
+
help="Skip the warm (record) phase; replay against existing fixtures",
|
|
601
|
+
)
|
|
602
|
+
parser.add_argument(
|
|
603
|
+
"--live", action="store_true",
|
|
604
|
+
help="Single live pass with in-memory dedup (no disk fixtures)",
|
|
605
|
+
)
|
|
374
606
|
args = parser.parse_args()
|
|
375
607
|
|
|
608
|
+
fixtures_dir = None
|
|
609
|
+
if not args.live and args.fixtures_dir:
|
|
610
|
+
fixtures_dir = Path(args.fixtures_dir)
|
|
611
|
+
cache = HTTPCache(fixtures_dir=fixtures_dir, mode="replay")
|
|
612
|
+
stats = RunStats()
|
|
613
|
+
|
|
376
614
|
log_lines: list[str] = []
|
|
377
615
|
|
|
378
616
|
# Override print in the check module's namespace
|
|
@@ -392,15 +630,18 @@ def main():
|
|
|
392
630
|
builtins.print = tee_print # type: ignore
|
|
393
631
|
|
|
394
632
|
try:
|
|
395
|
-
|
|
396
|
-
|
|
633
|
+
cache.install()
|
|
634
|
+
results, stats = asyncio.run(
|
|
635
|
+
run_all(args.timeout, cache, skip_warm=args.skip_warm, live=args.live)
|
|
636
|
+
)
|
|
637
|
+
exit_code = print_report(results, cache=cache, stats=stats)
|
|
397
638
|
finally:
|
|
639
|
+
cache.restore()
|
|
398
640
|
builtins.print = orig_print
|
|
399
641
|
|
|
400
642
|
if args.log:
|
|
401
643
|
with open(args.log, "w", encoding="utf-8") as f:
|
|
402
|
-
for line in _log_lines
|
|
403
|
-
f.write(line + "\n")
|
|
644
|
+
f.writelines(line + "\n" for line in _log_lines)
|
|
404
645
|
orig_print(f"\nLog saved to: {args.log}")
|
|
405
646
|
|
|
406
647
|
sys.exit(exit_code)
|