Files
BanGUI/backend/app/utils/pagination.py

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