plexus-python-common 1.0.47__tar.gz → 1.0.49__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 (93) hide show
  1. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/PKG-INFO +1 -1
  2. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/ormutils.py +9 -9
  3. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/s3utils.py +1 -1
  4. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/tagutils.py +95 -27
  5. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/PKG-INFO +1 -1
  6. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/ormutils_test.py +83 -0
  7. plexus_python_common-1.0.49/test/plexus_tests/common/utils/tagutils_test.py +104 -0
  8. plexus_python_common-1.0.47/test/plexus_tests/common/utils/tagutils_test.py +0 -94
  9. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/.editorconfig +0 -0
  10. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/.github/workflows/pr.yml +0 -0
  11. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/.github/workflows/push.yml +0 -0
  12. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/.gitignore +0 -0
  13. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/MANIFEST.in +0 -0
  14. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/README.md +0 -0
  15. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/VERSION +0 -0
  16. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/pyproject.toml +0 -0
  17. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  18. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  19. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  20. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/0-dummy +0 -0
  21. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/1-dummy +0 -0
  22. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/2-dummy +0 -0
  23. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
  24. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
  25. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
  26. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
  27. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
  28. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
  29. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
  30. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
  31. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
  32. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
  33. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
  34. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
  35. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/pathutils/dummy.txt +0 -0
  36. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  37. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  38. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  39. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  40. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  41. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  42. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  43. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  44. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  45. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  46. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  47. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  48. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/setup.cfg +0 -0
  49. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/setup.py +0 -0
  50. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/__init__.py +0 -0
  51. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/carto/OSMFile.py +0 -0
  52. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/carto/OSMNode.py +0 -0
  53. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/carto/OSMTags.py +0 -0
  54. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/carto/OSMWay.py +0 -0
  55. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/carto/__init__.py +0 -0
  56. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/pose.py +0 -0
  57. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/proj.py +0 -0
  58. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/resources/__init__.py +0 -0
  59. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/resources/tags/__init__.py +0 -0
  60. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/resources/tags/universal.tagset.yaml +0 -0
  61. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/__init__.py +0 -0
  62. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/apiutils.py +0 -0
  63. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/bagutils.py +0 -0
  64. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/config.py +0 -0
  65. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/datautils.py +0 -0
  66. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/dockerutils.py +0 -0
  67. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/jsonutils.py +0 -0
  68. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/pathutils.py +0 -0
  69. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/sqlutils.py +0 -0
  70. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/strutils.py +0 -0
  71. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus/common/utils/testutils.py +0 -0
  72. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/SOURCES.txt +0 -0
  73. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  74. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  75. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/requires.txt +0 -0
  76. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  77. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/__init__.py +0 -0
  78. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/__init__.py +0 -0
  79. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/carto/__init__.py +0 -0
  80. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
  81. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
  82. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/pose_test.py +0 -0
  83. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/proj_test.py +0 -0
  84. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/__init__.py +0 -0
  85. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
  86. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/datautils_test.py +0 -0
  87. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/dockerutils_test.py +0 -0
  88. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
  89. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/pathutils_test.py +0 -0
  90. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/s3utils_test.py +0 -0
  91. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/strutils_test.py +0 -0
  92. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/plexus_tests/common/utils/testutils_test.py +0 -0
  93. {plexus_python_common-1.0.47 → plexus_python_common-1.0.49}/test/testenv.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.47
3
+ Version: 1.0.49
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -1146,7 +1146,7 @@ def db_read_active_snapshot_model_of_record[SnapshotModelT: SnapshotModelMixin](
1146
1146
  ) -> SnapshotModelT:
1147
1147
  db_instance = db.query(model).where(model.record_sqn == record_sqn, model.expired_at.is_(None)).one_or_none()
1148
1148
  if db_instance is None:
1149
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1149
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1150
1150
 
1151
1151
  return db_instance
1152
1152
 
@@ -1218,7 +1218,7 @@ def db_update_snapshot_model[SnapshotModelT: SnapshotModelMixin](
1218
1218
  ) -> SnapshotModelT:
1219
1219
  db_instance = db.query(model).where(model.record_sqn == record_sqn, model.expired_at.is_(None)).one_or_none()
1220
1220
  if db_instance is None:
1221
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1221
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1222
1222
 
1223
1223
  db_instance.expired_at = updated_at
1224
1224
  db_instance = clone_snapshot_model_instance(model, db_instance, clear_meta_fields=False, inplace=True)
@@ -1247,7 +1247,7 @@ def db_expire_snapshot_model[SnapshotModelT: SnapshotModelMixin](
1247
1247
  .one_or_none()
1248
1248
  )
1249
1249
  if db_instance is None:
1250
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1250
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1251
1251
 
1252
1252
  db_instance.expired_at = updated_at
1253
1253
  db_instance = clone_snapshot_model_instance(model, db_instance, clear_meta_fields=False, inplace=True)
@@ -1275,7 +1275,7 @@ def db_activate_snapshot_model[SnapshotModelT: SnapshotModelMixin](
1275
1275
  .first()
1276
1276
  )
1277
1277
  if db_instance is None:
1278
- raise sa_exc.NoResultFound(f"Expired '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1278
+ raise sa_exc.NoResultFound(f"expired '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1279
1279
 
1280
1280
  db_new_instance = clone_snapshot_model_instance(model, db_instance)
1281
1281
  db_new_instance.record_sqn = record_sqn
@@ -1371,7 +1371,7 @@ def db_read_active_revision_model_of_record[RevisionModelT: RevisionModelMixin](
1371
1371
  ) -> RevisionModelT:
1372
1372
  db_instance = db.query(model).where(model.record_sqn == record_sqn, model.expired_at.is_(None)).one_or_none()
1373
1373
  if db_instance is None:
1374
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1374
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1375
1375
 
1376
1376
  return db_instance
1377
1377
 
@@ -1443,7 +1443,7 @@ def db_update_revision_model[RevisionModelT: RevisionModelMixin](
1443
1443
  ) -> RevisionModelT:
1444
1444
  db_instance = db.query(model).where(model.record_sqn == record_sqn, model.expired_at.is_(None)).one_or_none()
1445
1445
  if db_instance is None:
1446
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1446
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1447
1447
 
1448
1448
  db_instance.expired_at = updated_at
1449
1449
  db_instance = clone_revision_model_instance(model, db_instance, clear_meta_fields=False, inplace=True)
@@ -1474,7 +1474,7 @@ def db_expire_revision_model[RevisionModelT: RevisionModelMixin](
1474
1474
  .one_or_none()
1475
1475
  )
1476
1476
  if db_instance is None:
1477
- raise sa_exc.NoResultFound(f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1477
+ raise sa_exc.NoResultFound(f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1478
1478
 
1479
1479
  db_instance.expired_at = updated_at
1480
1480
  db_instance = clone_revision_model_instance(model, db_instance, clear_meta_fields=False, inplace=True)
@@ -1492,7 +1492,7 @@ def db_activate_revision_model[RevisionModelT: RevisionModelMixin](
1492
1492
  db_instance = db.query(model).where(model.record_sqn == record_sqn, model.expired_at.is_(None)).one_or_none()
1493
1493
  if db_instance is not None:
1494
1494
  raise sa_exc.MultipleResultsFound(
1495
- f"Active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' already exists")
1495
+ f"active '{model_name_of(model)}' of specified record_sqn '{record_sqn}' already exists")
1496
1496
 
1497
1497
  db_instance = (
1498
1498
  db
@@ -1502,7 +1502,7 @@ def db_activate_revision_model[RevisionModelT: RevisionModelMixin](
1502
1502
  .first()
1503
1503
  )
1504
1504
  if db_instance is None:
1505
- raise sa_exc.NoResultFound(f"Expired '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1505
+ raise sa_exc.NoResultFound(f"expired '{model_name_of(model)}' of specified record_sqn '{record_sqn}' not found")
1506
1506
 
1507
1507
  db_new_instance = clone_revision_model_instance(model, db_instance)
1508
1508
  db_new_instance.record_sqn = record_sqn
@@ -843,7 +843,7 @@ def s3_archive_open_members(
843
843
 
844
844
  archive_size, member_zip_infos, missed_members = s3_archive_list_files(client, bucket, key, members)
845
845
  if missed_members:
846
- raise FileNotFoundError(f"Archive members not found: {', '.join(missed_members)}")
846
+ raise FileNotFoundError(f"archive members not found: {', '.join(missed_members)}")
847
847
 
848
848
  if callable(use_ranged_requests):
849
849
  use_ranged_requests = use_ranged_requests(archive_size, member_zip_infos)
@@ -36,6 +36,7 @@ __all__ = [
36
36
  "RichDesc",
37
37
  "Tag",
38
38
  "Tagset",
39
+ "MutableTagset",
39
40
  "populate_tagset",
40
41
  "predefined_tagsets",
41
42
  "render_tagset_markdown_readme",
@@ -132,8 +133,13 @@ class Tag(object):
132
133
  def parent_tag_name(self) -> str | None:
133
134
  return head_or_none(self.name.rsplit(":", 1))
134
135
 
135
- def populate(self, vehicle_name: str, begin_dt: datetime.datetime, end_dt: datetime.datetime,
136
- props: JsonType) -> TagRecord:
136
+ def populate(
137
+ self,
138
+ vehicle_name: str,
139
+ begin_dt: datetime.datetime,
140
+ end_dt: datetime.datetime,
141
+ props: JsonType,
142
+ ) -> TagRecord:
137
143
  return TagRecord(
138
144
  vehicle_name=vehicle_name,
139
145
  begin_dt=begin_dt,
@@ -175,11 +181,49 @@ class Tagset(Sequence[Tag], Mapping[str, Tag]):
175
181
  tag_name = item.name if isinstance(item, Tag) else item
176
182
  return self.tags_dict.get(tag_name)
177
183
 
184
+ def clone(self) -> Self:
185
+ return clone_tagset(self)
186
+
187
+ def mutable(self):
188
+ return clone_mutable_tagset(self)
189
+
190
+ def frozen(self):
191
+ return self.clone()
192
+
178
193
  @property
179
194
  def tag_names(self) -> list[str]:
180
195
  return list(self.tags_dict.keys())
181
196
 
182
- def add(self, tag: Tag) -> None:
197
+ def child_tags(self, parent: str | Tag) -> list[Tag]:
198
+ if parent is None:
199
+ return []
200
+ if isinstance(parent, str):
201
+ return self.child_tags(self.get(parent))
202
+
203
+ subtree = dicttree_subtree(self.tags_tree, parent.tag_parts)
204
+ return list(dicttree_children(subtree)) if subtree else []
205
+
206
+ def parent_tags(self, child: str | Tag) -> list[Tag]:
207
+ if child is None:
208
+ return []
209
+ if isinstance(child, str):
210
+ return self.parent_tags(self.get(child))
211
+
212
+ return list(dicttree_lineage(self.tags_tree, child.tag_parts[:-1]))
213
+
214
+
215
+ class MutableTagset(Tagset):
216
+
217
+ def clone(self) -> Self:
218
+ return clone_mutable_tagset(self)
219
+
220
+ def mutable(self):
221
+ return self.clone()
222
+
223
+ def frozen(self):
224
+ return clone_tagset(self)
225
+
226
+ def add(self, tag: Tag) -> Self:
183
227
  if tag.name in self.tags_dict:
184
228
  raise ValueError(f"duplicate tag name '{tag.name}'")
185
229
 
@@ -188,7 +232,9 @@ class Tagset(Sequence[Tag], Mapping[str, Tag]):
188
232
 
189
233
  dicttree_add(self.tags_tree, tag.tag_parts, tag, create_prefix=True)
190
234
 
191
- def remove(self, tag_name: str) -> None:
235
+ return self
236
+
237
+ def remove(self, tag_name: str) -> Self:
192
238
  tag = self.get(tag_name)
193
239
  if tag is None:
194
240
  raise ValueError(f"tag '{tag_name}' not found")
@@ -198,22 +244,23 @@ class Tagset(Sequence[Tag], Mapping[str, Tag]):
198
244
 
199
245
  dicttree_remove(self.tags_tree, tag.tag_parts, recursive=True)
200
246
 
201
- def child_tags(self, parent: str | Tag) -> list[Tag]:
202
- if parent is None:
203
- return []
204
- if isinstance(parent, str):
205
- return self.child_tags(self.get(parent))
247
+ return self
206
248
 
207
- subtree = dicttree_subtree(self.tags_tree, parent.tag_parts)
208
- return list(dicttree_children(subtree)) if subtree else []
209
249
 
210
- def parent_tags(self, child: str | Tag) -> list[Tag]:
211
- if child is None:
212
- return []
213
- if isinstance(child, str):
214
- return self.parent_tags(self.get(child))
250
+ def clone_tagset(tagset: Tagset) -> Tagset:
251
+ tagset = clone_mutable_tagset(tagset)
252
+ new_tagset = Tagset(namespace=tagset.namespace, desc=tagset.desc)
253
+ new_tagset.tags = tagset.tags
254
+ new_tagset.tags_dict = tagset.tags_dict
255
+ new_tagset.tags_tree = tagset.tags_tree
256
+ return new_tagset
215
257
 
216
- return list(dicttree_lineage(self.tags_tree, child.tag_parts[:-1]))
258
+
259
+ def clone_mutable_tagset(tagset: Tagset) -> MutableTagset:
260
+ new_tagset = MutableTagset(namespace=tagset.namespace, desc=tagset.desc)
261
+ for tag in tagset.tags:
262
+ new_tagset.add(tag)
263
+ return new_tagset
217
264
 
218
265
 
219
266
  def populate_tagset(tagset_spec: JsonObject) -> Tagset:
@@ -268,12 +315,12 @@ def populate_tagset(tagset_spec: JsonObject) -> Tagset:
268
315
  if tags is None:
269
316
  raise ValueError("missing '$tags' in tagset spec")
270
317
 
271
- tagset = Tagset(namespace=namespace, desc=desc)
318
+ tagset = MutableTagset(namespace=namespace, desc=desc)
272
319
 
273
320
  for tag in validate_and_collect("", tags):
274
321
  tagset.add(tag)
275
322
 
276
- return tagset
323
+ return tagset.frozen()
277
324
 
278
325
 
279
326
  @singleton
@@ -330,25 +377,34 @@ def tag_cache_file_path() -> pathlib.Path:
330
377
 
331
378
 
332
379
  class TagCache(object):
333
- def __init__(self, *, file_path: str | os.PathLike[str] | None = None, fail_if_exists: bool = False):
380
+ def __init__(
381
+ self,
382
+ *,
383
+ file_path: str | os.PathLike[str] | None = None,
384
+ fail_if_exists: bool = False,
385
+ ):
334
386
  self.file_path = pathlib.Path(file_path or tag_cache_file_path())
335
387
  if fail_if_exists and self.file_path.exists():
336
388
  raise FileExistsError(f"tag cache file '{str(self.file_path)}' already exists")
337
389
  self.file_path.parent.mkdir(parents=True, exist_ok=True)
390
+
338
391
  self.conn_maker = ConnectionMaker.from_url(f"sqlite:///{str(self.file_path.absolute())}",
339
392
  engine_opts=dict(connect_args={"check_same_thread": False}))
340
393
  BaseModel.metadata.create_all(self.conn_maker.engine)
341
394
 
342
395
  def query(
343
396
  self,
344
- vehicle_name: str,
397
+ vehicle_name: str | None = None,
345
398
  begin_time: datetime.datetime | None = None,
346
399
  end_time: datetime.datetime | None = None,
347
400
  tag_pattern: str | None = None,
348
- ) -> list[TagRecordTable]:
401
+ *,
402
+ tagsets: Sequence[Tagset] | None = None,
403
+ ) -> Generator[TagRecordTable]:
349
404
  with self.conn_maker.make_session() as session:
350
- query = session.query(TagRecordTable).where(TagRecordTable.vehicle_name == vehicle_name)
351
-
405
+ query = session.query(TagRecordTable)
406
+ if vehicle_name:
407
+ query = query.filter(TagRecordTable.vehicle_name == vehicle_name)
352
408
  if begin_time:
353
409
  query = query.filter(TagRecordTable.end_dt >= begin_time)
354
410
  if end_time:
@@ -356,7 +412,15 @@ class TagCache(object):
356
412
  if tag_pattern:
357
413
  query = query.filter(TagRecordTable.tag.like(f"{escape_sql_like(tag_pattern)}%", escape="\\"))
358
414
 
359
- return query.all()
415
+ if not tagsets:
416
+ yield from query.all()
417
+ else:
418
+ yield from (db_tag_record for db_tag_record in query.all()
419
+ if any(db_tag_record.tag in tagset for tagset in tagsets))
420
+
421
+ def undefined(self, tagsets: Sequence[Tagset]) -> Generator[TagRecordTable, None, None]:
422
+ yield from (db_tag_record for db_tag_record in self.query()
423
+ if all(db_tag_record.tag not in tagset for tagset in tagsets))
360
424
 
361
425
  def add(
362
426
  self,
@@ -365,7 +429,7 @@ class TagCache(object):
365
429
  end_time: datetime.datetime,
366
430
  tag: str | Tag,
367
431
  props: JsonType | None = None,
368
- ):
432
+ ) -> Self:
369
433
  with self.conn_maker.make_session() as session:
370
434
  tag_record = TagRecord(
371
435
  vehicle_name=vehicle_name,
@@ -377,7 +441,9 @@ class TagCache(object):
377
441
  session.add(clone_sequence_model_instance(TagRecordTable, tag_record))
378
442
  session.commit()
379
443
 
380
- def remove(self, vehicle_name: str, begin_time: datetime.datetime, end_time: datetime.datetime):
444
+ return self
445
+
446
+ def remove(self, vehicle_name: str, begin_time: datetime.datetime, end_time: datetime.datetime) -> Self:
381
447
  with self.conn_maker.make_session() as session:
382
448
  session.execute(
383
449
  sa
@@ -390,6 +456,8 @@ class TagCache(object):
390
456
  )
391
457
  session.commit()
392
458
 
459
+ return self
460
+
393
461
  def clear(self):
394
462
  with self.conn_maker.make_session() as session:
395
463
  session.execute(sa.delete(TagRecordTable))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.47
3
+ Version: 1.0.49
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -30,9 +30,13 @@ from plexus.common.utils.ormutils import (
30
30
  db_activate_revision_model,
31
31
  db_activate_snapshot_model,
32
32
  db_create_changing_model,
33
+ db_create_changing_models,
33
34
  db_create_revision_model,
35
+ db_create_revision_models,
34
36
  db_create_sequence_model,
37
+ db_create_sequence_models,
35
38
  db_create_snapshot_model,
39
+ db_create_snapshot_models,
36
40
  db_delete_sequence_model,
37
41
  db_expire_revision_model,
38
42
  db_expire_snapshot_model,
@@ -223,6 +227,21 @@ def test_db_sequence_model_crud(fixture_postgresql_test_proc, fixture_postgresql
223
227
  results = db_read_sequence_models(session, DummySequenceModel, 0, 200)
224
228
  assert len(results) == 0
225
229
 
230
+ create_records = [random_record() for _ in range(0, 100)]
231
+
232
+ results = db_create_sequence_models(session, DummySequenceModel, create_records)
233
+ assert len(results) == len(create_records)
234
+
235
+ for i, result in zip(range(0, 100), results):
236
+ assert result.sqn == i + 101
237
+ assert result.dummy_uuid == create_records[i].dummy_uuid
238
+ assert result.dummy_int == create_records[i].dummy_int
239
+ assert result.dummy_str == create_records[i].dummy_str
240
+ assert result.dummy_float == create_records[i].dummy_float
241
+ assert result.dummy_bool == create_records[i].dummy_bool
242
+ assert result.dummy_array == create_records[i].dummy_array
243
+ assert result.dummy_json == create_records[i].dummy_json
244
+
226
245
 
227
246
  def test_db_changing_model_crud(fixture_postgresql_test_proc, fixture_postgresql_test):
228
247
  scheme = make_scheme(Dialects.postgresql, Drivers.psycopg)
@@ -359,6 +378,26 @@ def test_db_changing_model_crud(fixture_postgresql_test_proc, fixture_postgresql
359
378
  results = db_read_sequence_models(session, DummyChangingModel, 0, 200)
360
379
  assert len(results) == 0
361
380
 
381
+ create_records = [random_record() for _ in range(0, 100)]
382
+
383
+ results = db_create_changing_models(session,
384
+ DummyChangingModel,
385
+ create_records,
386
+ dt_parse_iso("2024-01-01T00:00:00+00:00"))
387
+ assert len(results) == len(create_records)
388
+
389
+ for i, result in zip(range(0, 100), results):
390
+ assert result.sqn == i + 101
391
+ assert result.created_at == dt_parse_iso("2024-01-01T00:00:00+00:00")
392
+ assert result.updated_at == dt_parse_iso("2024-01-01T00:00:00+00:00")
393
+ assert result.dummy_uuid == create_records[i].dummy_uuid
394
+ assert result.dummy_int == create_records[i].dummy_int
395
+ assert result.dummy_str == create_records[i].dummy_str
396
+ assert result.dummy_float == create_records[i].dummy_float
397
+ assert result.dummy_bool == create_records[i].dummy_bool
398
+ assert result.dummy_array == create_records[i].dummy_array
399
+ assert result.dummy_json == create_records[i].dummy_json
400
+
362
401
 
363
402
  def test_db_snapshot_model_crud(fixture_postgresql_test_proc, fixture_postgresql_test):
364
403
  scheme = make_scheme(Dialects.postgresql, Drivers.psycopg)
@@ -644,6 +683,27 @@ def test_db_snapshot_model_crud(fixture_postgresql_test_proc, fixture_postgresql
644
683
  assert result.dummy_array == update_records[i].dummy_array
645
684
  assert result.dummy_json == update_records[i].dummy_json
646
685
 
686
+ create_records = [random_record() for _ in range(0, 100)]
687
+
688
+ results = db_create_snapshot_models(session,
689
+ DummySnapshotModel,
690
+ create_records,
691
+ dt_parse_iso("2024-01-01T00:00:00+00:00"))
692
+ assert len(results) == len(create_records)
693
+
694
+ for i, result in zip(range(0, 100), results):
695
+ assert result.sqn == i + 301
696
+ assert result.record_sqn == i + 301
697
+ assert result.created_at == dt_parse_iso("2024-01-01T00:00:00+00:00")
698
+ assert result.expired_at is None
699
+ assert result.dummy_uuid == create_records[i].dummy_uuid
700
+ assert result.dummy_int == create_records[i].dummy_int
701
+ assert result.dummy_str == create_records[i].dummy_str
702
+ assert result.dummy_float == create_records[i].dummy_float
703
+ assert result.dummy_bool == create_records[i].dummy_bool
704
+ assert result.dummy_array == create_records[i].dummy_array
705
+ assert result.dummy_json == create_records[i].dummy_json
706
+
647
707
 
648
708
  def test_db_revision_model_crud(fixture_postgresql_test_proc, fixture_postgresql_test):
649
709
  scheme = make_scheme(Dialects.postgresql, Drivers.psycopg)
@@ -957,6 +1017,29 @@ def test_db_revision_model_crud(fixture_postgresql_test_proc, fixture_postgresql
957
1017
  assert result.dummy_array == update_records[i].dummy_array
958
1018
  assert result.dummy_json == update_records[i].dummy_json
959
1019
 
1020
+ create_records = [random_record() for _ in range(0, 100)]
1021
+
1022
+ results = db_create_revision_models(session,
1023
+ DummyRevisionModel,
1024
+ create_records,
1025
+ dt_parse_iso("2024-01-01T00:00:00+00:00"))
1026
+ assert len(results) == len(create_records)
1027
+
1028
+ for i, result in zip(range(0, 100), results):
1029
+ assert result.sqn == i + 301
1030
+ assert result.record_sqn == i + 301
1031
+ assert result.revision == 1
1032
+ assert result.created_at == dt_parse_iso("2024-01-01T00:00:00+00:00")
1033
+ assert result.updated_at == dt_parse_iso("2024-01-01T00:00:00+00:00")
1034
+ assert result.expired_at is None
1035
+ assert result.dummy_uuid == create_records[i].dummy_uuid
1036
+ assert result.dummy_int == create_records[i].dummy_int
1037
+ assert result.dummy_str == create_records[i].dummy_str
1038
+ assert result.dummy_float == create_records[i].dummy_float
1039
+ assert result.dummy_bool == create_records[i].dummy_bool
1040
+ assert result.dummy_array == create_records[i].dummy_array
1041
+ assert result.dummy_json == create_records[i].dummy_json
1042
+
960
1043
 
961
1044
  def test_make_snapshot_model_trigger(fixture_postgresql_test_proc, fixture_postgresql_test):
962
1045
  scheme = make_scheme(Dialects.postgresql, Drivers.psycopg)
@@ -0,0 +1,104 @@
1
+ import datetime
2
+ import unittest
3
+
4
+ import ddt
5
+ from iker.common.utils.dtutils import dt_parse_iso
6
+
7
+ from plexus.common.utils.tagutils import MutableTagset, Tag, TagCache, Tagset
8
+ from plexus.common.utils.tagutils import predefined_tagsets, render_tagset_markdown_readme
9
+ from plexus.common.utils.tagutils import tag_cache_file_path
10
+
11
+
12
+ @ddt.ddt
13
+ class TagUtilsTest(unittest.TestCase):
14
+
15
+ def test_predefined_tagsets(self):
16
+ tagsets = predefined_tagsets()
17
+ for _, tagset in tagsets.items():
18
+ self.assertIsInstance(tagset, Tagset)
19
+ self.assertNotIsInstance(tagset, MutableTagset)
20
+
21
+ for tag in tagset:
22
+ self.assertIn(tag, tagset)
23
+ self.assertIn(tag.name, tagset)
24
+ self.assertEqual(tag, tagset.get(tag))
25
+ self.assertEqual(tag, tagset.get(tag.name))
26
+
27
+ markdown = render_tagset_markdown_readme(tagset)
28
+ self.assertIsInstance(markdown, str)
29
+
30
+ print(markdown)
31
+
32
+ def test_tag_cache(self):
33
+ tag_cache = TagCache()
34
+
35
+ self.assertEqual(tag_cache.file_path, tag_cache_file_path())
36
+ self.assertTrue(tag_cache.file_path.exists())
37
+
38
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle"))), 0)
39
+
40
+ tagset = MutableTagset(namespace="tagset", desc="A dummy tagset for testing")
41
+
42
+ tagset.add(Tag(name="dummy:foo", desc="A dummy tag for testing"))
43
+ tagset.add(Tag(name="dummy:bar", desc="A dummy tag for testing"))
44
+
45
+ tags = [
46
+ "dummy:foo",
47
+ "dummy:bar",
48
+ "dummy:baz",
49
+ "dummy:qux",
50
+ ]
51
+ for i in range(1000):
52
+ tag_cache.add(
53
+ "dummy_vehicle",
54
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00") + datetime.timedelta(seconds=i),
55
+ dt_parse_iso("2021-01-01T00:00:00.000000+00:00") + datetime.timedelta(seconds=i + 1),
56
+ tags[i % len(tags)],
57
+ )
58
+
59
+ self.assertEqual(len(list(tag_cache.query())), 1000)
60
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle"))), 1000)
61
+ self.assertEqual(len(list(tag_cache.query(tagsets=[tagset]))), 500)
62
+ self.assertEqual(
63
+ len(list(tag_cache.query("dummy_vehicle",
64
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
65
+ dt_parse_iso("2020-01-01T00:01:00.000000+00:00")))),
66
+ 61,
67
+ )
68
+ self.assertEqual(
69
+ len(list(tag_cache.query("dummy_vehicle",
70
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
71
+ dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
72
+ tag_pattern="dummy:foo"))),
73
+ 16,
74
+ )
75
+ self.assertEqual(
76
+ len(list(tag_cache.query("dummy_vehicle",
77
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
78
+ dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
79
+ tag_pattern="dummy:bar"))),
80
+ 15,
81
+ )
82
+ self.assertEqual(
83
+ len(list(tag_cache.query("dummy_vehicle",
84
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
85
+ dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
86
+ tag_pattern="dummy"))),
87
+ 61,
88
+ )
89
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle", tag_pattern="dummy:foo"))), 250)
90
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle", tag_pattern="dummy:bar"))), 250)
91
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle", tag_pattern="dummy"))), 1000)
92
+ self.assertEqual(len(list(tag_cache.query("another_dummy_vehicle"))), 0)
93
+
94
+ self.assertEqual(len(list(tag_cache.undefined([tagset]))), 500)
95
+
96
+ tag_cache.remove("dummy_vehicle",
97
+ dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
98
+ dt_parse_iso("2020-01-01T00:01:00.000000+00:00"))
99
+
100
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle"))), 939)
101
+
102
+ tag_cache.clear()
103
+
104
+ self.assertEqual(len(list(tag_cache.query("dummy_vehicle"))), 0)
@@ -1,94 +0,0 @@
1
- import datetime
2
- import unittest
3
-
4
- import ddt
5
- from iker.common.utils.dtutils import dt_parse_iso
6
-
7
- from plexus.common.utils.tagutils import TagCache, Tagset
8
- from plexus.common.utils.tagutils import predefined_tagsets, render_tagset_markdown_readme
9
- from plexus.common.utils.tagutils import tag_cache_file_path
10
-
11
-
12
- @ddt.ddt
13
- class TagUtilsTest(unittest.TestCase):
14
-
15
- def test_predefined_tagsets(self):
16
- tagsets = predefined_tagsets()
17
- for _, tagset in tagsets.items():
18
- self.assertIsInstance(tagset, Tagset)
19
-
20
- for tag in tagset:
21
- self.assertIn(tag, tagset)
22
- self.assertIn(tag.name, tagset)
23
- self.assertEqual(tag, tagset.get(tag))
24
- self.assertEqual(tag, tagset.get(tag.name))
25
-
26
- markdown = render_tagset_markdown_readme(tagset)
27
- self.assertIsInstance(markdown, str)
28
-
29
- print(markdown)
30
-
31
- def test_tag_cache(self):
32
- tag_cache = TagCache()
33
-
34
- self.assertEqual(tag_cache.file_path, tag_cache_file_path())
35
- self.assertTrue(tag_cache.file_path.exists())
36
-
37
- self.assertEqual(len(tag_cache.query("dummy_vehicle")), 0)
38
-
39
- tags = [
40
- "dummy:foo",
41
- "dummy:bar",
42
- "dummy:baz",
43
- "dummy:qux",
44
- ]
45
- for i in range(1000):
46
- tag_cache.add(
47
- "dummy_vehicle",
48
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00") + datetime.timedelta(seconds=i),
49
- dt_parse_iso("2021-01-01T00:00:00.000000+00:00") + datetime.timedelta(seconds=i + 1),
50
- tags[i % len(tags)],
51
- )
52
-
53
- self.assertEqual(len(tag_cache.query("dummy_vehicle")), 1000)
54
- self.assertEqual(
55
- len(tag_cache.query("dummy_vehicle",
56
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
57
- dt_parse_iso("2020-01-01T00:01:00.000000+00:00"))),
58
- 61,
59
- )
60
- self.assertEqual(
61
- len(tag_cache.query("dummy_vehicle",
62
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
63
- dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
64
- tag_pattern="dummy:foo")),
65
- 16,
66
- )
67
- self.assertEqual(
68
- len(tag_cache.query("dummy_vehicle",
69
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
70
- dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
71
- tag_pattern="dummy:bar")),
72
- 15,
73
- )
74
- self.assertEqual(
75
- len(tag_cache.query("dummy_vehicle",
76
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
77
- dt_parse_iso("2020-01-01T00:01:00.000000+00:00"),
78
- tag_pattern="dummy")),
79
- 61,
80
- )
81
- self.assertEqual(len(tag_cache.query("dummy_vehicle", tag_pattern="dummy:foo")), 250)
82
- self.assertEqual(len(tag_cache.query("dummy_vehicle", tag_pattern="dummy:bar")), 250)
83
- self.assertEqual(len(tag_cache.query("dummy_vehicle", tag_pattern="dummy")), 1000)
84
- self.assertEqual(len(tag_cache.query("another_dummy_vehicle")), 0)
85
-
86
- tag_cache.remove("dummy_vehicle",
87
- dt_parse_iso("2020-01-01T00:00:00.000000+00:00"),
88
- dt_parse_iso("2020-01-01T00:01:00.000000+00:00"))
89
-
90
- self.assertEqual(len(tag_cache.query("dummy_vehicle")), 939)
91
-
92
- tag_cache.clear()
93
-
94
- self.assertEqual(len(tag_cache.query("dummy_vehicle")), 0)