feat: add Gitea request executor with result envelope, pagination and redaction

This commit is contained in:
Latte committed 2026-10-14 15:55:56 +02:00
1 parent 041b3ea7ff
commit 9e50859133
8 files changed
+3021

No files matched your search

+106
View File
@@ -0,0 +1,106 @@
"""Pagination metadata from Gitea response headers.
Gitea paginates with ``page``/``limit`` query parameters and reports the
neighbours in a ``Link`` header plus the overall count in ``X-Total-Count``.
Only the page numbers in the links are used; the link URLs themselves are never
followed or returned (they are untrusted).
"""
from __future__ import annotations
import re
from collections.abc import Mapping
from typing import Any
from urllib.parse import parse_qs, urlsplit
_LINK_ENTRY = re.compile(r"<([^>]*)>([^<]*)")
_REL = re.compile(r'rel\s*=\s*"?([^";,]+)"?', re.IGNORECASE)
def _int(value: Any, *, minimum: int = 1) -> int | None:
"""Return ``value`` as an int of at least ``minimum``, or None."""
try:
number = int(str(value).strip())
except ValueError:
return None
return number if number >= minimum else None
def parse_link_header(header: str | None) -> dict[str, dict[str, int | None]]:
"""Parse a ``Link`` header into ``{rel: {"page": n, "limit": n}}``.
Entries without a ``rel`` are ignored. Values that are not positive
integers become None.
"""
links: dict[str, dict[str, int | None]] = {}
for url, params in _LINK_ENTRY.findall(header or ""):
rel_match = _REL.search(params)
if rel_match is None:
continue
query = parse_qs(urlsplit(url).query)
numbers = {
key: _int(query[key][0]) if query.get(key) else None for key in ("page", "limit")
}
for rel in rel_match.group(1).split():
links[rel.lower()] = numbers
return links
def build_pagination(
headers: Mapping[str, str],
*,
requested_page: Any,
requested_limit: Any,
paginated: bool,
) -> dict[str, int | None] | None:
"""Build the envelope ``pagination`` object, or None when there is nothing to report.
Args:
headers: Response headers (case-insensitive mapping).
requested_page: ``page`` argument of the request, if any.
requested_limit: ``limit`` argument of the request, if any.
paginated: True when the operation supports pagination.
Returns:
``{"page", "limit", "total_count", "next_page", "last_page"}`` with None
for unknown values, or None when the operation is not paginated and the
response carries neither ``Link`` nor ``X-Total-Count``.
"""
link_header = headers.get("link")
total_header = headers.get("x-total-count")
if not paginated and link_header is None and total_header is None:
return None
links = parse_link_header(link_header)
total_count = _int(total_header, minimum=0)
next_page = links.get("next", {}).get("page")
prev_page = links.get("prev", {}).get("page")
last_page = links.get("last", {}).get("page")
page = _int(requested_page)
if page is None:
if prev_page is not None:
page = prev_page + 1
elif next_page is not None:
page = next_page - 1 or 1
else:
page = 1
limit = _int(requested_limit)
if limit is None:
for rel in ("next", "last", "prev", "first"):
limit = links.get(rel, {}).get("limit")
if limit is not None:
break
if next_page is None and last_page is None:
last_page = page
return {
"page": page,
"limit": limit,
"total_count": total_count,
"next_page": next_page,
"last_page": last_page,
}