Skip to content

feat: add news module - #48

Merged
Vanshbordia merged 9 commits into
Vanshbordia:mainfrom
DaviAlcanfor:feat/news-module
Aug 11, 2026
Merged

feat: add news module#48
Vanshbordia merged 9 commits into
Vanshbordia:mainfrom
DaviAlcanfor:feat/news-module

Conversation

@DaviAlcanfor

@DaviAlcanfor DaviAlcanfor commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds vlrdevapi.news(page=1) / client.news(page=1), listing items from vlr.gg/news
    (title, subtitle, link, country_name, date, author) with has_next_page
    and page_number.
  • Wires NewsNamespace into VLRClient and the module-level __getattr__ bindings.

Test plan

  • pytest tests/news/ (12 passed)
  • ruff check src/vlrdevapi/_news/ tests/news/

Summary by CodeRabbit

  • New Features

    • Added news browsing with paginated listings and article retrieval.
    • Added article metadata, event details, publication dates, and Markdown content.
    • Added support for links, images, tables, code, and media embeds in article content.
    • Added validation and clear errors for invalid pages and unavailable articles.
    • Updated the library version to 2.3.0.
  • Documentation

    • Added News API reference pages, examples, and quickstart guidance.
    • Added the 2.3.0 changelog and release timeline updates.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

News API

Layer / File(s) Summary
News models and listing parsing
src/vlrdevapi/_news/common.py, src/vlrdevapi/_news/list/*, src/vlrdevapi/commons/datetime.py, tests/news/test_parser.py
Adds News and NewsPage models, listing parsing, pagination detection, malformed-item handling, UTC date conversion, and parser tests.
Article models and Markdown parsing
src/vlrdevapi/_news/article/*, tests/news/test_parser.py
Adds NewsArticle, article metadata extraction, HTML-to-Markdown conversion, URL and media normalization, and content tests.
News retrieval and public exposure
src/vlrdevapi/_news/namespace.*, src/vlrdevapi/_client.py, src/vlrdevapi/__init__.*, src/vlrdevapi/_utils/paths.py, tests/news/test_namespace.py
Adds listing and article namespaces, client configuration forwarding, public exports, positive article ID validation, URL paths, not-found handling, fixtures, and API tests.
Release and API documentation
README.md, docs-py/*, official-docs/*, CHANGELOG.md, pyproject.toml
Documents the News API, updates examples and navigation, adds the 2.3.0 changelog, updates the official changelog timeline, and sets the package version to 2.3.0.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.96% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding the news module and its related API support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 74967f5 and 9b4f075.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • src/vlrdevapi/__init__.py
  • src/vlrdevapi/_client.py
  • src/vlrdevapi/_news/__init__.py
  • src/vlrdevapi/_news/common.py
  • src/vlrdevapi/_news/models.py
  • src/vlrdevapi/_news/namespace.py
  • src/vlrdevapi/_news/namespace.pyi
  • src/vlrdevapi/_news/parser.py
  • src/vlrdevapi/_utils/paths.py
  • tests/news/__init__.py
  • tests/news/test_models.py
  • tests/news/test_namespace.py
  • tests/news/test_parser.py

Comment thread src/vlrdevapi/_news/common.py Outdated
Comment thread src/vlrdevapi/_news/namespace.py Outdated
@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds typed APIs for paginated news listings and full news articles, including Markdown conversion, documentation, fixtures, and package integration.

  • Wires NewsNamespace into explicit clients and module-level lazy access.
  • Parses news listings, pagination state, article metadata, content, and publication dates.
  • Adds API documentation, changelog entries, type stubs, and fixture-backed tests.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

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
Loading

Reviews (2): Last reviewed commit: "feat(news): improve article Markdown, ab..." | Re-trigger Greptile

Comment thread src/vlrdevapi/_news/common.py Outdated
Comment thread src/vlrdevapi/__init__.py
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
@DaviAlcanfor DaviAlcanfor mentioned this pull request Aug 10, 2026
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lift

Coordinate in-flight cache misses per key.

_lookup releases its lock before orig_get runs. 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 win

Build the cache key from the complete resolved request.

_key hashes only per-call headers. Client-level headers and cookies are already resolved into request.headers, but they are omitted from the key. Requests with different client-level values can share cached HTML.

Pass params and cookies to build_request, then hash request.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

📥 Commits

Reviewing files that changed from the base of the PR and between 9b4f075 and 1d433cf.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (46)
  • CHANGELOG.md
  • README.md
  • docs-py/docs/api/index.md
  • docs-py/docs/api/news.md
  • docs-py/docs/changelog.md
  • docs-py/docs/index.md
  • docs-py/zensical.toml
  • official-docs/app/(home)/changelog/changelog-timeline.tsx
  • official-docs/app/(home)/changelog/page.tsx
  • official-docs/components/home/terminal-demo.tsx
  • official-docs/content/docs/getting-started.mdx
  • official-docs/content/docs/index.mdx
  • official-docs/content/docs/quickstart.mdx
  • official-docs/content/docs/reference/index.mdx
  • official-docs/content/docs/reference/meta.json
  • official-docs/content/docs/reference/news/article.mdx
  • official-docs/content/docs/reference/news/index.mdx
  • official-docs/content/docs/reference/news/list.mdx
  • official-docs/content/docs/reference/news/meta.json
  • official-docs/content/guides/getting-started-with-vlrdevapi.mdx
  • pyproject.toml
  • scripts/check_mdx_examples.py
  • scripts/download_fixtures.py
  • src/vlrdevapi/__init__.py
  • src/vlrdevapi/__init__.pyi
  • src/vlrdevapi/_news/__init__.py
  • src/vlrdevapi/_news/article/__init__.py
  • src/vlrdevapi/_news/article/models.py
  • src/vlrdevapi/_news/article/namespace.py
  • src/vlrdevapi/_news/article/namespace.pyi
  • src/vlrdevapi/_news/article/parser.py
  • src/vlrdevapi/_news/common.py
  • src/vlrdevapi/_news/list/__init__.py
  • src/vlrdevapi/_news/list/models.py
  • src/vlrdevapi/_news/list/namespace.py
  • src/vlrdevapi/_news/list/namespace.pyi
  • src/vlrdevapi/_news/list/parser.py
  • src/vlrdevapi/_news/namespace.py
  • src/vlrdevapi/_news/namespace.pyi
  • src/vlrdevapi/_utils/paths.py
  • src/vlrdevapi/commons/__init__.py
  • src/vlrdevapi/commons/datetime.py
  • src/vlrdevapi/validators.py
  • tests/news/test_models.py
  • tests/news/test_namespace.py
  • tests/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

Comment on lines +109 to +110
for item in result.news:
print(f"{item.date.date()} | {item.title} | by {item.author}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/"
PY

Repository: 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 -240

Repository: 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}")
PY

Repository: 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}")
PY

Repository: 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.

Comment on lines +339 to +344
text = node.text(strip=True)
if not text:
return ""
if "`" in text:
return f"``{text}``"
return f"`{text}`"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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:


🏁 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.

Comment on lines +377 to +383
def _render_image(node: Node) -> str:
"""Render an ``img`` element as ``![alt](src)``."""
src = _attr(node, "src")
if not src:
return ""
alt = _attr(node, "alt")
return f"![{alt}]({src})"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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"![{alt}]({src})"
+    return f"![{alt}]({_resolve_link_url(src)})"
📝 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.

Suggested change
def _render_image(node: Node) -> str:
"""Render an ``img`` element as ``![alt](src)``."""
src = _attr(node, "src")
if not src:
return ""
alt = _attr(node, "alt")
return f"![{alt}]({src})"
def _render_image(node: Node) -> str:
"""Render an ``img`` element as ``![alt](src)``."""
src = _attr(node, "src")
if not src:
return ""
alt = _attr(node, "alt")
return f"![{alt}]({_resolve_link_url(src)})"
🤖 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.

Comment on lines +46 to +47
self._source_tz = source_tz
self._sync = SyncNamespace(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/news

Repository: 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/news

Repository: 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/vlrdevapi

Repository: 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)
PY

Repository: 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)
PY

Repository: 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)
PY

Repository: 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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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")
PY

Repository: 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 || true

Repository: 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 || true

Repository: 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}")
PY

Repository: 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:


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.

@Vanshbordia
Vanshbordia merged commit 7306f14 into Vanshbordia:main Aug 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants