Standard Pagination
ZodiacCore provides a comprehensive pagination system that standardizes how your API handles list-based data. It includes request parameters, response models, and professional repository methods that automate pagination logic.
1. Request Parameters
The PageParams model handles typical pagination query strings (?page=1&size=20).
from typing import Annotated
from fastapi import Depends
from zodiac_core.pagination import PageParams
@router.get("/items")
async def list_items(
params: Annotated[PageParams, Depends()]
):
# Automatically validated:
# params.page defaults to 1 (min 1)
# params.size defaults to 20 (max 100)
...
Using Depends() vs Query()
For Pydantic models like PageParams, use Depends() instead of Query(). FastAPI will automatically extract query parameters and validate them against the model.
2. Standard Paged Response
The PagedResponse[T] is a generic model that wraps your data items along with metadata.
The Response Structure
{
"code": 0,
"message": "Success",
"data": {
"items": [...],
"total": 100,
"page": 1,
"size": 20
}
}
Building the Response
Use the .create() factory method to easily build the response from your query results and the input PageParams.
from zodiac_core.pagination import PagedResponse
return PagedResponse.create(
items=items,
total=total_count,
params=page_params
)
3. Professional Pagination with BaseSQLRepository
For database queries, BaseSQLRepository provides two methods that automate pagination:
paginate_query() - Recommended for Most Cases
This is the convenience method that automatically manages the database session. Use this in your repository methods:
from sqlalchemy import select
from zodiac_core.db.repository import BaseSQLRepository
from zodiac_core.pagination import PagedResponse, PageParams
class ItemRepository(BaseSQLRepository):
async def list_items(self, params: PageParams) -> PagedResponse[ItemModel]:
"""List items with pagination."""
stmt = select(ItemModel).order_by(ItemModel.id)
return await self.paginate_query(stmt, params)
What it does:
- ✅ Automatically manages database session
- ✅ Calculates total count (handles complex queries with joins/groups)
- ✅ Applies limit/offset
- ✅ Packages results into
PagedResponse
When to use: - Most repository methods that need pagination - Simple queries that don't require custom session management
paginate() - For Advanced Use Cases
This method requires you to manage the session yourself. Use this when you need more control:
async def list_items_with_custom_logic(self, params: PageParams) -> PagedResponse[ItemModel]:
"""Example with custom session management."""
async with self.session() as session:
# You can add custom logic here (e.g., filtering, joins)
stmt = select(ItemModel).where(ItemModel.status == "active")
stmt = stmt.order_by(ItemModel.created_at.desc())
return await self.paginate(session, stmt, params)
What it does:
- ✅ Calculates total count (handles complex queries)
- ✅ Applies limit/offset
- ✅ Packages results into
PagedResponse - ⚠️ Requires you to provide an active session
When to use:
- When you need custom session management
- When you want to perform multiple operations in a single transaction
- When you need to add complex query logic before pagination
How Count Calculation Works
Both methods handle complex queries correctly:
- Simple queries:
SELECT COUNT(*) FROM (SELECT ...) - Queries with joins: Automatically wraps in subquery
- Queries with GROUP BY: Handles correctly
- Queries with ORDER BY: Removed from count query (as expected)
The implementation removes limit/offset before counting and safely wraps complex queries in subqueries.
Transformation Support
Both methods support optional transformation to Pydantic models:
from app.api.schemas.item_schema import ItemSchema
# Transform DB models to response schemas
return await self.paginate_query(stmt, params, transformer=ItemSchema)
4. Complete Example
Here's a complete example showing the full flow:
Repository:
class ItemRepository(BaseSQLRepository):
async def list_items(self, params: PageParams) -> PagedResponse[ItemModel]:
stmt = select(ItemModel).order_by(ItemModel.id)
return await self.paginate_query(stmt, params)
Service:
class ItemService:
def __init__(self, item_repo: ItemRepository) -> None:
self.item_repo = item_repo
async def list_items(self, page_params: PageParams) -> PagedResponse[ItemModel]:
return await self.item_repo.list_items(page_params)
Router:
@router.get("", response_model=PagedResponse[ItemSchema])
@inject
async def list_items(
page_params: Annotated[PageParams, Depends()],
service: Annotated[ItemService, Depends(Provide[Container.item_service])],
):
return await service.list_items(page_params)
No manual calculations needed! The paginate_query method handles everything.
5. API Reference
Pagination Models
PageParams
Bases: BaseModel
Standard pagination query parameters.
Usage
Source code in zodiac_core/pagination.py
PagedResponse
Bases: BaseModel, Generic[T]
Standard generic paginated response model.
Usage
from typing import Annotated
from fastapi import Query
from zodiac_core.pagination import PagedResponse, PageParams
@app.get("/users", response_model=PagedResponse[UserSchema])
def list_users(page_params: Annotated[PageParams, Query()]):
users, total_count = db.find_users(...)
return PagedResponse.create(users, total_count, page_params)
Source code in zodiac_core/pagination.py
create(items, total, params)
classmethod
Factory method to create a PagedResponse from items, total count, and PageParams.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
List[T]
|
The list of data objects (Pydantic models or dicts). |
required |
total
|
int
|
The total number of records in the database matching the query. |
required |
params
|
PageParams
|
The PageParams object from the request. |
required |
Source code in zodiac_core/pagination.py
Repository Methods
Standard base class for SQL-based repositories.
Supports multiple database instances via db_name and provides
professional utilities for common operations like pagination.
Source code in zodiac_core/db/repository.py
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
paginate(session, statement, params, transformer=None)
async
Execute a paginated query with automatic count and paging.
Performs: 1. Automatic total count query using the provided statement. 2. Automatic limit/offset application. 3. Packaging results into a standardized PagedResponse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
AsyncSession
|
The active AsyncSession. |
required |
statement
|
Any
|
The SQLAlchemy select statement (without limit/offset). |
required |
params
|
PageParams
|
Standard PageParams (page, size). |
required |
transformer
|
Optional[Type[T]]
|
Optional Pydantic model to transform DB objects into. |
None
|
Example
Source code in zodiac_core/db/repository.py
paginate_query(statement, params, transformer=None)
async
Convenience method that automatically manages session for pagination.
This is a wrapper around paginate() that handles session management,
making it easier to use in repository methods.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
statement
|
Any
|
The SQLAlchemy select statement (without limit/offset). |
required |
params
|
PageParams
|
Standard PageParams (page, size). |
required |
transformer
|
Optional[Type[T]]
|
Optional Pydantic model to transform DB objects into. |
None
|