fastapi-sqla 2.10.1__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fastapi-sqla might be problematic. Click here for more details.
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/PKG-INFO +165 -28
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/README.md +162 -23
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/__init__.py +16 -3
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/_pytest_plugin.py +23 -14
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/async_pagination.py +16 -12
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/async_sqla.py +68 -48
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/aws_rds_iam_support.py +5 -3
- fastapi_sqla-3.0.0/fastapi_sqla/base.py +57 -0
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/pagination.py +12 -11
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/sqla.py +66 -44
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/pyproject.toml +4 -4
- fastapi_sqla-2.10.1/fastapi_sqla/base.py +0 -33
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/LICENSE +0 -0
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/aws_aurora_support.py +0 -0
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/models.py +0 -0
- {fastapi_sqla-2.10.1 → fastapi_sqla-3.0.0}/fastapi_sqla/py.typed +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi-sqla
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: SQLAlchemy extension for FastAPI with support for pagination, asyncio, and pytest, ready for production.
|
|
5
5
|
Home-page: https://github.com/dialoguemd/fastapi-sqla
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: FastAPI,SQLAlchemy,asyncio,pytest,alembic
|
|
8
8
|
Author: Hadrien David
|
|
9
9
|
Author-email: hadrien.david@dialogue.co
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4.0
|
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Environment :: Web Environment
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
@@ -19,8 +19,6 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
19
19
|
Classifier: Operating System :: OS Independent
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
21
|
Classifier: Programming Language :: Python :: 3
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
24
22
|
Classifier: Programming Language :: Python :: 3.9
|
|
25
23
|
Classifier: Programming Language :: Python :: 3.10
|
|
26
24
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -43,7 +41,7 @@ Requires-Dist: asgi_lifespan (>=1.0.1,<2.0.0) ; extra == "tests"
|
|
|
43
41
|
Requires-Dist: asyncpg (>=0.28.0,<0.29.0) ; extra == "asyncpg"
|
|
44
42
|
Requires-Dist: black (>=22.8.0,<23.0.0) ; extra == "tests"
|
|
45
43
|
Requires-Dist: boto3 (>=1.24.74,<2.0.0) ; extra == "aws-rds-iam"
|
|
46
|
-
Requires-Dist: fastapi (>=0.
|
|
44
|
+
Requires-Dist: fastapi (>=0.95.1)
|
|
47
45
|
Requires-Dist: greenlet (>=1.1.3,<2.0.0) ; extra == "tests"
|
|
48
46
|
Requires-Dist: httpx (>=0.23.0,<0.24.0) ; extra == "tests"
|
|
49
47
|
Requires-Dist: isort (>=5.5.3,<6.0.0) ; extra == "tests"
|
|
@@ -90,7 +88,7 @@ unique `email`:
|
|
|
90
88
|
|
|
91
89
|
```python
|
|
92
90
|
# main.py
|
|
93
|
-
from fastapi import
|
|
91
|
+
from fastapi import FastAPI, HTTPException
|
|
94
92
|
from fastapi_sqla import Base, Item, Page, Paginate, Session, setup
|
|
95
93
|
from pydantic import BaseModel, EmailStr
|
|
96
94
|
from sqlalchemy import select
|
|
@@ -118,12 +116,12 @@ class UserModel(UserIn):
|
|
|
118
116
|
|
|
119
117
|
|
|
120
118
|
@app.get("/users", response_model=Page[UserModel])
|
|
121
|
-
def list_users(paginate: Paginate
|
|
119
|
+
def list_users(paginate: Paginate):
|
|
122
120
|
return paginate(select(User))
|
|
123
121
|
|
|
124
122
|
|
|
125
123
|
@app.get("/users/{user_id}", response_model=Item[UserModel])
|
|
126
|
-
def get_user(user_id: int, session: Session
|
|
124
|
+
def get_user(user_id: int, session: Session):
|
|
127
125
|
user = session.get(User, user_id)
|
|
128
126
|
if user is None:
|
|
129
127
|
raise HTTPException(404)
|
|
@@ -131,7 +129,7 @@ def get_user(user_id: int, session: Session = Depends()):
|
|
|
131
129
|
|
|
132
130
|
|
|
133
131
|
@app.post("/users", response_model=Item[UserModel])
|
|
134
|
-
def create_user(new_user: UserIn, session: Session
|
|
132
|
+
def create_user(new_user: UserIn, session: Session):
|
|
135
133
|
user = User(**new_user.model_dump())
|
|
136
134
|
session.add(user)
|
|
137
135
|
try:
|
|
@@ -173,6 +171,21 @@ The only required key is `sqlalchemy_url`, which provides the database URL, exam
|
|
|
173
171
|
export sqlalchemy_url=postgresql://postgres@localhost
|
|
174
172
|
```
|
|
175
173
|
|
|
174
|
+
### Multi-session support
|
|
175
|
+
|
|
176
|
+
In order to configure multiple sessions for the application,
|
|
177
|
+
set the environment variables with this prefix format: `fastapi_sqla__MY_KEY__`.
|
|
178
|
+
|
|
179
|
+
As with the default session, each matching key (after the prefix is stripped)
|
|
180
|
+
is treated as though it were the corresponding keyword argument to [`sqlalchemy.create_engine`]
|
|
181
|
+
call.
|
|
182
|
+
|
|
183
|
+
For example, to configure a session with the `read_only` key:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
export fastapi_sqla__read_only__sqlalchemy_url=postgresql://postgres@localhost
|
|
187
|
+
```
|
|
188
|
+
|
|
176
189
|
### `asyncio` support using [`asyncpg`]
|
|
177
190
|
|
|
178
191
|
SQLAlchemy `>= 1.4` supports `asyncio`.
|
|
@@ -182,7 +195,7 @@ To enable `asyncio` support against a Postgres DB, install `asyncpg`:
|
|
|
182
195
|
pip install asyncpg
|
|
183
196
|
```
|
|
184
197
|
|
|
185
|
-
And define environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
|
|
198
|
+
And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
|
|
186
199
|
|
|
187
200
|
```bash
|
|
188
201
|
export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
|
|
@@ -216,23 +229,71 @@ class Entity(Base):
|
|
|
216
229
|
|
|
217
230
|
Use [FastAPI dependency injection] to get a session as a parameter of a path operation
|
|
218
231
|
function.
|
|
219
|
-
|
|
232
|
+
|
|
233
|
+
The SQLAlchemy session is committed before the response is returned or rollbacked if any
|
|
220
234
|
exception occurred:
|
|
221
235
|
|
|
222
236
|
```python
|
|
237
|
+
from fastapi import APIRouter
|
|
238
|
+
from fastapi_sqla import AsyncSession, Session
|
|
239
|
+
|
|
240
|
+
router = APIRouter()
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@router.get("/example")
|
|
244
|
+
def example(session: Session):
|
|
245
|
+
return session.execute("SELECT now()").scalar()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@router.get("/async_example")
|
|
249
|
+
async def async_example(session: AsyncSession):
|
|
250
|
+
return await session.scalar("SELECT now()")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
In order to get a session configured with a custom key:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from typing import Annotated
|
|
257
|
+
|
|
223
258
|
from fastapi import APIRouter, Depends
|
|
224
|
-
from fastapi_sqla import
|
|
259
|
+
from fastapi_sqla import (
|
|
260
|
+
AsyncSessionDependency,
|
|
261
|
+
SessionDependency,
|
|
262
|
+
SqlaAsyncSession,
|
|
263
|
+
SqlaSession,
|
|
264
|
+
)
|
|
225
265
|
|
|
226
266
|
router = APIRouter()
|
|
227
267
|
|
|
228
268
|
|
|
269
|
+
# Preferred
|
|
270
|
+
|
|
271
|
+
ReadOnlySession = Annotated[SqlaSession, Depends(SessionDependency(key="read_only"))]
|
|
272
|
+
AsyncReadOnlySession = Annotated[
|
|
273
|
+
SqlaAsyncSession, Depends(AsyncSessionDependency(key="read_only"))
|
|
274
|
+
]
|
|
275
|
+
|
|
229
276
|
@router.get("/example")
|
|
230
|
-
def example(session:
|
|
277
|
+
def example(session: ReadOnlySession):
|
|
231
278
|
return session.execute("SELECT now()").scalar()
|
|
232
279
|
|
|
233
280
|
|
|
234
281
|
@router.get("/async_example")
|
|
235
|
-
async def async_example(session:
|
|
282
|
+
async def async_example(session: AsyncReadOnlySession):
|
|
283
|
+
return await session.scalar("SELECT now()")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# Alternative
|
|
287
|
+
|
|
288
|
+
@router.get("/example/alt")
|
|
289
|
+
def example_alt(session: SqlaSession = Depends(SessionDependency(key="read_only"))):
|
|
290
|
+
return session.execute("SELECT now()").scalar()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@router.get("/async_example/alt")
|
|
294
|
+
async def async_example_alt(
|
|
295
|
+
session: SqlaAsyncSession = Depends(AsyncSessionDependency(key="read_only")),
|
|
296
|
+
):
|
|
236
297
|
return await session.scalar("SELECT now()")
|
|
237
298
|
```
|
|
238
299
|
|
|
@@ -240,12 +301,12 @@ async def async_example(session: AsyncSession = Depends()):
|
|
|
240
301
|
|
|
241
302
|
When needing a session outside of a path operation, like when using
|
|
242
303
|
[FastAPI background tasks], use `fastapi_sqla.open_session` context manager.
|
|
243
|
-
SQLAlchemy session is committed when exiting context or rollbacked if any exception
|
|
304
|
+
The SQLAlchemy session is committed when exiting context or rollbacked if any exception
|
|
244
305
|
occurred:
|
|
245
306
|
|
|
246
307
|
```python
|
|
247
308
|
from fastapi import APIRouter, BackgroundTasks
|
|
248
|
-
from fastapi_sqla import
|
|
309
|
+
from fastapi_sqla import open_async_session, open_session
|
|
249
310
|
|
|
250
311
|
router = APIRouter()
|
|
251
312
|
|
|
@@ -260,16 +321,23 @@ def run_bg():
|
|
|
260
321
|
with open_session() as session:
|
|
261
322
|
session.execute("SELECT now()").scalar()
|
|
262
323
|
|
|
324
|
+
def run_bg_with_key():
|
|
325
|
+
with open_session(key="read_only") as session:
|
|
326
|
+
session.execute("SELECT now()").scalar()
|
|
263
327
|
|
|
264
328
|
async def run_async_bg():
|
|
265
329
|
async with open_async_session() as session:
|
|
266
330
|
await session.scalar("SELECT now()")
|
|
331
|
+
|
|
332
|
+
async def run_async_bg_with_key():
|
|
333
|
+
async with open_async_session(key="read_only") as session:
|
|
334
|
+
await session.scalar("SELECT now()")
|
|
267
335
|
```
|
|
268
336
|
|
|
269
337
|
## Pagination
|
|
270
338
|
|
|
271
339
|
```python
|
|
272
|
-
from fastapi import APIRouter
|
|
340
|
+
from fastapi import APIRouter
|
|
273
341
|
from fastapi_sqla import Base, Page, Paginate
|
|
274
342
|
from pydantic import BaseModel
|
|
275
343
|
from sqlalchemy import select
|
|
@@ -290,7 +358,7 @@ class UserModel(BaseModel):
|
|
|
290
358
|
|
|
291
359
|
|
|
292
360
|
@router.get("/users", response_model=Page[UserModel])
|
|
293
|
-
def all_users(paginate: Paginate
|
|
361
|
+
def all_users(paginate: Paginate):
|
|
294
362
|
return paginate(select(User))
|
|
295
363
|
```
|
|
296
364
|
|
|
@@ -327,7 +395,7 @@ To paginate a query which doesn't return [scalars], specify `scalars=False` when
|
|
|
327
395
|
`paginate`:
|
|
328
396
|
|
|
329
397
|
```python
|
|
330
|
-
from fastapi import APIRouter
|
|
398
|
+
from fastapi import APIRouter
|
|
331
399
|
from fastapi_sqla import Base, Page, Paginate
|
|
332
400
|
from pydantic import BaseModel
|
|
333
401
|
from sqlalchemy import func, select
|
|
@@ -352,7 +420,7 @@ class UserModel(BaseModel):
|
|
|
352
420
|
|
|
353
421
|
|
|
354
422
|
@router.get("/users", response_model=Page[UserModel])
|
|
355
|
-
def all_users(paginate: Paginate
|
|
423
|
+
def all_users(paginate: Paginate):
|
|
356
424
|
query = (
|
|
357
425
|
select(User.id, User.name, func.count(Note.id).label("notes_count"))
|
|
358
426
|
.join(Note)
|
|
@@ -388,15 +456,15 @@ class UserModel(BaseModel):
|
|
|
388
456
|
name: str
|
|
389
457
|
|
|
390
458
|
|
|
391
|
-
def query_count(session: Session
|
|
459
|
+
def query_count(session: Session) -> int:
|
|
392
460
|
return session.execute(select(func.count()).select_from(User)).scalar()
|
|
393
461
|
|
|
394
462
|
|
|
395
|
-
|
|
463
|
+
CustomPaginate = Pagination(min_page_size=5, max_page_size=500, query_count=query_count)
|
|
396
464
|
|
|
397
465
|
|
|
398
466
|
@router.get("/users", response_model=Page[UserModel])
|
|
399
|
-
def all_users(paginate:
|
|
467
|
+
def all_users(paginate: CustomPaginate = Depends()):
|
|
400
468
|
return paginate(select(User))
|
|
401
469
|
```
|
|
402
470
|
|
|
@@ -405,7 +473,7 @@ def all_users(paginate: Paginate = Depends()):
|
|
|
405
473
|
When using the asyncio support, use the `AsyncPaginate` dependency:
|
|
406
474
|
|
|
407
475
|
```python
|
|
408
|
-
from fastapi import APIRouter
|
|
476
|
+
from fastapi import APIRouter
|
|
409
477
|
from fastapi_sqla import Base, Page, AsyncPaginate
|
|
410
478
|
from pydantic import BaseModel
|
|
411
479
|
from sqlalchemy import select
|
|
@@ -426,7 +494,7 @@ class UserModel(BaseModel):
|
|
|
426
494
|
|
|
427
495
|
|
|
428
496
|
@router.get("/users", response_model=Page[UserModel])
|
|
429
|
-
async def all_users(paginate: AsyncPaginate
|
|
497
|
+
async def all_users(paginate: AsyncPaginate):
|
|
430
498
|
return await paginate(select(User))
|
|
431
499
|
```
|
|
432
500
|
|
|
@@ -450,16 +518,85 @@ class UserModel(BaseModel):
|
|
|
450
518
|
name: str
|
|
451
519
|
|
|
452
520
|
|
|
453
|
-
async def query_count(session: AsyncSession
|
|
521
|
+
async def query_count(session: AsyncSession) -> int:
|
|
454
522
|
result = await session.execute(select(func.count()).select_from(User))
|
|
455
523
|
return result.scalar()
|
|
456
524
|
|
|
457
525
|
|
|
458
|
-
|
|
526
|
+
CustomPaginate = AsyncPagination(min_page_size=5, max_page_size=500, query_count=query_count)
|
|
459
527
|
|
|
460
528
|
|
|
461
529
|
@router.get("/users", response_model=Page[UserModel])
|
|
462
|
-
def all_users(paginate: CustomPaginate = Depends()):
|
|
530
|
+
async def all_users(paginate: CustomPaginate = Depends()):
|
|
531
|
+
return await paginate(select(User))
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Multi-session support
|
|
535
|
+
|
|
536
|
+
Pagination supports multiple sessions as well. To paginate using a session
|
|
537
|
+
configured with a custom key:
|
|
538
|
+
|
|
539
|
+
```python
|
|
540
|
+
from typing import Annotated
|
|
541
|
+
|
|
542
|
+
from fastapi import APIRouter, Depends
|
|
543
|
+
from fastapi_sqla import (
|
|
544
|
+
AsyncPaginateSignature,
|
|
545
|
+
AsyncPagination,
|
|
546
|
+
Base,
|
|
547
|
+
Page,
|
|
548
|
+
PaginateSignature,
|
|
549
|
+
Pagination,
|
|
550
|
+
)
|
|
551
|
+
from pydantic import BaseModel
|
|
552
|
+
from sqlalchemy import func, select
|
|
553
|
+
|
|
554
|
+
router = APIRouter()
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class User(Base):
|
|
558
|
+
__tablename__ = "user"
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class UserModel(BaseModel):
|
|
562
|
+
id: int
|
|
563
|
+
name: str
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Preferred
|
|
567
|
+
|
|
568
|
+
ReadOnlyPaginate = Annotated[
|
|
569
|
+
PaginateSignature, Depends(Pagination(session_key="read_only"))
|
|
570
|
+
]
|
|
571
|
+
AsyncReadOnlyPaginate = Annotated[
|
|
572
|
+
AsyncPaginateSignature, Depends(AsyncPagination(session_key="read_only"))
|
|
573
|
+
]
|
|
574
|
+
|
|
575
|
+
@router.get("/users", response_model=Page[UserModel])
|
|
576
|
+
def all_users(paginate: ReadOnlyPaginate):
|
|
577
|
+
return paginate(select(User))
|
|
578
|
+
|
|
579
|
+
@router.get("/async_users", response_model=Page[UserModel])
|
|
580
|
+
async def async_all_users(paginate: AsyncReadOnlyPaginate):
|
|
581
|
+
return await paginate(select(User))
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
# Alternative
|
|
585
|
+
|
|
586
|
+
@router.get("/users/alt", response_model=Page[UserModel])
|
|
587
|
+
def all_users_alt(
|
|
588
|
+
paginate: PaginateSignature = Depends(
|
|
589
|
+
Pagination(session_key="read_only")
|
|
590
|
+
),
|
|
591
|
+
):
|
|
592
|
+
return paginate(select(User))
|
|
593
|
+
|
|
594
|
+
@router.get("/async_users/alt", response_model=Page[UserModel])
|
|
595
|
+
async def async_all_users_alt(
|
|
596
|
+
paginate: AsyncPaginateSignature = Depends(
|
|
597
|
+
AsyncPagination(session_key="read_only")
|
|
598
|
+
),
|
|
599
|
+
):
|
|
463
600
|
return await paginate(select(User))
|
|
464
601
|
```
|
|
465
602
|
|
|
@@ -27,7 +27,7 @@ unique `email`:
|
|
|
27
27
|
|
|
28
28
|
```python
|
|
29
29
|
# main.py
|
|
30
|
-
from fastapi import
|
|
30
|
+
from fastapi import FastAPI, HTTPException
|
|
31
31
|
from fastapi_sqla import Base, Item, Page, Paginate, Session, setup
|
|
32
32
|
from pydantic import BaseModel, EmailStr
|
|
33
33
|
from sqlalchemy import select
|
|
@@ -55,12 +55,12 @@ class UserModel(UserIn):
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
@app.get("/users", response_model=Page[UserModel])
|
|
58
|
-
def list_users(paginate: Paginate
|
|
58
|
+
def list_users(paginate: Paginate):
|
|
59
59
|
return paginate(select(User))
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
@app.get("/users/{user_id}", response_model=Item[UserModel])
|
|
63
|
-
def get_user(user_id: int, session: Session
|
|
63
|
+
def get_user(user_id: int, session: Session):
|
|
64
64
|
user = session.get(User, user_id)
|
|
65
65
|
if user is None:
|
|
66
66
|
raise HTTPException(404)
|
|
@@ -68,7 +68,7 @@ def get_user(user_id: int, session: Session = Depends()):
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
@app.post("/users", response_model=Item[UserModel])
|
|
71
|
-
def create_user(new_user: UserIn, session: Session
|
|
71
|
+
def create_user(new_user: UserIn, session: Session):
|
|
72
72
|
user = User(**new_user.model_dump())
|
|
73
73
|
session.add(user)
|
|
74
74
|
try:
|
|
@@ -110,6 +110,21 @@ The only required key is `sqlalchemy_url`, which provides the database URL, exam
|
|
|
110
110
|
export sqlalchemy_url=postgresql://postgres@localhost
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
### Multi-session support
|
|
114
|
+
|
|
115
|
+
In order to configure multiple sessions for the application,
|
|
116
|
+
set the environment variables with this prefix format: `fastapi_sqla__MY_KEY__`.
|
|
117
|
+
|
|
118
|
+
As with the default session, each matching key (after the prefix is stripped)
|
|
119
|
+
is treated as though it were the corresponding keyword argument to [`sqlalchemy.create_engine`]
|
|
120
|
+
call.
|
|
121
|
+
|
|
122
|
+
For example, to configure a session with the `read_only` key:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
export fastapi_sqla__read_only__sqlalchemy_url=postgresql://postgres@localhost
|
|
126
|
+
```
|
|
127
|
+
|
|
113
128
|
### `asyncio` support using [`asyncpg`]
|
|
114
129
|
|
|
115
130
|
SQLAlchemy `>= 1.4` supports `asyncio`.
|
|
@@ -119,7 +134,7 @@ To enable `asyncio` support against a Postgres DB, install `asyncpg`:
|
|
|
119
134
|
pip install asyncpg
|
|
120
135
|
```
|
|
121
136
|
|
|
122
|
-
And define environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
|
|
137
|
+
And define the environment variable `sqlalchemy_url` with `postgres+asyncpg` scheme:
|
|
123
138
|
|
|
124
139
|
```bash
|
|
125
140
|
export sqlalchemy_url=postgresql+asyncpg://postgres@localhost
|
|
@@ -153,23 +168,71 @@ class Entity(Base):
|
|
|
153
168
|
|
|
154
169
|
Use [FastAPI dependency injection] to get a session as a parameter of a path operation
|
|
155
170
|
function.
|
|
156
|
-
|
|
171
|
+
|
|
172
|
+
The SQLAlchemy session is committed before the response is returned or rollbacked if any
|
|
157
173
|
exception occurred:
|
|
158
174
|
|
|
159
175
|
```python
|
|
176
|
+
from fastapi import APIRouter
|
|
177
|
+
from fastapi_sqla import AsyncSession, Session
|
|
178
|
+
|
|
179
|
+
router = APIRouter()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@router.get("/example")
|
|
183
|
+
def example(session: Session):
|
|
184
|
+
return session.execute("SELECT now()").scalar()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@router.get("/async_example")
|
|
188
|
+
async def async_example(session: AsyncSession):
|
|
189
|
+
return await session.scalar("SELECT now()")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
In order to get a session configured with a custom key:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from typing import Annotated
|
|
196
|
+
|
|
160
197
|
from fastapi import APIRouter, Depends
|
|
161
|
-
from fastapi_sqla import
|
|
198
|
+
from fastapi_sqla import (
|
|
199
|
+
AsyncSessionDependency,
|
|
200
|
+
SessionDependency,
|
|
201
|
+
SqlaAsyncSession,
|
|
202
|
+
SqlaSession,
|
|
203
|
+
)
|
|
162
204
|
|
|
163
205
|
router = APIRouter()
|
|
164
206
|
|
|
165
207
|
|
|
208
|
+
# Preferred
|
|
209
|
+
|
|
210
|
+
ReadOnlySession = Annotated[SqlaSession, Depends(SessionDependency(key="read_only"))]
|
|
211
|
+
AsyncReadOnlySession = Annotated[
|
|
212
|
+
SqlaAsyncSession, Depends(AsyncSessionDependency(key="read_only"))
|
|
213
|
+
]
|
|
214
|
+
|
|
166
215
|
@router.get("/example")
|
|
167
|
-
def example(session:
|
|
216
|
+
def example(session: ReadOnlySession):
|
|
168
217
|
return session.execute("SELECT now()").scalar()
|
|
169
218
|
|
|
170
219
|
|
|
171
220
|
@router.get("/async_example")
|
|
172
|
-
async def async_example(session:
|
|
221
|
+
async def async_example(session: AsyncReadOnlySession):
|
|
222
|
+
return await session.scalar("SELECT now()")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Alternative
|
|
226
|
+
|
|
227
|
+
@router.get("/example/alt")
|
|
228
|
+
def example_alt(session: SqlaSession = Depends(SessionDependency(key="read_only"))):
|
|
229
|
+
return session.execute("SELECT now()").scalar()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@router.get("/async_example/alt")
|
|
233
|
+
async def async_example_alt(
|
|
234
|
+
session: SqlaAsyncSession = Depends(AsyncSessionDependency(key="read_only")),
|
|
235
|
+
):
|
|
173
236
|
return await session.scalar("SELECT now()")
|
|
174
237
|
```
|
|
175
238
|
|
|
@@ -177,12 +240,12 @@ async def async_example(session: AsyncSession = Depends()):
|
|
|
177
240
|
|
|
178
241
|
When needing a session outside of a path operation, like when using
|
|
179
242
|
[FastAPI background tasks], use `fastapi_sqla.open_session` context manager.
|
|
180
|
-
SQLAlchemy session is committed when exiting context or rollbacked if any exception
|
|
243
|
+
The SQLAlchemy session is committed when exiting context or rollbacked if any exception
|
|
181
244
|
occurred:
|
|
182
245
|
|
|
183
246
|
```python
|
|
184
247
|
from fastapi import APIRouter, BackgroundTasks
|
|
185
|
-
from fastapi_sqla import
|
|
248
|
+
from fastapi_sqla import open_async_session, open_session
|
|
186
249
|
|
|
187
250
|
router = APIRouter()
|
|
188
251
|
|
|
@@ -197,16 +260,23 @@ def run_bg():
|
|
|
197
260
|
with open_session() as session:
|
|
198
261
|
session.execute("SELECT now()").scalar()
|
|
199
262
|
|
|
263
|
+
def run_bg_with_key():
|
|
264
|
+
with open_session(key="read_only") as session:
|
|
265
|
+
session.execute("SELECT now()").scalar()
|
|
200
266
|
|
|
201
267
|
async def run_async_bg():
|
|
202
268
|
async with open_async_session() as session:
|
|
203
269
|
await session.scalar("SELECT now()")
|
|
270
|
+
|
|
271
|
+
async def run_async_bg_with_key():
|
|
272
|
+
async with open_async_session(key="read_only") as session:
|
|
273
|
+
await session.scalar("SELECT now()")
|
|
204
274
|
```
|
|
205
275
|
|
|
206
276
|
## Pagination
|
|
207
277
|
|
|
208
278
|
```python
|
|
209
|
-
from fastapi import APIRouter
|
|
279
|
+
from fastapi import APIRouter
|
|
210
280
|
from fastapi_sqla import Base, Page, Paginate
|
|
211
281
|
from pydantic import BaseModel
|
|
212
282
|
from sqlalchemy import select
|
|
@@ -227,7 +297,7 @@ class UserModel(BaseModel):
|
|
|
227
297
|
|
|
228
298
|
|
|
229
299
|
@router.get("/users", response_model=Page[UserModel])
|
|
230
|
-
def all_users(paginate: Paginate
|
|
300
|
+
def all_users(paginate: Paginate):
|
|
231
301
|
return paginate(select(User))
|
|
232
302
|
```
|
|
233
303
|
|
|
@@ -264,7 +334,7 @@ To paginate a query which doesn't return [scalars], specify `scalars=False` when
|
|
|
264
334
|
`paginate`:
|
|
265
335
|
|
|
266
336
|
```python
|
|
267
|
-
from fastapi import APIRouter
|
|
337
|
+
from fastapi import APIRouter
|
|
268
338
|
from fastapi_sqla import Base, Page, Paginate
|
|
269
339
|
from pydantic import BaseModel
|
|
270
340
|
from sqlalchemy import func, select
|
|
@@ -289,7 +359,7 @@ class UserModel(BaseModel):
|
|
|
289
359
|
|
|
290
360
|
|
|
291
361
|
@router.get("/users", response_model=Page[UserModel])
|
|
292
|
-
def all_users(paginate: Paginate
|
|
362
|
+
def all_users(paginate: Paginate):
|
|
293
363
|
query = (
|
|
294
364
|
select(User.id, User.name, func.count(Note.id).label("notes_count"))
|
|
295
365
|
.join(Note)
|
|
@@ -325,15 +395,15 @@ class UserModel(BaseModel):
|
|
|
325
395
|
name: str
|
|
326
396
|
|
|
327
397
|
|
|
328
|
-
def query_count(session: Session
|
|
398
|
+
def query_count(session: Session) -> int:
|
|
329
399
|
return session.execute(select(func.count()).select_from(User)).scalar()
|
|
330
400
|
|
|
331
401
|
|
|
332
|
-
|
|
402
|
+
CustomPaginate = Pagination(min_page_size=5, max_page_size=500, query_count=query_count)
|
|
333
403
|
|
|
334
404
|
|
|
335
405
|
@router.get("/users", response_model=Page[UserModel])
|
|
336
|
-
def all_users(paginate:
|
|
406
|
+
def all_users(paginate: CustomPaginate = Depends()):
|
|
337
407
|
return paginate(select(User))
|
|
338
408
|
```
|
|
339
409
|
|
|
@@ -342,7 +412,7 @@ def all_users(paginate: Paginate = Depends()):
|
|
|
342
412
|
When using the asyncio support, use the `AsyncPaginate` dependency:
|
|
343
413
|
|
|
344
414
|
```python
|
|
345
|
-
from fastapi import APIRouter
|
|
415
|
+
from fastapi import APIRouter
|
|
346
416
|
from fastapi_sqla import Base, Page, AsyncPaginate
|
|
347
417
|
from pydantic import BaseModel
|
|
348
418
|
from sqlalchemy import select
|
|
@@ -363,7 +433,7 @@ class UserModel(BaseModel):
|
|
|
363
433
|
|
|
364
434
|
|
|
365
435
|
@router.get("/users", response_model=Page[UserModel])
|
|
366
|
-
async def all_users(paginate: AsyncPaginate
|
|
436
|
+
async def all_users(paginate: AsyncPaginate):
|
|
367
437
|
return await paginate(select(User))
|
|
368
438
|
```
|
|
369
439
|
|
|
@@ -387,16 +457,85 @@ class UserModel(BaseModel):
|
|
|
387
457
|
name: str
|
|
388
458
|
|
|
389
459
|
|
|
390
|
-
async def query_count(session: AsyncSession
|
|
460
|
+
async def query_count(session: AsyncSession) -> int:
|
|
391
461
|
result = await session.execute(select(func.count()).select_from(User))
|
|
392
462
|
return result.scalar()
|
|
393
463
|
|
|
394
464
|
|
|
395
|
-
|
|
465
|
+
CustomPaginate = AsyncPagination(min_page_size=5, max_page_size=500, query_count=query_count)
|
|
396
466
|
|
|
397
467
|
|
|
398
468
|
@router.get("/users", response_model=Page[UserModel])
|
|
399
|
-
def all_users(paginate: CustomPaginate = Depends()):
|
|
469
|
+
async def all_users(paginate: CustomPaginate = Depends()):
|
|
470
|
+
return await paginate(select(User))
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Multi-session support
|
|
474
|
+
|
|
475
|
+
Pagination supports multiple sessions as well. To paginate using a session
|
|
476
|
+
configured with a custom key:
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
from typing import Annotated
|
|
480
|
+
|
|
481
|
+
from fastapi import APIRouter, Depends
|
|
482
|
+
from fastapi_sqla import (
|
|
483
|
+
AsyncPaginateSignature,
|
|
484
|
+
AsyncPagination,
|
|
485
|
+
Base,
|
|
486
|
+
Page,
|
|
487
|
+
PaginateSignature,
|
|
488
|
+
Pagination,
|
|
489
|
+
)
|
|
490
|
+
from pydantic import BaseModel
|
|
491
|
+
from sqlalchemy import func, select
|
|
492
|
+
|
|
493
|
+
router = APIRouter()
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class User(Base):
|
|
497
|
+
__tablename__ = "user"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class UserModel(BaseModel):
|
|
501
|
+
id: int
|
|
502
|
+
name: str
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# Preferred
|
|
506
|
+
|
|
507
|
+
ReadOnlyPaginate = Annotated[
|
|
508
|
+
PaginateSignature, Depends(Pagination(session_key="read_only"))
|
|
509
|
+
]
|
|
510
|
+
AsyncReadOnlyPaginate = Annotated[
|
|
511
|
+
AsyncPaginateSignature, Depends(AsyncPagination(session_key="read_only"))
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
@router.get("/users", response_model=Page[UserModel])
|
|
515
|
+
def all_users(paginate: ReadOnlyPaginate):
|
|
516
|
+
return paginate(select(User))
|
|
517
|
+
|
|
518
|
+
@router.get("/async_users", response_model=Page[UserModel])
|
|
519
|
+
async def async_all_users(paginate: AsyncReadOnlyPaginate):
|
|
520
|
+
return await paginate(select(User))
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# Alternative
|
|
524
|
+
|
|
525
|
+
@router.get("/users/alt", response_model=Page[UserModel])
|
|
526
|
+
def all_users_alt(
|
|
527
|
+
paginate: PaginateSignature = Depends(
|
|
528
|
+
Pagination(session_key="read_only")
|
|
529
|
+
),
|
|
530
|
+
):
|
|
531
|
+
return paginate(select(User))
|
|
532
|
+
|
|
533
|
+
@router.get("/async_users/alt", response_model=Page[UserModel])
|
|
534
|
+
async def async_all_users_alt(
|
|
535
|
+
paginate: AsyncPaginateSignature = Depends(
|
|
536
|
+
AsyncPagination(session_key="read_only")
|
|
537
|
+
),
|
|
538
|
+
):
|
|
400
539
|
return await paginate(select(User))
|
|
401
540
|
```
|
|
402
541
|
|