Skip to content

Commit 394c791

Browse files
authored
Merge pull request #36 from igorbenav/paginated-return
Helper functions for paginated responses, new module created for pagination
2 parents cf2984e + a4d025a commit 394c791

File tree

5 files changed

+171
-38
lines changed

5 files changed

+171
-38
lines changed

README.md

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
5. [Alembic Migrations](#55-alembic-migrations)
9797
6. [CRUD](#56-crud)
9898
7. [Routes](#57-routes)
99+
1. [Paginated Responses](#571-paginated-responses)
99100
8. [Caching](#58-caching)
100101
9. [More Advanced Caching](#59-more-advanced-caching)
101102
10. [ARQ Job Queues](#510-arq-job-queues)
@@ -412,6 +413,7 @@ First, you may want to take a look at the project structure and understand what
412413
│ │ ├── __init__.py
413414
│ │ ├── dependencies.py # Defines dependencies that can be reused across the API endpoints.
414415
│ │ ├── exceptions.py # Contains custom exceptions for the API.
416+
│ │ ├── paginated.py # Provides utilities for paginated responses in APIs
415417
│ │ │
416418
│ │ └── v1 # Version 1 of the API.
417419
│ │ ├── __init__.py
@@ -690,10 +692,15 @@ from app.core.database import async_get_db
690692

691693
router = fastapi.APIRouter(tags=["entities"])
692694

693-
@router.get("/entities", response_model=List[EntityRead])
694-
async def read_entities(db: Annotated[AsyncSession, Depends(async_get_db)]):
695-
entities = await crud_entities.get_multi(db=db)
696-
return entities
695+
@router.get("/entities/{id}", response_model=List[EntityRead])
696+
async def read_entities(
697+
request: Request,
698+
id: int,
699+
db: Annotated[AsyncSession, Depends(async_get_db)]
700+
):
701+
entity = await crud_entities.get(db=db, id=id)
702+
703+
return entity
697704

698705
...
699706
```
@@ -708,6 +715,71 @@ router = APIRouter(prefix="/v1") # this should be there already
708715
router.include_router(entity_router)
709716
```
710717

718+
#### 5.7.1 Paginated Responses
719+
With the `get_multi` method we get a python `dict` with full suport for pagination:
720+
```javascript
721+
{
722+
"data": [
723+
{
724+
"id": 4,
725+
"name": "User Userson",
726+
"username": "userson4",
727+
"email": "[email protected]",
728+
"profile_image_url": "https://profileimageurl.com"
729+
},
730+
{
731+
"id": 5,
732+
"name": "User Userson",
733+
"username": "userson5",
734+
"email": "[email protected]",
735+
"profile_image_url": "https://profileimageurl.com"
736+
}
737+
],
738+
"total_count": 2,
739+
"has_more": false,
740+
"page": 1,
741+
"items_per_page": 10
742+
}
743+
```
744+
745+
And in the endpoint, we can import from `app/api/paginated` the following functions and Pydantic Schema:
746+
```python
747+
from app.api.paginated import (
748+
PaginatedListResponse, # What you'll use as a response_model to validate
749+
paginated_response, # Creates a paginated response based on the parameters
750+
compute_offset # Calculate the offset for pagination ((page - 1) * items_per_page)
751+
)
752+
```
753+
754+
Then let's create the endpoint:
755+
```python
756+
import fastapi
757+
758+
from app.schemas.entity imoport EntityRead
759+
...
760+
761+
@router.get("/entities", response_model=PaginatedListResponse[EntityRead])
762+
async def read_entities(
763+
request: Request,
764+
db: Annotated[AsyncSession, Depends(async_get_db)],
765+
page: int = 1,
766+
items_per_page: int = 10
767+
):
768+
entities_data = await crud_entity.get_multi(
769+
db=db,
770+
offset=compute_offset(page, items_per_page),
771+
limit=items_per_page,
772+
schema_to_select=UserRead,
773+
is_deleted=False
774+
)
775+
776+
return paginated_response(
777+
crud_data=entities_data,
778+
page=page,
779+
items_per_page=items_per_page
780+
)
781+
```
782+
711783
### 5.8 Caching
712784
The `cache` decorator allows you to cache the results of FastAPI endpoint functions, enhancing response times and reducing the load on your application by storing and retrieving data in a cache.
713785

src/app/api/paginated.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from typing import TypeVar, Generic, List
2+
3+
from pydantic import BaseModel
4+
5+
SchemaType = TypeVar("SchemaType", bound=BaseModel)
6+
7+
class ListResponse(BaseModel, Generic[SchemaType]):
8+
data: List[SchemaType]
9+
10+
11+
class PaginatedListResponse(ListResponse[SchemaType]):
12+
total_count: int
13+
has_more: bool
14+
page: int | None = None
15+
items_per_page: int | None = None
16+
17+
18+
def paginated_response(
19+
crud_data: ListResponse[SchemaType],
20+
page: int,
21+
items_per_page: int
22+
) -> PaginatedListResponse[SchemaType]:
23+
"""
24+
Create a paginated response based on the provided data and pagination parameters.
25+
26+
Parameters
27+
----------
28+
crud_data : ListResponse[SchemaType]
29+
Data to be paginated, including the list of items and total count.
30+
page : int
31+
Current page number.
32+
items_per_page : int
33+
Number of items per page.
34+
35+
Returns
36+
-------
37+
PaginatedListResponse[SchemaType]
38+
A structured paginated response containing the list of items, total count, pagination flags, and numbers.
39+
40+
Note
41+
----
42+
The function does not actually paginate the data but formats the response to indicate pagination metadata.
43+
"""
44+
return {
45+
"data": crud_data["data"],
46+
"total_count": crud_data["total_count"],
47+
"has_more": (page * items_per_page) < crud_data["total_count"],
48+
"page": page,
49+
"items_per_page": items_per_page
50+
}
51+
52+
def compute_offset(page: int, items_per_page: int) -> int:
53+
"""
54+
Calculate the offset for pagination based on the given page number and items per page.
55+
56+
The offset represents the starting point in a dataset for the items on a given page.
57+
For example, if each page displays 10 items and you want to display page 3, the offset will be 20,
58+
meaning the display should start with the 21st item.
59+
60+
Parameters
61+
----------
62+
page : int
63+
The current page number. Page numbers should start from 1.
64+
items_per_page : int
65+
The number of items to be displayed on each page.
66+
67+
Returns
68+
-------
69+
int
70+
The calculated offset.
71+
72+
Examples
73+
--------
74+
>>> offset(1, 10)
75+
0
76+
>>> offset(3, 10)
77+
20
78+
"""
79+
return (page - 1) * items_per_page

src/app/api/v1/posts.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from app.crud.crud_users import crud_users
1313
from app.api.exceptions import privileges_exception
1414
from app.core.cache import cache
15-
from app.core.models import PaginatedListResponse
15+
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
1616

1717
router = fastapi.APIRouter(tags=["posts"])
1818

@@ -53,22 +53,20 @@ async def read_posts(
5353

5454
posts_data = await crud_posts.get_multi(
5555
db=db,
56-
offset=(page - 1) * items_per_page,
56+
offset=compute_offset(page, items_per_page),
5757
limit=items_per_page,
5858
schema_to_select=PostRead,
5959
created_by_user_id=db_user["id"],
6060
is_deleted=False
6161
)
6262

63-
return {
64-
"data": posts_data["data"],
65-
"total_count": posts_data["total_count"],
66-
"has_more": (page * items_per_page) < posts_data["total_count"],
67-
"page": page,
68-
"items_per_page": items_per_page
69-
}
70-
71-
63+
return paginated_response(
64+
crud_data=posts_data,
65+
page=page,
66+
items_per_page=items_per_page
67+
)
68+
69+
7270
@router.get("/{username}/post/{id}", response_model=PostRead)
7371
@cache(key_prefix="{username}_post_cache", resource_id_name="id")
7472
async def read_post(

src/app/api/v1/users.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from app.core.security import get_password_hash
1212
from app.crud.crud_users import crud_users
1313
from app.api.exceptions import privileges_exception
14-
from app.core.models import PaginatedListResponse
14+
from app.api.paginated import PaginatedListResponse, paginated_response, compute_offset
1515

1616
router = fastapi.APIRouter(tags=["users"])
1717

@@ -46,19 +46,17 @@ async def read_users(
4646
):
4747
users_data = await crud_users.get_multi(
4848
db=db,
49-
offset=(page - 1) * items_per_page,
49+
offset=compute_offset(page, items_per_page),
5050
limit=items_per_page,
5151
schema_to_select=UserRead,
5252
is_deleted=False
5353
)
5454

55-
return {
56-
"data": users_data["data"],
57-
"total_count": users_data["total_count"],
58-
"has_more": (page * items_per_page) < users_data["total_count"],
59-
"page": page,
60-
"items_per_page": items_per_page
61-
}
55+
return paginated_response(
56+
crud_data=users_data,
57+
page=page,
58+
items_per_page=items_per_page
59+
)
6260

6361

6462
@router.get("/user/me/", response_model=UserRead)

src/app/core/models.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
1-
from typing import TypeVar, Generic, List
21
import uuid as uuid_pkg
32
from datetime import datetime
43

54
from pydantic import BaseModel, Field, field_serializer
65
from sqlalchemy import text
76

8-
ReadSchemaType = TypeVar("ReadSchemaType", bound=BaseModel)
9-
10-
class ListResponse(BaseModel, Generic[ReadSchemaType]):
11-
data: List[ReadSchemaType]
12-
13-
14-
class PaginatedListResponse(ListResponse[ReadSchemaType]):
15-
total_count: int
16-
has_more: bool
17-
page: int | None = None
18-
items_per_page: int | None = None
19-
20-
217
class HealthCheck(BaseModel):
228
name: str
239
version: str

0 commit comments

Comments
 (0)