fastapi-toolsets 3.1.0__tar.gz → 3.1.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 (38) hide show
  1. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/PKG-INFO +1 -1
  2. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/pyproject.toml +1 -1
  3. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/__init__.py +1 -1
  4. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/fixtures/utils.py +68 -29
  5. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/LICENSE +0 -0
  6. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/README.md +0 -0
  7. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/_imports.py +0 -0
  8. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/__init__.py +0 -0
  9. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/app.py +0 -0
  10. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  11. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
  12. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/config.py +0 -0
  13. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/pyproject.py +0 -0
  14. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/cli/utils.py +0 -0
  15. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/crud/__init__.py +0 -0
  16. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/crud/factory.py +0 -0
  17. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/crud/search.py +0 -0
  18. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/db.py +0 -0
  19. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/dependencies.py +0 -0
  20. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
  21. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
  22. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  23. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
  24. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  25. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/fixtures/registry.py +0 -0
  26. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/logger.py +0 -0
  27. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/metrics/__init__.py +0 -0
  28. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/metrics/handler.py +0 -0
  29. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/metrics/registry.py +0 -0
  30. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/models/__init__.py +0 -0
  31. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/models/columns.py +0 -0
  32. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/models/watched.py +0 -0
  33. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/py.typed +0 -0
  34. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  35. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/pytest/plugin.py +0 -0
  36. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/pytest/utils.py +0 -0
  37. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/schemas.py +0 -0
  38. {fastapi_toolsets-3.1.0 → fastapi_toolsets-3.1.1}/src/fastapi_toolsets/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: Production-ready utilities for FastAPI applications
5
5
  Keywords: fastapi,sqlalchemy,postgresql
6
6
  Author: d3vyce
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-toolsets"
3
- version = "3.1.0"
3
+ version = "3.1.1"
4
4
  description = "Production-ready utilities for FastAPI applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,4 +21,4 @@ Example usage:
21
21
  return Response(data={"user": user.username}, message="Success")
22
22
  """
23
23
 
24
- __version__ = "3.1.0"
24
+ __version__ = "3.1.1"
@@ -40,6 +40,32 @@ def _instance_to_dict(instance: DeclarativeBase) -> dict[str, Any]:
40
40
  return result
41
41
 
42
42
 
43
+ def _get_table_chain(model_cls: type[DeclarativeBase]) -> list[type[DeclarativeBase]]:
44
+ """Return [root, ..., model_cls] for joined-table inheritance, or [model_cls]."""
45
+ chain: list[type[DeclarativeBase]] = []
46
+ current = sa_inspect(model_cls)
47
+ while current is not None:
48
+ chain.append(current.class_)
49
+ current = current.inherits
50
+ chain.reverse()
51
+ seen: set[int] = set()
52
+ result: list[type[DeclarativeBase]] = []
53
+ for cls in chain:
54
+ tid = id(cls.__table__)
55
+ if tid not in seen: # pragma: no branch
56
+ seen.add(tid)
57
+ result.append(cls)
58
+ return result
59
+
60
+
61
+ def _instance_to_dict_for_cls(
62
+ instance: DeclarativeBase, cls: type[DeclarativeBase]
63
+ ) -> dict[str, Any]:
64
+ """Like _instance_to_dict but limited to columns belonging to cls's own table."""
65
+ own_cols = {col.key for col in cls.__table__.columns}
66
+ return {k: v for k, v in _instance_to_dict(instance).items() if k in own_cols}
67
+
68
+
43
69
  def _group_by_type(
44
70
  instances: list[DeclarativeBase],
45
71
  ) -> list[tuple[type[DeclarativeBase], list[DeclarativeBase]]]:
@@ -73,9 +99,11 @@ async def _batch_insert(
73
99
  instances: list[DeclarativeBase],
74
100
  ) -> None:
75
101
  """INSERT all instances — raises on conflict (no duplicate handling)."""
76
- dicts = [_instance_to_dict(i) for i in instances]
77
- for group_dicts, _ in _group_by_column_set(dicts, instances):
78
- await session.execute(pg_insert(model_cls).values(group_dicts))
102
+ for cls in _get_table_chain(model_cls):
103
+ dicts = [_instance_to_dict_for_cls(i, cls) for i in instances]
104
+ for group_dicts, _ in _group_by_column_set(dicts, instances):
105
+ if group_dicts and group_dicts[0]: # pragma: no branch
106
+ await session.execute(pg_insert(cls).values(group_dicts))
79
107
 
80
108
 
81
109
  async def _batch_merge(
@@ -84,31 +112,30 @@ async def _batch_merge(
84
112
  instances: list[DeclarativeBase],
85
113
  ) -> None:
86
114
  """UPSERT: insert new rows, update existing ones with the provided values."""
87
- mapper = model_cls.__mapper__
88
- pk_names = [col.name for col in mapper.primary_key]
89
- pk_names_set = set(pk_names)
90
- non_pk_cols = [
91
- prop.key
92
- for prop in mapper.column_attrs
93
- if not any(col.name in pk_names_set for col in prop.columns)
94
- ]
95
-
96
- dicts = [_instance_to_dict(i) for i in instances]
97
- for group_dicts, _ in _group_by_column_set(dicts, instances):
98
- stmt = pg_insert(model_cls).values(group_dicts)
99
-
100
- inserted_keys = set(group_dicts[0])
101
- update_cols = [col for col in non_pk_cols if col in inserted_keys]
102
-
103
- if update_cols:
104
- stmt = stmt.on_conflict_do_update(
105
- index_elements=pk_names,
106
- set_={col: stmt.excluded[col] for col in update_cols},
107
- )
108
- else:
109
- stmt = stmt.on_conflict_do_nothing(index_elements=pk_names)
115
+ for cls in _get_table_chain(model_cls):
116
+ pk_names = [col.name for col in cls.__table__.primary_key]
117
+ pk_names_set = set(pk_names)
118
+ own_col_keys = {col.key for col in cls.__table__.columns}
119
+ non_pk_cols = [k for k in own_col_keys if k not in pk_names_set]
120
+
121
+ dicts = [_instance_to_dict_for_cls(i, cls) for i in instances]
122
+ for group_dicts, _ in _group_by_column_set(dicts, instances):
123
+ if not group_dicts or not group_dicts[0]: # pragma: no cover
124
+ continue
125
+ stmt = pg_insert(cls).values(group_dicts)
110
126
 
111
- await session.execute(stmt)
127
+ inserted_keys = set(group_dicts[0])
128
+ update_cols = [col for col in non_pk_cols if col in inserted_keys]
129
+
130
+ if update_cols:
131
+ stmt = stmt.on_conflict_do_update(
132
+ index_elements=pk_names,
133
+ set_={col: stmt.excluded[col] for col in update_cols},
134
+ )
135
+ else:
136
+ stmt = stmt.on_conflict_do_nothing(index_elements=pk_names)
137
+
138
+ await session.execute(stmt)
112
139
 
113
140
 
114
141
  async def _batch_skip_existing(
@@ -117,6 +144,16 @@ async def _batch_skip_existing(
117
144
  instances: list[DeclarativeBase],
118
145
  ) -> list[DeclarativeBase]:
119
146
  """INSERT only rows that do not already exist; return the inserted ones."""
147
+ if len(_get_table_chain(model_cls)) > 1:
148
+ loaded: list[DeclarativeBase] = []
149
+ for inst in instances:
150
+ pk = _get_primary_key(inst)
151
+ if pk is None or not await session.get(model_cls, pk):
152
+ session.add(inst)
153
+ loaded.append(inst)
154
+ await session.flush()
155
+ return loaded
156
+
120
157
  mapper = model_cls.__mapper__
121
158
  pk_names = [col.name for col in mapper.primary_key]
122
159
 
@@ -129,7 +166,7 @@ async def _batch_skip_existing(
129
166
  else:
130
167
  with_pk_pairs.append((inst, pk))
131
168
 
132
- loaded: list[DeclarativeBase] = list(no_pk)
169
+ loaded = list(no_pk)
133
170
  if no_pk:
134
171
  no_pk_dicts = [_instance_to_dict(i) for i in no_pk]
135
172
  for group_dicts, _ in _group_by_column_set(no_pk_dicts, no_pk):
@@ -179,7 +216,7 @@ async def _load_ordered(
179
216
  if contexts is not None and not variants:
180
217
  variants = registry.get_variants(name)
181
218
 
182
- if not variants:
219
+ if not variants: # pragma: no cover
183
220
  results[name] = []
184
221
  continue
185
222
 
@@ -204,6 +241,8 @@ async def _load_ordered(
204
241
  case LoadStrategy.SKIP_EXISTING:
205
242
  inserted = await _batch_skip_existing(session, model_cls, group)
206
243
  loaded.extend(inserted)
244
+ case _: # pragma: no cover
245
+ pass
207
246
 
208
247
  results[name] = loaded
209
248
  logger.info(f"Loaded fixture '{name}': {len(loaded)} {model_name}(s)")