fastapi-sqlalchemy-toolkit 0.7.16__tar.gz → 0.7.18__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 (59) hide show
  1. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/PKG-INFO +1 -1
  2. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/fastapi_sqlalchemy_toolkit/model_manager.py +50 -3
  3. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/pyproject.toml +1 -1
  4. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/.flake8 +0 -0
  5. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/.github/workflows/docker-test.yml +0 -0
  6. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/.github/workflows/python-publish.yml +0 -0
  7. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/.gitignore +0 -0
  8. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/.pylintrc +0 -0
  9. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/AUTHORS +0 -0
  10. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/LICENSE +0 -0
  11. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/Makefile +0 -0
  12. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/README.md +0 -0
  13. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/benefits.md +0 -0
  14. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/db_validation.md +0 -0
  15. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/extension.md +0 -0
  16. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/filtering.md +0 -0
  17. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/index.md +0 -0
  18. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/benefits.md +0 -0
  19. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/db_validation.md +0 -0
  20. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/extension.md +0 -0
  21. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/filtering.md +0 -0
  22. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/index.md +0 -0
  23. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/sorting.md +0 -0
  24. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/transactions.md +0 -0
  25. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/usage.md +0 -0
  26. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/ru/utils.md +0 -0
  27. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/sorting.md +0 -0
  28. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/transactions.md +0 -0
  29. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/usage.md +0 -0
  30. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/docs/utils.md +0 -0
  31. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/.env +0 -0
  32. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/__init__.py +0 -0
  33. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/api/__init__.py +0 -0
  34. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/api/api.py +0 -0
  35. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/api/deps.py +0 -0
  36. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/api/endpoints/__init__.py +0 -0
  37. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/api/endpoints/child.py +0 -0
  38. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/config.py +0 -0
  39. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/db.py +0 -0
  40. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/main.py +0 -0
  41. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/managers.py +0 -0
  42. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/models.py +0 -0
  43. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/examples/app/schemas.py +0 -0
  44. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/fastapi_sqlalchemy_toolkit/__init__.py +0 -0
  45. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
  46. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
  47. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
  48. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/mkdocs.yml +0 -0
  49. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/requirements/base.txt +0 -0
  50. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/requirements/docs.txt +0 -0
  51. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/requirements/lint.txt +0 -0
  52. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/requirements/test.txt +0 -0
  53. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/Dockerfile +0 -0
  54. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/__init__.py +0 -0
  55. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/conftest.py +0 -0
  56. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/db.py +0 -0
  57. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/docker-compose.yml +0 -0
  58. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/models.py +0 -0
  59. {fastapi_sqlalchemy_toolkit-0.7.16 → fastapi_sqlalchemy_toolkit-0.7.18}/tests/test_public_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_sqlalchemy_toolkit
3
- Version: 0.7.16
3
+ Version: 0.7.18
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>
@@ -7,10 +7,12 @@ from fastapi_pagination.bases import BasePage
7
7
  from fastapi_pagination.ext.sqlalchemy import paginate
8
8
  from pydantic import BaseModel
9
9
  from sqlalchemy import (
10
+ Index,
10
11
  Integer,
11
12
  Row,
12
13
  String,
13
14
  UniqueConstraint,
15
+ and_,
14
16
  delete,
15
17
  func,
16
18
  insert,
@@ -65,6 +67,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
65
67
  self.fk_name_to_model: dict[str, type[ModelT]] = {}
66
68
 
67
69
  self.unique_constraints: List[List[str]] = []
70
+ self.unique_indexes: List[Index] = []
68
71
 
69
72
  if hasattr(self.model, "__table_args__"):
70
73
  for table_arg in self.model.__table_args__:
@@ -73,6 +76,8 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
73
76
  self.unique_constraints.append(table_arg.columns.keys())
74
77
  else:
75
78
  self.unique_constraints.append(table_arg._pending_colargs)
79
+ elif isinstance(table_arg, Index) and table_arg.unique is True:
80
+ self.unique_indexes.append(table_arg)
76
81
 
77
82
  self.reverse_relationships: dict[str, type[ModelT]] = {}
78
83
  self.m2m_relationships: dict[str, type[ModelT]] = {}
@@ -865,6 +870,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
865
870
  await self.handle_m2m_fields(session, in_obj)
866
871
  await self.validate_unique_fields(session, in_obj, db_obj=db_obj)
867
872
  await self.validate_unique_constraints(session, in_obj)
873
+ await self.validate_unique_indexes(session, in_obj)
868
874
  return in_obj
869
875
 
870
876
  def get_select(self, base_stmt: Select | None = None, **_kwargs: Any) -> Select:
@@ -1075,10 +1081,16 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1075
1081
  Проверить, не нарушаются ли UniqueConstraint модели.
1076
1082
  """
1077
1083
  for unique_constraint in self.unique_constraints:
1084
+ null_found = False
1078
1085
  query = {}
1079
1086
  for field in unique_constraint:
1080
- if in_obj[field] is not None:
1081
- query[field] = in_obj[field]
1087
+ if in_obj[field] is None:
1088
+ null_found = True
1089
+ break
1090
+
1091
+ query[field] = in_obj[field]
1092
+ if null_found:
1093
+ continue
1082
1094
  if query:
1083
1095
  object_exists = await self.exists(
1084
1096
  session, **query, where=(self.model.id != in_obj.get("id"))
@@ -1113,7 +1125,7 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1113
1125
  continue
1114
1126
  attrs_to_check = {column.name: in_obj[column.name]}
1115
1127
  object_exists = await self.exists(
1116
- session=session,
1128
+ session,
1117
1129
  **attrs_to_check,
1118
1130
  where=(self.model.id != in_obj.get("id")),
1119
1131
  )
@@ -1126,6 +1138,41 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1126
1138
  ),
1127
1139
  )
1128
1140
 
1141
+ async def validate_unique_indexes(
1142
+ self, session: AsyncSession, in_obj: ModelDict
1143
+ ) -> None:
1144
+ """
1145
+ Валидирует соблюдение уникальности индексов
1146
+ """
1147
+ for index in self.unique_indexes:
1148
+ condition = (
1149
+ index.dialect_options["postgresql"].get("where")
1150
+ if index.dialect_options.get("postgresql")
1151
+ else None
1152
+ )
1153
+ filters = []
1154
+
1155
+ for column in index.columns:
1156
+ try:
1157
+ filters.append(column == in_obj[column.name])
1158
+ except KeyError:
1159
+ if column.nullable:
1160
+ filters.append(column.is_(None))
1161
+
1162
+ object_exists = await self.exists(
1163
+ session,
1164
+ where=and_(*filters, condition, self.model.id != in_obj.get("id")),
1165
+ )
1166
+ if object_exists:
1167
+ raise HTTPException(
1168
+ status_code=status.HTTP_400_BAD_REQUEST,
1169
+ detail=(
1170
+ f"{self.model.__tablename__} с такими "
1171
+ + ", ".join([column.name for column in index.columns])
1172
+ + " уже существует."
1173
+ ),
1174
+ )
1175
+
1129
1176
  async def handle_m2m_fields(self, session: AsyncSession, in_obj: ModelDict) -> None:
1130
1177
  for field in in_obj:
1131
1178
  if field in self.m2m_relationships:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_sqlalchemy_toolkit"
7
- version = "0.7.16"
7
+ version = "0.7.18"
8
8
  authors = [
9
9
  { name="Egor Kondrashov", email="e.kondr01@gmail.com" },
10
10
  ]