pagination.py
1 """ 2 Pagination utilities for API responses. 3 4 Provides standard pagination patterns for consistent API responses across the application. 5 6 Default pagination settings: 7 - Page number: 1 8 - Page size: 20 9 - Max page size: 100 10 """ 11 12 from pydantic import BaseModel, Field 13 from typing import TypeVar, Generic 14 15 T = TypeVar("T") 16 17 DEFAULT_PAGE_NUMBER = 1 18 DEFAULT_PAGE_SIZE = 20 19 MAX_PAGE_SIZE = 100 20 21 22 class PaginationParams(BaseModel): 23 """ 24 Request parameters for pagination with sensible defaults. 25 26 Defaults: 27 - page_number: 1 28 - page_size: 20 29 """ 30 page_number: int = Field(default=DEFAULT_PAGE_NUMBER, ge=1, alias="pageNumber") 31 page_size: int = Field(default=DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE, alias="pageSize") 32 33 @property 34 def offset(self) -> int: 35 """Calculate the offset for database queries.""" 36 return (self.page_number - 1) * self.page_size 37 38 model_config = {"populate_by_name": True} 39 40 41 def get_pagination_or_default(pagination: PaginationParams | None = None) -> PaginationParams: 42 """ 43 Get pagination parameters or return defaults if None. 44 45 This helper ensures all paginated endpoints use the same defaults: 46 - page_number: 1 47 - page_size: 20 48 49 Args: 50 pagination: Optional pagination parameters 51 52 Returns: 53 PaginationParams with defaults if None provided 54 55 Example: 56 pagination = get_pagination_or_default(request_pagination) 57 # Always returns valid PaginationParams with defaults if None 58 """ 59 if pagination is None: 60 return PaginationParams() 61 return pagination 62 63 64 class PaginationMeta(BaseModel): 65 """Pagination metadata for API responses.""" 66 page_number: int = Field(..., alias="pageNumber") 67 count: int 68 page_size: int = Field(..., alias="pageSize") 69 next_page: int | None = Field(..., alias="nextPage") 70 total_pages: int = Field(..., alias="totalPages") 71 72 model_config = {"populate_by_name": True} 73 74 75 class Meta(BaseModel): 76 """Metadata container for API responses.""" 77 pagination: PaginationMeta 78 79 80 class PaginatedResponse(BaseModel, Generic[T]): 81 """Generic paginated response with data and metadata.""" 82 data: list[T] 83 meta: Meta 84 85 @classmethod 86 def create( 87 cls, data: list[T], total_count: int, pagination: PaginationParams 88 ) -> "PaginatedResponse[T]": 89 """ 90 Create a paginated response from data and pagination parameters. 91 92 Args: 93 data: List of items for current page 94 total_count: Total number of items across all pages 95 pagination: Pagination parameters used for the request 96 97 Returns: 98 PaginatedResponse with data and calculated metadata 99 """ 100 total_pages = (total_count + pagination.page_size - 1) // pagination.page_size 101 next_page = pagination.page_number + 1 if pagination.page_number < total_pages else None 102 103 pagination_meta = PaginationMeta( 104 page_number=pagination.page_number, 105 count=total_count, 106 page_size=pagination.page_size, 107 next_page=next_page, 108 total_pages=total_pages, 109 ) 110 111 return cls( 112 data=data, 113 meta=Meta(pagination=pagination_meta) 114 ) 115 116 model_config = {"populate_by_name": True} 117 118 119 class DataResponse(BaseModel, Generic[T]): 120 """Simple data response wrapper.""" 121 data: T 122 123 @classmethod 124 def create(cls, data: T) -> "DataResponse[T]": 125 """Create a data response from data.""" 126 return cls(data=data) 127 128 129 __all__ = [ 130 "PaginationParams", 131 "PaginationMeta", 132 "PaginatedResponse", 133 "DataResponse", 134 "get_pagination_or_default", 135 "DEFAULT_PAGE_NUMBER", 136 "DEFAULT_PAGE_SIZE", 137 "MAX_PAGE_SIZE", 138 ]