vlrdevapi 2.3.0__tar.gz → 2.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CHANGELOG.md +87 -0
  2. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/PKG-INFO +2 -2
  3. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/pyproject.toml +1 -1
  4. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/scripts/download_fixtures.py +149 -4
  5. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/__init__.py +1 -1
  6. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/models.py +4 -0
  7. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/parser.py +47 -3
  8. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/common.py +30 -6
  9. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/_utils.py +56 -2
  10. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/namespace.py +20 -7
  11. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/parser.py +7 -2
  12. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/models.py +29 -0
  13. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/namespace.py +3 -1
  14. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/parser.py +120 -46
  15. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/namespace.py +17 -5
  16. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/parser.py +6 -1
  17. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/__init__.py +14 -0
  18. vlrdevapi-2.4.0/src/vlrdevapi/commons/regions.py +242 -0
  19. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/uv.lock +1 -1
  20. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.gitattributes +0 -0
  21. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/cleanup-caches.yml +0 -0
  24. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/docs-check.yml +0 -0
  25. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/publish.yml +0 -0
  26. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.github/workflows/test.yml +0 -0
  27. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.gitignore +0 -0
  28. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.python-version +0 -0
  29. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/.readthedocs.yaml +0 -0
  30. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CODE_OF_CONDUCT.md +0 -0
  31. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/CONTRIBUTING.md +0 -0
  32. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/LICENCE +0 -0
  33. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/README.md +0 -0
  34. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/SECURITY.md +0 -0
  35. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/SUPPORT.md +0 -0
  36. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/scripts/check_mdx_examples.py +0 -0
  37. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/__init__.pyi +0 -0
  38. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_base.py +0 -0
  39. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_cache.py +0 -0
  40. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_client.py +0 -0
  41. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/__init__.py +0 -0
  42. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/_common.py +0 -0
  43. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/__init__.py +0 -0
  44. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/namespace.py +0 -0
  45. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/info/namespace.pyi +0 -0
  46. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/__init__.py +0 -0
  47. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/models.py +0 -0
  48. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/namespace.py +0 -0
  49. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/namespace.pyi +0 -0
  50. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/list/parser.py +0 -0
  51. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/__init__.py +0 -0
  52. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/models.py +0 -0
  53. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/namespace.py +0 -0
  54. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/namespace.pyi +0 -0
  55. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/matches/parser.py +0 -0
  56. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/namespace.py +0 -0
  57. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/namespace.pyi +0 -0
  58. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/__init__.py +0 -0
  59. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/models.py +0 -0
  60. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/namespace.py +0 -0
  61. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/namespace.pyi +0 -0
  62. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/stages/parser.py +0 -0
  63. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/__init__.py +0 -0
  64. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/models.py +0 -0
  65. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/namespace.py +0 -0
  66. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/namespace.pyi +0 -0
  67. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/standings/parser.py +0 -0
  68. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/__init__.py +0 -0
  69. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/models.py +0 -0
  70. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/namespace.py +0 -0
  71. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/namespace.pyi +0 -0
  72. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_event/teams/parser.py +0 -0
  73. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/__init__.py +0 -0
  74. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/__init__.py +0 -0
  75. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/models.py +0 -0
  76. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/namespace.py +0 -0
  77. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/namespace.pyi +0 -0
  78. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/completed/parser.py +0 -0
  79. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/__init__.py +0 -0
  80. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/models.py +0 -0
  81. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/namespace.py +0 -0
  82. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/namespace.pyi +0 -0
  83. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/live/parser.py +0 -0
  84. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/namespace.py +0 -0
  85. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/namespace.pyi +0 -0
  86. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/__init__.py +0 -0
  87. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/models.py +0 -0
  88. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/namespace.py +0 -0
  89. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/namespace.pyi +0 -0
  90. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_matches/upcoming/parser.py +0 -0
  91. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/__init__.py +0 -0
  92. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/__init__.py +0 -0
  93. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/models.py +0 -0
  94. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/namespace.py +0 -0
  95. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/namespace.pyi +0 -0
  96. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/article/parser.py +0 -0
  97. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/common.py +0 -0
  98. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/__init__.py +0 -0
  99. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/models.py +0 -0
  100. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/namespace.py +0 -0
  101. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/namespace.pyi +0 -0
  102. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/list/parser.py +0 -0
  103. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/namespace.py +0 -0
  104. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_news/namespace.pyi +0 -0
  105. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/__init__.py +0 -0
  106. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/_common_parsing.py +0 -0
  107. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/__init__.py +0 -0
  108. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/models.py +0 -0
  109. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/namespace.py +0 -0
  110. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/namespace.pyi +0 -0
  111. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/agents/parser.py +0 -0
  112. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/__init__.py +0 -0
  113. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/models.py +0 -0
  114. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/namespace.py +0 -0
  115. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/namespace.pyi +0 -0
  116. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/info/parser.py +0 -0
  117. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/__init__.py +0 -0
  118. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/models.py +0 -0
  119. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/namespace.py +0 -0
  120. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/namespace.pyi +0 -0
  121. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/matches/parser.py +0 -0
  122. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/namespace.py +0 -0
  123. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/namespace.pyi +0 -0
  124. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/__init__.py +0 -0
  125. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/models.py +0 -0
  126. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/namespace.py +0 -0
  127. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/namespace.pyi +0 -0
  128. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/profile/parser.py +0 -0
  129. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/__init__.py +0 -0
  130. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/models.py +0 -0
  131. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/namespace.py +0 -0
  132. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/namespace.pyi +0 -0
  133. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_player/teams/parser.py +0 -0
  134. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/__init__.py +0 -0
  135. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/__init__.py +0 -0
  136. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/models.py +0 -0
  137. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/economy/namespace.pyi +0 -0
  138. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/__init__.py +0 -0
  139. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/info/namespace.pyi +0 -0
  140. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/match_namespace.py +0 -0
  141. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/match_namespace.pyi +0 -0
  142. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/namespace.py +0 -0
  143. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/namespace.pyi +0 -0
  144. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/__init__.py +0 -0
  145. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/models.py +0 -0
  146. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/namespace.py +0 -0
  147. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/namespace.pyi +0 -0
  148. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/performance/parser.py +0 -0
  149. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/__init__.py +0 -0
  150. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/models.py +0 -0
  151. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/namespace.py +0 -0
  152. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/namespace.pyi +0 -0
  153. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/players/parser.py +0 -0
  154. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/__init__.py +0 -0
  155. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/models.py +0 -0
  156. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/rounds/namespace.pyi +0 -0
  157. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/__init__.py +0 -0
  158. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/models.py +0 -0
  159. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/namespace.py +0 -0
  160. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/namespace.pyi +0 -0
  161. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_series/vods/parser.py +0 -0
  162. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/__init__.py +0 -0
  163. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/__init__.py +0 -0
  164. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/models.py +0 -0
  165. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/namespace.py +0 -0
  166. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/namespace.pyi +0 -0
  167. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/completed_matches/parser.py +0 -0
  168. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/__init__.py +0 -0
  169. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/models.py +0 -0
  170. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/namespace.py +0 -0
  171. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/namespace.pyi +0 -0
  172. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/info/parser.py +0 -0
  173. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/namespace.py +0 -0
  174. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/namespace.pyi +0 -0
  175. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/__init__.py +0 -0
  176. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/models.py +0 -0
  177. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/namespace.py +0 -0
  178. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/namespace.pyi +0 -0
  179. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/placements/parser.py +0 -0
  180. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/__init__.py +0 -0
  181. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/models.py +0 -0
  182. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/namespace.py +0 -0
  183. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/namespace.pyi +0 -0
  184. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/roster/parser.py +0 -0
  185. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/__init__.py +0 -0
  186. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/models.py +0 -0
  187. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/namespace.py +0 -0
  188. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/namespace.pyi +0 -0
  189. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/stats/parser.py +0 -0
  190. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/__init__.py +0 -0
  191. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/models.py +0 -0
  192. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/namespace.py +0 -0
  193. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/namespace.pyi +0 -0
  194. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/transactions/parser.py +0 -0
  195. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/__init__.py +0 -0
  196. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/models.py +0 -0
  197. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/namespace.py +0 -0
  198. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/namespace.pyi +0 -0
  199. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_team/upcoming_matches/parser.py +0 -0
  200. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/__init__.py +0 -0
  201. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/pagination.py +0 -0
  202. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/paths.py +0 -0
  203. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/team_enrichment.py +0 -0
  204. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/_utils/team_parsing.py +0 -0
  205. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/countries.py +0 -0
  206. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/datetime.py +0 -0
  207. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/mappings.py +0 -0
  208. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/prizes.py +0 -0
  209. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/commons/timezone.py +0 -0
  210. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/exceptions.py +0 -0
  211. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/fetcher.py +0 -0
  212. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/py.typed +0 -0
  213. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/src/vlrdevapi/validators.py +0 -0
  214. {vlrdevapi-2.3.0 → vlrdevapi-2.4.0}/wrangler.jsonc +0 -0
@@ -5,6 +5,92 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [2.4.0] - 2026-08-19
9
+
10
+ ### Added
11
+
12
+ - **Bo1 series support** — single-map series now resolve games from
13
+ `.vm-stats-game` containers when nav tabs are absent. `resolve_game_id`
14
+ raises `ParsingError` instead of silently returning wrong data on
15
+ out-of-range or empty game lists.
16
+ - **Event region resolution** — `_parse_breadcrumb` resolves `subregion=`
17
+ breadcrumb links (e.g. "Japan", "North America") to their parent region
18
+ via `commons.regions.resolve_subregion_to_region`. A new
19
+ `EventRegion.subregion` field preserves the original subregion name
20
+ (e.g. `"Japan"`); `None` for `region=` links.
21
+ - **Event country-to-region fallback** — when no breadcrumb region is
22
+ present, the `region_location` country flag is resolved via
23
+ `resolve_country_to_region` to infer the parent region.
24
+ - **Event region deduplication** — duplicate region entries (from both
25
+ `region=` and `subregion=` breadcrumbs for the same parent region)
26
+ are collapsed, preferring the entry that carries a subregion name.
27
+ - **`commons.regions` module** — new shared utilities for region
28
+ resolution: `VALID_REGIONS`, `SUBREGION_TO_REGION` (17 VCL/GC
29
+ subregions), `COUNTRY_TO_REGION` (~100+ countries),
30
+ `resolve_subregion_to_region`, and `resolve_country_to_region`.
31
+ - **`commons.__init__` re-exports** — `resolve_subregion_to_region`,
32
+ `resolve_country_to_region`, `VALID_REGIONS`, `SUBREGION_TO_REGION`,
33
+ and `COUNTRY_TO_REGION` are now re-exported from `vlrdevapi.commons`.
34
+ - **Series forfeit detection** — new `ForfeitInfo` model on
35
+ `SeriesInfo.forfeit` exposes `forfeited`, `team`, `team_id`, and
36
+ `reason` fields. Parser detects `"forfeited by TEAM"` in the
37
+ `match-header-vs-note` element and maps the team name to the
38
+ corresponding `team1`/`team2` for ID resolution. Note-level
39
+ forfeit text (e.g. "Blind Esports Forfeit Map 2") is also parsed.
40
+ - **Series match notes** — new `SeriesInfo.notes` list captures
41
+ non-veto `match-header-note` text such as technical pause warnings
42
+ and lobby remake notices.
43
+
44
+ ### Changed
45
+
46
+ - **Series economy enrichment** — team matching now tries abbreviation
47
+ names from `.ovw-player-tag` elements first (e.g. "SEN"), then falls
48
+ back to full team names. This fixes mismatches where the economy tab
49
+ shows a different abbreviation than the overview header.
50
+ - **Series rounds enrichment** — same abbreviation-first matching
51
+ strategy as economy.
52
+ - **Series info veto parsing** — Bo1 veto fallback now handles the
53
+ "bans + decider" pattern (e.g. `"Ban1 remains"`) in addition to
54
+ pick/ban. The PICK badge is excluded from map names via
55
+ `span.text(deep=False)`.
56
+ - **Series info notes parsing** — the parser now iterates all
57
+ `match-header-note` elements instead of only the first. This
58
+ correctly separates forfeit reasons, informational notes, and
59
+ map veto text when multiple notes are present.
60
+
61
+ ### Fixed
62
+
63
+ - **Economy bank/spend parsing** — `int()` calls on `.rnd-sq` `title`
64
+ attributes and `.rnd-num` text are wrapped in `try/except ValueError`;
65
+ malformed values default to `0` instead of crashing the parser.
66
+ - **Event region from location country** — events that only have a
67
+ "Location" label with a flag (no separate "Region" label) now resolve
68
+ the region from `location.country` via `resolve_country_to_region`.
69
+ Previously the country-to-region fallback only checked `region_location`.
70
+ - **Phantom pagination in matches** — terminal pages no longer report
71
+ `has_next_page = True` (from DaviAlcanfor PR #50).
72
+
73
+ ### Documentation
74
+
75
+ - **Event info reference** — `EventRegion` fields table now documents
76
+ the `subregion` field. Region example demonstrates subregion access.
77
+ `regions` field description explains deduplication and International
78
+ collapse.
79
+ - **Series info reference** — `ForfeitInfo` fields table documents the
80
+ new `forfeit` nested model. `notes` field description explains the
81
+ distinction between forfeit reasons and informational notes.
82
+ - **Series economy reference** — docstrings reference correct model
83
+ types (`RoundEconomyData`, `bank_team1`).
84
+ - **Series rounds reference** — docstrings reference correct model
85
+ types (`RoundData`, `winner_team_name`).
86
+ - **`commons.regions` module** — docstrings describe all mapping
87
+ dictionaries and resolution functions.
88
+ - **Project structure** — contributing guide updated with actual
89
+ underscore-prefixed directory names and new modules (`_news/`,
90
+ `_utils/`, `commons/regions.py`).
91
+ - **Module docstrings** — added to `economy/parser.py`,
92
+ `rounds/parser.py`, and `commons/__init__.py`.
93
+
8
94
  ## [2.3.0] - 2026-08-11
9
95
 
10
96
  ### Added
@@ -193,6 +279,7 @@ The API surface, module structure, and type system are all new.
193
279
  - `aiohttp` and `asyncio` dependencies.
194
280
  - v1.x match/event/team parsing modules.
195
281
 
282
+ [2.4.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.3.0...v2.4.0
196
283
  [2.3.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.2.0...v2.3.0
197
284
  [2.2.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.1.0...v2.2.0
198
285
  [2.1.0]: https://github.com/Vanshbordia/vlrdevapi/compare/v2.0.1...v2.1.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vlrdevapi
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: A Python library to scrape data from vlr.gg
5
5
  Project-URL: Homepage, https://vlrdevapi.pages.dev/
6
6
  Project-URL: Repository, https://github.com/Vanshbordia/vlrdevapi
@@ -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.0"
8
8
  description = "A Python library to scrape data from vlr.gg"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.parser import HTMLParser
424
+
425
+ doc = HTMLParser(html)
426
+ pagination = doc.css_first("div.action-container-pages")
427
+ if pagination is None:
428
+ return None
429
+ max_page = 0
430
+ for el in pagination.css("a.btn.mod-page, span.btn.mod-page"):
431
+ try:
432
+ num = int(el.text(strip=True))
433
+ if num > max_page:
434
+ max_page = num
435
+ except ValueError:
436
+ continue
437
+ return max_page if max_page > 0 else None
438
+
439
+
440
+ async def _download_news_last_page(client: httpx.AsyncClient) -> None:
441
+ """Dynamically determine and download the last news page."""
442
+ out = FIXTURES_DIR / "news" / "news_last_page.html"
443
+ out.parent.mkdir(parents=True, exist_ok=True)
444
+ if out.exists():
445
+ print("SKIP (exists) news/news_last_page.html")
446
+ return
447
+
448
+ print("GET /news (to find last page number)")
449
+ try:
450
+ resp = await client.get("/news")
451
+ resp.raise_for_status()
452
+ last_page = _parse_last_page(resp.text)
453
+ if last_page is None:
454
+ print(" WARNING: could not parse last page number")
455
+ return
456
+
457
+ last_path = f"/news/?page={last_page}"
458
+ print(f"GET {BASE_URL}{last_path}")
459
+ resp2 = await client.get(last_path)
460
+ resp2.raise_for_status()
461
+ out.write_text(resp2.text, encoding="utf-8")
462
+ print(f" -> {out.relative_to(FIXTURES_DIR.parent)}")
463
+ except (httpx.HTTPStatusError, httpx.RequestError) as e:
464
+ print(f" ERROR {e}")
465
+
466
+
324
467
  async def download_all():
325
468
  async with httpx.AsyncClient(
326
469
  base_url=BASE_URL, headers=HEADERS, follow_redirects=True, timeout=30
327
470
  ) as client:
471
+ await _download_news_last_page(client)
472
+
328
473
  total = len(ROUTES)
329
474
  for i, (path, filename, cookies) in enumerate(ROUTES, 1):
330
475
  out = FIXTURES_DIR / filename
@@ -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.0"
58
58
 
59
59
  _default_client: "VLRClient | None" = None
60
60
 
@@ -63,6 +63,10 @@ class EventRegion(BaseModel):
63
63
  default="",
64
64
  description="Region name (e.g., 'EMEA', 'Pacific', 'Americas', 'China', 'International')",
65
65
  )
66
+ subregion: str | None = Field(
67
+ default=None,
68
+ description="Subregion name if parsed from a subregion= breadcrumb (e.g., 'Japan', 'North America'); None for region= links",
69
+ )
66
70
  href: str = Field(default="", description="Relative path (e.g., '/vct/?region=27')")
67
71
 
68
72
 
@@ -16,12 +16,25 @@ from vlrdevapi._event.info.models import (
16
16
  )
17
17
  from vlrdevapi.commons.countries import get_country_name
18
18
  from vlrdevapi.commons.prizes import parse_prize_amount
19
+ from vlrdevapi.commons.regions import resolve_country_to_region, resolve_subregion_to_region
19
20
  import contextlib
20
21
 
21
22
 
22
23
  def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
23
24
  """Parse event info from the event page HTML.
24
25
 
26
+ After basic parsing the region list is post-processed:
27
+
28
+ 1. Duplicates (same region name from ``region=`` and ``subregion=``
29
+ breadcrumbs) are collapsed; the entry carrying a ``subregion``
30
+ value is kept.
31
+ 2. Two or more distinct regions collapse to a single
32
+ ``EventRegion(name="International")``.
33
+ 3. When no breadcrumb region was found, the ``region_location``
34
+ country is tried first; if absent, ``location.country`` is
35
+ tried. Either is resolved via ``resolve_country_to_region``
36
+ to infer the parent region.
37
+
25
38
  Args:
26
39
  html: Parsed HTML from the event page.
27
40
  event_id: Unique event identifier on vlr.gg.
@@ -42,6 +55,28 @@ def parse_event_info(html: HTMLParser, event_id: int) -> EventInfo:
42
55
  _parse_title(header, event)
43
56
  _parse_desc_items(header, event)
44
57
 
58
+ seen: dict[str, EventRegion] = {}
59
+ for r in event.regions:
60
+ if r.name not in seen:
61
+ seen[r.name] = r
62
+ elif r.subregion and not seen[r.name].subregion:
63
+ seen[r.name] = r
64
+ event.regions = list(seen.values())
65
+
66
+ if len(event.regions) > 1:
67
+ event.regions = [EventRegion(name="International")]
68
+
69
+ if not event.regions:
70
+ country = None
71
+ if event.region_location and event.region_location.country:
72
+ country = event.region_location.country
73
+ elif event.location and event.location.country:
74
+ country = event.location.country
75
+ if country:
76
+ parent = resolve_country_to_region(country)
77
+ if parent:
78
+ event.regions = [EventRegion(name=parent)]
79
+
45
80
  return event
46
81
 
47
82
 
@@ -62,7 +97,14 @@ def _parse_image(header: Node, event: EventInfo) -> None:
62
97
 
63
98
 
64
99
  def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
65
- """Parse breadcrumb links (series, stage, regions) from the header.
100
+ """Parse breadcrumb links (series, stage, regions/subregions) from the header.
101
+
102
+ Region resolution priority:
103
+
104
+ 1. ``region=`` links are stored directly.
105
+ 2. ``subregion=`` links are resolved to their parent region via
106
+ ``resolve_subregion_to_region``; the original subregion name is
107
+ preserved in ``EventRegion.subregion``.
66
108
 
67
109
  Args:
68
110
  header: The event-header root node.
@@ -93,10 +135,12 @@ def _parse_breadcrumb(header: Node, event: EventInfo) -> None:
93
135
 
94
136
  if "stage=" in href:
95
137
  event.stage = EventStageTag(name=name, href=href)
138
+ elif "subregion=" in href:
139
+ parent = resolve_subregion_to_region(name)
140
+ if parent:
141
+ event.regions.append(EventRegion(name=parent, subregion=name, href=href))
96
142
  elif "region=" in href:
97
143
  event.regions.append(EventRegion(name=name, href=href))
98
- else:
99
- event.regions.append(EventRegion(name=name, href=href))
100
144
 
101
145
 
102
146
  def _parse_title(header: Node, event: EventInfo) -> None:
@@ -1,6 +1,8 @@
1
1
  """Shared utilities for match enrichment and parsing across live/upcoming/completed modules."""
2
2
 
3
+ import contextlib
3
4
  import logging
5
+ import re
4
6
  from collections.abc import Callable
5
7
  from datetime import date, datetime, tzinfo
6
8
  from typing import Any, Protocol
@@ -55,20 +57,42 @@ def parse_date_header(text: str) -> date | None:
55
57
 
56
58
 
57
59
  def check_pagination(html: HTMLParser) -> bool:
58
- """Check whether the page has pagination links to additional pages.
60
+ """Check whether a page after the current one exists.
61
+
62
+ The active page is rendered as a ``mod-active`` span while other pages
63
+ are anchor links. A terminal page still links back to earlier pages, so
64
+ the mere presence of page links does not imply a next page. Compare the
65
+ active page number against the highest page number offered instead.
59
66
 
60
67
  Args:
61
68
  html: Parsed HTML document.
62
69
 
63
70
  Returns:
64
- ``True`` if at least one page link exists, ``False`` otherwise.
71
+ ``True`` if a page after the active one exists, ``False`` otherwise.
65
72
 
66
73
  """
67
74
  pagination_el = html.css_first("div.action-container-pages")
68
- if pagination_el:
69
- page_links = pagination_el.css("a.btn.mod-page")
70
- return len(page_links) > 0
71
- return False
75
+ if pagination_el is None:
76
+ return False
77
+
78
+ current_page = 1
79
+ active = pagination_el.css_first(".btn.mod-page.mod-active")
80
+ if active:
81
+ with contextlib.suppress(ValueError):
82
+ current_page = int(active.text(strip=True))
83
+
84
+ max_page = current_page
85
+ for btn in pagination_el.css(".btn.mod-page"):
86
+ text = btn.text(strip=True)
87
+ try:
88
+ max_page = max(max_page, int(text))
89
+ except ValueError:
90
+ href = btn.attributes.get("href") or ""
91
+ page_match = re.search(r"page=(\d+)", href)
92
+ if page_match:
93
+ max_page = max(max_page, int(page_match.group(1)))
94
+
95
+ return current_page < max_page
72
96
 
73
97
 
74
98
  # ---------------------------------------------------------------------------
@@ -2,6 +2,31 @@
2
2
 
3
3
  from selectolax.parser import HTMLParser
4
4
 
5
+ from vlrdevapi.exceptions import ParsingError
6
+
7
+
8
+ def _extract_team_abbreviations(html: HTMLParser) -> tuple[str, str]:
9
+ """Extract short team names from ``.ovw-player-tag`` in overview tables.
10
+
11
+ The series overview page renders two ``.ovw-table`` elements per
12
+ section (current-map and series), one for each team. The first
13
+ table always belongs to **team 1** and the second to **team 2**.
14
+
15
+ Args:
16
+ html: Parsed series overview page.
17
+
18
+ Returns:
19
+ ``(team1_abbreviation, team2_abbreviation)`` — empty strings when
20
+ the expected structure is missing.
21
+
22
+ """
23
+ tables = html.css("div.ovw-table")
24
+ if len(tables) < 2:
25
+ return "", ""
26
+ t1_el = tables[0].css_first(".ovw-player-tag")
27
+ t2_el = tables[1].css_first(".ovw-player-tag")
28
+ return (t1_el.text(strip=True) if t1_el else ""), (t2_el.text(strip=True) if t2_el else "")
29
+
5
30
 
6
31
  def _ordered_game_ids(html: HTMLParser) -> list[str]:
7
32
  """Return the real VLR game IDs in map order from the series nav tabs.
@@ -10,6 +35,10 @@ def _ordered_game_ids(html: HTMLParser) -> list[str]:
10
35
  "All Maps" item (``mod-all``), followed by each map in play order.
11
36
  Each item carries the real VLR ``data-game-id``.
12
37
 
38
+ For single-map (Bo1) series, VLR omits the nav tabs entirely.
39
+ In that case the IDs are discovered from the ``.vm-stats-game``
40
+ content containers instead.
41
+
13
42
  Args:
14
43
  html: Parsed series stats page (overview, performance, or economy).
15
44
 
@@ -24,6 +53,15 @@ def _ordered_game_ids(html: HTMLParser) -> list[str]:
24
53
  gid = (el.attributes.get("data-game-id") or "").strip()
25
54
  if gid:
26
55
  ids.append(gid)
56
+
57
+ if ids:
58
+ return ids
59
+
60
+ for el in html.css(".vm-stats-game[data-game-id]"):
61
+ gid = (el.attributes.get("data-game-id") or "").strip()
62
+ if gid and gid != "all":
63
+ ids.append(gid)
64
+
27
65
  return ids
28
66
 
29
67
 
@@ -34,9 +72,11 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
34
72
 
35
73
  - ``"all"``: aggregate across the whole series (returned unchanged).
36
74
  - A 1-based game number (e.g. ``1``): the first map played in the
37
- series, resolved from the stats nav tabs.
75
+ series, resolved from the stats nav tabs or game containers.
38
76
  - A real VLR game ID (e.g. ``"233478"``): passed through unchanged.
39
77
 
78
+ Raises ``ParsingError`` when the identifier cannot be resolved.
79
+
40
80
  Args:
41
81
  html: Parsed series stats page containing the game nav tabs.
42
82
  game_id: User-supplied game identifier.
@@ -44,6 +84,11 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
44
84
  Returns:
45
85
  str: Real VLR game ID (or ``"all"``) to use for selectors/URLs.
46
86
 
87
+ Raises:
88
+ ParsingError: If ``game_id`` is a positional index that exceeds
89
+ the number of games found, or if no games can be discovered
90
+ at all.
91
+
47
92
  """
48
93
  as_str = str(game_id).strip()
49
94
  if as_str.lower() == "all":
@@ -61,7 +106,16 @@ def resolve_game_id(html: HTMLParser, game_id: int | str) -> str:
61
106
  if 1 <= index <= len(real_ids):
62
107
  return real_ids[index - 1]
63
108
 
64
- return as_str
109
+ if not real_ids:
110
+ raise ParsingError(
111
+ f"Cannot resolve game_id={game_id!r}: no games found on the page. "
112
+ "The series page may have an unexpected structure."
113
+ )
114
+
115
+ raise ParsingError(
116
+ f"Cannot resolve game_id={game_id!r}: index out of range. "
117
+ f"Series has {len(real_ids)} game(s) (IDs: {real_ids})."
118
+ )
65
119
 
66
120
 
67
121
  class PlayerMap:
@@ -7,6 +7,7 @@ import httpx
7
7
  from selectolax.parser import HTMLParser
8
8
 
9
9
  from vlrdevapi._base import SyncNamespace
10
+ from vlrdevapi._series._utils import _extract_team_abbreviations
10
11
  from vlrdevapi._series.economy.models import EconomyData
11
12
  from vlrdevapi._series.economy.parser import parse_economy_data
12
13
  from vlrdevapi._series.info.parser import parse_series_info
@@ -46,7 +47,7 @@ class SeriesEconomyNamespace:
46
47
 
47
48
  Returns:
48
49
  EconomyData: Economy data including ``rounds`` (list of
49
- ``EconomyRound`` with purchases, remaining credits, and
50
+ ``RoundEconomyData`` with purchases, remaining credits, and
50
51
  team spending per round), ``team1`` and ``team2`` names,
51
52
  and enriched team IDs.
52
53
 
@@ -60,8 +61,8 @@ class SeriesEconomyNamespace:
60
61
 
61
62
  Examples:
62
63
  >>> econ = vlrdevapi.series.economy(series_id=12345, game_id=1)
63
- >>> econ.rounds[0].team1_creds
64
- 24000
64
+ >>> econ.rounds[0].bank_team1
65
+ 24000.0
65
66
  >>> econ.rounds[0].winner.name
66
67
  'FNATIC'
67
68
 
@@ -77,20 +78,32 @@ class SeriesEconomyNamespace:
77
78
 
78
79
 
79
80
  def _enrich_economy(result: EconomyData, html_series: HTMLParser) -> EconomyData:
80
- """Enrich economy data with team IDs from the series info.
81
+ """Enrich economy data with team IDs and round winners from the series info.
82
+
83
+ Team matching first tries abbreviation-level names extracted from
84
+ ``.ovw-player-tag`` elements (e.g. "SEN"), then falls back to full
85
+ team names from the series header. Round winners are resolved from
86
+ the stored ``_temp_winner`` tag set during parsing.
81
87
 
82
88
  Args:
83
89
  result: The EconomyData to enrich.
84
90
  html_series: HTML of the series page for team info parsing.
85
91
 
86
92
  Returns:
87
- EconomyData: Enriched economy data with team IDs.
93
+ EconomyData: Enriched economy data with team IDs and round winners.
88
94
 
89
95
  """
90
96
  series_info = parse_series_info(html_series)
91
- if series_info.team1 and series_info.team1.tag == result.team1:
97
+ team1_abbr, team2_abbr = _extract_team_abbreviations(html_series)
98
+
99
+ if team1_abbr == result.team1:
100
+ result.team1_id = series_info.team1.id
101
+ elif result.team1 and series_info.team1.name == result.team1:
92
102
  result.team1_id = series_info.team1.id
93
- if series_info.team2 and series_info.team2.tag == result.team2:
103
+
104
+ if team2_abbr == result.team2:
105
+ result.team2_id = series_info.team2.id
106
+ elif result.team2 and series_info.team2.name == result.team2:
94
107
  result.team2_id = series_info.team2.id
95
108
 
96
109
  for round_data in result.rounds:
@@ -1,3 +1,5 @@
1
+ """Series economy parser — extracts per-round bank and spend data."""
2
+
1
3
  from selectolax.parser import HTMLParser
2
4
 
3
5
  from vlrdevapi._series._utils import resolve_game_id
@@ -101,8 +103,11 @@ def parse_economy_data(html: HTMLParser, game_id: int | str = "all") -> EconomyD
101
103
  sq1 = rnd_sqs[0] # team1
102
104
  sq2 = rnd_sqs[1] # team2
103
105
 
104
- spent_team1 = int(sq1.attributes.get("title") or "0")
105
- spent_team2 = int(sq2.attributes.get("title") or "0")
106
+ try:
107
+ spent_team1 = int(sq1.attributes.get("title") or "0")
108
+ spent_team2 = int(sq2.attributes.get("title") or "0")
109
+ except ValueError:
110
+ continue
106
111
 
107
112
  buy_type_team1 = _get_buy_type(sq1.text(strip=True))
108
113
  buy_type_team2 = _get_buy_type(sq2.text(strip=True))