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.
Files changed (218) hide show
  1. vlrdevapi-2.4.1/.nvmrc +1 -0
  2. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.readthedocs.yaml +3 -2
  3. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CHANGELOG.md +125 -0
  4. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/PKG-INFO +5 -5
  5. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/pyproject.toml +11 -11
  6. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/scripts/check_mdx_examples.py +31 -0
  7. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/scripts/download_fixtures.py +149 -4
  8. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/__init__.py +1 -1
  9. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_base.py +1 -1
  10. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/models.py +4 -0
  11. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/parser.py +49 -4
  12. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/parser.py +2 -1
  13. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/parser.py +2 -1
  14. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/parser.py +2 -1
  15. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/parser.py +2 -1
  16. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/parser.py +2 -1
  17. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/common.py +32 -7
  18. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/parser.py +2 -1
  19. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/parser.py +2 -1
  20. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/parser.py +2 -1
  21. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/parser.py +2 -1
  22. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/common.py +1 -1
  23. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/parser.py +2 -1
  24. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/_common_parsing.py +1 -1
  25. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/parser.py +1 -1
  26. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/parser.py +1 -1
  27. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/parser.py +2 -1
  28. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/parser.py +1 -1
  29. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/parser.py +1 -1
  30. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/_utils.py +57 -3
  31. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/namespace.py +24 -8
  32. vlrdevapi-2.4.1/src/vlrdevapi/_series/economy/parser.py +169 -0
  33. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/models.py +29 -0
  34. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/namespace.py +3 -1
  35. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/parser.py +130 -47
  36. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/parser.py +2 -1
  37. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/parser.py +54 -16
  38. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/namespace.py +18 -6
  39. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/parser.py +7 -2
  40. vlrdevapi-2.4.1/src/vlrdevapi/_series/vods/parser.py +50 -0
  41. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/parser.py +2 -1
  42. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/namespace.py +1 -1
  43. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/parser.py +1 -1
  44. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/parser.py +2 -1
  45. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/parser.py +2 -1
  46. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/parser.py +2 -1
  47. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/parser.py +2 -1
  48. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/parser.py +2 -1
  49. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/pagination.py +1 -1
  50. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/team_enrichment.py +1 -1
  51. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/team_parsing.py +1 -1
  52. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/__init__.py +14 -0
  53. vlrdevapi-2.4.1/src/vlrdevapi/commons/regions.py +242 -0
  54. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/timezone.py +1 -1
  55. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/fetcher.py +1 -1
  56. vlrdevapi-2.4.1/uv.lock +1165 -0
  57. vlrdevapi-2.3.0/src/vlrdevapi/_series/economy/parser.py +0 -139
  58. vlrdevapi-2.3.0/src/vlrdevapi/_series/vods/parser.py +0 -30
  59. vlrdevapi-2.3.0/uv.lock +0 -1049
  60. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.gitattributes +0 -0
  61. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  62. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  63. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/cleanup-caches.yml +0 -0
  64. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/docs-check.yml +0 -0
  65. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/publish.yml +0 -0
  66. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.github/workflows/test.yml +0 -0
  67. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.gitignore +0 -0
  68. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/.python-version +0 -0
  69. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CODE_OF_CONDUCT.md +0 -0
  70. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/CONTRIBUTING.md +0 -0
  71. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/LICENCE +0 -0
  72. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/README.md +0 -0
  73. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/SECURITY.md +0 -0
  74. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/SUPPORT.md +0 -0
  75. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/__init__.pyi +0 -0
  76. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_cache.py +0 -0
  77. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_client.py +0 -0
  78. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/__init__.py +0 -0
  79. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/_common.py +0 -0
  80. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/__init__.py +0 -0
  81. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/namespace.py +0 -0
  82. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/info/namespace.pyi +0 -0
  83. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/__init__.py +0 -0
  84. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/models.py +0 -0
  85. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/namespace.py +0 -0
  86. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/list/namespace.pyi +0 -0
  87. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/__init__.py +0 -0
  88. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/models.py +0 -0
  89. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/namespace.py +0 -0
  90. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/matches/namespace.pyi +0 -0
  91. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/namespace.py +0 -0
  92. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/namespace.pyi +0 -0
  93. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/__init__.py +0 -0
  94. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/models.py +0 -0
  95. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/namespace.py +0 -0
  96. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/stages/namespace.pyi +0 -0
  97. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/__init__.py +0 -0
  98. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/models.py +0 -0
  99. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/namespace.py +0 -0
  100. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/standings/namespace.pyi +0 -0
  101. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/__init__.py +0 -0
  102. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/models.py +0 -0
  103. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/namespace.py +0 -0
  104. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_event/teams/namespace.pyi +0 -0
  105. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/__init__.py +0 -0
  106. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/__init__.py +0 -0
  107. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/models.py +0 -0
  108. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/namespace.py +0 -0
  109. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/completed/namespace.pyi +0 -0
  110. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/__init__.py +0 -0
  111. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/models.py +0 -0
  112. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/namespace.py +0 -0
  113. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/live/namespace.pyi +0 -0
  114. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/namespace.py +0 -0
  115. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/namespace.pyi +0 -0
  116. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/__init__.py +0 -0
  117. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/models.py +0 -0
  118. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/namespace.py +0 -0
  119. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_matches/upcoming/namespace.pyi +0 -0
  120. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/__init__.py +0 -0
  121. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/__init__.py +0 -0
  122. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/models.py +0 -0
  123. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/namespace.py +0 -0
  124. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/article/namespace.pyi +0 -0
  125. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/__init__.py +0 -0
  126. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/models.py +0 -0
  127. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/namespace.py +0 -0
  128. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/list/namespace.pyi +0 -0
  129. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/namespace.py +0 -0
  130. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_news/namespace.pyi +0 -0
  131. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/__init__.py +0 -0
  132. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/__init__.py +0 -0
  133. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/models.py +0 -0
  134. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/namespace.py +0 -0
  135. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/agents/namespace.pyi +0 -0
  136. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/__init__.py +0 -0
  137. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/models.py +0 -0
  138. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/namespace.py +0 -0
  139. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/info/namespace.pyi +0 -0
  140. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/__init__.py +0 -0
  141. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/models.py +0 -0
  142. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/namespace.py +0 -0
  143. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/matches/namespace.pyi +0 -0
  144. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/namespace.py +0 -0
  145. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/namespace.pyi +0 -0
  146. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/__init__.py +0 -0
  147. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/models.py +0 -0
  148. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/namespace.py +0 -0
  149. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/profile/namespace.pyi +0 -0
  150. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/__init__.py +0 -0
  151. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/models.py +0 -0
  152. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/namespace.py +0 -0
  153. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_player/teams/namespace.pyi +0 -0
  154. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/__init__.py +0 -0
  155. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/__init__.py +0 -0
  156. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/models.py +0 -0
  157. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/economy/namespace.pyi +0 -0
  158. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/__init__.py +0 -0
  159. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/info/namespace.pyi +0 -0
  160. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/match_namespace.py +0 -0
  161. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/match_namespace.pyi +0 -0
  162. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/namespace.py +0 -0
  163. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/namespace.pyi +0 -0
  164. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/__init__.py +0 -0
  165. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/models.py +0 -0
  166. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/namespace.py +0 -0
  167. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/performance/namespace.pyi +0 -0
  168. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/__init__.py +0 -0
  169. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/models.py +0 -0
  170. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/namespace.py +0 -0
  171. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/players/namespace.pyi +0 -0
  172. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/__init__.py +0 -0
  173. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/models.py +0 -0
  174. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/rounds/namespace.pyi +0 -0
  175. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/__init__.py +0 -0
  176. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/models.py +0 -0
  177. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/namespace.py +0 -0
  178. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_series/vods/namespace.pyi +0 -0
  179. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/__init__.py +0 -0
  180. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/__init__.py +0 -0
  181. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/models.py +0 -0
  182. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/namespace.py +0 -0
  183. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/completed_matches/namespace.pyi +0 -0
  184. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/__init__.py +0 -0
  185. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/models.py +0 -0
  186. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/info/namespace.pyi +0 -0
  187. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/namespace.py +0 -0
  188. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/namespace.pyi +0 -0
  189. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/__init__.py +0 -0
  190. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/models.py +0 -0
  191. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/namespace.py +0 -0
  192. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/placements/namespace.pyi +0 -0
  193. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/__init__.py +0 -0
  194. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/models.py +0 -0
  195. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/namespace.py +0 -0
  196. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/roster/namespace.pyi +0 -0
  197. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/__init__.py +0 -0
  198. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/models.py +0 -0
  199. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/namespace.py +0 -0
  200. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/stats/namespace.pyi +0 -0
  201. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/__init__.py +0 -0
  202. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/models.py +0 -0
  203. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/namespace.py +0 -0
  204. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/transactions/namespace.pyi +0 -0
  205. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/__init__.py +0 -0
  206. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/models.py +0 -0
  207. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/namespace.py +0 -0
  208. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_team/upcoming_matches/namespace.pyi +0 -0
  209. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/__init__.py +0 -0
  210. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/_utils/paths.py +0 -0
  211. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/countries.py +0 -0
  212. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/datetime.py +0 -0
  213. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/mappings.py +0 -0
  214. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/commons/prizes.py +0 -0
  215. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/exceptions.py +0 -0
  216. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/py.typed +0 -0
  217. {vlrdevapi-2.3.0 → vlrdevapi-2.4.1}/src/vlrdevapi/validators.py +0 -0
  218. {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 zensical mkdocstrings mkdocstrings-python
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: vlrdevapi
3
- Version: 2.3.0
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.12.5
42
- Requires-Dist: selectolax>=0.4.7
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>=8.0; extra == 'test'
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.3.0"
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.12.5",
26
- "selectolax>=0.4.7",
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>=8.0",
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.15.10",
44
- "pytest>=8.0",
45
- "tzdata>=2026.1",
46
- "ty>=0.0.31",
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.46",
50
- "mkdocstrings>=0.28.0",
51
- "mkdocstrings-python>=1.16.0",
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=126", "news/news_page126.html", {}),
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/2283/overview.html", {}),
132
- ("/event/2682", "event/2682_americas_kickoff/overview.html", {}),
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
@@ -54,7 +54,7 @@ if TYPE_CHECKING:
54
54
  news: NewsNamespace
55
55
  client: type[VLRClient]
56
56
 
57
- __version__ = "2.3.0"
57
+ __version__ = "2.4.1"
58
58
 
59
59
  _default_client: "VLRClient | None" = None
60
60
 
@@ -6,7 +6,7 @@ from collections.abc import Callable
6
6
  from typing import Any
7
7
 
8
8
  import httpx
9
- from selectolax.parser import HTMLParser
9
+ from selectolax.lexbor import LexborHTMLParser as HTMLParser
10
10
 
11
11
  from vlrdevapi.fetcher import (
12
12
  BASE_URL,
@@ -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.parser import HTMLParser, Node
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.parser import HTMLParser, Node
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.parser import HTMLParser, Node
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.parser import HTMLParser, Node
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.parser import HTMLParser, Node
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
@@ -1,4 +1,5 @@
1
- from selectolax.parser import HTMLParser, Node
1
+ from selectolax.lexbor import LexborHTMLParser as HTMLParser
2
+ from selectolax.lexbor import LexborNode as Node
2
3
 
3
4
  from vlrdevapi._event.teams.models import Team, TeamPlayer
4
5