"""Generic pagination envelope (D-016). Frontend reads `{items, total, page, page_size}`. Sprint 2 uses this on `/users` and `/audit/log`; existing endpoints (`/engagements`) stay non-paginated for backwards-compatibility and will migrate together in a later opt-in (`?paginate=true` or `/api/v2/`). """ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field DEFAULT_PAGE_SIZE = 50 MAX_PAGE_SIZE = 200 class Page[T](BaseModel): """Paginated response envelope.""" model_config = ConfigDict(arbitrary_types_allowed=True) items: list[T] total: int = Field(ge=0) page: int = Field(ge=1) page_size: int = Field(ge=1, le=MAX_PAGE_SIZE) class PageQuery(BaseModel): """Parsed `?page=` / `?page_size=` query string (always normalised).""" page: int = Field(default=1, ge=1) page_size: int = Field(default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE) @property def offset(self) -> int: return (self.page - 1) * self.page_size @property def limit(self) -> int: return self.page_size