103 lines
2.7 KiB
Python
103 lines
2.7 KiB
Python
"""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
|