plexus-python-common 1.0.53__tar.gz → 1.0.55__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 (92) hide show
  1. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/PKG-INFO +1 -1
  2. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/datautils.py +3 -1
  3. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/ormutils.py +50 -4
  4. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/tagutils.py +87 -75
  5. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/PKG-INFO +1 -1
  6. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/tagutils_test.py +152 -0
  7. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/.editorconfig +0 -0
  8. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/.github/workflows/pr.yml +0 -0
  9. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/.github/workflows/push.yml +0 -0
  10. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/.gitignore +0 -0
  11. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/MANIFEST.in +0 -0
  12. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/README.md +0 -0
  13. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/VERSION +0 -0
  14. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/pyproject.toml +0 -0
  15. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  16. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  17. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  18. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/0-dummy +0 -0
  19. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/1-dummy +0 -0
  20. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/2-dummy +0 -0
  21. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
  22. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
  23. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
  24. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
  25. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
  26. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
  27. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
  28. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
  29. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
  30. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
  31. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
  32. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
  33. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/pathutils/dummy.txt +0 -0
  34. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  35. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  36. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  37. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  38. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  39. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  40. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  41. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  42. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  43. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  44. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  45. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  46. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/setup.cfg +0 -0
  47. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/setup.py +0 -0
  48. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/__init__.py +0 -0
  49. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/carto/OSMFile.py +0 -0
  50. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/carto/OSMNode.py +0 -0
  51. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/carto/OSMTags.py +0 -0
  52. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/carto/OSMWay.py +0 -0
  53. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/carto/__init__.py +0 -0
  54. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/pose.py +0 -0
  55. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/proj.py +0 -0
  56. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/resources/__init__.py +0 -0
  57. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/resources/tags/__init__.py +0 -0
  58. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/resources/tags/universal.tagset.yaml +0 -0
  59. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/__init__.py +0 -0
  60. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/apiutils.py +0 -0
  61. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/bagutils.py +0 -0
  62. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/config.py +0 -0
  63. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/dockerutils.py +0 -0
  64. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/jsonutils.py +0 -0
  65. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/pathutils.py +0 -0
  66. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/s3utils.py +0 -0
  67. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/sqlutils.py +0 -0
  68. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/strutils.py +0 -0
  69. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus/common/utils/testutils.py +0 -0
  70. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/SOURCES.txt +0 -0
  71. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  72. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  73. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/requires.txt +0 -0
  74. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  75. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/__init__.py +0 -0
  76. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/__init__.py +0 -0
  77. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/carto/__init__.py +0 -0
  78. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
  79. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
  80. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/pose_test.py +0 -0
  81. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/proj_test.py +0 -0
  82. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/__init__.py +0 -0
  83. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
  84. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/datautils_test.py +0 -0
  85. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/dockerutils_test.py +0 -0
  86. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
  87. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/ormutils_test.py +0 -0
  88. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/pathutils_test.py +0 -0
  89. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/s3utils_test.py +0 -0
  90. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/strutils_test.py +0 -0
  91. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/test/plexus_tests/common/utils/testutils_test.py +0 -0
  92. {plexus_python_common-1.0.53 → plexus_python_common-1.0.55}/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.53
3
+ Version: 1.0.55
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -118,7 +118,9 @@ validate_vehicle_name = make_validate_parse_string(parse_vehicle_name)
118
118
  validate_bag_name = make_validate_parse_string(parse_bag_name)
119
119
 
120
120
 
121
- def validate_dt_timezone(dt: datetime.datetime):
121
+ def validate_dt_timezone(dt: datetime.datetime, *, allow_naive: bool = False):
122
+ if allow_naive and dt.tzinfo is None:
123
+ return
122
124
  if dt.tzinfo != datetime.timezone.utc:
123
125
  raise ValueError(f"dt '{dt}' is not in UTC")
124
126
 
@@ -1,4 +1,5 @@
1
1
  import datetime
2
+ from collections.abc import Iterable, Sequence
2
3
  from typing import Protocol, Self
3
4
 
4
5
  import pydantic as pdt
@@ -7,7 +8,8 @@ import sqlalchemy.dialects.postgresql as sa_pg
7
8
  import sqlalchemy.dialects.sqlite as sa_sqlite
8
9
  import sqlalchemy.exc as sa_exc
9
10
  import sqlalchemy.orm as sa_orm
10
- from iker.common.utils.dbutils import dialects
11
+ from iker.common.utils.dbutils import dialects, orm_to_dict
12
+ from iker.common.utils.iterutils import head
11
13
  from sqlmodel import Field, SQLModel
12
14
 
13
15
  from plexus.common.utils.datautils import validate_dt_timezone
@@ -17,6 +19,7 @@ __all__ = [
17
19
  "compare_postgresql_types",
18
20
  "compare_sqlite_types",
19
21
  "model_name_of",
22
+ "print_model_results",
20
23
  "validate_model_extended",
21
24
  "collect_model_tables",
22
25
  "model_copy_from",
@@ -137,13 +140,56 @@ def compare_sqlite_types(type_a, type_b) -> bool:
137
140
  }
138
141
 
139
142
 
140
- def model_name_of(model: type[SQLModel], fallback_classname: bool = True) -> str | None:
143
+ def model_name_of(model: type[SQLModel] | SQLModel, fallback_classname: bool = True) -> str | None:
141
144
  table_name = getattr(model, "__tablename__")
142
- if not table_name:
143
- return model.__name__ if fallback_classname else None
145
+ if not table_name and fallback_classname:
146
+ return getattr(model, "__name__") if isinstance(model, type) else getattr(model.__class__, "__name__")
144
147
  return table_name
145
148
 
146
149
 
150
+ def print_model_results(results: Iterable[SQLModel | Sequence[SQLModel]]):
151
+ """
152
+ Pretty-print an iterable of ``SQLModel`` instances or sequences of ``SQLModel`` instances in a tabular format
153
+ using the ``rich`` library.
154
+
155
+ :param results: An iterable of ``SQLModel`` instances or sequences of ``SQLModel`` instances to print.
156
+ """
157
+ results = list(results)
158
+ if not results:
159
+ print("Query results: (empty)")
160
+ return
161
+
162
+ first_result = head(results)
163
+
164
+ cols = [f"{model_name_of(db_model)}.{key}"
165
+ for db_model in (first_result if isinstance(first_result, Sequence) else [first_result])
166
+ for key in orm_to_dict(db_model).keys()]
167
+
168
+ rows = [
169
+ {f"{model_name_of(db_model)}.{key}": str(value)
170
+ for db_model in (result if isinstance(result, Sequence) else (result,))
171
+ for key, value in orm_to_dict(db_model).items()}
172
+ for result in results
173
+ ]
174
+
175
+ col_widths = [len(col) for col in cols]
176
+ for row in rows:
177
+ for i, col in enumerate(cols):
178
+ col_widths[i] = max(col_widths[i], len(str(row.get(col, ""))))
179
+
180
+ from rich.console import Console
181
+ from rich.table import Table
182
+
183
+ console = Console()
184
+ table = Table(show_header=True)
185
+ for col, col_width in zip(cols, col_widths):
186
+ table.add_column(col, min_width=col_width)
187
+ for row in rows:
188
+ table.add_row(*[str(row.get(col)) for col in cols])
189
+
190
+ console.print(table, crop=False)
191
+
192
+
147
193
  def validate_model_extended(model_base: type[SQLModel], model_extended: type[SQLModel]) -> bool:
148
194
  """
149
195
  Validates if ``model_extended`` is an extension of ``model_base`` by checking if all fields in ``model_base``
@@ -40,9 +40,9 @@ __all__ = [
40
40
  "populate_tagset",
41
41
  "predefined_tagsets",
42
42
  "render_tagset_markdown_readme",
43
- "TagTargetInfo",
43
+ "TagTarget",
44
44
  "TagRecord",
45
- "TagTargetInfoTable",
45
+ "TagTargetTable",
46
46
  "TagRecordTable",
47
47
  "tag_cache_file_path",
48
48
  "TagCache",
@@ -295,10 +295,10 @@ def render_tagset_markdown_readme(tagset: Tagset) -> str:
295
295
  BaseModel = make_base_model()
296
296
 
297
297
 
298
- class TagTargetInfo(BaseModel):
299
- name: str = Field(
298
+ class TagTarget(BaseModel):
299
+ identifier: str = Field(
300
300
  sa_column=sa.Column(sa_sqlite.VARCHAR(256), nullable=False, unique=True),
301
- description="Name of the tag target",
301
+ description="Identifier of the tag target",
302
302
  )
303
303
  tagger_name: str = Field(
304
304
  sa_column=sa.Column(sa_sqlite.VARCHAR(128), nullable=False),
@@ -321,9 +321,9 @@ class TagTargetInfo(BaseModel):
321
321
  description="End datetime of the target range associated with the tag record",
322
322
  )
323
323
 
324
- @pdt.field_validator("name", mode="after")
324
+ @pdt.field_validator("identifier", mode="after")
325
325
  @classmethod
326
- def validate_name(cls, v: str) -> str:
326
+ def validate_identifier(cls, v: str) -> str:
327
327
  validate_slash_tag(v)
328
328
  return v
329
329
 
@@ -348,13 +348,13 @@ class TagTargetInfo(BaseModel):
348
348
  @pdt.field_validator("begin_dt", mode="after")
349
349
  @classmethod
350
350
  def validate_begin_dt(cls, v: datetime.datetime) -> datetime.datetime:
351
- validate_dt_timezone(v)
351
+ validate_dt_timezone(v, allow_naive=True)
352
352
  return v
353
353
 
354
354
  @pdt.field_validator("end_dt", mode="after")
355
355
  @classmethod
356
356
  def validate_end_dt(cls, v: datetime.datetime) -> datetime.datetime:
357
- validate_dt_timezone(v)
357
+ validate_dt_timezone(v, allow_naive=True)
358
358
  return v
359
359
 
360
360
  @pdt.model_validator(mode="after")
@@ -390,13 +390,13 @@ class TagRecord(BaseModel):
390
390
  @pdt.field_validator("begin_dt", mode="after")
391
391
  @classmethod
392
392
  def validate_begin_dt(cls, v: datetime.datetime) -> datetime.datetime:
393
- validate_dt_timezone(v)
393
+ validate_dt_timezone(v, allow_naive=True)
394
394
  return v
395
395
 
396
396
  @pdt.field_validator("end_dt", mode="after")
397
397
  @classmethod
398
398
  def validate_end_dt(cls, v: datetime.datetime) -> datetime.datetime:
399
- validate_dt_timezone(v)
399
+ validate_dt_timezone(v, allow_naive=True)
400
400
  return v
401
401
 
402
402
  @pdt.model_validator(mode="after")
@@ -412,7 +412,7 @@ class TagRecord(BaseModel):
412
412
  return v
413
413
 
414
414
 
415
- class TagTargetInfoTable(TagTargetInfo, make_sequence_model_mixin("sqlite"), table=True):
415
+ class TagTargetTable(TagTarget, make_sequence_model_mixin("sqlite"), table=True):
416
416
  __tablename__ = "tag_target_info"
417
417
 
418
418
 
@@ -421,8 +421,8 @@ class TagRecordTable(TagRecord, make_sequence_model_mixin("sqlite"), table=True)
421
421
 
422
422
 
423
423
  if typing.TYPE_CHECKING:
424
- class TagTargetInfoTable(SQLModel, SequenceModelMixinProtocol):
425
- name: sa_orm.Mapped[str] = ...
424
+ class TagTargetTable(SQLModel, SequenceModelMixinProtocol):
425
+ identifier: sa_orm.Mapped[str] = ...
426
426
  tagger_name: sa_orm.Mapped[str] = ...
427
427
  tagger_version: sa_orm.Mapped[str] = ...
428
428
  vehicle_name: sa_orm.Mapped[str] = ...
@@ -468,63 +468,63 @@ class TagCache(object):
468
468
  with self.conn_maker.make_session() as session:
469
469
  yield session
470
470
 
471
- def get_target(self, name: str) -> TagTargetInfoTable | None:
471
+ def get_target(self, identifier: str) -> TagTargetTable | None:
472
472
  with self.make_session() as session:
473
- return session.query(TagTargetInfoTable).filter(TagTargetInfoTable.name == name).one_or_none()
473
+ return session.query(TagTargetTable).filter(TagTargetTable.identifier == identifier).one_or_none()
474
474
 
475
475
  def query_targets(
476
476
  self,
477
- name: str | None = None,
477
+ identifier: str | None = None,
478
478
  tagger_name: str | None = None,
479
479
  tagger_version: str | None = None,
480
480
  vehicle_name: str | None = None,
481
481
  begin_dt: datetime.datetime | None = None,
482
482
  end_dt: datetime.datetime | None = None,
483
- ) -> Generator[TagTargetInfoTable, None, None]:
483
+ ) -> list[TagTargetTable]:
484
484
  with self.make_session() as session:
485
- query = session.query(TagTargetInfoTable)
486
- if name:
487
- query = query.filter(TagTargetInfoTable.name == name)
485
+ query = session.query(TagTargetTable)
486
+ if identifier:
487
+ query = query.filter(TagTargetTable.identifier == identifier)
488
488
  if tagger_name:
489
- query = query.filter(TagTargetInfoTable.tagger_name == tagger_name)
489
+ query = query.filter(TagTargetTable.tagger_name == tagger_name)
490
490
  if tagger_version:
491
- query = query.filter(TagTargetInfoTable.tagger_version == tagger_version)
491
+ query = query.filter(TagTargetTable.tagger_version == tagger_version)
492
492
  if vehicle_name:
493
- query = query.filter(TagTargetInfoTable.vehicle_name == vehicle_name)
493
+ query = query.filter(TagTargetTable.vehicle_name == vehicle_name)
494
494
  if begin_dt:
495
- query = query.filter(TagTargetInfoTable.end_dt >= begin_dt)
495
+ query = query.filter(TagTargetTable.end_dt >= begin_dt)
496
496
  if end_dt:
497
- query = query.filter(TagTargetInfoTable.begin_dt <= end_dt)
497
+ query = query.filter(TagTargetTable.begin_dt <= end_dt)
498
498
 
499
- yield from query.all()
499
+ return query.all()
500
500
 
501
501
  def add_target(
502
502
  self,
503
- name: str,
503
+ identifier: str,
504
504
  tagger_name: str,
505
505
  tagger_version: str,
506
506
  vehicle_name: str,
507
507
  begin_dt: datetime.datetime,
508
508
  end_dt: datetime.datetime,
509
- ) -> TagTargetInfoTable:
509
+ ) -> TagTargetTable:
510
510
  with self.make_session() as session:
511
- target_info = TagTargetInfo(
512
- name=name,
511
+ target_info = TagTarget(
512
+ identifier=identifier,
513
513
  tagger_name=tagger_name,
514
514
  tagger_version=tagger_version,
515
515
  vehicle_name=vehicle_name,
516
516
  begin_dt=begin_dt,
517
517
  end_dt=end_dt,
518
518
  )
519
- db_target_info = clone_sequence_model_instance(TagTargetInfoTable, target_info)
519
+ db_target_info = clone_sequence_model_instance(TagTargetTable, target_info)
520
520
  session.add(db_target_info)
521
521
  session.commit()
522
522
 
523
- return self.get_target(name)
523
+ return self.get_target(identifier)
524
524
 
525
525
  def remove_targets(
526
526
  self,
527
- name: str | None = None,
527
+ identifier: str | None = None,
528
528
  tagger_name: str | None = None,
529
529
  tagger_version: str | None = None,
530
530
  vehicle_name: str | None = None,
@@ -532,19 +532,19 @@ class TagCache(object):
532
532
  end_dt: datetime.datetime | None = None,
533
533
  ):
534
534
  with self.make_session() as session:
535
- query = session.query(TagTargetInfoTable)
536
- if name:
537
- query = query.filter(TagTargetInfoTable.name == name)
535
+ query = session.query(TagTargetTable)
536
+ if identifier:
537
+ query = query.filter(TagTargetTable.identifier == identifier)
538
538
  if tagger_name:
539
- query = query.filter(TagTargetInfoTable.tagger_name == tagger_name)
539
+ query = query.filter(TagTargetTable.tagger_name == tagger_name)
540
540
  if tagger_version:
541
- query = query.filter(TagTargetInfoTable.tagger_version == tagger_version)
541
+ query = query.filter(TagTargetTable.tagger_version == tagger_version)
542
542
  if vehicle_name:
543
- query = query.filter(TagTargetInfoTable.vehicle_name == vehicle_name)
543
+ query = query.filter(TagTargetTable.vehicle_name == vehicle_name)
544
544
  if begin_dt:
545
- query = query.filter(TagTargetInfoTable.end_dt >= begin_dt)
545
+ query = query.filter(TagTargetTable.end_dt >= begin_dt)
546
546
  if end_dt:
547
- query = query.filter(TagTargetInfoTable.begin_dt <= end_dt)
547
+ query = query.filter(TagTargetTable.begin_dt <= end_dt)
548
548
 
549
549
  query.delete()
550
550
  session.commit()
@@ -552,7 +552,7 @@ class TagCache(object):
552
552
  (
553
553
  session
554
554
  .query(TagRecordTable)
555
- .filter(TagRecordTable.target_sqn.notin_(session.query(TagTargetInfoTable.sqn)))
555
+ .filter(TagRecordTable.target_sqn.notin_(session.query(TagTargetTable.sqn)))
556
556
  .delete()
557
557
  )
558
558
  session.commit()
@@ -611,7 +611,7 @@ class TagCache(object):
611
611
  end_dt: datetime.datetime | None = None,
612
612
  tag_pattern: str | None = None,
613
613
  *,
614
- target_name: str | None = None,
614
+ target_identifier: str | None = None,
615
615
  target_tagger_name: str | None = None,
616
616
  target_tagger_version: str | None = None,
617
617
  target_vehicle_name: str | None = None,
@@ -620,7 +620,7 @@ class TagCache(object):
620
620
  tagsets: Sequence[Tagset] | None = None,
621
621
  tagset_inverted: bool = False,
622
622
  batch_size: int = 1000,
623
- ) -> Generator[tuple[TagRecordTable, TagTargetInfoTable], None, None]:
623
+ ) -> Generator[tuple[TagRecordTable, TagTargetTable], None, None]:
624
624
  """
625
625
  Query tag records along with their target info in the cache with optional filters.
626
626
 
@@ -628,7 +628,7 @@ class TagCache(object):
628
628
  :param end_dt: Filter by end time (inclusive)
629
629
  :param tag_pattern: Filter by tag name pattern (SQL LIKE syntax, e.g. "dummy_tag:%" to match all tags starting
630
630
  with "dummy_tag:")
631
- :param target_name: Filter by target name (exact match)
631
+ :param target_identifier: Filter by target identifier (exact match)
632
632
  :param target_tagger_name: Filter by target tagger name (exact match)
633
633
  :param target_tagger_version: Filter by target tagger version (exact match)
634
634
  :param target_vehicle_name: Filter by target vehicle name (exact match)
@@ -643,8 +643,8 @@ class TagCache(object):
643
643
  with self.make_session() as session:
644
644
  query = (
645
645
  session
646
- .query(TagRecordTable, TagTargetInfoTable)
647
- .join(TagTargetInfoTable, TagRecordTable.target_sqn == TagTargetInfoTable.sqn)
646
+ .query(TagRecordTable, TagTargetTable)
647
+ .join(TagTargetTable, TagRecordTable.target_sqn == TagTargetTable.sqn)
648
648
  )
649
649
  if begin_dt:
650
650
  query = query.filter(TagRecordTable.end_dt >= begin_dt)
@@ -652,18 +652,18 @@ class TagCache(object):
652
652
  query = query.filter(TagRecordTable.begin_dt <= end_dt)
653
653
  if tag_pattern:
654
654
  query = query.filter(TagRecordTable.tag.like(f"{escape_sql_like(tag_pattern)}%", escape="\\"))
655
- if target_name:
656
- query = query.filter(TagTargetInfoTable.name == target_name)
655
+ if target_identifier:
656
+ query = query.filter(TagTargetTable.identifier == target_identifier)
657
657
  if target_tagger_name:
658
- query = query.filter(TagTargetInfoTable.tagger_name == target_tagger_name)
658
+ query = query.filter(TagTargetTable.tagger_name == target_tagger_name)
659
659
  if target_tagger_version:
660
- query = query.filter(TagTargetInfoTable.tagger_version == target_tagger_version)
660
+ query = query.filter(TagTargetTable.tagger_version == target_tagger_version)
661
661
  if target_vehicle_name:
662
- query = query.filter(TagTargetInfoTable.vehicle_name == target_vehicle_name)
662
+ query = query.filter(TagTargetTable.vehicle_name == target_vehicle_name)
663
663
  if target_begin_dt:
664
- query = query.filter(TagTargetInfoTable.end_dt >= target_begin_dt)
664
+ query = query.filter(TagTargetTable.end_dt >= target_begin_dt)
665
665
  if target_end_dt:
666
- query = query.filter(TagTargetInfoTable.begin_dt <= target_end_dt)
666
+ query = query.filter(TagTargetTable.begin_dt <= target_end_dt)
667
667
  if tagsets:
668
668
  if tagset_inverted:
669
669
  query = query.filter(
@@ -717,49 +717,61 @@ class TagCache(object):
717
717
  def clear(self):
718
718
  with self.make_session() as session:
719
719
  session.execute(sa.delete(TagRecordTable))
720
- session.execute(sa.delete(TagTargetInfoTable))
720
+ session.execute(sa.delete(TagTargetTable))
721
721
  session.commit()
722
722
 
723
723
  def append_to(self, target_file_path: str, *, overwrite: bool = False):
724
724
  target_tag_cache = TagCache(file_path=target_file_path)
725
725
  if overwrite:
726
726
  target_tag_cache.clear()
727
- TagCache.clone_to(self, target_tag_cache)
727
+ TagCache.copy_to(self, target_tag_cache)
728
728
 
729
729
  def merge_from(self, source_file_path: str, *, overwrite: bool = False):
730
730
  source_tag_cache = TagCache(file_path=source_file_path)
731
731
  if overwrite:
732
732
  self.clear()
733
- TagCache.clone_to(source_tag_cache, self)
733
+ TagCache.copy_to(source_tag_cache, self)
734
734
 
735
735
  @staticmethod
736
- def clone_to(source: "TagCache", target: "TagCache"):
737
- if source == target:
736
+ def copy_to(src: "TagCache", dst: "TagCache"):
737
+ # If src and dst are the same instance or point to the same file path,
738
+ # do nothing to avoid accidentally clearing the cache
739
+ if src == dst or src.file_path == dst.file_path:
738
740
  return
739
741
 
740
- target.clear()
741
- with source.make_session() as source_session, target.make_session() as target_session:
742
- target_session.add_all(
743
- [clone_sequence_model_instance(TagTargetInfoTable, db_tag_target_info, clear_meta_fields=True)
744
- for db_tag_target_info in source_session.query(TagTargetInfoTable).all()],
745
- )
746
- for results in batched(source_session.query(TagRecordTable).yield_per(1000), 1000):
747
- target_session.add_all(
748
- [clone_sequence_model_instance(TagRecordTable, db_tag_record, clear_meta_fields=True)
749
- for db_tag_record in results],
750
- )
751
- target_session.commit()
742
+ with src.make_session() as src_session, dst.make_session() as dst_session:
743
+ src_targets = src_session.query(TagTargetTable).all()
744
+ dst_targets = [
745
+ clone_sequence_model_instance(TagTargetTable, db_tag_target_info, clear_meta_fields=True)
746
+ for db_tag_target_info in src_targets
747
+ ]
748
+ dst_session.add_all(dst_targets)
749
+ dst_session.flush() # ensure new sqn values are assigned
750
+
751
+ sqn_map = {src_target.sqn: dst_target.sqn for src_target, dst_target in zip(src_targets, dst_targets)}
752
+
753
+ for results in batched(src_session.query(TagRecordTable).yield_per(1000), 1000):
754
+ clones = []
755
+ for db_tag_record in results:
756
+ cloned = clone_sequence_model_instance(TagRecordTable, db_tag_record, clear_meta_fields=True)
757
+ try:
758
+ cloned.target_sqn = sqn_map[db_tag_record.target_sqn]
759
+ except KeyError as e:
760
+ raise ValueError(f"no cloned target for target_sqn '{db_tag_record.target_sqn}'") from e
761
+ clones.append(cloned)
762
+ dst_session.add_all(clones)
763
+ dst_session.commit()
752
764
 
753
765
 
754
766
  class TargetedTagCache(object):
755
- def __init__(self, cache: "TagCache", target_info: TagTargetInfoTable):
767
+ def __init__(self, cache: TagCache, target_info: TagTargetTable):
756
768
  self.target_info = target_info
757
769
  self.cache = cache
758
770
 
759
771
  @contextlib.contextmanager
760
772
  def make_session(self) -> Generator[sa_orm.Session, None, None]:
761
773
  with self.cache.make_session() as session:
762
- if not session.query(TagTargetInfoTable).filter(TagTargetInfoTable.sqn == self.target_info.sqn).first():
774
+ if not session.query(TagTargetTable).filter(TagTargetTable.sqn == self.target_info.sqn).first():
763
775
  raise ValueError(f"target info with sqn '{self.target_info.sqn}' is no longer present in cache")
764
776
  yield session
765
777
 
@@ -844,7 +856,7 @@ class TargetedTagCache(object):
844
856
  :param props: Additional properties of the tag record in JSON format (optional)
845
857
  :return: Self instance for chaining
846
858
  """
847
- return self.add_ranged(
859
+ return self.add_ranged_tag(
848
860
  begin_dt=self.target_info.begin_dt,
849
861
  end_dt=self.target_info.end_dt,
850
862
  tag=tag,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.53
3
+ Version: 1.0.55
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -196,3 +196,155 @@ class TagUtilsTest(unittest.TestCase):
196
196
  self.assertEqual(len(list(cache.iter_tag_and_targets())), total_tasks_count)
197
197
  self.assertEqual(len(list(cache.iter_tag_and_targets(tag_pattern="dummy:bar"))),
198
198
  total_tasks_count // len(tags))
199
+
200
+ def test_tag_cache__clone(self):
201
+ tagset = MutableTagset(namespace="tagset", desc="A dummy tagset for testing")
202
+ tagset.add(Tag(name="dummy:foo", desc="A dummy tag for testing"))
203
+ tagset.add(Tag(name="dummy:bar", desc="Another dummy tag for testing"))
204
+
205
+ tags = [
206
+ "dummy:foo",
207
+ "dummy:bar",
208
+ "dummy:baz",
209
+ "dummy:qux",
210
+ ]
211
+
212
+ with tempfile.TemporaryDirectory() as temp_directory:
213
+ temp_directory = pathlib.Path(temp_directory)
214
+
215
+ src_cache = TagCache(file_path=temp_directory / "src_tag_cache.db")
216
+
217
+ src_cache.add_target("awesome_tagger/20200101_000000/dummy_vehicle/0",
218
+ "awesome_tagger",
219
+ "1.0.0",
220
+ "dummy_vehicle",
221
+ dt_parse_iso("2020-01-01T00:00:00+00:00"),
222
+ dt_parse_iso("2020-01-01T01:00:00+00:00"))
223
+
224
+ src_target_cache = src_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
225
+
226
+ tags_count = 1000
227
+
228
+ for i in range(tags_count):
229
+ src_target_cache.add_tag(tags[i % len(tags)])
230
+
231
+ dst_cache = TagCache(file_path=temp_directory / "dst_tag_cache.db")
232
+
233
+ TagCache.copy_to(src_cache, dst_cache)
234
+
235
+ for i in range(tags_count):
236
+ src_target_cache.add_tag(tags[i % len(tags)])
237
+
238
+ dst_target_cache = dst_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
239
+
240
+ self.assertEqual(len(list(src_target_cache.iter_tags())), tags_count * 2)
241
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset]))), tags_count * 2 // 2)
242
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
243
+ tags_count * 2 // 2)
244
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
245
+ tags_count * 2 // 2)
246
+
247
+ self.assertEqual(len(list(dst_target_cache.iter_tags())), tags_count)
248
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset]))), tags_count // 2)
249
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
250
+ tags_count // 2)
251
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
252
+ tags_count // 2)
253
+
254
+ def test_tag_cache__clone_same_file(self):
255
+ tagset = MutableTagset(namespace="tagset", desc="A dummy tagset for testing")
256
+ tagset.add(Tag(name="dummy:foo", desc="A dummy tag for testing"))
257
+ tagset.add(Tag(name="dummy:bar", desc="Another dummy tag for testing"))
258
+
259
+ tags = [
260
+ "dummy:foo",
261
+ "dummy:bar",
262
+ "dummy:baz",
263
+ "dummy:qux",
264
+ ]
265
+
266
+ # Clone to the same file path should not cause any issue, and the cloned cache should be able to read
267
+ # the tags added to the source cache after cloning.
268
+ with tempfile.TemporaryDirectory() as temp_directory:
269
+ temp_directory = pathlib.Path(temp_directory)
270
+
271
+ src_cache = TagCache(file_path=temp_directory / "tag_cache.db")
272
+
273
+ src_cache.add_target("awesome_tagger/20200101_000000/dummy_vehicle/0",
274
+ "awesome_tagger",
275
+ "1.0.0",
276
+ "dummy_vehicle",
277
+ dt_parse_iso("2020-01-01T00:00:00+00:00"),
278
+ dt_parse_iso("2020-01-01T01:00:00+00:00"))
279
+
280
+ src_target_cache = src_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
281
+
282
+ tags_count = 1000
283
+
284
+ for i in range(tags_count):
285
+ src_target_cache.add_tag(tags[i % len(tags)])
286
+
287
+ dst_cache = TagCache(file_path=temp_directory / "tag_cache.db")
288
+
289
+ TagCache.copy_to(src_cache, dst_cache)
290
+
291
+ for i in range(tags_count):
292
+ src_target_cache.add_tag(tags[i % len(tags)])
293
+
294
+ dst_target_cache = dst_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
295
+
296
+ self.assertEqual(len(list(src_target_cache.iter_tags())), tags_count * 2)
297
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset]))), tags_count * 2 // 2)
298
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
299
+ tags_count * 2 // 2)
300
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
301
+ tags_count * 2 // 2)
302
+
303
+ self.assertEqual(len(list(dst_target_cache.iter_tags())), tags_count * 2)
304
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset]))), tags_count * 2 // 2)
305
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
306
+ tags_count * 2 // 2)
307
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
308
+ tags_count * 2 // 2)
309
+
310
+ with tempfile.TemporaryDirectory() as temp_directory:
311
+ temp_directory = pathlib.Path(temp_directory)
312
+
313
+ src_cache = TagCache(file_path=temp_directory / "tag_cache.db")
314
+
315
+ src_cache.add_target("awesome_tagger/20200101_000000/dummy_vehicle/0",
316
+ "awesome_tagger",
317
+ "1.0.0",
318
+ "dummy_vehicle",
319
+ dt_parse_iso("2020-01-01T00:00:00+00:00"),
320
+ dt_parse_iso("2020-01-01T01:00:00+00:00"))
321
+
322
+ src_target_cache = src_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
323
+
324
+ tags_count = 1000
325
+
326
+ for i in range(tags_count):
327
+ src_target_cache.add_tag(tags[i % len(tags)])
328
+
329
+ dst_cache = src_cache
330
+
331
+ TagCache.copy_to(src_cache, dst_cache)
332
+
333
+ for i in range(tags_count):
334
+ src_target_cache.add_tag(tags[i % len(tags)])
335
+
336
+ dst_target_cache = dst_cache.with_target("awesome_tagger/20200101_000000/dummy_vehicle/0")
337
+
338
+ self.assertEqual(len(list(src_target_cache.iter_tags())), tags_count * 2)
339
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset]))), tags_count * 2 // 2)
340
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
341
+ tags_count * 2 // 2)
342
+ self.assertEqual(len(list(src_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
343
+ tags_count * 2 // 2)
344
+
345
+ self.assertEqual(len(list(dst_target_cache.iter_tags())), tags_count * 2)
346
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset]))), tags_count * 2 // 2)
347
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=True))),
348
+ tags_count * 2 // 2)
349
+ self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
350
+ tags_count * 2 // 2)