fastapi-sqlalchemy-toolkit 0.8.2__tar.gz → 0.8.2.2__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 (60) hide show
  1. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/PKG-INFO +1 -1
  2. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/filtering.md +25 -0
  3. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/filtering.md +25 -0
  4. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/fastapi_sqlalchemy_toolkit/model_manager.py +29 -10
  5. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/pyproject.toml +1 -1
  6. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/test_public_methods.py +89 -0
  7. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/.flake8 +0 -0
  8. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/.github/workflows/docker-test.yml +0 -0
  9. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/.github/workflows/python-publish.yml +0 -0
  10. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/.gitignore +0 -0
  11. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/.pylintrc +0 -0
  12. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/AUTHORS +0 -0
  13. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/LICENSE +0 -0
  14. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/Makefile +0 -0
  15. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/README.md +0 -0
  16. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/benefits.md +0 -0
  17. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/db_validation.md +0 -0
  18. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/extension.md +0 -0
  19. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/index.md +0 -0
  20. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/benefits.md +0 -0
  21. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/db_validation.md +0 -0
  22. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/extension.md +0 -0
  23. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/index.md +0 -0
  24. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/sorting.md +0 -0
  25. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/transactions.md +0 -0
  26. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/usage.md +0 -0
  27. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/ru/utils.md +0 -0
  28. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/sorting.md +0 -0
  29. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/transactions.md +0 -0
  30. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/usage.md +0 -0
  31. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/docs/utils.md +0 -0
  32. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/.env +0 -0
  33. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/__init__.py +0 -0
  34. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/api/__init__.py +0 -0
  35. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/api/api.py +0 -0
  36. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/api/deps.py +0 -0
  37. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/api/endpoints/__init__.py +0 -0
  38. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/api/endpoints/child.py +0 -0
  39. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/config.py +0 -0
  40. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/db.py +0 -0
  41. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/main.py +0 -0
  42. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/managers.py +0 -0
  43. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/models.py +0 -0
  44. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/examples/app/schemas.py +0 -0
  45. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/fastapi_sqlalchemy_toolkit/__init__.py +0 -0
  46. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
  47. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
  48. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
  49. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/mkdocs.yml +0 -0
  50. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/poetry.lock +0 -0
  51. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/requirements/base.txt +0 -0
  52. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/requirements/docs.txt +0 -0
  53. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/requirements/lint.txt +0 -0
  54. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/requirements/test.txt +0 -0
  55. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/Dockerfile +0 -0
  56. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/__init__.py +0 -0
  57. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/conftest.py +0 -0
  58. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/db.py +0 -0
  59. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/docker-compose.yml +0 -0
  60. {fastapi_sqlalchemy_toolkit-0.8.2 → fastapi_sqlalchemy_toolkit-0.8.2.2}/tests/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_sqlalchemy_toolkit
3
- Version: 0.8.2
3
+ Version: 0.8.2.2
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>
@@ -280,6 +280,31 @@ async def get_parents(
280
280
 
281
281
  > **Note**: nesting of compound expressions (e.g. `(a & b) | c`) is not supported.
282
282
 
283
+ **Multiple expressions as separate arguments**
284
+
285
+ Instead of using `&` to combine expressions, you can pass them as separate arguments
286
+ in a tuple. Each argument supports all three expression kinds above. Non-`None`
287
+ expressions are combined with `AND`.
288
+
289
+ ```python
290
+ @router.get("/parents")
291
+ async def get_parents(
292
+ session: Session,
293
+ title: str | None = None,
294
+ slug: str | None = None,
295
+ ) -> list[ParentListSchema]:
296
+ return await parent_manager.list(
297
+ session,
298
+ optional_where=(Parent.title == title, Parent.slug == slug),
299
+ )
300
+ ```
301
+
302
+ `GET /parents` — no filter applied, all `Parent` objects are returned.
303
+
304
+ `GET /parents?title=foo` — only the `title` filter is applied.
305
+
306
+ `GET /parents?title=foo&slug=bar` — both filters are applied with `AND`.
307
+
283
308
  ### Filtering by `null` via API
284
309
 
285
310
  If in a list API endpoint, you need to be able to filter the field value
@@ -257,6 +257,31 @@ async def get_parents(
257
257
 
258
258
  > **Примечание**: вложенные составные выражения (например, `(a & b) | c`) не поддерживаются.
259
259
 
260
+ **Несколько выражений как отдельные аргументы**
261
+
262
+ Вместо использования `&` для объединения выражений можно передать их как отдельные
263
+ аргументы в виде кортежа. Каждый аргумент поддерживает все три вида выражений выше.
264
+ Оставшиеся (не-`None`) выражения объединяются через `AND`.
265
+
266
+ ```python
267
+ @router.get("/parents")
268
+ async def get_parents(
269
+ session: Session,
270
+ title: str | None = None,
271
+ slug: str | None = None,
272
+ ) -> list[ParentListSchema]:
273
+ return await parent_manager.list(
274
+ session,
275
+ optional_where=(Parent.title == title, Parent.slug == slug),
276
+ )
277
+ ```
278
+
279
+ Запрос `GET /parents` — фильтр не применяется, возвращаются все объекты `Parent`.
280
+
281
+ Запрос `GET /parents?title=foo` — применяется только фильтр по `title`.
282
+
283
+ Запрос `GET /parents?title=foo&slug=bar` — применяются оба фильтра через `AND`.
284
+
260
285
  ## Фильтрация без дополнительной обработки
261
286
 
262
287
  Для фильтрации без дополнительной обработки в методах `list` и `paginated_list` можно
@@ -1148,10 +1148,8 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1148
1148
  model = filter_expression.parent._identity_class
1149
1149
  elif isinstance(filter_expression, Function):
1150
1150
  model = filter_expression.entity_namespace
1151
- elif isinstance(filter_expression, ColumnElement):
1152
- model = self.model # Not supported
1153
1151
  else:
1154
- model = filter_expression.__self__.parent._identity_class
1152
+ model = self.model # Not supported
1155
1153
  if model != self.model:
1156
1154
  models_to_join.add(model)
1157
1155
  for model in models_to_join:
@@ -1235,10 +1233,10 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1235
1233
  related_pk = _get_model_pk(related_model)
1236
1234
  return relationship.any(related_pk.in_(value))
1237
1235
 
1238
- @staticmethod
1239
- def handle_optional_where(optional_where: Any) -> Any:
1236
+ @classmethod
1237
+ def handle_optional_where_expression(cls, optional_where: Any) -> Any:
1240
1238
  """
1241
- Обрабатывает выражение optional_where, пропуская фильтры, значения которых None.
1239
+ Обрабатывает одно выражение optional_where, пропуская фильтр, если его значение None.
1242
1240
 
1243
1241
  Поддерживает:
1244
1242
  1. Простые выражения вида MyModel.field == value
@@ -1273,6 +1271,30 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1273
1271
  return None
1274
1272
  return optional_where
1275
1273
 
1274
+ @classmethod
1275
+ def handle_optional_where(cls, optional_where: Any) -> Any:
1276
+ """
1277
+ Обрабатывает выражения optional_where, пропуская фильтры, значения которых None.
1278
+
1279
+ Принимает одно выражение SQLAlchemy или кортеж выражений.
1280
+ Каждое выражение обрабатывается через handle_optional_where_expression
1281
+ и поддерживает все три вида выражений.
1282
+ При передаче кортежа оставшиеся (не-None) выражения
1283
+ объединяются через оператор &.
1284
+ """
1285
+ if isinstance(optional_where, tuple):
1286
+ remaining = []
1287
+ for arg in optional_where:
1288
+ processed = cls.handle_optional_where_expression(arg)
1289
+ if processed is not None:
1290
+ remaining.append(processed)
1291
+ if not remaining:
1292
+ return None
1293
+ if len(remaining) == 1:
1294
+ return remaining[0]
1295
+ return and_(*remaining)
1296
+ return cls.handle_optional_where_expression(optional_where)
1297
+
1276
1298
  def assemble_stmt(
1277
1299
  self,
1278
1300
  base_stmt: Select | None = None,
@@ -1317,10 +1339,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1317
1339
  stmt = stmt.options(option)
1318
1340
 
1319
1341
  if where is not None:
1320
- if isinstance(where, tuple):
1321
- stmt = stmt.where(*where)
1322
- else:
1323
- stmt = stmt.where(where)
1342
+ stmt = stmt.where(*where) if isinstance(where, tuple) else stmt.where(where)
1324
1343
 
1325
1344
  if limit is not None:
1326
1345
  stmt = stmt.limit(limit)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_sqlalchemy_toolkit"
7
- version = "0.8.2"
7
+ version = "0.8.2.2"
8
8
  authors = [
9
9
  { name="Egor Kondrashov", email="e.kondr01@gmail.com" },
10
10
  ]
@@ -990,6 +990,95 @@ async def test_list_with_optional_where_case3_or_one_none(session: AsyncSession)
990
990
  assert parents[0].title == target_title
991
991
 
992
992
 
993
+ async def test_list_with_optional_where_multi_all_applied(session: AsyncSession):
994
+ """Multi-arg: both values not None — both filters applied with AND."""
995
+ target_title = "optional-where-multi-title"
996
+ target_slug = "optional-where-multi-slug1"
997
+ await session.execute(
998
+ insert(Parent),
999
+ [
1000
+ {
1001
+ "title": target_title,
1002
+ "slug": target_slug,
1003
+ },
1004
+ {
1005
+ "title": target_title,
1006
+ "slug": "optional-where-multi-slug2",
1007
+ },
1008
+ {
1009
+ "title": "optional-where-multi-title-other",
1010
+ "slug": "optional-where-multi-slug3",
1011
+ },
1012
+ ],
1013
+ )
1014
+ await session.commit()
1015
+
1016
+ parents = await parent_manager.list(
1017
+ session=session,
1018
+ optional_where=(Parent.title == target_title, Parent.slug == target_slug),
1019
+ )
1020
+ assert len(parents) == 1
1021
+ assert parents[0].title == target_title
1022
+ assert parents[0].slug == target_slug
1023
+
1024
+
1025
+ async def test_list_with_optional_where_multi_one_none(session: AsyncSession):
1026
+ """Multi-arg: one value is None — only non-None filter applied."""
1027
+ target_title = "optional-where-multi-none-title"
1028
+ await session.execute(
1029
+ insert(Parent),
1030
+ [
1031
+ {
1032
+ "title": target_title,
1033
+ "slug": "optional-where-multi-none-slug1",
1034
+ },
1035
+ {
1036
+ "title": target_title,
1037
+ "slug": "optional-where-multi-none-slug2",
1038
+ },
1039
+ {
1040
+ "title": "optional-where-multi-none-title-other",
1041
+ "slug": "optional-where-multi-none-slug3",
1042
+ },
1043
+ ],
1044
+ )
1045
+ await session.commit()
1046
+
1047
+ none_value = None
1048
+ parents = await parent_manager.list(
1049
+ session=session,
1050
+ optional_where=(Parent.title == target_title, Parent.slug == none_value),
1051
+ )
1052
+ assert len(parents) == 2
1053
+ for parent in parents:
1054
+ assert parent.title == target_title
1055
+
1056
+
1057
+ async def test_list_with_optional_where_multi_all_none(session: AsyncSession):
1058
+ """Multi-arg: all values are None — filter skipped (all returned)."""
1059
+ await session.execute(
1060
+ insert(Parent),
1061
+ [
1062
+ {
1063
+ "title": "optional-where-multi-all-none-title-1",
1064
+ "slug": "optional-where-multi-all-none-slug1",
1065
+ },
1066
+ {
1067
+ "title": "optional-where-multi-all-none-title-2",
1068
+ "slug": "optional-where-multi-all-none-slug2",
1069
+ },
1070
+ ],
1071
+ )
1072
+ await session.commit()
1073
+
1074
+ none_value = None
1075
+ parents = await parent_manager.list(
1076
+ session=session,
1077
+ optional_where=(Parent.title == none_value, Parent.slug == none_value),
1078
+ )
1079
+ assert len(parents) == 2
1080
+
1081
+
993
1082
  async def test_create_unique_constraint_validation(session: AsyncSession):
994
1083
  await parent_manager.create(
995
1084
  session=session,