fastapi-toolsets 2.4.1__tar.gz → 2.4.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 (38) hide show
  1. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/PKG-INFO +1 -1
  2. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/pyproject.toml +1 -1
  3. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/__init__.py +1 -1
  4. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/models/watched.py +40 -2
  5. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/LICENSE +0 -0
  6. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/README.md +0 -0
  7. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/_imports.py +0 -0
  8. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/__init__.py +0 -0
  9. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/app.py +0 -0
  10. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/commands/__init__.py +0 -0
  11. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/commands/fixtures.py +0 -0
  12. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/config.py +0 -0
  13. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/pyproject.py +0 -0
  14. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/cli/utils.py +0 -0
  15. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/crud/__init__.py +0 -0
  16. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/crud/factory.py +0 -0
  17. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/crud/search.py +0 -0
  18. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/db.py +0 -0
  19. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/dependencies.py +0 -0
  20. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/exceptions/__init__.py +0 -0
  21. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/exceptions/exceptions.py +0 -0
  22. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/exceptions/handler.py +0 -0
  23. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/fixtures/__init__.py +0 -0
  24. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/fixtures/enum.py +0 -0
  25. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/fixtures/registry.py +0 -0
  26. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/fixtures/utils.py +0 -0
  27. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/logger.py +0 -0
  28. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/metrics/__init__.py +0 -0
  29. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/metrics/handler.py +0 -0
  30. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/metrics/registry.py +0 -0
  31. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/models/__init__.py +0 -0
  32. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/models/columns.py +0 -0
  33. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/py.typed +0 -0
  34. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/pytest/__init__.py +0 -0
  35. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/pytest/plugin.py +0 -0
  36. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/pytest/utils.py +0 -0
  37. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/schemas.py +0 -0
  38. {fastapi_toolsets-2.4.1 → fastapi_toolsets-2.4.2}/src/fastapi_toolsets/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-toolsets
3
- Version: 2.4.1
3
+ Version: 2.4.2
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 = "2.4.1"
3
+ version = "2.4.2"
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__ = "2.4.1"
24
+ __version__ = "2.4.2"
@@ -1,6 +1,7 @@
1
1
  """Field-change monitoring via SQLAlchemy session events."""
2
2
 
3
3
  import asyncio
4
+ import inspect
4
5
  import weakref
5
6
  from collections.abc import Awaitable
6
7
  from enum import Enum
@@ -25,6 +26,7 @@ _SESSION_PENDING_NEW = "_ft_pending_new"
25
26
  _SESSION_CREATES = "_ft_creates"
26
27
  _SESSION_DELETES = "_ft_deletes"
27
28
  _SESSION_UPDATES = "_ft_updates"
29
+ _SESSION_SAVEPOINT_DEPTH = "_ft_sp_depth"
28
30
 
29
31
 
30
32
  class ModelEvent(str, Enum):
@@ -65,6 +67,14 @@ def _snapshot_column_attrs(obj: Any) -> dict[str, Any]:
65
67
  }
66
68
 
67
69
 
70
+ def _get_watched_fields(cls: type) -> list[str] | None:
71
+ """Return the watched fields for *cls*, walking the MRO to inherit from parents."""
72
+ for klass in cls.__mro__:
73
+ if klass in _WATCHED_FIELDS:
74
+ return _WATCHED_FIELDS[klass]
75
+ return None
76
+
77
+
68
78
  def _upsert_changes(
69
79
  pending: dict[int, tuple[Any, dict[str, dict[str, Any]]]],
70
80
  obj: Any,
@@ -83,6 +93,22 @@ def _upsert_changes(
83
93
  pending[key] = (obj, changes)
84
94
 
85
95
 
96
+ @event.listens_for(AsyncSession.sync_session_class, "after_transaction_create")
97
+ def _after_transaction_create(session: Any, transaction: Any) -> None:
98
+ if transaction.nested:
99
+ session.info[_SESSION_SAVEPOINT_DEPTH] = (
100
+ session.info.get(_SESSION_SAVEPOINT_DEPTH, 0) + 1
101
+ )
102
+
103
+
104
+ @event.listens_for(AsyncSession.sync_session_class, "after_transaction_end")
105
+ def _after_transaction_end(session: Any, transaction: Any) -> None:
106
+ if transaction.nested:
107
+ depth = session.info.get(_SESSION_SAVEPOINT_DEPTH, 0)
108
+ if depth > 0: # pragma: no branch
109
+ session.info[_SESSION_SAVEPOINT_DEPTH] = depth - 1
110
+
111
+
86
112
  @event.listens_for(AsyncSession.sync_session_class, "after_flush")
87
113
  def _after_flush(session: Any, flush_context: Any) -> None:
88
114
  # New objects: capture references while session.new is still populated.
@@ -102,7 +128,7 @@ def _after_flush(session: Any, flush_context: Any) -> None:
102
128
  continue
103
129
 
104
130
  # None = not in dict = watch all fields; list = specific fields only
105
- watched = _WATCHED_FIELDS.get(type(obj))
131
+ watched = _get_watched_fields(type(obj))
106
132
  changes: dict[str, dict[str, Any]] = {}
107
133
 
108
134
  attrs = (
@@ -169,7 +195,7 @@ def _schedule_with_snapshot(
169
195
  _sa_set_committed_value(obj, key, value)
170
196
  try:
171
197
  result = fn(*args)
172
- if asyncio.iscoroutine(result):
198
+ if inspect.isawaitable(result):
173
199
  await result
174
200
  except Exception as exc:
175
201
  _logger.error(_CALLBACK_ERROR_MSG, exc_info=exc)
@@ -180,12 +206,24 @@ def _schedule_with_snapshot(
180
206
 
181
207
  @event.listens_for(AsyncSession.sync_session_class, "after_commit")
182
208
  def _after_commit(session: Any) -> None:
209
+ if session.info.get(_SESSION_SAVEPOINT_DEPTH, 0) > 0:
210
+ return
211
+
183
212
  creates: list[Any] = session.info.pop(_SESSION_CREATES, [])
184
213
  deletes: list[Any] = session.info.pop(_SESSION_DELETES, [])
185
214
  field_changes: dict[int, tuple[Any, dict[str, dict[str, Any]]]] = session.info.pop(
186
215
  _SESSION_UPDATES, {}
187
216
  )
188
217
 
218
+ if creates and deletes:
219
+ transient_ids = {id(o) for o in creates} & {id(o) for o in deletes}
220
+ if transient_ids:
221
+ creates = [o for o in creates if id(o) not in transient_ids]
222
+ deletes = [o for o in deletes if id(o) not in transient_ids]
223
+ field_changes = {
224
+ k: v for k, v in field_changes.items() if k not in transient_ids
225
+ }
226
+
189
227
  if not creates and not deletes and not field_changes:
190
228
  return
191
229