feat: add news module - #48
Conversation
📝 WalkthroughWalkthroughThe pull request adds a synchronous News API for paginated listings and full articles. It includes HTML parsing, Markdown conversion, timezone-aware dates, validation, client exports, tests, documentation, and version 2.3.0 release updates. ChangesNews API
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/vlrdevapi/_news/common.py`:
- Around line 35-38: Update has_next_page in
src/vlrdevapi/_news/common.py#L35-L38 to return True only when a pagination link
represents a page number greater than get_page_number(html), rather than merely
when any page links exist. Add a final-page fixture and assertion in
tests/news/test_parser.py#L105-L115 verifying has_next_page is False when links
only point to earlier pages.
In `@src/vlrdevapi/_news/namespace.py`:
- Around line 63-65: Ensure the news namespace preserves the requested page
number by passing page into parse_news_page or assigning the returned
NewsPage.page_number from page; update src/vlrdevapi/_news/namespace.py lines
63-65 accordingly. Add an assertion in tests/news/test_namespace.py lines 45-50
that vlrdevapi.news(page=2).page_number equals 2.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c7c8b29d-a691-4d3d-b061-337dbb4b4aa0
📒 Files selected for processing (14)
CHANGELOG.mdsrc/vlrdevapi/__init__.pysrc/vlrdevapi/_client.pysrc/vlrdevapi/_news/__init__.pysrc/vlrdevapi/_news/common.pysrc/vlrdevapi/_news/models.pysrc/vlrdevapi/_news/namespace.pysrc/vlrdevapi/_news/namespace.pyisrc/vlrdevapi/_news/parser.pysrc/vlrdevapi/_utils/paths.pytests/news/__init__.pytests/news/test_models.pytests/news/test_namespace.pytests/news/test_parser.py
|
| Filename | Overview |
|---|---|
| src/vlrdevapi/_news/common.py | Derives the active page and reports a next page only when a numerically greater pagination link exists, resolving the prior terminal-page defect. |
| src/vlrdevapi/_news/list/parser.py | Parses listing items and delegates pagination metadata to the corrected shared helpers. |
| src/vlrdevapi/_news/article/parser.py | Converts article metadata and body content into typed plain-text and Markdown representations. |
| src/vlrdevapi/init.py | Adds the lazy news binding and its TYPE_CHECKING annotation, resolving the prior static-discovery issue. |
| src/vlrdevapi/init.pyi | Publicly declares news: NewsNamespace for type checkers and IDEs. |
| tests/news/test_parser.py | Covers listing parsing, terminal pagination, article parsing, Markdown conversion, and timezone handling. |
Sequence Diagram
sequenceDiagram
participant User
participant API as vlrdevapi.news
participant Client as VLRClient
participant VLR as vlr.gg
participant Parser as News parser
User->>API: news(page) or news.article(id)
API->>Client: Invoke bound namespace
Client->>VLR: Fetch listing or article HTML
VLR-->>Client: HTML response
Client->>Parser: Parse response
Parser-->>User: NewsPage or NewsArticle
Reviews (2): Last reviewed commit: "feat(news): improve article Markdown, ab..." | Re-trigger Greptile
Adds missing news-related API functionality, improves error clarity for out-of-range page requests, and synchronizes all project documentation with the new 2.3.0 release. - Added `vlrdevapi.news()` for paginated vlr.gg news listings, returning metadata including title, author, date, and pagination state - Added `vlrdevapi.news.article(article_id)` to fetch full news article content, available as plain text (`content`) and formatted Markdown (`content_md`) with preserved headings, lists, and embeds - Out-of-range news page requests now raise `NotFoundError` instead of silently returning empty results for clearer error handling - Updated core changelog, docs changelog, API reference index, docs navigation configuration, and official docs changelog page to reflect the new release and endpoints
…ndling - Convert relative Markdown links to absolute https://www.vlr.gg URLs - Fix clip embeds to use direct watch URLs for Twitch, YouTube, and Soop - Improve Markdown rendering: whitespace, emphasis, nested lists, tables, inline code - Parse article dates as timezone-aware UTC datetimes instead of local machine timezone - Raise NotFoundError for non-existent article IDs instead of returning empty results - Update documentation to cover news namespace usage and examples
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
scripts/check_mdx_examples.py (2)
169-180: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy liftCoordinate in-flight cache misses per key.
_lookupreleases its lock beforeorig_getruns. Two threads can miss the same key and both perform the network request before either call reaches_store. This violates the class documentation's de-duplication guarantee and can increase rate-limit pressure.Use a per-key event or condition. Let one thread fetch and let other threads wait, then recheck the cache. Signal waiters when the fetch fails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check_mdx_examples.py` around lines 169 - 180, Update the cache-miss flow in the request wrapper around _lookup and orig_get to coordinate in-flight fetches per key using an event or condition. Allow only one thread to fetch each missing key; have other threads wait, recheck the cache, and reuse the stored response, while always signaling waiters when the fetch succeeds or fails and preserving existing counters and storage behavior.
115-118: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winBuild the cache key from the complete resolved request.
_keyhashes only per-call headers. Client-level headers and cookies are already resolved intorequest.headers, but they are omitted from the key. Requests with different client-level values can share cached HTML.Pass
paramsandcookiestobuild_request, then hashrequest.headers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/check_mdx_examples.py` around lines 115 - 118, Update _key to build the complete resolved request by passing params and cookies to build_request, then derive the cache key from request.headers rather than only the per-call headers. Preserve the existing URL and hashing behavior while ensuring client-level headers and cookies affect the key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@official-docs/content/docs/reference/news/list.mdx`:
- Around line 109-110: Update the news-list example’s `item.date` formatting to
handle `None` before calling `.date()`, printing an appropriate fallback value
when no publication date is available while preserving the existing formatted
date for present values.
In `@scripts/check_mdx_examples.py`:
- Line 42: Normalize request paths by removing trailing slashes before the
dynamic-path membership check in the example validation flow, so `/news/`
matches the existing `/news` entry in DYNAMIC_PATHS. Preserve the current
behavior for the other dynamic paths and root path handling.
In `@src/vlrdevapi/_news/article/parser.py`:
- Around line 377-383: Update _render_image to pass the extracted src through
_resolve_link_url() before constructing the Markdown image, so relative VLR
image paths become absolute URLs while existing missing-src behavior remains
unchanged. Add a test covering a relative image source and asserting the
rendered content_md contains the resolved absolute URL.
- Around line 339-344: Update the inline-code rendering logic around Node.text
in the relevant parser function to preserve the code text verbatim, including
leading and trailing whitespace, instead of using strip=True. Generate a fence
with sufficient backticks plus padding so boundary spaces and any embedded
consecutive backtick runs remain intact. Add tests covering boundary whitespace
and consecutive backticks.
In `@src/vlrdevapi/_news/list/namespace.py`:
- Around line 46-47: Extend parse_news_page and date_to_utc_datetime to accept
and apply source_tz when parsing listing dates, then pass self._source_tz from
NewsListNamespace through the listing flow. Add a regression test covering a
non-UTC source timezone and verify the resulting UTC datetime is correct.
In `@src/vlrdevapi/validators.py`:
- Line 14: Update validation for parameters listed in _ID_PARAMS to reject
boolean values before applying the positive-integer check, preventing Pydantic
from coercing True or False into integers. Preserve the existing integer
validation behavior for non-boolean values and ensure invalid booleans raise
ValidationError.
---
Outside diff comments:
In `@scripts/check_mdx_examples.py`:
- Around line 169-180: Update the cache-miss flow in the request wrapper around
_lookup and orig_get to coordinate in-flight fetches per key using an event or
condition. Allow only one thread to fetch each missing key; have other threads
wait, recheck the cache, and reuse the stored response, while always signaling
waiters when the fetch succeeds or fails and preserving existing counters and
storage behavior.
- Around line 115-118: Update _key to build the complete resolved request by
passing params and cookies to build_request, then derive the cache key from
request.headers rather than only the per-call headers. Preserve the existing URL
and hashing behavior while ensuring client-level headers and cookies affect the
key.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: ae16d070-a978-4b37-881e-bd0f5260917c
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (46)
CHANGELOG.mdREADME.mddocs-py/docs/api/index.mddocs-py/docs/api/news.mddocs-py/docs/changelog.mddocs-py/docs/index.mddocs-py/zensical.tomlofficial-docs/app/(home)/changelog/changelog-timeline.tsxofficial-docs/app/(home)/changelog/page.tsxofficial-docs/components/home/terminal-demo.tsxofficial-docs/content/docs/getting-started.mdxofficial-docs/content/docs/index.mdxofficial-docs/content/docs/quickstart.mdxofficial-docs/content/docs/reference/index.mdxofficial-docs/content/docs/reference/meta.jsonofficial-docs/content/docs/reference/news/article.mdxofficial-docs/content/docs/reference/news/index.mdxofficial-docs/content/docs/reference/news/list.mdxofficial-docs/content/docs/reference/news/meta.jsonofficial-docs/content/guides/getting-started-with-vlrdevapi.mdxpyproject.tomlscripts/check_mdx_examples.pyscripts/download_fixtures.pysrc/vlrdevapi/__init__.pysrc/vlrdevapi/__init__.pyisrc/vlrdevapi/_news/__init__.pysrc/vlrdevapi/_news/article/__init__.pysrc/vlrdevapi/_news/article/models.pysrc/vlrdevapi/_news/article/namespace.pysrc/vlrdevapi/_news/article/namespace.pyisrc/vlrdevapi/_news/article/parser.pysrc/vlrdevapi/_news/common.pysrc/vlrdevapi/_news/list/__init__.pysrc/vlrdevapi/_news/list/models.pysrc/vlrdevapi/_news/list/namespace.pysrc/vlrdevapi/_news/list/namespace.pyisrc/vlrdevapi/_news/list/parser.pysrc/vlrdevapi/_news/namespace.pysrc/vlrdevapi/_news/namespace.pyisrc/vlrdevapi/_utils/paths.pysrc/vlrdevapi/commons/__init__.pysrc/vlrdevapi/commons/datetime.pysrc/vlrdevapi/validators.pytests/news/test_models.pytests/news/test_namespace.pytests/news/test_parser.py
🚧 Files skipped from review as they are similar to previous changes (2)
- src/vlrdevapi/_news/init.py
- src/vlrdevapi/_news/common.py
| for item in result.news: | ||
| print(f"{item.date.date()} | {item.title} | by {item.author}") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle an absent publication date in the example.
News.date can be None, as documented on Lines 84-89. The call to item.date.date() on Line 110 then raises AttributeError. Print a fallback value when item.date is absent.
Proposed fix
for item in result.news:
- print(f"{item.date.date()} | {item.title} | by {item.author}")
+ published = item.date.date() if item.date else "Unknown date"
+ print(f"{published} | {item.title} | by {item.author}")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for item in result.news: | |
| print(f"{item.date.date()} | {item.title} | by {item.author}") | |
| for item in result.news: | |
| published = item.date.date() if item.date else "Unknown date" | |
| print(f"{published} | {item.title} | by {item.author}") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@official-docs/content/docs/reference/news/list.mdx` around lines 109 - 110,
Update the news-list example’s `item.date` formatting to handle `None` before
calling `.date()`, printing an appropriate fallback value when no publication
date is available while preserving the existing formatted date for present
values.
| DEFAULT_FIXTURES_DIR = REPO_ROOT / ".cache" / "mdx-html" | ||
|
|
||
| DYNAMIC_PATHS = frozenset({"/matches", "/matches/results", "/events"}) | ||
| DYNAMIC_PATHS = frozenset({"/matches", "/matches/results", "/events", "/news"}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import httpx
assert httpx.URL("https://www.vlr.gg/news/?page=2").path == "/news/"
PYRepository: Vanshbordia/vlrdevapi
Length of output: 159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- candidate file ---'
sed -n '1,180p' scripts/check_mdx_examples.py
printf '%s\n' '--- news namespace and pagination references ---'
rg -n -C 4 'class NewsListNamespace|NewsListNamespace|/news/|page=' . \
-g '*.py' -g '*.md' -g '*.mdx' -g '*.toml' -g '*.yaml' -g '*.yml' \
| head -240Repository: Vanshbordia/vlrdevapi
Length of output: 22144
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- cache handler remainder ---'
sed -n '130,250p' scripts/check_mdx_examples.py
printf '%s\n' '--- HTTPX path and dynamic-membership probe ---'
python3 - <<'PY'
import httpx
dynamic_paths = frozenset({"/matches", "/matches/results", "/events", "/news"})
for url in (
"https://www.vlr.gg/news",
"https://www.vlr.gg/news/?page=2",
"https://www.vlr.gg/news/?page=126",
"https://www.vlr.gg/news/?page=176",
):
path = httpx.Request("GET", url).url.path
print(f"{url} -> path={path!r}, current={path in dynamic_paths}, normalized={path.rstrip('/') in dynamic_paths}")
PYRepository: Vanshbordia/vlrdevapi
Length of output: 4728
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import httpx
dynamic_paths = frozenset({"/matches", "/matches/results", "/events", "/news"})
for url in (
"https://www.vlr.gg/news",
"https://www.vlr.gg/news/?page=2",
"https://www.vlr.gg/news/?page=126",
"https://www.vlr.gg/news/?page=176",
):
path = httpx.Request("GET", url).url.path
print(f"{url} -> path={path!r}, current={path in dynamic_paths}, normalized={path.rstrip('/') in dynamic_paths}")
PYRepository: Vanshbordia/vlrdevapi
Length of output: 477
Normalize trailing slashes for dynamic news pages.
httpx resolves /news/?page=<n> to /news/, which does not match /news in DYNAMIC_PATHS. Normalize the path before checking membership.
Proposed fix
- dynamic = request.url.path in DYNAMIC_PATHS
+ dynamic = request.url.path.rstrip("/") in DYNAMIC_PATHS🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/check_mdx_examples.py` at line 42, Normalize request paths by
removing trailing slashes before the dynamic-path membership check in the
example validation flow, so `/news/` matches the existing `/news` entry in
DYNAMIC_PATHS. Preserve the current behavior for the other dynamic paths and
root path handling.
| text = node.text(strip=True) | ||
| if not text: | ||
| return "" | ||
| if "`" in text: | ||
| return f"``{text}``" | ||
| return f"`{text}`" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For selectolax 0.4.7, does Node.text(strip=True)remove leading and trailing whitespace from a text node inside?
💡 Result:
Yes, in selectolax 0.4.7, when you call Node.text(strip=True), it removes leading and trailing whitespace from the text node's content [1][2]. The documentation explicitly states that when the strip parameter is set to True, the library calls the str.strip method on each text part to remove extra whitespace [1][2]. This behavior applies to text nodes regardless of whether they are located inside a tag or any other element, as Node.text processes the text content of the node (and optionally its descendants if deep=True) [1][2].
Citations:
- 1: https://selectolax.readthedocs.io/en/latest/lexbor.html
- 2: https://selectolax.readthedocs.io/en/latest/parser.html
🏁 Script executed:
sed -n '300,365p' src/vlrdevapi/_news/article/parser.py
printf '\n-- related tests and code rendering --\n'
rg -n "inline|code|Node\.text|article.*parser|parser" tests src/vlrdevapi/_news pyproject.toml setup.cfg 2>/dev/null | head -200
Repository: Vanshbordia/vlrdevapi
Length of output: 15249
🏁 Script executed:
sed -n '240,305p' src/vlrdevapi/_news/article/parser.py
sed -n '337,372p' tests/news/test_parser.py
sed -n '1,40p' tests/news/test_parser.py
Repository: Vanshbordia/vlrdevapi
Length of output: 4851
🏁 Script executed:
python3 - <<'PY'
import re
samples = [" code ", "a`b", "a``b", "a```b"]
try:
from selectolax.parser import HTMLParser
except Exception as exc:
print(f"selectolax unavailable: {exc}")
else:
for value in samples[:1]:
node = HTMLParser(f"<code>{value}</code>").css_first("code")
print("selectolax", repr(value), "text()", repr(node.text()), "text(strip=True)", repr(node.text(strip=True)))
def current_render_code(text):
if not text:
return ""
return f"``{text}``" if "`" in text else f"`{text}`"
def current_collapse(value):
spans = []
def protect(match):
spans.append(match.group(0))
return f"\x00{len(spans) - 1}\x00"
protected = re.sub(r"(`+).*?\1", protect, value, flags=re.DOTALL)
protected = re.sub(r" {2,}(?!\n)", " ", protected)
for index, span in enumerate(spans):
protected = protected.replace(f"\x00{index}\x00", span)
return protected
for value in samples:
rendered = current_render_code(value)
print("current", repr(value), "=>", repr(rendered), "=>", repr(current_collapse(rendered)))
PY
Repository: Vanshbordia/vlrdevapi
Length of output: 388
Preserve boundary whitespace in inline code.
Node.text(strip=True) removes boundary whitespace before rendering. Preserve the code text verbatim. Use a fence and padding strategy that preserves boundary spaces and supports any embedded backtick run. Add tests for boundary spaces and consecutive backticks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vlrdevapi/_news/article/parser.py` around lines 339 - 344, Update the
inline-code rendering logic around Node.text in the relevant parser function to
preserve the code text verbatim, including leading and trailing whitespace,
instead of using strip=True. Generate a fence with sufficient backticks plus
padding so boundary spaces and any embedded consecutive backtick runs remain
intact. Add tests covering boundary whitespace and consecutive backticks.
| def _render_image(node: Node) -> str: | ||
| """Render an ``img`` element as ````.""" | ||
| src = _attr(node, "src") | ||
| if not src: | ||
| return "" | ||
| alt = _attr(node, "alt") | ||
| return f"" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Resolve relative image sources.
Line 383 emits relative image URLs unchanged. This breaks content_md when consumers render it outside vlr.gg, and it conflicts with the PR requirement to convert relative VLR URLs to absolute URLs. Resolve src with _resolve_link_url() and add a relative-image test.
Proposed fix
- return f""
+ return f"})"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def _render_image(node: Node) -> str: | |
| """Render an ``img`` element as ````.""" | |
| src = _attr(node, "src") | |
| if not src: | |
| return "" | |
| alt = _attr(node, "alt") | |
| return f"" | |
| def _render_image(node: Node) -> str: | |
| """Render an ``img`` element as ````.""" | |
| src = _attr(node, "src") | |
| if not src: | |
| return "" | |
| alt = _attr(node, "alt") | |
| return f"})" |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vlrdevapi/_news/article/parser.py` around lines 377 - 383, Update
_render_image to pass the extracted src through _resolve_link_url() before
constructing the Markdown image, so relative VLR image paths become absolute
URLs while existing missing-src behavior remains unchanged. Add a test covering
a relative image source and asserting the rendered content_md contains the
resolved absolute URL.
| self._source_tz = source_tz | ||
| self._sync = SyncNamespace( |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 'def parse_news_page|parse_news_page\(' \
src/vlrdevapi/_news/list/parser.py tests/newsRepository: Vanshbordia/vlrdevapi
Length of output: 5004
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- namespace implementation ---'
ast-grep outline src/vlrdevapi/_news/list/namespace.py
sed -n '1,130p' src/vlrdevapi/_news/list/namespace.py
printf '%s\n' '--- parser implementation ---'
ast-grep outline src/vlrdevapi/_news/list/parser.py
sed -n '1,180p' src/vlrdevapi/_news/list/parser.py
printf '%s\n' '--- timezone-related symbols ---'
rg -n -C 3 'source_tz|timezone|tzinfo|parse_datetime|datetime' src/vlrdevapi/_news tests/newsRepository: Vanshbordia/vlrdevapi
Length of output: 27032
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- datetime helpers ---'
ast-grep outline src/vlrdevapi/commons/datetime.py
sed -n '1,220p' src/vlrdevapi/commons/datetime.py
printf '%s\n' '--- listing date tests and namespace tests ---'
rg -n -C 5 'news\.|NewsListNamespace|source_tz|news\.date|date_to_utc_datetime' tests src/vlrdevapi/_news/list src/vlrdevapi/_news/namespace.py
printf '%s\n' '--- parser and helper call signatures ---'
rg -n -C 3 'date_to_utc_datetime|parse_vlr_iso_datetime|def parse_news_page|def _parse_news_item' src/vlrdevapiRepository: Vanshbordia/vlrdevapi
Length of output: 40848
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from pathlib import Path
from datetime import UTC, datetime
from zoneinfo import ZoneInfo
parser_path = Path("src/vlrdevapi/_news/list/parser.py")
namespace_path = Path("src/vlrdevapi/_news/list/namespace.py")
datetime_path = Path("src/vlrdevapi/commons/datetime.py")
parser_tree = ast.parse(parser_path.read_text())
namespace_tree = ast.parse(namespace_path.read_text())
datetime_tree = ast.parse(datetime_path.read_text())
parse_news_page = next(
node for node in parser_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "parse_news_page"
)
parse_call = next(
node for node in ast.walk(namespace_tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "parse_news_page"
)
date_helper = next(
node for node in datetime_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "date_to_utc_datetime"
)
assert [arg.arg for arg in parse_news_page.args.args] == ["html"]
assert not parse_call.keywords
assert [arg.arg for arg in date_helper.args.args] == ["d"]
source_date = datetime(2026, 8, 9)
default_result = source_date.replace(tzinfo=UTC)
new_york_result = source_date.replace(
tzinfo=ZoneInfo("America/New_York")
).astimezone(UTC)
print("parse_news_page parameters:", [arg.arg for arg in parse_news_page.args.args])
print("namespace parse_news_page keyword arguments:", [kw.arg for kw in parse_call.keywords])
print("date_to_utc_datetime parameters:", [arg.arg for arg in date_helper.args.args])
print("date interpreted as UTC:", default_result.isoformat())
print("date interpreted as America/New_York:", new_york_result.isoformat())
print("configured timezone changes the UTC instant:", default_result != new_york_result)
PYRepository: Vanshbordia/vlrdevapi
Length of output: 321
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo
parser_tree = ast.parse(Path("src/vlrdevapi/_news/list/parser.py").read_text())
namespace_tree = ast.parse(Path("src/vlrdevapi/_news/list/namespace.py").read_text())
datetime_tree = ast.parse(Path("src/vlrdevapi/commons/datetime.py").read_text())
parse_news_page = next(
node for node in parser_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "parse_news_page"
)
parse_call = next(
node for node in ast.walk(namespace_tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "parse_news_page"
)
date_helper = next(
node for node in datetime_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "date_to_utc_datetime"
)
assert [arg.arg for arg in parse_news_page.args.args] == ["html"]
assert not parse_call.keywords
assert [arg.arg for arg in date_helper.args.args] == ["d"]
source_date = datetime(2026, 8, 9)
default_result = source_date.replace(tzinfo=timezone.utc)
new_york_result = source_date.replace(
tzinfo=ZoneInfo("America/New_York")
).astimezone(timezone.utc)
print("parse_news_page parameters:", [arg.arg for arg in parse_news_page.args.args])
print("namespace parse_news_page keyword arguments:", [kw.arg for kw in parse_call.keywords])
print("date_to_utc_datetime parameters:", [arg.arg for arg in date_helper.args.args])
print("date interpreted as UTC:", default_result.isoformat())
print("date interpreted as America/New_York:", new_york_result.isoformat())
print("configured timezone changes the UTC instant:", default_result != new_york_result)
PYRepository: Vanshbordia/vlrdevapi
Length of output: 2031
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from datetime import datetime, timedelta, timezone
from pathlib import Path
parser_tree = ast.parse(Path("src/vlrdevapi/_news/list/parser.py").read_text())
namespace_tree = ast.parse(Path("src/vlrdevapi/_news/list/namespace.py").read_text())
datetime_tree = ast.parse(Path("src/vlrdevapi/commons/datetime.py").read_text())
parse_news_page = next(
node for node in parser_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "parse_news_page"
)
parse_call = next(
node for node in ast.walk(namespace_tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "parse_news_page"
)
date_helper = next(
node for node in datetime_tree.body
if isinstance(node, ast.FunctionDef) and node.name == "date_to_utc_datetime"
)
assert [arg.arg for arg in parse_news_page.args.args] == ["html"]
assert not parse_call.keywords
assert [arg.arg for arg in date_helper.args.args] == ["d"]
source_date = datetime(2026, 8, 9)
default_result = source_date.replace(tzinfo=timezone.utc)
source_tz_result = source_date.replace(
tzinfo=timezone(timedelta(hours=-4))
).astimezone(timezone.utc)
print("parse_news_page parameters:", [arg.arg for arg in parse_news_page.args.args])
print("namespace parse_news_page keyword arguments:", [kw.arg for kw in parse_call.keywords])
print("date_to_utc_datetime parameters:", [arg.arg for arg in date_helper.args.args])
print("date interpreted as UTC:", default_result.isoformat())
print("date interpreted with UTC-04:00:", source_tz_result.isoformat())
print("configured timezone changes the UTC instant:", default_result != source_tz_result)
PYRepository: Vanshbordia/vlrdevapi
Length of output: 443
Propagate source_tz through news listing date parsing.
NewsListNamespace stores source_tz, but parse_news_page accepts only html, and date_to_utc_datetime always uses UTC. Extend the listing parser and date conversion to use source_tz, then pass self._source_tz from the namespace. Add a regression test for a non-UTC timezone.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vlrdevapi/_news/list/namespace.py` around lines 46 - 47, Extend
parse_news_page and date_to_utc_datetime to accept and apply source_tz when
parsing listing dates, then pass self._source_tz from NewsListNamespace through
the listing flow. Add a regression test covering a non-UTC source timezone and
verify the resulting UTC datetime is correct.
| from vlrdevapi.exceptions import ValidationError | ||
|
|
||
| _ID_PARAMS = frozenset({"event_id", "team_id", "player_id", "series_id", "page", "limit", "subseries_id", "last_days"}) | ||
| _ID_PARAMS = frozenset({"event_id", "team_id", "player_id", "series_id", "page", "limit", "subseries_id", "last_days", "article_id"}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
assert isinstance(True, int)
print("bool is an int subclass")
PYRepository: Vanshbordia/vlrdevapi
Length of output: 183
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- validators.py symbols ---'
ast-grep outline src/vlrdevapi/validators.py --match '$_' --view compact 2>/dev/null || true
printf '%s\n' '--- validator implementation ---'
sed -n '1,220p' src/vlrdevapi/validators.py
printf '%s\n' '--- article endpoint and validator call sites ---'
rg -n -C 5 'news_article|article_id|validate|ValidationError' src tests 2>/dev/null || trueRepository: Vanshbordia/vlrdevapi
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- files containing news_article ---'
rg -l --glob '*.py' 'news_article|article_id' src/vlrdevapi | sort
printf '%s\n' '--- exact article definitions and path construction ---'
rg -n -C 12 --glob '*.py' 'news_article|article_id' src/vlrdevapi
printf '%s\n' '--- dependency and test context ---'
rg -n -C 3 'pydantic|validate_call|news_article|article_id|positive integer|bool' pyproject.toml setup.cfg requirements*.txt tests 2>/dev/null || trueRepository: Vanshbordia/vlrdevapi
Length of output: 30221
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pydantic import __version__, validate_call
`@validate_call`(validate_return=False)
def article(article_id: int) -> str:
return f"/{article_id}"
print(f"pydantic={__version__}")
for value in (True, False, 1, 0):
try:
print(f"{value!r} -> {article(value)!r}")
except Exception as exc:
print(f"{value!r} -> {type(exc).__name__}: {exc}")
PYRepository: Vanshbordia/vlrdevapi
Length of output: 280
🌐 Web query:
Pydantic v2 validate_call int annotation boolean True coercion strict mode
💡 Result:
In Pydantic v2, when using @validate_call, the default behavior is to perform type coercion (lax mode), which allows various inputs (including booleans) to be converted to integers [1][2]. If you enable strict mode, Pydantic disables this coercion. In strict mode, an integer field will only accept an exact instance of an integer [3]. Therefore, if a boolean (True or False) is passed to an integer field annotated with strict validation, it will raise a ValidationError [3]. To enable strict mode within @validate_call, you must pass a configuration object using ConfigDict [3]: from pydantic import ConfigDict, validate_call @validate_call(config=ConfigDict(strict=True)) def my_function(x: int) -> int: return x In this strict configuration, passing True or False to x will result in an error, as booleans are not instances of int and will no longer be coerced to 1 or 0 [3]. You can also apply strictness to specific fields rather than the entire function call by using Annotated with the strict constraint: from typing import Annotated from pydantic import Field, validate_call @validate_call def my_function(x: Annotated[int, Field(strict=True)]): return x This ensures that the specific field x will reject boolean inputs even if the rest of the function operates in default lax mode [4][3].
Citations:
- 1: https://docs.pydantic.dev/latest/concepts/validation_decorator/
- 2: https://pydantic.dev/docs/validation/2.12/concepts/validation_decorator/
- 3: https://pydantic.dev/docs/validation/2.11/concepts/strict_mode/
- 4: https://pydantic.dev/docs/validation/2.12/concepts/strict_mode/
Reject boolean values for identifier parameters.
article_id=True passes the custom positive-integer check. Pydantic then coerces it to 1, so the request can target /1 instead of raising ValidationError. Reject booleans before the integer check for _ID_PARAMS.
Proposed validation fix
- if not isinstance(value, int) or value <= 0:
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vlrdevapi/validators.py` at line 14, Update validation for parameters
listed in _ID_PARAMS to reject boolean values before applying the
positive-integer check, preventing Pydantic from coercing True or False into
integers. Preserve the existing integer validation behavior for non-boolean
values and ensure invalid booleans raise ValidationError.
Summary
vlrdevapi.news(page=1)/client.news(page=1), listing items from vlr.gg/news(
title,subtitle,link,country_name,date,author) withhas_next_pageand
page_number.NewsNamespaceintoVLRClientand the module-level__getattr__bindings.Test plan
pytest tests/news/(12 passed)ruff check src/vlrdevapi/_news/ tests/news/Summary by CodeRabbit
New Features
Documentation