1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Bound paged retrieval responses

This commit is contained in:
Andraxion 2026-07-29 06:02:07 -04:00
parent 176b2d2784
commit 529accf858
15 changed files with 1567 additions and 30 deletions

163
src/docforge/pagination.py Normal file
View file

@ -0,0 +1,163 @@
"""Deterministic, generation-bound pagination cursors for bounded public results."""
from __future__ import annotations
import base64
import binascii
import hashlib
import hmac
import json
from collections.abc import Mapping
from typing import cast
from .errors import DocForgeError
CURSOR_SCHEMA_VERSION = 1
MAX_CURSOR_CHARS = 8_192
_CURSOR_DOMAIN = b"docforge-page-cursor-v1\0"
_CURSOR_KEYS = frozenset({"schema_version", "kind", "binding", "position", "checksum"})
def canonical_hash(value: object) -> str:
"""Hash one JSON-compatible value using DocForge's deterministic JSON form."""
try:
encoded = _canonical_bytes(value)
except (TypeError, ValueError) as error:
raise DocForgeError(
"invalid_pagination_source",
"Pagination source data is not deterministic JSON",
) from error
return hashlib.sha256(encoded).hexdigest()
def page_limit(limit: int | None, *, default: int, maximum: int) -> int:
"""Validate one additive page size against the project result policy."""
selected = default if limit is None else limit
if type(selected) is not int or selected < 1 or selected > maximum:
raise DocForgeError(
"invalid_limit",
"Page limit is outside the configured result limit",
maximum=maximum,
)
return selected
def encode_cursor(
*,
kind: str,
binding: Mapping[str, object],
position: int,
) -> str:
"""Encode a corruption-detecting cursor bound to an immutable result identity."""
if not kind or type(position) is not int or position < 0:
raise ValueError("Cursor kind and position must be valid")
body: dict[str, object] = {
"schema_version": CURSOR_SCHEMA_VERSION,
"kind": kind,
"binding": dict(binding),
"position": position,
}
checksum = hashlib.sha256(_CURSOR_DOMAIN + _canonical_bytes(body)).hexdigest()
envelope = {**body, "checksum": checksum}
return base64.urlsafe_b64encode(_canonical_bytes(envelope)).decode("ascii").rstrip("=")
def decode_cursor(
cursor: str | None,
*,
kind: str,
binding: Mapping[str, object],
total_count: int,
) -> int:
"""Return a validated position, rejecting corrupt, foreign, or stale cursors."""
if cursor is None:
return 0
if not cursor or len(cursor) > MAX_CURSOR_CHARS or not cursor.isascii():
raise _invalid_cursor()
padding = "=" * (-len(cursor) % 4)
try:
raw = base64.b64decode(
(cursor + padding).encode("ascii"),
altchars=b"-_",
validate=True,
)
parsed: object = json.loads(raw.decode("utf-8"))
except (binascii.Error, UnicodeDecodeError, json.JSONDecodeError):
raise _invalid_cursor() from None
if base64.urlsafe_b64encode(raw).decode("ascii").rstrip("=") != cursor or not isinstance(
parsed, dict
):
raise _invalid_cursor()
payload = cast(dict[str, object], parsed)
checksum = payload.get("checksum")
position = payload.get("position")
stored_binding = payload.get("binding")
if (
frozenset(payload) != _CURSOR_KEYS
or payload.get("schema_version") != CURSOR_SCHEMA_VERSION
or payload.get("kind") != kind
or not isinstance(stored_binding, dict)
or type(position) is not int
or position < 0
or position >= total_count
or not isinstance(checksum, str)
or len(checksum) != 64
):
raise _invalid_cursor()
body = {key: payload[key] for key in payload if key != "checksum"}
expected = hashlib.sha256(_CURSOR_DOMAIN + _canonical_bytes(body)).hexdigest()
if not hmac.compare_digest(checksum, expected):
raise _invalid_cursor()
if stored_binding != dict(binding):
raise DocForgeError(
"stale_cursor",
"Pagination cursor does not match the current result generation",
)
return position
def page_receipt(
*,
kind: str,
binding: Mapping[str, object],
position: int,
count: int,
limit: int,
total_count: int,
) -> dict[str, object]:
"""Return one bounded page receipt and the next generation-bound cursor."""
next_position = position + count
has_more = next_position < total_count
return {
"schema_version": CURSOR_SCHEMA_VERSION,
"kind": kind,
"returned_count": count,
"limit": limit,
"total_count": total_count,
"has_more": has_more,
"next_cursor": (
encode_cursor(kind=kind, binding=binding, position=next_position) if has_more else None
),
}
def _canonical_bytes(value: object) -> bytes:
return json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
allow_nan=False,
).encode("utf-8")
def _invalid_cursor() -> DocForgeError:
return DocForgeError(
"invalid_cursor",
"Pagination cursor is malformed or does not match its operation",
)