plexus-python-pulse-common 1.1.84__py3-none-any.whl → 1.1.85__py3-none-any.whl
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.
- plexus/pulse/common/utils/apiutils.py +24 -8
- plexus/pulse/common/utils/ormutils.py +72 -14
- {plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/METADATA +1 -1
- {plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/RECORD +6 -6
- {plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/WHEEL +0 -0
- {plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,7 @@ from contextlib import AbstractContextManager
|
|
|
2
2
|
|
|
3
3
|
import sqlalchemy.orm as sa_orm
|
|
4
4
|
from fastapi import HTTPException
|
|
5
|
+
from plexus.common.utils import logger
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"managed_db_session",
|
|
@@ -17,14 +18,29 @@ class DBSessionExceptionManager(AbstractContextManager):
|
|
|
17
18
|
return self
|
|
18
19
|
|
|
19
20
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
if exc_type is None:
|
|
22
|
+
if self.commit_on_exit:
|
|
23
|
+
try:
|
|
24
|
+
self.db.commit()
|
|
25
|
+
except BaseException as commit_exc_value:
|
|
26
|
+
self.handle_exception(commit_exc_value)
|
|
27
|
+
raise
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
self.handle_exception(exc_value)
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def handle_exception(self, exc_value: BaseException | None) -> None:
|
|
34
|
+
try:
|
|
35
|
+
self.db.rollback()
|
|
36
|
+
except Exception:
|
|
37
|
+
logger.exception("Failed to roll back database session")
|
|
38
|
+
|
|
39
|
+
if isinstance(exc_value, HTTPException) or not isinstance(exc_value, Exception):
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
logger.error("Database session failed", exc_info=(type(exc_value), exc_value, exc_value.__traceback__))
|
|
43
|
+
raise HTTPException(status_code=500, detail="Internal server error") from exc_value
|
|
28
44
|
|
|
29
45
|
|
|
30
46
|
def managed_db_session(db: sa_orm.Session, commit_on_exit: bool = True) -> DBSessionExceptionManager:
|
|
@@ -224,24 +224,72 @@ def collect_model_tables[ModelT: SQLModel](*models: ModelT) -> sa.MetaData:
|
|
|
224
224
|
return metadata
|
|
225
225
|
|
|
226
226
|
|
|
227
|
-
def model_copy_from[ModelT: SQLModel, ModelU: SQLModel](
|
|
227
|
+
def model_copy_from[ModelT: SQLModel, ModelU: SQLModel](
|
|
228
|
+
dst: ModelT,
|
|
229
|
+
src: ModelU,
|
|
230
|
+
*,
|
|
231
|
+
include: set[str] | frozenset[str] | None = None,
|
|
232
|
+
exclude: set[str] | frozenset[str] | None = None,
|
|
233
|
+
exclude_none: bool = False,
|
|
234
|
+
exclude_unset: bool = False,
|
|
235
|
+
exclude_defaults: bool = False,
|
|
236
|
+
) -> ModelT:
|
|
237
|
+
"""Copy native field values without invoking serializers.
|
|
238
|
+
|
|
239
|
+
``include`` and ``exclude`` select top-level field names only; nested filter
|
|
240
|
+
mappings and serialization options are not supported. The exclusion flags
|
|
241
|
+
also apply when recursively copying nested models.
|
|
242
|
+
"""
|
|
228
243
|
if not isinstance(dst, SQLModel) or not isinstance(src, SQLModel):
|
|
229
244
|
raise TypeError("both 'dst' and 'src' must be instances of SQLModel or its subclasses")
|
|
245
|
+
if include is not None and (
|
|
246
|
+
not isinstance(include, (set, frozenset)) or not all(isinstance(item, str) for item in include)):
|
|
247
|
+
raise TypeError(f"'include' must be a set of field names or None")
|
|
248
|
+
if exclude is not None and (
|
|
249
|
+
not isinstance(exclude, (set, frozenset)) or not all(isinstance(item, str) for item in exclude)):
|
|
250
|
+
raise TypeError(f"'exclude' must be a set of field names or None")
|
|
230
251
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
src_model = type(src)
|
|
253
|
+
dst_model = type(dst)
|
|
254
|
+
|
|
255
|
+
for field, src_field_info in src_model.model_fields.items():
|
|
234
256
|
# Skip fields that are not present in the destination model
|
|
235
|
-
if
|
|
236
|
-
|
|
257
|
+
if field not in dst_model.model_fields:
|
|
258
|
+
continue
|
|
259
|
+
if include is not None and field not in include:
|
|
260
|
+
continue
|
|
261
|
+
if exclude is not None and field in exclude:
|
|
262
|
+
continue
|
|
263
|
+
if exclude_unset and field not in src.model_fields_set:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
value = getattr(src, field)
|
|
267
|
+
if exclude_none and value is None:
|
|
268
|
+
continue
|
|
269
|
+
if exclude_defaults and not src_field_info.is_required():
|
|
270
|
+
default = src_field_info.get_default(call_default_factory=True, validated_data=src.__dict__)
|
|
271
|
+
if value == default:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
field_info = dst_model.model_fields[field]
|
|
275
|
+
if value is None:
|
|
276
|
+
pdt.TypeAdapter(field_info.annotation).validate_python(None)
|
|
237
277
|
|
|
238
278
|
# Only set the field if it exists in the destination model
|
|
239
279
|
if hasattr(dst, field):
|
|
240
280
|
# If the field is a SQLModel, recursively copy it
|
|
241
281
|
if isinstance(value, SQLModel):
|
|
242
|
-
value = model_copy_from(getattr(dst, field),
|
|
282
|
+
value = model_copy_from(getattr(dst, field),
|
|
283
|
+
value,
|
|
284
|
+
exclude_none=exclude_none,
|
|
285
|
+
exclude_unset=exclude_unset,
|
|
286
|
+
exclude_defaults=exclude_defaults)
|
|
243
287
|
elif isinstance(value, list) and all(isinstance(item, SQLModel) for item in value):
|
|
244
|
-
value = [model_copy_from(dst_item,
|
|
288
|
+
value = [model_copy_from(dst_item,
|
|
289
|
+
src_item,
|
|
290
|
+
exclude_none=exclude_none,
|
|
291
|
+
exclude_unset=exclude_unset,
|
|
292
|
+
exclude_defaults=exclude_defaults)
|
|
245
293
|
for dst_item, src_item in zip(getattr(dst, field), value)]
|
|
246
294
|
|
|
247
295
|
setattr(dst, field, value)
|
|
@@ -503,6 +551,16 @@ def model_revision_type(dialect: str | None = None) -> sa.types.TypeEngine[int]:
|
|
|
503
551
|
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
504
552
|
|
|
505
553
|
|
|
554
|
+
def index_condition_kwarg(dialect: str | None = None, clause: sa.TextClause | None = None) -> dict[str, sa.TextClause]:
|
|
555
|
+
if dialect is None:
|
|
556
|
+
dialect = dialects.postgresql
|
|
557
|
+
if dialect == dialects.postgresql:
|
|
558
|
+
return dict(postgresql_where=clause)
|
|
559
|
+
if dialect == dialects.sqlite:
|
|
560
|
+
return dict(sqlite_where=clause)
|
|
561
|
+
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
562
|
+
|
|
563
|
+
|
|
506
564
|
# At the present time, we cannot express intersection of Protocol and SQLModel directly.
|
|
507
565
|
# Thus, we define union types here for the mixins.
|
|
508
566
|
SequenceModelMixin = SequenceModelMixinProtocol | pdt.BaseModel
|
|
@@ -670,7 +728,7 @@ def make_snapshot_model_mixin(dialect: str | None = None) -> type[SnapshotModelM
|
|
|
670
728
|
index_name,
|
|
671
729
|
"record_sqn",
|
|
672
730
|
unique=True,
|
|
673
|
-
|
|
731
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
674
732
|
)
|
|
675
733
|
|
|
676
734
|
@classmethod
|
|
@@ -678,7 +736,7 @@ def make_snapshot_model_mixin(dialect: str | None = None) -> type[SnapshotModelM
|
|
|
678
736
|
return sa.Index(
|
|
679
737
|
index_name,
|
|
680
738
|
*fields,
|
|
681
|
-
|
|
739
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
682
740
|
)
|
|
683
741
|
|
|
684
742
|
@classmethod
|
|
@@ -687,7 +745,7 @@ def make_snapshot_model_mixin(dialect: str | None = None) -> type[SnapshotModelM
|
|
|
687
745
|
index_name,
|
|
688
746
|
*fields,
|
|
689
747
|
unique=True,
|
|
690
|
-
|
|
748
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
691
749
|
)
|
|
692
750
|
|
|
693
751
|
return ModelMixin
|
|
@@ -807,7 +865,7 @@ def make_revision_model_mixin(dialect: str | None = None) -> type[RevisionModelM
|
|
|
807
865
|
index_name,
|
|
808
866
|
"record_sqn",
|
|
809
867
|
unique=True,
|
|
810
|
-
|
|
868
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
811
869
|
)
|
|
812
870
|
|
|
813
871
|
@classmethod
|
|
@@ -815,7 +873,7 @@ def make_revision_model_mixin(dialect: str | None = None) -> type[RevisionModelM
|
|
|
815
873
|
return sa.Index(
|
|
816
874
|
index_name,
|
|
817
875
|
*fields,
|
|
818
|
-
|
|
876
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
819
877
|
)
|
|
820
878
|
|
|
821
879
|
@classmethod
|
|
@@ -824,7 +882,7 @@ def make_revision_model_mixin(dialect: str | None = None) -> type[RevisionModelM
|
|
|
824
882
|
index_name,
|
|
825
883
|
*fields,
|
|
826
884
|
unique=True,
|
|
827
|
-
|
|
885
|
+
**index_condition_kwarg(dialect, sa.text('"expired_at" IS NULL')),
|
|
828
886
|
)
|
|
829
887
|
|
|
830
888
|
return ModelMixin
|
{plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/RECORD
RENAMED
|
@@ -9,7 +9,7 @@ plexus/pulse/common/resources/tags/__init__.py,sha256=1BmOq9aIT2Fr4YTzDwTrYySDhi
|
|
|
9
9
|
plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml,sha256=8zWjsYp3OxGfBJSiS6-r08q61Jhulol65iV5-pvF1-M,5338
|
|
10
10
|
plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml,sha256=PRxyS5xwWcNwUjAbYqN-SafLZVviE-QSzU-DXyXNffw,80736
|
|
11
11
|
plexus/pulse/common/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
plexus/pulse/common/utils/apiutils.py,sha256=
|
|
12
|
+
plexus/pulse/common/utils/apiutils.py,sha256=AEGyzteSJrCq13EvQO07xDs3rROMSvG6OaFdj7swFdA,1530
|
|
13
13
|
plexus/pulse/common/utils/bagutils.py,sha256=JcTD-9xJAhV_BYXw-Zfa_85hkyO5wUVqTqw2eMJFgVI,13116
|
|
14
14
|
plexus/pulse/common/utils/config.py,sha256=YPtFK5QHTVsXwcFBGV0rlZad0GFtsgwysQNyKYjdzvk,2409
|
|
15
15
|
plexus/pulse/common/utils/datautils.py,sha256=OWlnyFSa6UaPbSSG3HzCJHxdiHyfHAE-FUVHaLfNZoM,9335
|
|
@@ -17,14 +17,14 @@ plexus/pulse/common/utils/dockerutils.py,sha256=7DF9yFGz8uHwpYk115xBWqOjs5etz0UE
|
|
|
17
17
|
plexus/pulse/common/utils/flowutils.py,sha256=MGu15KL9mw-LyGGLceVmY-c2FGer5PURnMZWFSJN--I,10182
|
|
18
18
|
plexus/pulse/common/utils/gisutils.py,sha256=Cx0ZB9-NA_7VuzbuzRArTJ5UfltG-KmvVC8YxPIgPE4,11600
|
|
19
19
|
plexus/pulse/common/utils/jsonutils.py,sha256=zSbW94klN2jrkMdBECu4RNJX6zzNaSxauQ3FuihuB6E,3342
|
|
20
|
-
plexus/pulse/common/utils/ormutils.py,sha256=
|
|
20
|
+
plexus/pulse/common/utils/ormutils.py,sha256=FPkkdmiTZ1M0ohLTQakA8shO1MD_2mhM6NolpnD4OWo,64472
|
|
21
21
|
plexus/pulse/common/utils/pathutils.py,sha256=eYfNK48wf1xZGK2IbpKxYOn4skZhWWA8OTyUwDXGMIc,9222
|
|
22
22
|
plexus/pulse/common/utils/s3utils.py,sha256=5q5wSOavt-n5EdrflQX7WPGgDeWQkGO9lG5Nq3QXjCk,35904
|
|
23
23
|
plexus/pulse/common/utils/sqlutils.py,sha256=D6kTBjhO5YlNRt3uFlPt6z3uH61m9ajEzPYmsI6NoFc,231
|
|
24
24
|
plexus/pulse/common/utils/strutils.py,sha256=PkmMHCcHN9vALCy_8fJTf00Dg7I2N4QTCXdBmB3t7rk,16440
|
|
25
25
|
plexus/pulse/common/utils/tagutils.py,sha256=V8eUxtWSijX6nHSCuRX2dTJz3why3TCOzPVK5YXt28s,66944
|
|
26
26
|
plexus/pulse/common/utils/testutils.py,sha256=hKPRvoN4mg8QGQJH121l59a0uo8jN5rntGQAVYplGBw,6166
|
|
27
|
-
plexus_python_pulse_common-1.1.
|
|
28
|
-
plexus_python_pulse_common-1.1.
|
|
29
|
-
plexus_python_pulse_common-1.1.
|
|
30
|
-
plexus_python_pulse_common-1.1.
|
|
27
|
+
plexus_python_pulse_common-1.1.85.dist-info/METADATA,sha256=khRqgCk5BE_g8XkuhmSDVTfVKQhEd_hTr6u9CrRb2-Y,1555
|
|
28
|
+
plexus_python_pulse_common-1.1.85.dist-info/WHEEL,sha256=a-udL2m4V_6Yn8b-uKzWb12RjRxwwMo6ohYFM24tpaY,91
|
|
29
|
+
plexus_python_pulse_common-1.1.85.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
|
|
30
|
+
plexus_python_pulse_common-1.1.85.dist-info/RECORD,,
|
{plexus_python_pulse_common-1.1.84.dist-info → plexus_python_pulse_common-1.1.85.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|