plexus-python-pulse-common 1.1.83__tar.gz → 1.1.85__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.
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/PKG-INFO +1 -1
- plexus_python_pulse_common-1.1.85/src/plexus/pulse/common/utils/apiutils.py +47 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/ormutils.py +72 -14
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/PKG-INFO +1 -1
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/SOURCES.txt +1 -0
- plexus_python_pulse_common-1.1.85/test/plexus_tests/pulse/common/utils/apiutils_test.py +71 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/ormutils_test.py +59 -10
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/s3utils_test.py +4 -7
- plexus_python_pulse_common-1.1.83/src/plexus/pulse/common/utils/apiutils.py +0 -31
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.editorconfig +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.github/workflows/pr.yml +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.github/workflows/push.yml +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.gitignore +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/MANIFEST.in +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/README.md +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/VERSION +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/pyproject.toml +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/0-dummy +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/1-dummy +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/2-dummy +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/pathutils/dummy.txt +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/setup.cfg +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/setup.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/carto/OSMFile.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/carto/OSMNode.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/carto/OSMTags.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/carto/OSMWay.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/resources/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/resources/tags/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/bagutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/config.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/datautils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/dockerutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/flowutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/gisutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/jsonutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/pathutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/s3utils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/sqlutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/strutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/tagutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus/pulse/common/utils/testutils.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/dependency_links.txt +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/not-zip-safe +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/requires.txt +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/src/plexus_python_pulse_common.egg-info/top_level.txt +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/carto/osm_file_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/carto/osm_tags_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/bagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/datautils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/dockerutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/flowutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/gisutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/jsonutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/pathutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/strutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/tagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/plexus_tests/pulse/common/utils/testutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/test/testenv.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from contextlib import AbstractContextManager
|
|
2
|
+
|
|
3
|
+
import sqlalchemy.orm as sa_orm
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
from plexus.common.utils import logger
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"managed_db_session",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DBSessionExceptionManager(AbstractContextManager):
|
|
13
|
+
def __init__(self, db: sa_orm.Session, commit_on_exit: bool):
|
|
14
|
+
self.db = db
|
|
15
|
+
self.commit_on_exit = commit_on_exit
|
|
16
|
+
|
|
17
|
+
def __enter__(self):
|
|
18
|
+
return self
|
|
19
|
+
|
|
20
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
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
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def managed_db_session(db: sa_orm.Session, commit_on_exit: bool = True) -> DBSessionExceptionManager:
|
|
47
|
+
return DBSessionExceptionManager(db, commit_on_exit)
|
|
@@ -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
|
|
@@ -78,6 +78,7 @@ test/plexus_tests/pulse/common/carto/__init__.py
|
|
|
78
78
|
test/plexus_tests/pulse/common/carto/osm_file_test.py
|
|
79
79
|
test/plexus_tests/pulse/common/carto/osm_tags_test.py
|
|
80
80
|
test/plexus_tests/pulse/common/utils/__init__.py
|
|
81
|
+
test/plexus_tests/pulse/common/utils/apiutils_test.py
|
|
81
82
|
test/plexus_tests/pulse/common/utils/bagutils_test.py
|
|
82
83
|
test/plexus_tests/pulse/common/utils/datautils_test.py
|
|
83
84
|
test/plexus_tests/pulse/common/utils/dockerutils_test.py
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import Mock, patch
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
from plexus.pulse.common.utils.apiutils import managed_db_session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiUtilsTest(unittest.TestCase):
|
|
10
|
+
def test_managed_db_session__success(self):
|
|
11
|
+
for commit_on_exit in (True, False):
|
|
12
|
+
with self.subTest(commit_on_exit=commit_on_exit):
|
|
13
|
+
db = Mock()
|
|
14
|
+
with managed_db_session(db, commit_on_exit):
|
|
15
|
+
pass
|
|
16
|
+
self.assertEqual(db.commit.call_count, int(commit_on_exit))
|
|
17
|
+
db.rollback.assert_not_called()
|
|
18
|
+
|
|
19
|
+
def test_managed_db_session__http_exception(self):
|
|
20
|
+
db = Mock()
|
|
21
|
+
error = HTTPException(404, "Missing", headers={"X-Reason": "missing"})
|
|
22
|
+
with self.assertRaises(HTTPException) as caught:
|
|
23
|
+
with managed_db_session(db):
|
|
24
|
+
raise error
|
|
25
|
+
self.assertIs(caught.exception, error)
|
|
26
|
+
db.rollback.assert_called_once_with()
|
|
27
|
+
db.commit.assert_not_called()
|
|
28
|
+
|
|
29
|
+
def test_managed_db_session__body_and_commit_errors_are_sanitized(self):
|
|
30
|
+
for during_commit in (False, True):
|
|
31
|
+
with self.subTest(during_commit=during_commit):
|
|
32
|
+
db = Mock()
|
|
33
|
+
error = RuntimeError("Sensitive SQL parameters")
|
|
34
|
+
if during_commit:
|
|
35
|
+
db.commit.side_effect = error
|
|
36
|
+
with patch("plexus.pulse.common.utils.apiutils.logger") as logger:
|
|
37
|
+
with self.assertRaises(HTTPException) as caught:
|
|
38
|
+
with managed_db_session(db):
|
|
39
|
+
if not during_commit:
|
|
40
|
+
raise error
|
|
41
|
+
logger.error.assert_called_once()
|
|
42
|
+
self.assertEqual(caught.exception.status_code, 500)
|
|
43
|
+
self.assertEqual(caught.exception.detail, "Internal server error")
|
|
44
|
+
self.assertIs(caught.exception.__cause__, error)
|
|
45
|
+
db.rollback.assert_called_once_with()
|
|
46
|
+
|
|
47
|
+
def test_managed_db_session__process_control_exceptions_are_preserved(self):
|
|
48
|
+
for error_type in (KeyboardInterrupt, SystemExit):
|
|
49
|
+
for during_commit in (False, True):
|
|
50
|
+
with self.subTest(error_type=error_type, during_commit=during_commit):
|
|
51
|
+
db = Mock()
|
|
52
|
+
error = error_type()
|
|
53
|
+
if during_commit:
|
|
54
|
+
db.commit.side_effect = error
|
|
55
|
+
with self.assertRaises(error_type) as caught:
|
|
56
|
+
with managed_db_session(db):
|
|
57
|
+
if not during_commit:
|
|
58
|
+
raise error
|
|
59
|
+
self.assertIs(caught.exception, error)
|
|
60
|
+
db.rollback.assert_called_once_with()
|
|
61
|
+
|
|
62
|
+
def test_managed_db_session__rollback_failure_preserves_original_http_exception(self):
|
|
63
|
+
db = Mock()
|
|
64
|
+
db.rollback.side_effect = RuntimeError("Rollback failed")
|
|
65
|
+
error = HTTPException(409, "Conflict")
|
|
66
|
+
with patch("plexus.pulse.common.utils.apiutils.logger") as logger:
|
|
67
|
+
with self.assertRaises(HTTPException) as caught:
|
|
68
|
+
with managed_db_session(db):
|
|
69
|
+
raise error
|
|
70
|
+
logger.exception.assert_called_once()
|
|
71
|
+
self.assertIs(caught.exception, error)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import unittest
|
|
2
3
|
import uuid as py_uuid
|
|
3
4
|
|
|
5
|
+
import pydantic as pdt
|
|
4
6
|
import pytest
|
|
5
7
|
import pytest_postgresql.factories
|
|
6
8
|
import sqlalchemy as sa
|
|
@@ -15,16 +17,9 @@ from sqlmodel import Field, SQLModel
|
|
|
15
17
|
|
|
16
18
|
from plexus.pulse.common.utils.apiutils import managed_db_session
|
|
17
19
|
from plexus.pulse.common.utils.ormutils import (
|
|
18
|
-
changing_model_mixin,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
make_revision_model_mixin,
|
|
22
|
-
make_revision_model_trigger,
|
|
23
|
-
make_sequence_model_mixin,
|
|
24
|
-
make_snapshot_model_mixin,
|
|
25
|
-
make_snapshot_model_trigger,
|
|
26
|
-
revision_model_mixin,
|
|
27
|
-
snapshot_model_mixin,
|
|
20
|
+
changing_model_mixin, make_base_model, make_changing_model_mixin,
|
|
21
|
+
make_revision_model_mixin, make_revision_model_trigger, make_sequence_model_mixin, make_snapshot_model_mixin,
|
|
22
|
+
make_snapshot_model_trigger, model_copy_from, revision_model_mixin, snapshot_model_mixin,
|
|
28
23
|
)
|
|
29
24
|
from plexus.pulse.common.utils.ormutils import (
|
|
30
25
|
db_activate_revision_model,
|
|
@@ -1217,3 +1212,57 @@ def test_make_revision_model_trigger(fixture_postgresql_test_proc, fixture_postg
|
|
|
1217
1212
|
assert updated_record.updated_at is not None
|
|
1218
1213
|
assert updated_record.updated_at == initial_record.expired_at
|
|
1219
1214
|
assert updated_record.expired_at is None
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
class CopyRecord(SQLModel):
|
|
1218
|
+
name: str | None = None
|
|
1219
|
+
count: int = 0
|
|
1220
|
+
values: list[int] = Field(default_factory=list)
|
|
1221
|
+
created_at: datetime.datetime | None = None
|
|
1222
|
+
|
|
1223
|
+
@pdt.field_serializer("created_at")
|
|
1224
|
+
def serialize_created_at(self, value):
|
|
1225
|
+
raise AssertionError("Copying must not invoke serializers")
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
class OrmUtilsTest(unittest.TestCase):
|
|
1229
|
+
def test_model_copy_from__native_datetime_without_serialization(self):
|
|
1230
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
1231
|
+
result = model_copy_from(CopyRecord(), CopyRecord(created_at=now))
|
|
1232
|
+
self.assertIs(result.created_at, now)
|
|
1233
|
+
|
|
1234
|
+
def test_field_filters(self):
|
|
1235
|
+
for options in (
|
|
1236
|
+
{"include": {"count"}},
|
|
1237
|
+
{"exclude": frozenset({"name", "values", "created_at"})},
|
|
1238
|
+
{"exclude_unset": True},
|
|
1239
|
+
{"exclude_defaults": True},
|
|
1240
|
+
):
|
|
1241
|
+
with self.subTest(options=options):
|
|
1242
|
+
dst = CopyRecord(name="keep", count=9, values=[1])
|
|
1243
|
+
model_copy_from(dst, CopyRecord(count=4), **options)
|
|
1244
|
+
self.assertEqual(dst.name, "keep")
|
|
1245
|
+
self.assertEqual(dst.count, 4)
|
|
1246
|
+
self.assertEqual(dst.values, [1])
|
|
1247
|
+
|
|
1248
|
+
def test_model_copy_from__none_filter(self):
|
|
1249
|
+
dst = CopyRecord(name="keep")
|
|
1250
|
+
model_copy_from(dst, CopyRecord(), exclude_none=True)
|
|
1251
|
+
self.assertEqual(dst.name, "keep")
|
|
1252
|
+
model_copy_from(dst, CopyRecord())
|
|
1253
|
+
self.assertIsNone(dst.name)
|
|
1254
|
+
|
|
1255
|
+
def test_model_copy_from__explicit_default_is_set(self):
|
|
1256
|
+
dst = CopyRecord(count=9)
|
|
1257
|
+
model_copy_from(dst, CopyRecord(count=0), exclude_unset=True)
|
|
1258
|
+
self.assertEqual(dst.count, 0)
|
|
1259
|
+
|
|
1260
|
+
def test_model_copy_from__exclude_wins_over_include(self):
|
|
1261
|
+
dst = CopyRecord(count=9)
|
|
1262
|
+
model_copy_from(dst, CopyRecord(count=4), include={"count"}, exclude={"count"})
|
|
1263
|
+
self.assertEqual(dst.count, 9)
|
|
1264
|
+
|
|
1265
|
+
def test_model_copy_from__unsupported_options_fail(self):
|
|
1266
|
+
for options in ({"include": {"name": True}}, {"by_alias": True}):
|
|
1267
|
+
with self.subTest(options=options), self.assertRaises(TypeError):
|
|
1268
|
+
model_copy_from(CopyRecord(), CopyRecord(), **options)
|
|
@@ -15,7 +15,7 @@ from testenv import resources_directory
|
|
|
15
15
|
|
|
16
16
|
@contextlib.contextmanager
|
|
17
17
|
def moto_server_endpoint_url():
|
|
18
|
-
server = moto.server.ThreadedMotoServer(port=0) # random free port
|
|
18
|
+
server = moto.server.ThreadedMotoServer(ip_address="127.0.0.1", port=0) # random free port
|
|
19
19
|
try:
|
|
20
20
|
server.start()
|
|
21
21
|
host, port = server.get_host_and_port()
|
|
@@ -481,9 +481,8 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
481
481
|
|
|
482
482
|
def test_s3_archive_list_files(self):
|
|
483
483
|
with (
|
|
484
|
-
moto.mock_aws(),
|
|
485
484
|
moto_server_endpoint_url() as endpoint_url,
|
|
486
|
-
s3_make_client(endpoint_url=endpoint_url) as client,
|
|
485
|
+
s3_make_client("testing", "testing", "us-east-1", endpoint_url=endpoint_url) as client,
|
|
487
486
|
):
|
|
488
487
|
client.create_bucket(Bucket="dummy-bucket")
|
|
489
488
|
|
|
@@ -544,9 +543,8 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
544
543
|
|
|
545
544
|
def test_s3_archive_open_member(self):
|
|
546
545
|
with (
|
|
547
|
-
moto.mock_aws(),
|
|
548
546
|
moto_server_endpoint_url() as endpoint_url,
|
|
549
|
-
s3_make_client(endpoint_url=endpoint_url) as client,
|
|
547
|
+
s3_make_client("testing", "testing", "us-east-1", endpoint_url=endpoint_url) as client,
|
|
550
548
|
):
|
|
551
549
|
client.create_bucket(Bucket="dummy-bucket")
|
|
552
550
|
|
|
@@ -571,9 +569,8 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
571
569
|
|
|
572
570
|
def test_s3_archive_open_members(self):
|
|
573
571
|
with (
|
|
574
|
-
moto.mock_aws(),
|
|
575
572
|
moto_server_endpoint_url() as endpoint_url,
|
|
576
|
-
s3_make_client(endpoint_url=endpoint_url) as client,
|
|
573
|
+
s3_make_client("testing", "testing", "us-east-1", endpoint_url=endpoint_url) as client,
|
|
577
574
|
):
|
|
578
575
|
client.create_bucket(Bucket="dummy-bucket")
|
|
579
576
|
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
from contextlib import AbstractContextManager
|
|
2
|
-
|
|
3
|
-
import sqlalchemy.orm as sa_orm
|
|
4
|
-
from fastapi import HTTPException
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"managed_db_session",
|
|
8
|
-
]
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DBSessionExceptionManager(AbstractContextManager):
|
|
12
|
-
def __init__(self, db: sa_orm.Session, commit_on_exit: bool):
|
|
13
|
-
self.db = db
|
|
14
|
-
self.commit_on_exit = commit_on_exit
|
|
15
|
-
|
|
16
|
-
def __enter__(self):
|
|
17
|
-
return self
|
|
18
|
-
|
|
19
|
-
def __exit__(self, exc_type, exc_value, traceback):
|
|
20
|
-
if isinstance(exc_type, HTTPException):
|
|
21
|
-
return False # Propagate HTTPException
|
|
22
|
-
if exc_type is not None:
|
|
23
|
-
self.db.rollback() # Raise a new HTTPException (or any other exception type)
|
|
24
|
-
raise HTTPException(status_code=500, detail=str(exc_value)) from exc_value
|
|
25
|
-
if self.commit_on_exit:
|
|
26
|
-
self.db.commit()
|
|
27
|
-
return True
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def managed_db_session(db: sa_orm.Session, commit_on_exit: bool = True) -> DBSessionExceptionManager:
|
|
31
|
-
return DBSessionExceptionManager(db, commit_on_exit)
|
|
File without changes
|
{plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.github/workflows/pr.yml
RENAMED
|
File without changes
|
{plexus_python_pulse_common-1.1.83 → plexus_python_pulse_common-1.1.85}/.github/workflows/push.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|