fastapi-sqlalchemy-toolkit 0.7.17__tar.gz → 0.7.19__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.17 → fastapi_sqlalchemy_toolkit-0.7.19}/PKG-INFO +1 -1
  2. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/fastapi_sqlalchemy_toolkit/model_manager.py +88 -15
  3. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/pyproject.toml +1 -1
  4. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/test_public_methods.py +2 -2
  5. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/.flake8 +0 -0
  6. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/.github/workflows/docker-test.yml +0 -0
  7. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/.github/workflows/python-publish.yml +0 -0
  8. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/.gitignore +0 -0
  9. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/.pylintrc +0 -0
  10. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/AUTHORS +0 -0
  11. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/LICENSE +0 -0
  12. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/Makefile +0 -0
  13. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/README.md +0 -0
  14. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/benefits.md +0 -0
  15. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/db_validation.md +0 -0
  16. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/extension.md +0 -0
  17. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/filtering.md +0 -0
  18. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/index.md +0 -0
  19. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/benefits.md +0 -0
  20. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/db_validation.md +0 -0
  21. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/extension.md +0 -0
  22. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/filtering.md +0 -0
  23. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/index.md +0 -0
  24. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/sorting.md +0 -0
  25. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/transactions.md +0 -0
  26. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/usage.md +0 -0
  27. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/ru/utils.md +0 -0
  28. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/sorting.md +0 -0
  29. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/transactions.md +0 -0
  30. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/usage.md +0 -0
  31. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/docs/utils.md +0 -0
  32. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/.env +0 -0
  33. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/__init__.py +0 -0
  34. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/api/__init__.py +0 -0
  35. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/api/api.py +0 -0
  36. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/api/deps.py +0 -0
  37. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/api/endpoints/__init__.py +0 -0
  38. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/api/endpoints/child.py +0 -0
  39. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/config.py +0 -0
  40. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/db.py +0 -0
  41. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/main.py +0 -0
  42. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/managers.py +0 -0
  43. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/models.py +0 -0
  44. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/examples/app/schemas.py +0 -0
  45. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/fastapi_sqlalchemy_toolkit/__init__.py +0 -0
  46. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/fastapi_sqlalchemy_toolkit/filters.py +0 -0
  47. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/fastapi_sqlalchemy_toolkit/ordering.py +0 -0
  48. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/fastapi_sqlalchemy_toolkit/utils.py +0 -0
  49. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/mkdocs.yml +0 -0
  50. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/requirements/base.txt +0 -0
  51. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/requirements/docs.txt +0 -0
  52. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/requirements/lint.txt +0 -0
  53. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/requirements/test.txt +0 -0
  54. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/Dockerfile +0 -0
  55. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/__init__.py +0 -0
  56. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/conftest.py +0 -0
  57. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/db.py +0 -0
  58. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/docker-compose.yml +0 -0
  59. {fastapi_sqlalchemy_toolkit-0.7.17 → fastapi_sqlalchemy_toolkit-0.7.19}/tests/models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_sqlalchemy_toolkit
3
- Version: 0.7.17
3
+ Version: 0.7.19
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:
@@ -1045,6 +1051,37 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1045
1051
 
1046
1052
  return stmt
1047
1053
 
1054
+ @staticmethod
1055
+ def get_error_msg(
1056
+ tablename: str,
1057
+ fields: List[str],
1058
+ *,
1059
+ exists: bool = True,
1060
+ field_values: List[Any] | None = None,
1061
+ ) -> str:
1062
+ """
1063
+ Формирует сообщение об ошибке, которое будет возвращаться при ошибках валидации.
1064
+ """
1065
+
1066
+ error_msg = tablename
1067
+ error_msg += " с "
1068
+ if field_values:
1069
+ fields_with_values = []
1070
+ for field, value in zip(fields, field_values, strict=True):
1071
+ fields_with_values.append(f"{field} {value}")
1072
+ error_msg += ", ".join(fields_with_values)
1073
+ else:
1074
+ error_msg += "таким"
1075
+ if len(fields) > 1:
1076
+ error_msg += "и"
1077
+ error_msg += " "
1078
+ error_msg += ", ".join(fields)
1079
+ if exists:
1080
+ error_msg += " уже существует."
1081
+ else:
1082
+ error_msg += " не существует."
1083
+ return error_msg
1084
+
1048
1085
  async def validate_fk_exists(
1049
1086
  self, session: AsyncSession, in_obj: ModelDict
1050
1087
  ) -> None:
@@ -1062,9 +1099,11 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1062
1099
  if not related_object_exists:
1063
1100
  raise HTTPException(
1064
1101
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1065
- detail=(
1066
- f"{self.fk_name_to_model[key].__tablename__} с id "
1067
- f"{in_obj[key]} не существует."
1102
+ detail=self.get_error_msg(
1103
+ self.fk_name_to_model[key].__tablename__,
1104
+ ["id"],
1105
+ field_values=[in_obj[key]],
1106
+ exists=False,
1068
1107
  ),
1069
1108
  )
1070
1109
 
@@ -1090,13 +1129,10 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1090
1129
  session, **query, where=(self.model.id != in_obj.get("id"))
1091
1130
  )
1092
1131
  if object_exists:
1093
- conflicting_fields = ", ".join(unique_constraint)
1094
1132
  raise HTTPException(
1095
1133
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1096
- detail=(
1097
- f"{self.model.__tablename__} с такими "
1098
- + conflicting_fields
1099
- + " уже существует."
1134
+ detail=self.get_error_msg(
1135
+ self.model.__tablename__, unique_constraint
1100
1136
  ),
1101
1137
  )
1102
1138
 
@@ -1119,19 +1155,54 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1119
1155
  continue
1120
1156
  attrs_to_check = {column.name: in_obj[column.name]}
1121
1157
  object_exists = await self.exists(
1122
- session=session,
1158
+ session,
1123
1159
  **attrs_to_check,
1124
1160
  where=(self.model.id != in_obj.get("id")),
1125
1161
  )
1126
1162
  if object_exists:
1127
1163
  raise HTTPException(
1128
1164
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1129
- detail=(
1130
- f"{self.model.__tablename__} c {column.name} "
1131
- f"{in_obj[column.name]} уже существует"
1165
+ detail=self.get_error_msg(
1166
+ self.model.__tablename__,
1167
+ [column.name],
1168
+ field_values=[in_obj[column.name]],
1132
1169
  ),
1133
1170
  )
1134
1171
 
1172
+ async def validate_unique_indexes(
1173
+ self, session: AsyncSession, in_obj: ModelDict
1174
+ ) -> None:
1175
+ """
1176
+ Валидирует соблюдение уникальности индексов
1177
+ """
1178
+ for index in self.unique_indexes:
1179
+ condition = (
1180
+ index.dialect_options["postgresql"].get("where")
1181
+ if index.dialect_options.get("postgresql")
1182
+ else None
1183
+ )
1184
+ filters = []
1185
+
1186
+ for column in index.columns:
1187
+ try:
1188
+ filters.append(column == in_obj[column.name])
1189
+ except KeyError:
1190
+ if column.nullable:
1191
+ filters.append(column.is_(None))
1192
+
1193
+ object_exists = await self.exists(
1194
+ session,
1195
+ where=and_(*filters, condition, self.model.id != in_obj.get("id")),
1196
+ )
1197
+ if object_exists:
1198
+ raise HTTPException(
1199
+ status_code=status.HTTP_400_BAD_REQUEST,
1200
+ detail=self.get_error_msg(
1201
+ self.model.__tablename__,
1202
+ [column.name for column in index.columns],
1203
+ ),
1204
+ )
1205
+
1135
1206
  async def handle_m2m_fields(self, session: AsyncSession, in_obj: ModelDict) -> None:
1136
1207
  for field in in_obj:
1137
1208
  if field in self.m2m_relationships:
@@ -1142,9 +1213,11 @@ class ModelManager(Generic[ModelT, CreateSchemaT, UpdateSchemaT]):
1142
1213
  if not related_object:
1143
1214
  raise HTTPException(
1144
1215
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
1145
- detail=(
1146
- f"{related_model.__tablename__} с id "
1147
- f"{related_object_id} не существует."
1216
+ detail=self.get_error_msg(
1217
+ related_model.__tablename__,
1218
+ ["id"],
1219
+ field_values=[related_object_id],
1220
+ exists=False,
1148
1221
  ),
1149
1222
  )
1150
1223
  related_objects.append(related_object)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_sqlalchemy_toolkit"
7
- version = "0.7.17"
7
+ version = "0.7.19"
8
8
  authors = [
9
9
  { name="Egor Kondrashov", email="e.kondr01@gmail.com" },
10
10
  ]
@@ -288,7 +288,7 @@ async def test_create_unique_filed_validation(session: AsyncSession):
288
288
  )
289
289
  with pytest.raises(
290
290
  HTTPException,
291
- match="422: category c title test-create-category-title уже существует",
291
+ match="422: category с title test-create-category-title уже существует.",
292
292
  ):
293
293
  await category_manager.create(
294
294
  session=session, title="test-create-category-title"
@@ -353,7 +353,7 @@ async def test_update_unique_filed_validation(session: AsyncSession):
353
353
  )
354
354
  with pytest.raises(
355
355
  HTTPException,
356
- match="422: category c title test-category-title1 уже существует",
356
+ match="422: category с title test-category-title1 уже существует.",
357
357
  ):
358
358
  await category_manager.update(
359
359
  session=session,