plexus-python-pulse-common 1.1.85__tar.gz → 1.1.87__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.85 → plexus_python_pulse_common-1.1.87}/PKG-INFO +1 -1
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/ormutils.py +110 -11
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/PKG-INFO +1 -1
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/ormutils_test.py +197 -3
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/s3utils_test.py +47 -29
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.editorconfig +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.github/workflows/pr.yml +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.github/workflows/push.yml +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.gitignore +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/MANIFEST.in +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/README.md +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/VERSION +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/pyproject.toml +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/0-dummy +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/1-dummy +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/2-dummy +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/pathutils/dummy.txt +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/setup.cfg +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/setup.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/carto/OSMFile.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/carto/OSMNode.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/carto/OSMTags.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/carto/OSMWay.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/resources/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/resources/tags/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/apiutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/bagutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/config.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/datautils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/dockerutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/flowutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/gisutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/jsonutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/pathutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/s3utils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/sqlutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/strutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/tagutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus/pulse/common/utils/testutils.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/SOURCES.txt +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/dependency_links.txt +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/not-zip-safe +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/requires.txt +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/src/plexus_python_pulse_common.egg-info/top_level.txt +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/carto/osm_file_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/carto/osm_tags_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/apiutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/bagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/datautils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/dockerutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/flowutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/gisutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/jsonutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/pathutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/strutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/tagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/plexus_tests/pulse/common/utils/testutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/test/testenv.py +0 -0
|
@@ -948,6 +948,14 @@ def make_snapshot_model_trigger[SnapshotModelT: SnapshotModelMixin](engine: sa.E
|
|
|
948
948
|
NEW."expired_at" := NULL;
|
|
949
949
|
NEW."record_sqn" := nextval('{record_sqn_seq_name}');
|
|
950
950
|
ELSE
|
|
951
|
+
-- Serialize transitions even when there is no active version.
|
|
952
|
+
PERFORM "sqn" FROM "{table_name}"
|
|
953
|
+
WHERE "record_sqn" = NEW."record_sqn"
|
|
954
|
+
ORDER BY "sqn" LIMIT 1 FOR UPDATE;
|
|
955
|
+
IF NOT FOUND THEN
|
|
956
|
+
RAISE EXCEPTION 'record_sqn % does not exist', NEW."record_sqn";
|
|
957
|
+
END IF;
|
|
958
|
+
|
|
951
959
|
IF NEW."created_at" IS NULL THEN
|
|
952
960
|
NEW."created_at" := CURRENT_TIMESTAMP;
|
|
953
961
|
END IF;
|
|
@@ -1019,6 +1027,14 @@ def make_revision_model_trigger[RevisionModelT: RevisionModelMixin](engine: sa.E
|
|
|
1019
1027
|
NEW."record_sqn" := nextval('{record_sqn_seq_name}');
|
|
1020
1028
|
NEW."revision" := 1;
|
|
1021
1029
|
ELSE
|
|
1030
|
+
-- Serialize transitions even when there is no active version.
|
|
1031
|
+
PERFORM "sqn" FROM "{table_name}"
|
|
1032
|
+
WHERE "record_sqn" = NEW."record_sqn"
|
|
1033
|
+
ORDER BY "sqn" LIMIT 1 FOR UPDATE;
|
|
1034
|
+
IF NOT FOUND THEN
|
|
1035
|
+
RAISE EXCEPTION 'record_sqn % does not exist', NEW."record_sqn";
|
|
1036
|
+
END IF;
|
|
1037
|
+
|
|
1022
1038
|
SELECT MAX("created_at") INTO NEW."created_at"
|
|
1023
1039
|
FROM "{table_name}"
|
|
1024
1040
|
WHERE "record_sqn" = NEW."record_sqn";
|
|
@@ -1070,6 +1086,7 @@ def clone_sequence_model_instance[SequenceModelT: SequenceModelMixin](
|
|
|
1070
1086
|
return instance
|
|
1071
1087
|
if clear_meta_fields:
|
|
1072
1088
|
result.sqn = None
|
|
1089
|
+
result.model_fields_set.discard("sqn")
|
|
1073
1090
|
return result
|
|
1074
1091
|
|
|
1075
1092
|
|
|
@@ -1087,6 +1104,9 @@ def clone_changing_model_instance[ChangingModelT: ChangingModelMixin](
|
|
|
1087
1104
|
result.sqn = None
|
|
1088
1105
|
result.created_at = None
|
|
1089
1106
|
result.updated_at = None
|
|
1107
|
+
result.model_fields_set.discard("sqn")
|
|
1108
|
+
result.model_fields_set.discard("created_at")
|
|
1109
|
+
result.model_fields_set.discard("updated_at")
|
|
1090
1110
|
return result
|
|
1091
1111
|
|
|
1092
1112
|
|
|
@@ -1105,6 +1125,10 @@ def clone_snapshot_model_instance[SnapshotModelT: SnapshotModelMixin](
|
|
|
1105
1125
|
result.created_at = None
|
|
1106
1126
|
result.expired_at = None
|
|
1107
1127
|
result.record_sqn = None
|
|
1128
|
+
result.model_fields_set.discard("sqn")
|
|
1129
|
+
result.model_fields_set.discard("created_at")
|
|
1130
|
+
result.model_fields_set.discard("expired_at")
|
|
1131
|
+
result.model_fields_set.discard("record_sqn")
|
|
1108
1132
|
return result
|
|
1109
1133
|
|
|
1110
1134
|
|
|
@@ -1125,6 +1149,12 @@ def clone_revision_model_instance[RevisionModelT: RevisionModelMixin](
|
|
|
1125
1149
|
result.expired_at = None
|
|
1126
1150
|
result.record_sqn = None
|
|
1127
1151
|
result.revision = None
|
|
1152
|
+
result.model_fields_set.discard("sqn")
|
|
1153
|
+
result.model_fields_set.discard("created_at")
|
|
1154
|
+
result.model_fields_set.discard("updated_at")
|
|
1155
|
+
result.model_fields_set.discard("expired_at")
|
|
1156
|
+
result.model_fields_set.discard("record_sqn")
|
|
1157
|
+
result.model_fields_set.discard("revision")
|
|
1128
1158
|
return result
|
|
1129
1159
|
|
|
1130
1160
|
|
|
@@ -1144,6 +1174,41 @@ def db_make_order_by_clause[SequenceModelT: SequenceModelMixin](
|
|
|
1144
1174
|
return order_criteria
|
|
1145
1175
|
|
|
1146
1176
|
|
|
1177
|
+
def db_lock_model_record[ModelT: RevisionModelMixin | SnapshotModelMixin](
|
|
1178
|
+
db: sa_orm.Session,
|
|
1179
|
+
model: type[ModelT],
|
|
1180
|
+
record_sqn: int,
|
|
1181
|
+
) -> None:
|
|
1182
|
+
"""
|
|
1183
|
+
Lock the first history row, including when the record is inactive.
|
|
1184
|
+
|
|
1185
|
+
Select by ``record_sqn`` rather than assuming it equals the first ``sqn``: trigger-managed
|
|
1186
|
+
records use a separate sequence. Every lifecycle writer must acquire this same lock before
|
|
1187
|
+
reading current state.
|
|
1188
|
+
Locks last until caller commit or rollback. Do not delete the first history row.
|
|
1189
|
+
"""
|
|
1190
|
+
db_anchor = (
|
|
1191
|
+
db
|
|
1192
|
+
.query(model)
|
|
1193
|
+
.where(model.record_sqn == record_sqn)
|
|
1194
|
+
.order_by(model.sqn)
|
|
1195
|
+
.limit(1)
|
|
1196
|
+
.with_for_update()
|
|
1197
|
+
.populate_existing()
|
|
1198
|
+
.one_or_none()
|
|
1199
|
+
)
|
|
1200
|
+
if db_anchor is None:
|
|
1201
|
+
raise sa_exc.NoResultFound(f"'{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
# Mutating helpers flush but never begin, commit, or roll back transactions. Callers must roll
|
|
1205
|
+
# back on failure. Row locks serialize cooperating writers on databases supporting SELECT FOR UPDATE;
|
|
1206
|
+
# SQLite callers must serialize writes themselves (e.g. begin an IMMEDIATE transaction before
|
|
1207
|
+
# reading).
|
|
1208
|
+
# At stronger isolation levels, callers must also handle serialization failures by retrying
|
|
1209
|
+
# transactions. History rows used as lock anchors must not be deleted while a record exists.
|
|
1210
|
+
|
|
1211
|
+
|
|
1147
1212
|
def db_create_sequence_model[SequenceModelT: SequenceModelMixin](
|
|
1148
1213
|
db: sa_orm.Session,
|
|
1149
1214
|
model: type[SequenceModelT],
|
|
@@ -1201,11 +1266,11 @@ def db_update_sequence_model[SequenceModelT: SequenceModelMixin](
|
|
|
1201
1266
|
instance: SequenceModelMixin,
|
|
1202
1267
|
sqn: int,
|
|
1203
1268
|
) -> SequenceModelT:
|
|
1204
|
-
db_instance = db.query(model).where(model.sqn == sqn).one_or_none()
|
|
1269
|
+
db_instance = db.query(model).where(model.sqn == sqn).with_for_update().populate_existing().one_or_none()
|
|
1205
1270
|
if db_instance is None:
|
|
1206
1271
|
raise sa_exc.NoResultFound(f"'{model_name_of(model)}' of specified sqn '{sqn}' not found")
|
|
1207
1272
|
|
|
1208
|
-
db_instance = model_copy_from(db_instance, clone_sequence_model_instance(model, instance),
|
|
1273
|
+
db_instance = model_copy_from(db_instance, clone_sequence_model_instance(model, instance), exclude_unset=True)
|
|
1209
1274
|
clone_sequence_model_instance(model, db_instance, validate_only=True)
|
|
1210
1275
|
db.flush()
|
|
1211
1276
|
|
|
@@ -1217,7 +1282,7 @@ def db_delete_sequence_model[SequenceModelT: SequenceModelMixin](
|
|
|
1217
1282
|
model: type[SequenceModelT],
|
|
1218
1283
|
sqn: int,
|
|
1219
1284
|
) -> SequenceModelT:
|
|
1220
|
-
db_instance = db.query(model).where(model.sqn == sqn).one_or_none()
|
|
1285
|
+
db_instance = db.query(model).where(model.sqn == sqn).with_for_update().populate_existing().one_or_none()
|
|
1221
1286
|
if db_instance is None:
|
|
1222
1287
|
raise sa_exc.NoResultFound(f"'{model_name_of(model)}' of specified sqn '{sqn}' not found")
|
|
1223
1288
|
|
|
@@ -1265,11 +1330,11 @@ def db_update_changing_model[ChangingModelT: ChangingModelMixin](
|
|
|
1265
1330
|
sqn: int,
|
|
1266
1331
|
updated_at: datetime.datetime,
|
|
1267
1332
|
) -> ChangingModelT:
|
|
1268
|
-
db_instance = db.query(model).where(model.sqn == sqn).one_or_none()
|
|
1333
|
+
db_instance = db.query(model).where(model.sqn == sqn).with_for_update().populate_existing().one_or_none()
|
|
1269
1334
|
if db_instance is None:
|
|
1270
1335
|
raise sa_exc.NoResultFound(f"'{model_name_of(model)}' of specified sqn '{sqn}' not found")
|
|
1271
1336
|
|
|
1272
|
-
db_instance = model_copy_from(db_instance, clone_changing_model_instance(model, instance),
|
|
1337
|
+
db_instance = model_copy_from(db_instance, clone_changing_model_instance(model, instance), exclude_unset=True)
|
|
1273
1338
|
db_instance.updated_at = updated_at
|
|
1274
1339
|
clone_changing_model_instance(model, db_instance, validate_only=True)
|
|
1275
1340
|
db.flush()
|
|
@@ -1425,7 +1490,14 @@ def db_update_snapshot_model[SnapshotModelT: SnapshotModelMixin](
|
|
|
1425
1490
|
record_sqn: int,
|
|
1426
1491
|
updated_at: datetime.datetime,
|
|
1427
1492
|
) -> SnapshotModelT:
|
|
1428
|
-
|
|
1493
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1494
|
+
db_instance = (
|
|
1495
|
+
db
|
|
1496
|
+
.query(model)
|
|
1497
|
+
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1498
|
+
.populate_existing()
|
|
1499
|
+
.one_or_none()
|
|
1500
|
+
)
|
|
1429
1501
|
if db_instance is None:
|
|
1430
1502
|
raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
|
|
1431
1503
|
|
|
@@ -1450,10 +1522,12 @@ def db_expire_snapshot_model[SnapshotModelT: SnapshotModelMixin](
|
|
|
1450
1522
|
record_sqn: int,
|
|
1451
1523
|
updated_at: datetime.datetime,
|
|
1452
1524
|
) -> SnapshotModelT:
|
|
1525
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1453
1526
|
db_instance = (
|
|
1454
1527
|
db
|
|
1455
1528
|
.query(model)
|
|
1456
1529
|
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1530
|
+
.populate_existing()
|
|
1457
1531
|
.one_or_none()
|
|
1458
1532
|
)
|
|
1459
1533
|
if db_instance is None:
|
|
@@ -1472,16 +1546,24 @@ def db_activate_snapshot_model[SnapshotModelT: SnapshotModelMixin](
|
|
|
1472
1546
|
record_sqn: int,
|
|
1473
1547
|
updated_at: datetime.datetime,
|
|
1474
1548
|
) -> SnapshotModelT:
|
|
1475
|
-
|
|
1549
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1550
|
+
db_instance = (
|
|
1551
|
+
db
|
|
1552
|
+
.query(model)
|
|
1553
|
+
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1554
|
+
.populate_existing()
|
|
1555
|
+
.one_or_none()
|
|
1556
|
+
)
|
|
1476
1557
|
if db_instance is not None:
|
|
1477
1558
|
raise sa_exc.MultipleResultsFound(
|
|
1478
|
-
f"
|
|
1559
|
+
f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' already exists")
|
|
1479
1560
|
|
|
1480
1561
|
db_instance = (
|
|
1481
1562
|
db
|
|
1482
1563
|
.query(model)
|
|
1483
1564
|
.where(model.record_sqn == record_sqn, model.expired_at.is_not(None))
|
|
1484
|
-
.order_by(model.created_at.desc())
|
|
1565
|
+
.order_by(model.created_at.desc(), model.sqn.desc())
|
|
1566
|
+
.populate_existing()
|
|
1485
1567
|
.first()
|
|
1486
1568
|
)
|
|
1487
1569
|
if db_instance is None:
|
|
@@ -1653,7 +1735,14 @@ def db_update_revision_model[RevisionModelT: RevisionModelMixin](
|
|
|
1653
1735
|
record_sqn: int,
|
|
1654
1736
|
updated_at: datetime.datetime,
|
|
1655
1737
|
) -> RevisionModelT:
|
|
1656
|
-
|
|
1738
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1739
|
+
db_instance = (
|
|
1740
|
+
db
|
|
1741
|
+
.query(model)
|
|
1742
|
+
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1743
|
+
.populate_existing()
|
|
1744
|
+
.one_or_none()
|
|
1745
|
+
)
|
|
1657
1746
|
if db_instance is None:
|
|
1658
1747
|
raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
|
|
1659
1748
|
|
|
@@ -1680,10 +1769,12 @@ def db_expire_revision_model[RevisionModelT: RevisionModelMixin](
|
|
|
1680
1769
|
record_sqn: int,
|
|
1681
1770
|
updated_at: datetime.datetime,
|
|
1682
1771
|
) -> RevisionModelT:
|
|
1772
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1683
1773
|
db_instance = (
|
|
1684
1774
|
db
|
|
1685
1775
|
.query(model)
|
|
1686
1776
|
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1777
|
+
.populate_existing()
|
|
1687
1778
|
.one_or_none()
|
|
1688
1779
|
)
|
|
1689
1780
|
if db_instance is None:
|
|
@@ -1702,7 +1793,14 @@ def db_activate_revision_model[RevisionModelT: RevisionModelMixin](
|
|
|
1702
1793
|
record_sqn: int,
|
|
1703
1794
|
updated_at: datetime.datetime,
|
|
1704
1795
|
) -> RevisionModelT:
|
|
1705
|
-
|
|
1796
|
+
db_lock_model_record(db, model, record_sqn)
|
|
1797
|
+
db_instance = (
|
|
1798
|
+
db
|
|
1799
|
+
.query(model)
|
|
1800
|
+
.where(model.record_sqn == record_sqn, model.expired_at.is_(None))
|
|
1801
|
+
.populate_existing()
|
|
1802
|
+
.one_or_none()
|
|
1803
|
+
)
|
|
1706
1804
|
if db_instance is not None:
|
|
1707
1805
|
raise sa_exc.MultipleResultsFound(
|
|
1708
1806
|
f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' already exists")
|
|
@@ -1712,6 +1810,7 @@ def db_activate_revision_model[RevisionModelT: RevisionModelMixin](
|
|
|
1712
1810
|
.query(model)
|
|
1713
1811
|
.where(model.record_sqn == record_sqn, model.expired_at.is_not(None))
|
|
1714
1812
|
.order_by(model.revision.desc())
|
|
1813
|
+
.populate_existing()
|
|
1715
1814
|
.first()
|
|
1716
1815
|
)
|
|
1717
1816
|
if db_instance is None:
|
|
@@ -17,9 +17,17 @@ from sqlmodel import Field, SQLModel
|
|
|
17
17
|
|
|
18
18
|
from plexus.pulse.common.utils.apiutils import managed_db_session
|
|
19
19
|
from plexus.pulse.common.utils.ormutils import (
|
|
20
|
-
changing_model_mixin,
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
changing_model_mixin,
|
|
21
|
+
make_base_model,
|
|
22
|
+
make_changing_model_mixin,
|
|
23
|
+
make_revision_model_mixin,
|
|
24
|
+
make_revision_model_trigger,
|
|
25
|
+
make_sequence_model_mixin,
|
|
26
|
+
make_snapshot_model_mixin,
|
|
27
|
+
make_snapshot_model_trigger,
|
|
28
|
+
model_copy_from,
|
|
29
|
+
revision_model_mixin,
|
|
30
|
+
snapshot_model_mixin,
|
|
23
31
|
)
|
|
24
32
|
from plexus.pulse.common.utils.ormutils import (
|
|
25
33
|
db_activate_revision_model,
|
|
@@ -1266,3 +1274,189 @@ class OrmUtilsTest(unittest.TestCase):
|
|
|
1266
1274
|
for options in ({"include": {"name": True}}, {"by_alias": True}):
|
|
1267
1275
|
with self.subTest(options=options), self.assertRaises(TypeError):
|
|
1268
1276
|
model_copy_from(CopyRecord(), CopyRecord(), **options)
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
@pytest.mark.parametrize(
|
|
1280
|
+
"functions",
|
|
1281
|
+
[
|
|
1282
|
+
(DummySnapshotModel,
|
|
1283
|
+
db_create_snapshot_model,
|
|
1284
|
+
db_update_snapshot_model,
|
|
1285
|
+
db_expire_snapshot_model,
|
|
1286
|
+
db_activate_snapshot_model,
|
|
1287
|
+
),
|
|
1288
|
+
(DummyRevisionModel,
|
|
1289
|
+
db_create_revision_model,
|
|
1290
|
+
db_update_revision_model,
|
|
1291
|
+
db_expire_revision_model,
|
|
1292
|
+
db_activate_revision_model,
|
|
1293
|
+
),
|
|
1294
|
+
],
|
|
1295
|
+
)
|
|
1296
|
+
@pytest.mark.parametrize("operation", ["update", "expire", "activate"])
|
|
1297
|
+
def test_lifecycle_serializes_on_history_row(fixture_postgresql_test, functions, operation):
|
|
1298
|
+
"""An uncommitted transition blocks every other transition, even activation."""
|
|
1299
|
+
info = fixture_postgresql_test.info
|
|
1300
|
+
maker = ConnectionMaker.create(
|
|
1301
|
+
make_scheme(Dialects.postgresql, Drivers.psycopg),
|
|
1302
|
+
host=info.host,
|
|
1303
|
+
port=info.port,
|
|
1304
|
+
username=info.user,
|
|
1305
|
+
database=info.dbname,
|
|
1306
|
+
engine_opts={"connect_args": {"options": "-c timezone=UTC"}},
|
|
1307
|
+
)
|
|
1308
|
+
engine = maker.engine
|
|
1309
|
+
model, create, update, expire, activate = functions
|
|
1310
|
+
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
|
|
1311
|
+
payload = DummyModel(dummy_array=[], dummy_json={})
|
|
1312
|
+
try:
|
|
1313
|
+
DummyBaseModel.metadata.create_all(engine)
|
|
1314
|
+
with maker.make_session() as session, session.begin():
|
|
1315
|
+
record_sqn = create(session, model, payload, now).record_sqn
|
|
1316
|
+
# The first history row is now inactive, but remains the lock anchor.
|
|
1317
|
+
update(session, model, payload, record_sqn, now + datetime.timedelta(hours=1))
|
|
1318
|
+
|
|
1319
|
+
with maker.make_session() as first_session, maker.make_session() as second_session:
|
|
1320
|
+
# Expiration locks the anchor until the caller finishes its transaction.
|
|
1321
|
+
expire(first_session, model, record_sqn, now + datetime.timedelta(days=1))
|
|
1322
|
+
second_session.execute(sa.text("SET LOCAL lock_timeout = '200ms'"))
|
|
1323
|
+
with pytest.raises(sa_exc.OperationalError) as error:
|
|
1324
|
+
if operation == "update":
|
|
1325
|
+
update(second_session, model, payload, record_sqn, now + datetime.timedelta(days=2))
|
|
1326
|
+
elif operation == "expire":
|
|
1327
|
+
expire(second_session, model, record_sqn, now + datetime.timedelta(days=2))
|
|
1328
|
+
else:
|
|
1329
|
+
activate(second_session, model, record_sqn, now + datetime.timedelta(days=2))
|
|
1330
|
+
assert error.value.orig.sqlstate == "55P03" # lock_not_available
|
|
1331
|
+
second_session.rollback()
|
|
1332
|
+
first_session.commit()
|
|
1333
|
+
|
|
1334
|
+
# The anchor still exists with no active version; activation can proceed.
|
|
1335
|
+
active = activate(second_session, model, record_sqn, now + datetime.timedelta(days=2))
|
|
1336
|
+
assert active.expired_at is None
|
|
1337
|
+
second_session.commit()
|
|
1338
|
+
finally:
|
|
1339
|
+
maker.reset_engine()
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def test_revision_update_refreshes_cached_version(fixture_postgresql_test):
|
|
1343
|
+
"""A long-lived session must not overwrite a concurrently expired version."""
|
|
1344
|
+
info = fixture_postgresql_test.info
|
|
1345
|
+
maker = ConnectionMaker.create(
|
|
1346
|
+
make_scheme(Dialects.postgresql, Drivers.psycopg),
|
|
1347
|
+
host=info.host,
|
|
1348
|
+
port=info.port,
|
|
1349
|
+
username=info.user,
|
|
1350
|
+
database=info.dbname,
|
|
1351
|
+
engine_opts={"connect_args": {"options": "-c timezone=UTC"}},
|
|
1352
|
+
)
|
|
1353
|
+
engine = maker.engine
|
|
1354
|
+
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
|
|
1355
|
+
payload = DummyModel(dummy_array=[], dummy_json={})
|
|
1356
|
+
try:
|
|
1357
|
+
DummyBaseModel.metadata.create_all(engine)
|
|
1358
|
+
with maker.make_session() as session, session.begin():
|
|
1359
|
+
record_sqn = db_create_revision_model(session, DummyRevisionModel, payload, now).record_sqn
|
|
1360
|
+
with maker.make_session() as stale_session, maker.make_session() as writer_session:
|
|
1361
|
+
cached = db_read_active_revision_model_of_record(stale_session, DummyRevisionModel, record_sqn)
|
|
1362
|
+
db_update_revision_model(writer_session,
|
|
1363
|
+
DummyRevisionModel,
|
|
1364
|
+
payload,
|
|
1365
|
+
record_sqn,
|
|
1366
|
+
now + datetime.timedelta(days=1))
|
|
1367
|
+
writer_session.commit()
|
|
1368
|
+
result = db_update_revision_model(stale_session,
|
|
1369
|
+
DummyRevisionModel,
|
|
1370
|
+
payload,
|
|
1371
|
+
record_sqn,
|
|
1372
|
+
now + datetime.timedelta(days=2))
|
|
1373
|
+
assert result.revision == 3
|
|
1374
|
+
assert cached.expired_at == now + datetime.timedelta(days=1)
|
|
1375
|
+
stale_session.commit()
|
|
1376
|
+
finally:
|
|
1377
|
+
maker.reset_engine()
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
NullableUpdateBase = make_base_model()
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
class NullableSequenceUpdateModel(NullableUpdateBase, make_sequence_model_mixin("sqlite"), table=True):
|
|
1384
|
+
name: str | None = None
|
|
1385
|
+
count: int = 0
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
class NullableChangingUpdateModel(NullableUpdateBase, make_changing_model_mixin("sqlite"), table=True):
|
|
1389
|
+
name: str | None = None
|
|
1390
|
+
count: int = 0
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
class NullableUpdateInput(pdt.BaseModel):
|
|
1394
|
+
"""Non-table input, including metadata that the helpers must ignore."""
|
|
1395
|
+
sqn: int | None = None
|
|
1396
|
+
name: str | None = None
|
|
1397
|
+
count: int | None = 0
|
|
1398
|
+
created_at: datetime.datetime | None = None
|
|
1399
|
+
updated_at: datetime.datetime | None = None
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
@pytest.fixture(params=[NullableSequenceUpdateModel, NullableChangingUpdateModel])
|
|
1403
|
+
def nullable_update_session(request):
|
|
1404
|
+
model = request.param
|
|
1405
|
+
engine = sa.create_engine("sqlite://")
|
|
1406
|
+
NullableUpdateBase.metadata.create_all(engine)
|
|
1407
|
+
now = datetime.datetime(2024, 1, 1, tzinfo=datetime.timezone.utc)
|
|
1408
|
+
try:
|
|
1409
|
+
with sa.orm.Session(engine) as session:
|
|
1410
|
+
row = model(name="keep", count=7)
|
|
1411
|
+
if model is NullableChangingUpdateModel:
|
|
1412
|
+
row.created_at = now
|
|
1413
|
+
row.updated_at = now
|
|
1414
|
+
session.add(row)
|
|
1415
|
+
session.flush()
|
|
1416
|
+
sqn = row.sqn
|
|
1417
|
+
|
|
1418
|
+
def update(payload):
|
|
1419
|
+
if model is NullableChangingUpdateModel:
|
|
1420
|
+
return db_update_changing_model(session, model, payload, sqn, now + datetime.timedelta(days=1))
|
|
1421
|
+
return db_update_sequence_model(session, model, payload, sqn)
|
|
1422
|
+
|
|
1423
|
+
yield session, model, sqn, now, update
|
|
1424
|
+
finally:
|
|
1425
|
+
engine.dispose()
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def test_update_explicit_none_clears_nullable_field(nullable_update_session):
|
|
1429
|
+
session, model, sqn, now, update = nullable_update_session
|
|
1430
|
+
supplied_time = now - datetime.timedelta(days=1)
|
|
1431
|
+
result = update(NullableUpdateInput(
|
|
1432
|
+
name=None, count=7, sqn=sqn + 100,
|
|
1433
|
+
created_at=supplied_time, updated_at=supplied_time,
|
|
1434
|
+
))
|
|
1435
|
+
# Reload to verify persistence, not just the Python object's value.
|
|
1436
|
+
session.expire_all()
|
|
1437
|
+
stored = session.get(model, sqn)
|
|
1438
|
+
assert stored is result
|
|
1439
|
+
assert stored.name is None
|
|
1440
|
+
assert stored.count == 7
|
|
1441
|
+
assert stored.sqn == sqn
|
|
1442
|
+
if model is NullableChangingUpdateModel:
|
|
1443
|
+
assert stored.created_at == now
|
|
1444
|
+
assert stored.updated_at == now + datetime.timedelta(days=1)
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def test_update_explicit_default_is_copied(nullable_update_session):
|
|
1448
|
+
session, model, sqn, _, update = nullable_update_session
|
|
1449
|
+
update(NullableUpdateInput(name="changed", count=0))
|
|
1450
|
+
session.expire_all()
|
|
1451
|
+
stored = session.get(model, sqn)
|
|
1452
|
+
assert stored.name == "changed"
|
|
1453
|
+
assert stored.count == 0
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def test_update_none_rejected_for_non_nullable_field(nullable_update_session):
|
|
1457
|
+
session, model, sqn, _, update = nullable_update_session
|
|
1458
|
+
with pytest.raises(pdt.ValidationError):
|
|
1459
|
+
update(NullableUpdateInput(name="changed", count=None))
|
|
1460
|
+
stored = session.get(model, sqn)
|
|
1461
|
+
assert stored.name == "keep"
|
|
1462
|
+
assert stored.count == 7
|
|
@@ -43,8 +43,9 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
43
43
|
result = list(s3_list_objects(client, "dummy-bucket", "dummy_prefix/"))
|
|
44
44
|
|
|
45
45
|
for key, text in data:
|
|
46
|
-
self.
|
|
47
|
-
|
|
46
|
+
with self.subTest(progressed_client=False, key=key):
|
|
47
|
+
self.assertTrue(any(o.key == key for o in result))
|
|
48
|
+
self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
|
|
48
49
|
|
|
49
50
|
with moto.mock_aws(), s3_make_progressed_client() as client:
|
|
50
51
|
client.create_bucket(Bucket="dummy-bucket")
|
|
@@ -61,8 +62,9 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
61
62
|
result = list(s3_list_objects(client, "dummy-bucket", "dummy_prefix/"))
|
|
62
63
|
|
|
63
64
|
for key, text in data:
|
|
64
|
-
self.
|
|
65
|
-
|
|
65
|
+
with self.subTest(progressed_client=True, key=key):
|
|
66
|
+
self.assertTrue(any(o.key == key for o in result))
|
|
67
|
+
self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
|
|
66
68
|
|
|
67
69
|
def test_s3_list_files__random_text_files(self):
|
|
68
70
|
with moto.mock_aws(), s3_make_client() as client:
|
|
@@ -80,8 +82,9 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
80
82
|
result = list(s3_list_files(client, "dummy-bucket", "dummy_prefix/"))
|
|
81
83
|
|
|
82
84
|
for key, text in data:
|
|
83
|
-
self.
|
|
84
|
-
|
|
85
|
+
with self.subTest(progressed_client=False, key=key):
|
|
86
|
+
self.assertTrue(any(o.key == key for o in result))
|
|
87
|
+
self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
|
|
85
88
|
|
|
86
89
|
with moto.mock_aws(), s3_make_progressed_client() as client:
|
|
87
90
|
client.create_bucket(Bucket="dummy-bucket")
|
|
@@ -98,8 +101,9 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
98
101
|
result = list(s3_list_files(client, "dummy-bucket", "dummy_prefix/"))
|
|
99
102
|
|
|
100
103
|
for key, text in data:
|
|
101
|
-
self.
|
|
102
|
-
|
|
104
|
+
with self.subTest(progressed_client=True, key=key):
|
|
105
|
+
self.assertTrue(any(o.key == key for o in result))
|
|
106
|
+
self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
|
|
103
107
|
|
|
104
108
|
data_s3_list_files = [
|
|
105
109
|
(
|
|
@@ -498,14 +502,15 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
498
502
|
["s3utils_archive/archive.uncompressed.zip", "s3utils_archive/archive.compressed.zip"],
|
|
499
503
|
[local_members, None],
|
|
500
504
|
):
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
+
with self.subTest(archive_key=archive_key, members=members):
|
|
506
|
+
archive_size, member_zip_infos, missed_members = s3_archive_list_files(client,
|
|
507
|
+
"dummy-bucket",
|
|
508
|
+
archive_key,
|
|
509
|
+
members)
|
|
505
510
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
self.assertEqual(archive_size - sum(info.compress_size for info in member_zip_infos), 2470)
|
|
512
|
+
self.assertEqual(set(info.filename for info in member_zip_infos), set(local_members))
|
|
513
|
+
self.assertEqual(len(missed_members), 0)
|
|
509
514
|
|
|
510
515
|
for archive_key, (members, members_expect) in itertools.product(
|
|
511
516
|
["s3utils_archive/archive.uncompressed.zip", "s3utils_archive/archive.compressed.zip"],
|
|
@@ -533,13 +538,14 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
533
538
|
),
|
|
534
539
|
],
|
|
535
540
|
):
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
541
|
+
with self.subTest(archive_key=archive_key, members=members):
|
|
542
|
+
archive_size, member_zip_infos, missed_members = s3_archive_list_files(client,
|
|
543
|
+
"dummy-bucket",
|
|
544
|
+
archive_key,
|
|
545
|
+
members)
|
|
540
546
|
|
|
541
|
-
|
|
542
|
-
|
|
547
|
+
self.assertEqual(sorted(list(info.filename for info in member_zip_infos)), sorted(members_expect))
|
|
548
|
+
self.assertEqual(len(missed_members), 0)
|
|
543
549
|
|
|
544
550
|
def test_s3_archive_open_member(self):
|
|
545
551
|
with (
|
|
@@ -562,6 +568,7 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
562
568
|
):
|
|
563
569
|
for local_member in local_members:
|
|
564
570
|
with (
|
|
571
|
+
self.subTest(archive_key=archive_key, mode=mode, member=local_member),
|
|
565
572
|
open(local_root / local_member, mode) as local_fh,
|
|
566
573
|
s3_archive_open_member(client, "dummy-bucket", archive_key, local_member, mode) as s3_fh,
|
|
567
574
|
):
|
|
@@ -589,14 +596,25 @@ class S3UtilsTest(unittest.TestCase):
|
|
|
589
596
|
[False, True, s3_archive_use_ranged_requests()],
|
|
590
597
|
[False, True, s3_archive_use_chunked_reads()],
|
|
591
598
|
):
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
members,
|
|
597
|
-
mode,
|
|
599
|
+
with self.subTest(
|
|
600
|
+
archive_key=archive_key,
|
|
601
|
+
members=members,
|
|
602
|
+
mode=mode,
|
|
598
603
|
use_ranged_requests=use_ranged_requests,
|
|
599
604
|
use_chunked_reads=use_chunked_reads,
|
|
600
605
|
):
|
|
601
|
-
|
|
602
|
-
|
|
606
|
+
for member, opener in s3_archive_open_members(
|
|
607
|
+
client,
|
|
608
|
+
"dummy-bucket",
|
|
609
|
+
archive_key,
|
|
610
|
+
members,
|
|
611
|
+
mode,
|
|
612
|
+
use_ranged_requests=use_ranged_requests,
|
|
613
|
+
use_chunked_reads=use_chunked_reads,
|
|
614
|
+
):
|
|
615
|
+
with (
|
|
616
|
+
self.subTest(member=member),
|
|
617
|
+
open(local_root / member, mode) as local_fh,
|
|
618
|
+
opener() as s3_fh,
|
|
619
|
+
):
|
|
620
|
+
self.assertEqual(local_fh.read(), s3_fh.read())
|
|
File without changes
|
{plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.github/workflows/pr.yml
RENAMED
|
File without changes
|
{plexus_python_pulse_common-1.1.85 → plexus_python_pulse_common-1.1.87}/.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|