fastapi-sqlalchemy-toolkit 0.6.13__tar.gz → 0.7.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.
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/PKG-INFO +85 -18
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/README.md +84 -17
- fastapi_sqlalchemy_toolkit-0.7.0/examples/app/api/deps.py +14 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/endpoints/child.py +11 -10
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/config.py +0 -2
- fastapi_sqlalchemy_toolkit-0.7.0/examples/app/db.py +7 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/models.py +5 -5
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/fastapi_sqlalchemy_toolkit/__init__.py +1 -1
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/fastapi_sqlalchemy_toolkit/model_manager.py +49 -38
- fastapi_sqlalchemy_toolkit-0.7.0/fastapi_sqlalchemy_toolkit/ordering.py +68 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/pyproject.toml +1 -1
- fastapi_sqlalchemy_toolkit-0.6.13/examples/app/db.py +0 -10
- fastapi_sqlalchemy_toolkit-0.6.13/examples/app/deps.py +0 -13
- fastapi_sqlalchemy_toolkit-0.6.13/fastapi_sqlalchemy_toolkit/ordering.py +0 -71
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/.flake8 +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/.github/workflows/python-publish.yml +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/.gitignore +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/.pylintrc +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/AUTHORS +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/LICENSE +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/.env +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/api.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/endpoints/__init__.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/main.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/managers.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/schemas.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
- {fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/requirements.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fastapi_sqlalchemy_toolkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: FastAPI SQLAlchemy Toolkit
|
|
5
5
|
Project-URL: Homepage, https://github.com/e-kondr01/fastapi-sqlalchemy-toolkit
|
|
6
6
|
Author-email: Egor Kondrashov <e.kondr01@gmail.com>
|
|
@@ -34,7 +34,7 @@ REST API и взаимодействии с СУБД через `SQLAlchemy`;
|
|
|
34
34
|
|
|
35
35
|
- Фильтрация с обработкой необязательных параметров запроса (см. раздел **Фильтрация**)
|
|
36
36
|
|
|
37
|
-
- Декларативная сортировка с помощью `
|
|
37
|
+
- Декларативная сортировка с помощью `ordering_depends` (см. раздел **Сортировка**)
|
|
38
38
|
|
|
39
39
|
- Валидация существования внешних ключей
|
|
40
40
|
|
|
@@ -426,37 +426,49 @@ async def get_my_objects(
|
|
|
426
426
|
связанных моделей join'ы будут сделаны автоматически.
|
|
427
427
|
|
|
428
428
|
Для применения декларативной сортировки нужно:
|
|
429
|
-
1. Определить
|
|
430
|
-
|
|
431
|
-
|
|
429
|
+
1. Определить поля, по которым доступна фильтрация.
|
|
430
|
+
|
|
431
|
+
Это может быть либо списком/кортежем полей основной модели:
|
|
432
432
|
|
|
433
433
|
```python
|
|
434
|
-
from app.models import
|
|
434
|
+
from app.models import Child
|
|
435
435
|
|
|
436
436
|
child_ordering_fields = (
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
Parent.title,
|
|
440
|
-
Parent.created_at
|
|
437
|
+
Child.title,
|
|
438
|
+
Child.created_at
|
|
441
439
|
)
|
|
442
440
|
```
|
|
443
441
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
В таком случае, будут доступны следующий параметря для сортировки:
|
|
443
|
+
`title`, `-title`, `created_at`, `-created_at`.
|
|
444
|
+
|
|
445
|
+
Дефис первым символом означает направление сортировки по убыванию.
|
|
446
|
+
|
|
447
|
+
Либо можно определить маппинг строковых полей для сортировки
|
|
448
|
+
на соответствующие поля моделей:
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
from app.models import Child, Parent
|
|
452
|
+
|
|
453
|
+
child_ordering_fields = (
|
|
454
|
+
"title": MyModel.title,
|
|
455
|
+
"parent_title": ParentModel.title
|
|
456
|
+
)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
В таком случае, будут доступны следующий параметря для сортировки:
|
|
460
|
+
`title`, `-title`, `parent_title`, `-parent_title`.
|
|
449
461
|
|
|
450
462
|
2. В параметрах энпдоинта передать определённый выше список
|
|
451
|
-
в `
|
|
463
|
+
в `ordering_depends`
|
|
452
464
|
|
|
453
465
|
```python
|
|
454
|
-
from fastapi_sqlalchemy_toolkit import
|
|
466
|
+
from fastapi_sqlalchemy_toolkit import ordering_depends
|
|
455
467
|
|
|
456
468
|
@router.get("/children")
|
|
457
469
|
async def get_child_objects(
|
|
458
470
|
session: Session,
|
|
459
|
-
order_by:
|
|
471
|
+
order_by: ordering_depends(child_ordering_fields)
|
|
460
472
|
) -> list[ChildListSchema]
|
|
461
473
|
...
|
|
462
474
|
```
|
|
@@ -467,6 +479,61 @@ async def get_child_objects(
|
|
|
467
479
|
return await child_manager.list(session=session, order_by=order_by)
|
|
468
480
|
```
|
|
469
481
|
|
|
482
|
+
Если `order_by` передаётся в методы `list` или `paginated_list`,
|
|
483
|
+
и поле для сортировки относится к модели, напрямую связанную с основной,
|
|
484
|
+
то будет выполнен необходимый `join` для применения сортировки.
|
|
485
|
+
|
|
486
|
+
## Транзакции
|
|
487
|
+
|
|
488
|
+
`fastapi-sqlalchemy-toolkit` поддерживает оба подхода к работе с транзакциями `SQAlchemy`.
|
|
489
|
+
|
|
490
|
+
### Commit as you go
|
|
491
|
+
|
|
492
|
+
https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#commit-as-you-go
|
|
493
|
+
|
|
494
|
+
```python
|
|
495
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
496
|
+
from app.managers import my_model_manager
|
|
497
|
+
|
|
498
|
+
...
|
|
499
|
+
|
|
500
|
+
engine = create_async_engine(
|
|
501
|
+
"...",
|
|
502
|
+
)
|
|
503
|
+
async with async_sessionmaker(engine) as session:
|
|
504
|
+
# This call produces SQL COMMIT
|
|
505
|
+
created_obj = await my_model_manager.create(session, input_data)
|
|
506
|
+
# This call does not produce SQL COMMIT
|
|
507
|
+
await my_model_manager.update(session, created_obj, name="updated_name", commit=False)
|
|
508
|
+
# Only 1st statement is persisted
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### Begin once
|
|
512
|
+
|
|
513
|
+
https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#begin-once
|
|
514
|
+
|
|
515
|
+
```python
|
|
516
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
517
|
+
from app.managers import my_model_manager
|
|
518
|
+
|
|
519
|
+
...
|
|
520
|
+
|
|
521
|
+
engine = create_async_engine(
|
|
522
|
+
"...",
|
|
523
|
+
)
|
|
524
|
+
# Start transaction with context manager
|
|
525
|
+
async with async_sessionmaker(engine) as session, session.begin():
|
|
526
|
+
# This call only flushes, no SQL COMMIT yet
|
|
527
|
+
created_obj = await my_model_manager.create(session, input_data)
|
|
528
|
+
# This call only flushes, no SQL COMMIT yet
|
|
529
|
+
await my_model_manager.update(session, created_obj, name="updated_name")
|
|
530
|
+
# Everything is SQL COMMITed, if no errors occured in nested block
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
## Создание и обновление объектов
|
|
534
|
+
|
|
535
|
+
TODO...
|
|
536
|
+
|
|
470
537
|
|
|
471
538
|
## Расширение
|
|
472
539
|
Методы `ModelManager` легко расширить дополнительной логикой.
|
|
@@ -17,7 +17,7 @@ REST API и взаимодействии с СУБД через `SQLAlchemy`;
|
|
|
17
17
|
|
|
18
18
|
- Фильтрация с обработкой необязательных параметров запроса (см. раздел **Фильтрация**)
|
|
19
19
|
|
|
20
|
-
- Декларативная сортировка с помощью `
|
|
20
|
+
- Декларативная сортировка с помощью `ordering_depends` (см. раздел **Сортировка**)
|
|
21
21
|
|
|
22
22
|
- Валидация существования внешних ключей
|
|
23
23
|
|
|
@@ -409,37 +409,49 @@ async def get_my_objects(
|
|
|
409
409
|
связанных моделей join'ы будут сделаны автоматически.
|
|
410
410
|
|
|
411
411
|
Для применения декларативной сортировки нужно:
|
|
412
|
-
1. Определить
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
1. Определить поля, по которым доступна фильтрация.
|
|
413
|
+
|
|
414
|
+
Это может быть либо списком/кортежем полей основной модели:
|
|
415
415
|
|
|
416
416
|
```python
|
|
417
|
-
from app.models import
|
|
417
|
+
from app.models import Child
|
|
418
418
|
|
|
419
419
|
child_ordering_fields = (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
Parent.title,
|
|
423
|
-
Parent.created_at
|
|
420
|
+
Child.title,
|
|
421
|
+
Child.created_at
|
|
424
422
|
)
|
|
425
423
|
```
|
|
426
424
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
425
|
+
В таком случае, будут доступны следующий параметря для сортировки:
|
|
426
|
+
`title`, `-title`, `created_at`, `-created_at`.
|
|
427
|
+
|
|
428
|
+
Дефис первым символом означает направление сортировки по убыванию.
|
|
429
|
+
|
|
430
|
+
Либо можно определить маппинг строковых полей для сортировки
|
|
431
|
+
на соответствующие поля моделей:
|
|
432
|
+
|
|
433
|
+
```python
|
|
434
|
+
from app.models import Child, Parent
|
|
435
|
+
|
|
436
|
+
child_ordering_fields = (
|
|
437
|
+
"title": MyModel.title,
|
|
438
|
+
"parent_title": ParentModel.title
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
В таком случае, будут доступны следующий параметря для сортировки:
|
|
443
|
+
`title`, `-title`, `parent_title`, `-parent_title`.
|
|
432
444
|
|
|
433
445
|
2. В параметрах энпдоинта передать определённый выше список
|
|
434
|
-
в `
|
|
446
|
+
в `ordering_depends`
|
|
435
447
|
|
|
436
448
|
```python
|
|
437
|
-
from fastapi_sqlalchemy_toolkit import
|
|
449
|
+
from fastapi_sqlalchemy_toolkit import ordering_depends
|
|
438
450
|
|
|
439
451
|
@router.get("/children")
|
|
440
452
|
async def get_child_objects(
|
|
441
453
|
session: Session,
|
|
442
|
-
order_by:
|
|
454
|
+
order_by: ordering_depends(child_ordering_fields)
|
|
443
455
|
) -> list[ChildListSchema]
|
|
444
456
|
...
|
|
445
457
|
```
|
|
@@ -450,6 +462,61 @@ async def get_child_objects(
|
|
|
450
462
|
return await child_manager.list(session=session, order_by=order_by)
|
|
451
463
|
```
|
|
452
464
|
|
|
465
|
+
Если `order_by` передаётся в методы `list` или `paginated_list`,
|
|
466
|
+
и поле для сортировки относится к модели, напрямую связанную с основной,
|
|
467
|
+
то будет выполнен необходимый `join` для применения сортировки.
|
|
468
|
+
|
|
469
|
+
## Транзакции
|
|
470
|
+
|
|
471
|
+
`fastapi-sqlalchemy-toolkit` поддерживает оба подхода к работе с транзакциями `SQAlchemy`.
|
|
472
|
+
|
|
473
|
+
### Commit as you go
|
|
474
|
+
|
|
475
|
+
https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#commit-as-you-go
|
|
476
|
+
|
|
477
|
+
```python
|
|
478
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
479
|
+
from app.managers import my_model_manager
|
|
480
|
+
|
|
481
|
+
...
|
|
482
|
+
|
|
483
|
+
engine = create_async_engine(
|
|
484
|
+
"...",
|
|
485
|
+
)
|
|
486
|
+
async with async_sessionmaker(engine) as session:
|
|
487
|
+
# This call produces SQL COMMIT
|
|
488
|
+
created_obj = await my_model_manager.create(session, input_data)
|
|
489
|
+
# This call does not produce SQL COMMIT
|
|
490
|
+
await my_model_manager.update(session, created_obj, name="updated_name", commit=False)
|
|
491
|
+
# Only 1st statement is persisted
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Begin once
|
|
495
|
+
|
|
496
|
+
https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#begin-once
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
500
|
+
from app.managers import my_model_manager
|
|
501
|
+
|
|
502
|
+
...
|
|
503
|
+
|
|
504
|
+
engine = create_async_engine(
|
|
505
|
+
"...",
|
|
506
|
+
)
|
|
507
|
+
# Start transaction with context manager
|
|
508
|
+
async with async_sessionmaker(engine) as session, session.begin():
|
|
509
|
+
# This call only flushes, no SQL COMMIT yet
|
|
510
|
+
created_obj = await my_model_manager.create(session, input_data)
|
|
511
|
+
# This call only flushes, no SQL COMMIT yet
|
|
512
|
+
await my_model_manager.update(session, created_obj, name="updated_name")
|
|
513
|
+
# Everything is SQL COMMITed, if no errors occured in nested block
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Создание и обновление объектов
|
|
517
|
+
|
|
518
|
+
TODO...
|
|
519
|
+
|
|
453
520
|
|
|
454
521
|
## Расширение
|
|
455
522
|
Методы `ModelManager` легко расширить дополнительной логикой.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Annotated, AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from app.db import async_session_factory
|
|
4
|
+
from fastapi import Depends
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
9
|
+
async_session = async_session_factory() # type: ignore
|
|
10
|
+
async with async_session:
|
|
11
|
+
yield async_session
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Session = Annotated[AsyncSession, Depends(get_async_session)]
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from datetime import date
|
|
2
|
-
from typing import Annotated
|
|
3
2
|
from uuid import UUID
|
|
4
3
|
|
|
5
|
-
from app.deps import
|
|
4
|
+
from app.api.deps import Session
|
|
6
5
|
from app.managers import child_manager
|
|
7
6
|
from app.models import Child, Parent
|
|
8
7
|
from app.schemas import (
|
|
@@ -12,26 +11,28 @@ from app.schemas import (
|
|
|
12
11
|
HTTPErrorSchema,
|
|
13
12
|
PatchChildSchema,
|
|
14
13
|
)
|
|
15
|
-
from fastapi import APIRouter,
|
|
16
|
-
from fastapi_pagination import Page
|
|
14
|
+
from fastapi import APIRouter, Response, status
|
|
15
|
+
from fastapi_pagination import Page
|
|
17
16
|
from sqlalchemy import func
|
|
18
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
19
17
|
from sqlalchemy.orm import joinedload
|
|
20
18
|
|
|
21
|
-
from fastapi_sqlalchemy_toolkit import
|
|
19
|
+
from fastapi_sqlalchemy_toolkit import ordering_depends
|
|
22
20
|
|
|
23
21
|
router = APIRouter()
|
|
24
22
|
|
|
25
|
-
Session = Annotated[AsyncSession, Depends(get_async_session)]
|
|
26
|
-
PaginationParams = Annotated[Params, Depends()]
|
|
27
23
|
|
|
28
|
-
children_ordering_fields =
|
|
24
|
+
children_ordering_fields = {
|
|
25
|
+
"title": Child.title,
|
|
26
|
+
"created_at": Child.created_at,
|
|
27
|
+
"parent_title": Parent.title,
|
|
28
|
+
"parent_created_at": Parent.created_at,
|
|
29
|
+
}
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@router.get("")
|
|
32
33
|
async def get_list(
|
|
33
34
|
session: Session,
|
|
34
|
-
order_by:
|
|
35
|
+
order_by: ordering_depends(children_ordering_fields),
|
|
35
36
|
title: str | None = None,
|
|
36
37
|
slug: str | None = None,
|
|
37
38
|
parent_title: str | None = None,
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/config.py
RENAMED
|
@@ -18,8 +18,6 @@ class Settings(BaseSettings):
|
|
|
18
18
|
POSTGRES_DB: str
|
|
19
19
|
SQLALCHEMY_DATABASE_URL: str | None = None
|
|
20
20
|
|
|
21
|
-
SENTRY_DSN: str | None = None
|
|
22
|
-
|
|
23
21
|
@field_validator("SQLALCHEMY_DATABASE_URL", mode="before")
|
|
24
22
|
def assemble_db_connection_string(
|
|
25
23
|
cls, value: PostgresDsn | None, info: FieldValidationInfo
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
2
|
+
|
|
3
|
+
from .config import settings
|
|
4
|
+
|
|
5
|
+
engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URL, future=True, echo=False)
|
|
6
|
+
|
|
7
|
+
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/models.py
RENAMED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from uuid import UUID
|
|
3
|
-
from uuid import UUID as _py_uuid
|
|
4
|
-
from uuid import uuid4
|
|
2
|
+
from uuid import UUID, uuid4
|
|
5
3
|
|
|
6
4
|
from sqlalchemy import DateTime, ForeignKey, func
|
|
7
5
|
from sqlalchemy.orm import (
|
|
@@ -14,8 +12,10 @@ from sqlalchemy.orm import (
|
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class Base(DeclarativeBase):
|
|
17
|
-
id: Mapped[
|
|
18
|
-
|
|
15
|
+
id: Mapped[UUID] = mapped_column(
|
|
16
|
+
primary_key=True,
|
|
17
|
+
default=uuid4,
|
|
18
|
+
server_default=func.gen_random_uuid(),
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
created_at: Mapped[datetime] = mapped_column(
|
|
@@ -15,7 +15,6 @@ from sqlalchemy.sql.functions import Function
|
|
|
15
15
|
from sqlalchemy.sql.selectable import Exists
|
|
16
16
|
|
|
17
17
|
from .filters import null_query_values
|
|
18
|
-
from .ordering import OrderingField
|
|
19
18
|
|
|
20
19
|
ModelT = TypeVar("ModelT", bound=DeclarativeBase)
|
|
21
20
|
CreateSchemaT = TypeVar("CreateSchemaT", bound=BaseModel)
|
|
@@ -129,7 +128,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
129
128
|
await self.run_db_validation(session, in_obj=create_data)
|
|
130
129
|
db_obj = self.model(**create_data)
|
|
131
130
|
session.add(db_obj)
|
|
132
|
-
await self.
|
|
131
|
+
await self.persist(session, commit)
|
|
133
132
|
await session.refresh(db_obj, attribute_names=refresh_attribute_names)
|
|
134
133
|
return db_obj
|
|
135
134
|
|
|
@@ -137,7 +136,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
137
136
|
self,
|
|
138
137
|
session: AsyncSession,
|
|
139
138
|
options: List[Any] | Any | None = None,
|
|
140
|
-
order_by:
|
|
139
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
141
140
|
where: Any | None = None,
|
|
142
141
|
base_stmt: Select | None = None,
|
|
143
142
|
**simple_filters: Any,
|
|
@@ -149,7 +148,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
149
148
|
|
|
150
149
|
:param options: параметры для метода .options() загрузчика SQLAlchemy
|
|
151
150
|
|
|
152
|
-
:param order_by: поле для сортировки
|
|
151
|
+
:param order_by: поле для сортировки
|
|
153
152
|
|
|
154
153
|
:param where: выражение, которое будет передано в метод .where() SQLAlchemy
|
|
155
154
|
|
|
@@ -174,7 +173,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
174
173
|
self,
|
|
175
174
|
session: AsyncSession,
|
|
176
175
|
options: List[Any] | Any | None = None,
|
|
177
|
-
order_by:
|
|
176
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
178
177
|
where: Any | None = None,
|
|
179
178
|
base_stmt: Select | None = None,
|
|
180
179
|
**simple_filters: Any,
|
|
@@ -186,7 +185,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
186
185
|
|
|
187
186
|
:param options: параметры для метода .options() загрузчика SQLAlchemy
|
|
188
187
|
|
|
189
|
-
:param order_by: поле для сортировки
|
|
188
|
+
:param order_by: поле для сортировки
|
|
190
189
|
|
|
191
190
|
:param where: выражение, которое будет передано в метод .where() SQLAlchemy
|
|
192
191
|
|
|
@@ -294,7 +293,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
294
293
|
async def paginated_filter(
|
|
295
294
|
self,
|
|
296
295
|
session: AsyncSession,
|
|
297
|
-
order_by:
|
|
296
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
298
297
|
options: List[Any] | Any | None = None,
|
|
299
298
|
where: Any | None = None,
|
|
300
299
|
base_stmt: Select | None = None,
|
|
@@ -306,7 +305,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
306
305
|
|
|
307
306
|
:param session: сессия SQLAlchemy
|
|
308
307
|
|
|
309
|
-
:param order_by: поле для сортировки
|
|
308
|
+
:param order_by: поле для сортировки
|
|
310
309
|
|
|
311
310
|
:param options: параметры для метода .options() загрузчика SQLAlchemy
|
|
312
311
|
|
|
@@ -330,7 +329,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
330
329
|
async def paginated_list(
|
|
331
330
|
self,
|
|
332
331
|
session: AsyncSession,
|
|
333
|
-
order_by:
|
|
332
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
334
333
|
filter_expressions: dict[InstrumentedAttribute | Callable, Any] | None = None,
|
|
335
334
|
nullable_filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
336
335
|
| None = None,
|
|
@@ -346,7 +345,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
346
345
|
|
|
347
346
|
:param session: сессия SQLAlchemy
|
|
348
347
|
|
|
349
|
-
:param order_by: поле для сортировки
|
|
348
|
+
:param order_by: поле для сортировки
|
|
350
349
|
|
|
351
350
|
:param filter_expressions: словарь, отображающий поля для фильтрации
|
|
352
351
|
на их значения. Фильтрация по None не применяется. См. раздел "фильтрация"
|
|
@@ -405,7 +404,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
405
404
|
async def filter(
|
|
406
405
|
self,
|
|
407
406
|
session: AsyncSession,
|
|
408
|
-
order_by:
|
|
407
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
409
408
|
options: List[Any] | Any | None = None,
|
|
410
409
|
where: Any | None = None,
|
|
411
410
|
unique: bool = False,
|
|
@@ -417,7 +416,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
417
416
|
|
|
418
417
|
:param session: сессия SQLAlchemy
|
|
419
418
|
|
|
420
|
-
:param order_by: поле для сортировки
|
|
419
|
+
:param order_by: поле для сортировки
|
|
421
420
|
|
|
422
421
|
:param options: параметры для метода .options() загрузчика SQLAlchemy
|
|
423
422
|
|
|
@@ -446,7 +445,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
446
445
|
async def list(
|
|
447
446
|
self,
|
|
448
447
|
session: AsyncSession,
|
|
449
|
-
order_by:
|
|
448
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
450
449
|
filter_expressions: dict[InstrumentedAttribute | Callable, Any] | None = None,
|
|
451
450
|
nullable_filter_expressions: dict[InstrumentedAttribute | Callable, Any]
|
|
452
451
|
| None = None,
|
|
@@ -462,7 +461,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
462
461
|
|
|
463
462
|
:param session: сессия SQLAlchemy
|
|
464
463
|
|
|
465
|
-
:param order_by: поле для сортировки
|
|
464
|
+
:param order_by: поле для сортировки
|
|
466
465
|
|
|
467
466
|
:param filter_expressions: словарь, отображающий поля для фильтрации
|
|
468
467
|
на их значения. Фильтрация по None не применяется. См. раздел "фильтрация"
|
|
@@ -525,7 +524,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
525
524
|
self,
|
|
526
525
|
session: AsyncSession,
|
|
527
526
|
options: List[Any] | Any | None = None,
|
|
528
|
-
order_by:
|
|
527
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
529
528
|
where: Any | None = None,
|
|
530
529
|
**simple_filters: Any,
|
|
531
530
|
) -> int:
|
|
@@ -536,7 +535,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
536
535
|
|
|
537
536
|
:param options: параметры для метода .options() загрузчика SQLAlchemy
|
|
538
537
|
|
|
539
|
-
:param order_by: поле для сортировки
|
|
538
|
+
:param order_by: поле для сортировки
|
|
540
539
|
|
|
541
540
|
:param where: выражение, которое будет передано в метод .where() SQLAlchemy
|
|
542
541
|
|
|
@@ -603,13 +602,13 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
603
602
|
for field in update_data:
|
|
604
603
|
setattr(db_obj, field, update_data[field])
|
|
605
604
|
session.add(db_obj)
|
|
606
|
-
await self.
|
|
605
|
+
await self.persist(session, commit)
|
|
607
606
|
await session.refresh(db_obj, attribute_names=refresh_attribute_names)
|
|
608
607
|
return db_obj
|
|
609
608
|
|
|
610
609
|
async def delete(
|
|
611
610
|
self, session: AsyncSession, db_obj: ModelT, commit: bool = True
|
|
612
|
-
) -> ModelT
|
|
611
|
+
) -> ModelT:
|
|
613
612
|
"""
|
|
614
613
|
Удаление экземпляра модели из БД.
|
|
615
614
|
|
|
@@ -619,9 +618,11 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
619
618
|
|
|
620
619
|
:param commit: нужно ли вызывать `session.commit()`, если используется
|
|
621
620
|
подход commit as you go
|
|
621
|
+
|
|
622
|
+
:returns: переданный в функцию экземпляр модели
|
|
622
623
|
"""
|
|
623
624
|
await session.delete(db_obj)
|
|
624
|
-
await self.
|
|
625
|
+
await self.persist(session, commit)
|
|
625
626
|
return db_obj
|
|
626
627
|
|
|
627
628
|
##################################################################################
|
|
@@ -629,21 +630,25 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
629
630
|
##################################################################################
|
|
630
631
|
|
|
631
632
|
@staticmethod
|
|
632
|
-
async def
|
|
633
|
+
async def persist(session: AsyncSession, commit: bool = True) -> None:
|
|
633
634
|
"""
|
|
634
|
-
|
|
635
|
-
|
|
635
|
+
Сохраняет изменения в БД, обрабатывая разное использовании сессии:
|
|
636
|
+
"commit as you go" или "begin once".
|
|
636
637
|
|
|
637
|
-
Если используется подход commit as you go, и параметр :commit: передан
|
|
638
|
-
True, то выполняется commit
|
|
638
|
+
Если используется подход "commit as you go", и параметр :commit: передан
|
|
639
|
+
True, то выполняется `commit()`; иначе `flush()`.
|
|
639
640
|
|
|
640
|
-
Если используется подход begin once (`async with session.begin():`),
|
|
641
|
-
то
|
|
641
|
+
Если используется подход "begin once" (`async with session.begin():`),
|
|
642
|
+
то не выполняется `flush()`, чтобы не закрывать транзакцию в контекстном
|
|
642
643
|
менеджере.
|
|
643
644
|
"""
|
|
644
645
|
|
|
645
|
-
if
|
|
646
|
+
if session.sync_session._trans_context_manager:
|
|
647
|
+
await session.flush()
|
|
648
|
+
elif commit:
|
|
646
649
|
await session.commit()
|
|
650
|
+
else:
|
|
651
|
+
await session.flush()
|
|
647
652
|
|
|
648
653
|
async def run_db_validation(
|
|
649
654
|
self,
|
|
@@ -676,7 +681,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
676
681
|
base_query: Select,
|
|
677
682
|
filter_expressions: dict[InstrumentedAttribute | Callable, Any],
|
|
678
683
|
options: List[Any] | None = None,
|
|
679
|
-
order_by:
|
|
684
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
680
685
|
) -> Select:
|
|
681
686
|
"""
|
|
682
687
|
Делает необходимые join'ы при фильтрации и сортировке по полям
|
|
@@ -691,12 +696,16 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
691
696
|
|
|
692
697
|
joined_query = base_query
|
|
693
698
|
models_to_join = set()
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
699
|
+
|
|
700
|
+
if order_by:
|
|
701
|
+
if isinstance(order_by, InstrumentedAttribute):
|
|
702
|
+
ordering_model = order_by.parent.class_
|
|
703
|
+
else:
|
|
704
|
+
ordering_model = order_by._propagate_attrs[
|
|
705
|
+
"plugin_subject"
|
|
706
|
+
]._identity_class
|
|
707
|
+
if ordering_model != self.model:
|
|
708
|
+
models_to_join.add(ordering_model)
|
|
700
709
|
|
|
701
710
|
for filter_expression in filter_expressions.keys():
|
|
702
711
|
if isinstance(filter_expression, InstrumentedAttribute):
|
|
@@ -725,11 +734,13 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
725
734
|
options.append(contains_eager(self.models_to_relationship_attrs[model]))
|
|
726
735
|
return joined_query
|
|
727
736
|
|
|
728
|
-
def get_order_by_expression(
|
|
737
|
+
def get_order_by_expression(
|
|
738
|
+
self, order_by: InstrumentedAttribute | UnaryExpression | None
|
|
739
|
+
):
|
|
729
740
|
if order_by is not None:
|
|
730
741
|
if self.default_ordering is not None:
|
|
731
|
-
return order_by
|
|
732
|
-
return order_by
|
|
742
|
+
return order_by, self.default_ordering
|
|
743
|
+
return order_by
|
|
733
744
|
return self.default_ordering
|
|
734
745
|
|
|
735
746
|
@staticmethod
|
|
@@ -777,7 +788,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
|
|
|
777
788
|
def assemble_stmt(
|
|
778
789
|
self,
|
|
779
790
|
base_stmt: Select | None = None,
|
|
780
|
-
order_by:
|
|
791
|
+
order_by: InstrumentedAttribute | UnaryExpression | None = None,
|
|
781
792
|
options: List[Any] | Any | None = None,
|
|
782
793
|
where: Any | None = None,
|
|
783
794
|
**simple_filters: Any,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Annotated, Sequence
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends
|
|
6
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
7
|
+
from sqlalchemy.sql.elements import UnaryExpression
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_ordering_enum(
|
|
11
|
+
ordering_fields_mapping: dict[str, InstrumentedAttribute],
|
|
12
|
+
) -> Enum:
|
|
13
|
+
"""
|
|
14
|
+
Собирает Enum из возможных значений сортировки для документации OpenAPI
|
|
15
|
+
"""
|
|
16
|
+
enum_attrs = {}
|
|
17
|
+
for field_name in ordering_fields_mapping:
|
|
18
|
+
enum_attrs[field_name] = field_name
|
|
19
|
+
enum_attrs[f"desc_{field_name}"] = "-" + field_name
|
|
20
|
+
return Enum(str(uuid4()), enum_attrs)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ordering_depends(
|
|
24
|
+
ordering_fields: Sequence[InstrumentedAttribute] | dict[str, InstrumentedAttribute]
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Создаёт fastapi.Depends для квери параметра сортировки по переданным полям модели.
|
|
28
|
+
|
|
29
|
+
:ordering_fields: поля для сортировки.
|
|
30
|
+
Может быть последовательностью полей основной модели:
|
|
31
|
+
ordering_fields=(MyModel.title, MyModel.created_at)
|
|
32
|
+
В таком случае будут доступны параметры сортировки "title", "-title",
|
|
33
|
+
"created_at", "-created_at".
|
|
34
|
+
Дефис первым символом означает сортировку по убыванию.
|
|
35
|
+
Либо может быть маппингом строковых полей для сортировки
|
|
36
|
+
на соответствующие поля моделей:
|
|
37
|
+
ordering_fields={
|
|
38
|
+
"title": MyModel.title,
|
|
39
|
+
"parent_title": ParentModel.title
|
|
40
|
+
}
|
|
41
|
+
В таком случае будут доступны параметры сортировки "title", "-title",
|
|
42
|
+
"parent_title", "-parent_title".
|
|
43
|
+
Если order_by передаётся в методы list или paginated_list,
|
|
44
|
+
и поле для сортировки относится к модели, напрямую связанную с основной,
|
|
45
|
+
то будет выполнен необходимый join для применения сортировки.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
if isinstance(ordering_fields, dict):
|
|
49
|
+
ordering_fields_mapping = ordering_fields
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
ordering_fields_mapping = {field.name: field for field in ordering_fields}
|
|
53
|
+
|
|
54
|
+
def get_ordering_field(
|
|
55
|
+
order_by: get_ordering_enum(ordering_fields_mapping) = None,
|
|
56
|
+
) -> InstrumentedAttribute | UnaryExpression | None:
|
|
57
|
+
if order_by:
|
|
58
|
+
desc = order_by.value.startswith("-")
|
|
59
|
+
field = ordering_fields_mapping[order_by.value.lstrip("-")]
|
|
60
|
+
if desc:
|
|
61
|
+
return field.desc()
|
|
62
|
+
return field
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
return Annotated[
|
|
66
|
+
InstrumentedAttribute | UnaryExpression | None,
|
|
67
|
+
Depends(get_ordering_field),
|
|
68
|
+
]
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
|
2
|
-
from sqlalchemy.orm import sessionmaker
|
|
3
|
-
|
|
4
|
-
from .config import settings
|
|
5
|
-
|
|
6
|
-
engine = create_async_engine(settings.SQLALCHEMY_DATABASE_URL, future=True, echo=False)
|
|
7
|
-
|
|
8
|
-
async_session_factory = sessionmaker(
|
|
9
|
-
engine, class_=AsyncSession, expire_on_commit=False
|
|
10
|
-
)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
from typing import AsyncGenerator
|
|
2
|
-
|
|
3
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
|
-
|
|
5
|
-
from .db import async_session_factory
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
|
9
|
-
async_session: AsyncSession = async_session_factory() # type: ignore
|
|
10
|
-
try:
|
|
11
|
-
yield async_session
|
|
12
|
-
finally:
|
|
13
|
-
await async_session.close()
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from typing import Annotated, Sequence, Type
|
|
4
|
-
from uuid import uuid4
|
|
5
|
-
|
|
6
|
-
from fastapi import Depends
|
|
7
|
-
from sqlalchemy.orm import DeclarativeBase
|
|
8
|
-
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@dataclass
|
|
12
|
-
class OrderingField:
|
|
13
|
-
"""
|
|
14
|
-
Класс для направленный сортировки по полям модели SQLAlchemy
|
|
15
|
-
или полям связанных с ней моделей.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
field: InstrumentedAttribute | str
|
|
19
|
-
desc: bool = False
|
|
20
|
-
|
|
21
|
-
def get_directed_field(self, model: Type[DeclarativeBase]):
|
|
22
|
-
if isinstance(self.field, str):
|
|
23
|
-
field = getattr(model, self.field)
|
|
24
|
-
else:
|
|
25
|
-
field = self.field
|
|
26
|
-
if self.desc:
|
|
27
|
-
return field.desc()
|
|
28
|
-
return field
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def get_ordering_enum(
|
|
32
|
-
ordering_fields: Sequence[str | InstrumentedAttribute],
|
|
33
|
-
) -> Type[Enum]:
|
|
34
|
-
"""
|
|
35
|
-
Собирает Enum из возможных значений сортировки для документации OpenAPI
|
|
36
|
-
"""
|
|
37
|
-
enum_attrs = {}
|
|
38
|
-
for field in ordering_fields:
|
|
39
|
-
if isinstance(field, str):
|
|
40
|
-
field_name = field
|
|
41
|
-
else:
|
|
42
|
-
# Если передан атрибут модели
|
|
43
|
-
field_name = str(field).lower().replace(".", "_")
|
|
44
|
-
enum_attrs[field_name] = field_name
|
|
45
|
-
enum_attrs[f"desc_{field_name}"] = "-" + field_name
|
|
46
|
-
return Enum(str(uuid4()), enum_attrs)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def ordering_dep(ordering_fields: Sequence[str | InstrumentedAttribute]):
|
|
50
|
-
"""
|
|
51
|
-
Создаёт Depends из FastAPI для квери параметра сортировки по переданным полям.
|
|
52
|
-
Поля могут быть строками (поле основной модели) либо атрибутами моделей SQLAlchemy,
|
|
53
|
-
связанных с основной.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def get_ordering_field(
|
|
57
|
-
order_by: get_ordering_enum(ordering_fields) = None,
|
|
58
|
-
) -> OrderingField | None:
|
|
59
|
-
if order_by:
|
|
60
|
-
desc = False
|
|
61
|
-
if order_by.value.startswith("-"):
|
|
62
|
-
desc = True
|
|
63
|
-
for field in ordering_fields:
|
|
64
|
-
if str(field).lower().replace(".", "_") == order_by.value.lstrip("-"):
|
|
65
|
-
return OrderingField(field=field, desc=desc)
|
|
66
|
-
return None
|
|
67
|
-
|
|
68
|
-
return Annotated[
|
|
69
|
-
OrderingField | None,
|
|
70
|
-
Depends(get_ordering_field),
|
|
71
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/__init__.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/__init__.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/api/api.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/managers.py
RENAMED
|
File without changes
|
{fastapi_sqlalchemy_toolkit-0.6.13 → fastapi_sqlalchemy_toolkit-0.7.0}/examples/app/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|