"""Pagination utilities and standardized query parameter handling. This module provides reusable utilities for implementing consistent pagination across all endpoints. All paginated endpoints should use these utilities to ensure a uniform API contract. Standard Pagination Contract: Query parameters: page (1-based), page_size (1-500) Response: PaginatedListResponse[T] with items, total, page, page_size Usage in routers: ```python from app.utils.pagination import PAGINATION_DEFAULTS @router.get("/items") async def get_items( page: int = Query(default=PAGINATION_DEFAULTS["page"], ge=1), page_size: int = Query( default=PAGINATION_DEFAULTS["page_size"], ge=1, le=PAGINATION_DEFAULTS["max_page_size"], ), ): ... ``` """ from typing import Final __all__ = ["PAGINATION_DEFAULTS", "get_offset", "compute_total_pages"] # Standardized pagination defaults PAGINATION_DEFAULTS: Final[dict[str, int]] = { "page": 1, "page_size": 100, "max_page_size": 500, } def get_offset(page: int, page_size: int) -> int: """Calculate the database offset for a given page and page size. Args: page: 1-based page number. page_size: Items per page. Returns: 0-based database offset (number of items to skip). Raises: ValueError: If page or page_size is invalid (< 1). Example: ```python # Page 1, size 10 → offset 0 assert get_offset(1, 10) == 0 # Page 2, size 10 → offset 10 assert get_offset(2, 10) == 10 # Page 3, size 50 → offset 100 assert get_offset(3, 50) == 100 ``` """ if page < 1: raise ValueError(f"page must be >= 1, got {page}") if page_size < 1: raise ValueError(f"page_size must be >= 1, got {page_size}") return (page - 1) * page_size def compute_total_pages(total: int, page_size: int) -> int: """Calculate the total number of pages needed. Args: total: Total number of items across all pages. page_size: Items per page. Returns: The number of pages required to hold all items. Always at least 1, even when total is 0 (an empty page is still a page). Raises: ValueError: If page_size is invalid (< 1). Example: ```python assert compute_total_pages(0, 10) == 1 assert compute_total_pages(10, 10) == 1 assert compute_total_pages(11, 10) == 2 assert compute_total_pages(25, 5) == 5 ``` """ if page_size < 1: raise ValueError(f"page_size must be >= 1, got {page_size}") if total == 0: return 1 # Ceiling division: (total + page_size - 1) // page_size return (total + page_size - 1) // page_size