Skip to content

Commit 737bff2

Browse files
authored
feat: add with_pagination dependency (#13)
1 parent 56121c7 commit 737bff2

5 files changed

Lines changed: 549 additions & 192 deletions

File tree

README.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ app = FastAPI()
2222
fastapi_sqla.setup(app)
2323
```
2424

25+
## SQLAlchemy
26+
2527
### Adding a new entity class:
2628

2729
```python
@@ -46,7 +48,88 @@ def example(session: Session = Depends(with_session)):
4648
return session.execute("SELECT now()").scalar()
4749
```
4850

51+
### Pagination
52+
53+
```python
54+
from fastapi import APIRouter, Depends
55+
from fastapi_sqla import Base, Paginated, Session, with_pagination, with_session
56+
from pydantic import BaseModel
57+
58+
router = APIRouter()
59+
60+
61+
class UserEntity(Base):
62+
__tablename__ = "user"
63+
64+
65+
class User(BaseModel):
66+
id: int
67+
name: str
68+
69+
70+
@router.get("/users", response_model=Paginated[User])
71+
def all_users(
72+
session: Session = Depends(with_session),
73+
paginated_result=Depends(with_pagination),
74+
):
75+
query = session.query(UserEntity)
76+
return paginated_result(query)
77+
```
78+
79+
By default:
80+
* It returns pages of 10 items, up to 100 items;
81+
* Total number of items in the collection is queried using
82+
[`Query.count`](https://docs.sqlalchemy.org/en/13/orm/query.html#sqlalchemy.orm.query.Query.count)
83+
84+
### Custom pagination
85+
86+
You can customize:
87+
- Minimum and maximunm number of items per pages;
88+
- How the total number of items in the collection is queried;
89+
90+
To customize pagination, create a dependency using `fastapi_sqla.Pagination`
91+
92+
```python
93+
from fastapi import APIRouter, Depends
94+
from fastapi_sqla import Base, Paginated, Pagination, Session, with_session
95+
from pydantic import BaseModel
96+
from sqlalchemy import func
97+
from sqlalchemy.orm import Query
98+
99+
router = APIRouter()
100+
101+
102+
class UserEntity(Base):
103+
__tablename__ = "user"
104+
105+
106+
class User(BaseModel):
107+
id: int
108+
name: str
109+
110+
111+
def query_count(session: Session, query: Query):
112+
return query.statement.with_only_columns([func.count()]).scalar()
113+
114+
115+
with_custom_pagination = Pagination(
116+
min_page_size=5,
117+
max_page_size=500,
118+
query_count=query_count,
119+
)
120+
121+
122+
@router.get("/users", response_model=Paginated[User])
123+
def all_users(
124+
session: Session = Depends(with_session),
125+
paginated_result=Depends(with_custom_pagination),
126+
):
127+
query = session.query(UserEntity)
128+
return paginated_result(query)
129+
```
130+
49131
## Pytest fixtures
132+
50133
This library provides a set of utility fixtures, through its PyTest plugin, which is
51134
automatically installed with the library.
52135

@@ -75,7 +158,8 @@ def sqla_modules():
75158

76159
The DB url to use.
77160

78-
When `CI` key is set in environment variables, it defaults to using `postgres` as the host name:
161+
When `CI` key is set in environment variables, it defaults to using `postgres` as the
162+
host name:
79163

80164
```
81165
postgresql://postgres@posgres/postgres

fastapi_sqla/__init__.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import asyncio
2+
import math
23
import os
34
from contextlib import contextmanager
5+
from typing import Callable, Generic, List, TypeVar
46

57
import structlog
6-
from fastapi import FastAPI, Request
8+
from fastapi import Depends, FastAPI, Query, Request
79
from fastapi.concurrency import contextmanager_in_threadpool
10+
from pydantic import BaseModel, Field
11+
from pydantic.generics import GenericModel
812
from sqlalchemy import engine_from_config
913
from sqlalchemy.ext.declarative import DeferredReflection, declarative_base
14+
from sqlalchemy.orm import Query as DbQuery
1015
from sqlalchemy.orm.session import Session, sessionmaker
1116

1217
__all__ = ["Base", "setup", "with_session"]
@@ -108,3 +113,76 @@ def get_users(session: sqla.Session = Depends(sqla.new_session)):
108113
await loop.run_in_executor(None, session.rollback)
109114

110115
return response
116+
117+
118+
T = TypeVar("T")
119+
120+
121+
class Item(GenericModel, Generic[T]):
122+
"""Item container."""
123+
124+
data: T
125+
126+
127+
class Collection(GenericModel, Generic[T]):
128+
"""Collection container."""
129+
130+
data: List[T]
131+
132+
133+
class Meta(BaseModel):
134+
"""Meta information on current page and collection"""
135+
136+
offset: int = Field(..., description="Current page offset")
137+
total_items: int = Field(..., description="Total number of items in the collection")
138+
total_pages: int = Field(..., description="Total number of pages in the collection")
139+
page_number: int = Field(..., description="Current page number. Starts at 1.")
140+
141+
142+
class Paginated(Collection, Generic[T]):
143+
"""Paginated collection with information on current page and total items in meta."""
144+
145+
meta: Meta
146+
147+
148+
def _query_count(session: Session, query: DbQuery) -> int:
149+
"""Default function used to count items returned by a query.
150+
151+
Default Query.count is slower than a manually written query could be: It runs the
152+
query in a subquery, and count the number of elements returned:
153+
154+
See https://gist.github.com/hest/8798884
155+
"""
156+
return query.count()
157+
158+
159+
def Pagination(
160+
min_page_size: int = 10,
161+
max_page_size: int = 100,
162+
query_count: Callable[[Session, DbQuery], int] = _query_count,
163+
) -> Callable[[Session, int, int], Callable[[DbQuery], Paginated[T]]]:
164+
def dependency(
165+
session: Session = Depends(with_session),
166+
offset: int = Query(0, ge=0),
167+
limit: int = Query(min_page_size, ge=1, le=max_page_size),
168+
) -> Callable[[DbQuery], Paginated[T]]:
169+
def paginated_result(query: DbQuery) -> Paginated[T]:
170+
total_items = query_count(session, query)
171+
total_pages = math.ceil(total_items / limit)
172+
page_number = offset / limit + 1
173+
return Paginated[T](
174+
data=query.offset(offset).limit(limit).all(),
175+
meta={
176+
"offset": offset,
177+
"total_items": total_items,
178+
"total_pages": total_pages,
179+
"page_number": page_number,
180+
},
181+
)
182+
183+
return paginated_result
184+
185+
return dependency
186+
187+
188+
with_pagination = Pagination()

0 commit comments

Comments
 (0)