fastapi-sqlalchemy-toolkit 0.6.14__tar.gz → 0.7.0.1__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.
Files changed (31) hide show
  1. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/PKG-INFO +34 -18
  2. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/README.md +33 -17
  3. fastapi_sqlalchemy_toolkit-0.7.0.1/examples/app/api/deps.py +14 -0
  4. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/api/endpoints/child.py +11 -10
  5. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/config.py +0 -2
  6. fastapi_sqlalchemy_toolkit-0.7.0.1/examples/app/db.py +7 -0
  7. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/models.py +5 -5
  8. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/fastapi_sqlalchemy_toolkit/__init__.py +1 -1
  9. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/fastapi_sqlalchemy_toolkit/model_manager.py +31 -26
  10. fastapi_sqlalchemy_toolkit-0.7.0.1/fastapi_sqlalchemy_toolkit/ordering.py +68 -0
  11. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/pyproject.toml +1 -1
  12. fastapi_sqlalchemy_toolkit-0.6.14/examples/app/db.py +0 -10
  13. fastapi_sqlalchemy_toolkit-0.6.14/examples/app/deps.py +0 -13
  14. fastapi_sqlalchemy_toolkit-0.6.14/fastapi_sqlalchemy_toolkit/ordering.py +0 -71
  15. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/.flake8 +0 -0
  16. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/.github/workflows/python-publish.yml +0 -0
  17. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/.gitignore +0 -0
  18. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/.pylintrc +0 -0
  19. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/AUTHORS +0 -0
  20. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/LICENSE +0 -0
  21. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/.env +0 -0
  22. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/__init__.py +0 -0
  23. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/api/__init__.py +0 -0
  24. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/api/api.py +0 -0
  25. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/api/endpoints/__init__.py +0 -0
  26. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/main.py +0 -0
  27. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/managers.py +0 -0
  28. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/examples/app/schemas.py +0 -0
  29. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
  30. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
  31. {fastapi_sqlalchemy_toolkit-0.6.14 → fastapi_sqlalchemy_toolkit-0.7.0.1}/requirements.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: fastapi_sqlalchemy_toolkit
3
- Version: 0.6.14
3
+ Version: 0.7.0.1
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
- - Декларативная сортировка с помощью `ordering_dep` (см. раздел **Сортировка**)
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 Parent
434
+ from app.models import Child
435
435
 
436
436
  child_ordering_fields = (
437
- "title",
438
- "created_at",
439
- Parent.title,
440
- Parent.created_at
437
+ Child.title,
438
+ Child.created_at
441
439
  )
442
440
  ```
443
441
 
444
- Для каждого из указаных полей будет доступна сортировка по возрастанию и убыванию.
445
- Чтобы сортировать по полю по убыванию, нужно в квери параметре сортировки
446
- передать его название, начиная с дефиса (Django style).
447
- Таким образом, `?order_by=title` сортирует по `title` по возрастанию,
448
- а `?order_by=-title` сортирует по `title` по убыванию.
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
- в `ordering_dep`
463
+ в `ordering_depends`
452
464
 
453
465
  ```python
454
- from fastapi_sqlalchemy_toolkit import ordering_dep
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: ordering_dep(child_ordering_fields)
471
+ order_by: ordering_depends(child_ordering_fields)
460
472
  ) -> list[ChildListSchema]
461
473
  ...
462
474
  ```
@@ -467,6 +479,10 @@ 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
+
470
486
  ## Транзакции
471
487
 
472
488
  `fastapi-sqlalchemy-toolkit` поддерживает оба подхода к работе с транзакциями `SQAlchemy`.
@@ -17,7 +17,7 @@ REST API и взаимодействии с СУБД через `SQLAlchemy`;
17
17
 
18
18
  - Фильтрация с обработкой необязательных параметров запроса (см. раздел **Фильтрация**)
19
19
 
20
- - Декларативная сортировка с помощью `ordering_dep` (см. раздел **Сортировка**)
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 Parent
417
+ from app.models import Child
418
418
 
419
419
  child_ordering_fields = (
420
- "title",
421
- "created_at",
422
- Parent.title,
423
- Parent.created_at
420
+ Child.title,
421
+ Child.created_at
424
422
  )
425
423
  ```
426
424
 
427
- Для каждого из указаных полей будет доступна сортировка по возрастанию и убыванию.
428
- Чтобы сортировать по полю по убыванию, нужно в квери параметре сортировки
429
- передать его название, начиная с дефиса (Django style).
430
- Таким образом, `?order_by=title` сортирует по `title` по возрастанию,
431
- а `?order_by=-title` сортирует по `title` по убыванию.
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
- в `ordering_dep`
446
+ в `ordering_depends`
435
447
 
436
448
  ```python
437
- from fastapi_sqlalchemy_toolkit import ordering_dep
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: ordering_dep(child_ordering_fields)
454
+ order_by: ordering_depends(child_ordering_fields)
443
455
  ) -> list[ChildListSchema]
444
456
  ...
445
457
  ```
@@ -450,6 +462,10 @@ 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
+
453
469
  ## Транзакции
454
470
 
455
471
  `fastapi-sqlalchemy-toolkit` поддерживает оба подхода к работе с транзакциями `SQAlchemy`.
@@ -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 get_async_session
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, Depends, Response, status
16
- from fastapi_pagination import Page, Params
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 ordering_dep
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 = (Parent.created_at, Parent.title, "created_at", "title")
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: ordering_dep(children_ordering_fields),
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,
@@ -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)
@@ -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[_py_uuid] = mapped_column(
18
- UUID(as_uuid=True), primary_key=True, default=uuid4
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(
@@ -1,4 +1,4 @@
1
1
  from .filters import NullableQuery
2
2
  from .model_manager import ModelManager, sqlalchemy_model_to_dict
3
- from .ordering import ordering_dep
3
+ from .ordering import ordering_depends
4
4
  from .utils import CommaSepQuery, comma_sep_q_to_list, make_partial_model
@@ -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)
@@ -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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
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: OrderingField | None = None,
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: поле для сортировки (экземпляр OrderingField)
538
+ :param order_by: поле для сортировки
540
539
 
541
540
  :param where: выражение, которое будет передано в метод .where() SQLAlchemy
542
541
 
@@ -682,7 +681,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
682
681
  base_query: Select,
683
682
  filter_expressions: dict[InstrumentedAttribute | Callable, Any],
684
683
  options: List[Any] | None = None,
685
- order_by: OrderingField | None = None,
684
+ order_by: InstrumentedAttribute | UnaryExpression | None = None,
686
685
  ) -> Select:
687
686
  """
688
687
  Делает необходимые join'ы при фильтрации и сортировке по полям
@@ -697,12 +696,16 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
697
696
 
698
697
  joined_query = base_query
699
698
  models_to_join = set()
700
- if (
701
- order_by
702
- and not isinstance(order_by.field, str)
703
- and order_by.field.parent.class_ != self.model
704
- ):
705
- models_to_join.add(order_by.field.parent.class_)
699
+
700
+ if order_by is not None:
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)
706
709
 
707
710
  for filter_expression in filter_expressions.keys():
708
711
  if isinstance(filter_expression, InstrumentedAttribute):
@@ -731,11 +734,13 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
731
734
  options.append(contains_eager(self.models_to_relationship_attrs[model]))
732
735
  return joined_query
733
736
 
734
- def get_order_by_expression(self, order_by: OrderingField | None):
737
+ def get_order_by_expression(
738
+ self, order_by: InstrumentedAttribute | UnaryExpression | None
739
+ ):
735
740
  if order_by is not None:
736
741
  if self.default_ordering is not None:
737
- return order_by.get_directed_field(self.model), self.default_ordering
738
- return order_by.get_directed_field(self.model)
742
+ return order_by, self.default_ordering
743
+ return order_by
739
744
  return self.default_ordering
740
745
 
741
746
  @staticmethod
@@ -783,7 +788,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
783
788
  def assemble_stmt(
784
789
  self,
785
790
  base_stmt: Select | None = None,
786
- order_by: OrderingField | None = None,
791
+ order_by: InstrumentedAttribute | UnaryExpression | None = None,
787
792
  options: List[Any] | Any | None = None,
788
793
  where: Any | None = None,
789
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
+ ]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_sqlalchemy_toolkit"
7
- version = "0.6.14"
7
+ version = "0.7.0.1"
8
8
  authors = [
9
9
  { name="Egor Kondrashov", email="e.kondr01@gmail.com" },
10
10
  ]
@@ -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
- ]