plexus-python-common 1.0.63__tar.gz → 1.0.65__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 (91) hide show
  1. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/PKG-INFO +1 -1
  2. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/tagutils.py +145 -10
  3. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/PKG-INFO +1 -1
  4. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/tagutils_test.py +175 -0
  5. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/.editorconfig +0 -0
  6. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/.github/workflows/pr.yml +0 -0
  7. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/.github/workflows/push.yml +0 -0
  8. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/.gitignore +0 -0
  9. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/MANIFEST.in +0 -0
  10. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/README.md +0 -0
  11. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/VERSION +0 -0
  12. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/pyproject.toml +0 -0
  13. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  14. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  15. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  16. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/0-dummy +0 -0
  17. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/1-dummy +0 -0
  18. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/2-dummy +0 -0
  19. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
  20. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
  21. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
  22. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
  23. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
  24. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
  25. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
  26. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
  27. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
  28. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
  29. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
  30. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
  31. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/pathutils/dummy.txt +0 -0
  32. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  33. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  34. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  35. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  36. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  37. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  38. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  39. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  40. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  41. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  42. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  43. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  44. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/setup.cfg +0 -0
  45. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/setup.py +0 -0
  46. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/__init__.py +0 -0
  47. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/carto/OSMFile.py +0 -0
  48. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/carto/OSMNode.py +0 -0
  49. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/carto/OSMTags.py +0 -0
  50. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/carto/OSMWay.py +0 -0
  51. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/carto/__init__.py +0 -0
  52. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/resources/__init__.py +0 -0
  53. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/resources/tags/__init__.py +0 -0
  54. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -0
  55. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/resources/tags/universal-1.0.0.tagset.yaml +0 -0
  56. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/__init__.py +0 -0
  57. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/apiutils.py +0 -0
  58. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/bagutils.py +0 -0
  59. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/config.py +0 -0
  60. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/datautils.py +0 -0
  61. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/dockerutils.py +0 -0
  62. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/gisutils.py +0 -0
  63. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/jsonutils.py +0 -0
  64. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/ormutils.py +0 -0
  65. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/pathutils.py +0 -0
  66. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/s3utils.py +0 -0
  67. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/sqlutils.py +0 -0
  68. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/strutils.py +0 -0
  69. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus/common/utils/testutils.py +0 -0
  70. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/SOURCES.txt +0 -0
  71. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  72. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  73. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/requires.txt +0 -0
  74. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  75. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/__init__.py +0 -0
  76. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/__init__.py +0 -0
  77. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/carto/__init__.py +0 -0
  78. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
  79. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
  80. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/__init__.py +0 -0
  81. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
  82. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/datautils_test.py +0 -0
  83. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/dockerutils_test.py +0 -0
  84. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/gisutils_test.py +0 -0
  85. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
  86. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/ormutils_test.py +0 -0
  87. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/pathutils_test.py +0 -0
  88. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/s3utils_test.py +0 -0
  89. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/strutils_test.py +0 -0
  90. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/test/plexus_tests/common/utils/testutils_test.py +0 -0
  91. {plexus_python_common-1.0.63 → plexus_python_common-1.0.65}/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.63
3
+ Version: 1.0.65
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
  import dataclasses
3
3
  import datetime
4
+ import math
4
5
  import os
5
6
  import pathlib
6
7
  import textwrap
@@ -16,7 +17,9 @@ import sqlalchemy as sa
16
17
  import sqlalchemy.dialects.sqlite as sa_sqlite
17
18
  import sqlalchemy.orm as sa_orm
18
19
  from iker.common.utils.dbutils import ConnectionMaker
19
- from iker.common.utils.funcutils import memorized, singleton
20
+ from iker.common.utils.dtutils import dt_from_ts_us, dt_to_ts_us
21
+ from iker.common.utils.funcutils import Chainable
22
+ from iker.common.utils.funcutils import chainable, memorized, singleton
20
23
  from iker.common.utils.iterutils import batched, head_or_none
21
24
  from iker.common.utils.iterutils import dicttree
22
25
  from iker.common.utils.iterutils import dicttree_add, dicttree_remove
@@ -29,6 +32,7 @@ from sqlmodel import Field, SQLModel
29
32
  from plexus.common.resources.tags import predefined_tagset_specs
30
33
  from plexus.common.utils.datautils import validate_colon_tag, validate_snake_case, validate_vehicle_name
31
34
  from plexus.common.utils.datautils import validate_dt_timezone, validate_semver, validate_slash_tag
35
+ from plexus.common.utils.jsonutils import json_datetime_encoder
32
36
  from plexus.common.utils.ormutils import SequenceModelMixinProtocol
33
37
  from plexus.common.utils.ormutils import clone_sequence_model_instance, make_base_model, make_sequence_model_mixin
34
38
  from plexus.common.utils.sqlutils import escape_sql_like
@@ -48,6 +52,8 @@ __all__ = [
48
52
  "tag_cache_file_path",
49
53
  "TagCache",
50
54
  "tag_cache",
55
+ "standard_clip_duration_us",
56
+ "populate_clip_ranges",
51
57
  ]
52
58
 
53
59
 
@@ -440,6 +446,14 @@ class TagTarget(BaseModel):
440
446
  raise ValueError(f"begin_dt '{self.begin_dt}' is greater than end_dt '{self.end_dt}'")
441
447
  return self
442
448
 
449
+ @pdt.field_serializer("begin_dt", mode="plain")
450
+ def serialize_begin_dt(self, v: datetime.datetime) -> str:
451
+ return json_datetime_encoder(v)
452
+
453
+ @pdt.field_serializer("end_dt", mode="plain")
454
+ def serialize_end_dt(self, v: datetime.datetime) -> str:
455
+ return json_datetime_encoder(v)
456
+
443
457
 
444
458
  class TagRecord(BaseModel):
445
459
  target_sqn: int = Field(
@@ -473,6 +487,11 @@ class TagRecord(BaseModel):
473
487
  default=None,
474
488
  description="Additional properties of the tag record in JSON format",
475
489
  )
490
+ flags: int = Field(
491
+ sa_column=sa.Column(sa_sqlite.INTEGER),
492
+ default=0,
493
+ description="Integer bitmask storing status or metadata flags for this tag record",
494
+ )
476
495
 
477
496
  @pdt.field_validator("begin_dt", mode="after")
478
497
  @classmethod
@@ -512,6 +531,14 @@ class TagRecord(BaseModel):
512
531
  validate_colon_tag(v)
513
532
  return v
514
533
 
534
+ @pdt.field_serializer("begin_dt", mode="plain")
535
+ def serialize_begin_dt(self, v: datetime.datetime) -> str:
536
+ return json_datetime_encoder(v)
537
+
538
+ @pdt.field_serializer("end_dt", mode="plain")
539
+ def serialize_end_dt(self, v: datetime.datetime) -> str:
540
+ return json_datetime_encoder(v)
541
+
515
542
 
516
543
  class TagTargetTable(TagTarget, make_sequence_model_mixin("sqlite"), table=True):
517
544
  __tablename__ = "tag_target_info"
@@ -539,6 +566,7 @@ if typing.TYPE_CHECKING:
539
566
  tagset_version: sa_orm.Mapped[str | None] = ...
540
567
  tag: sa_orm.Mapped[str] = ...
541
568
  props: sa_orm.Mapped[JsonType | None] = ...
569
+ flags: sa_orm.Mapped[int] = ...
542
570
 
543
571
 
544
572
  @singleton
@@ -571,9 +599,12 @@ class TagCache(object):
571
599
  with self.conn_maker.make_session() as session:
572
600
  yield session
573
601
 
574
- def get_target(self, identifier: str) -> TagTargetTable | None:
602
+ def get_target(self, target: int | str) -> TagTargetTable | None:
575
603
  with self.make_session() as session:
576
- return session.query(TagTargetTable).filter(TagTargetTable.identifier == identifier).one_or_none()
604
+ if isinstance(target, int):
605
+ return session.get(TagTargetTable, target)
606
+ else:
607
+ return session.query(TagTargetTable).filter(TagTargetTable.identifier == target).one_or_none()
577
608
 
578
609
  def query_targets(
579
610
  self,
@@ -660,10 +691,10 @@ class TagCache(object):
660
691
  )
661
692
  session.commit()
662
693
 
663
- def with_target(self, name: str) -> "TargetedTagCache":
664
- target_info = self.get_target(name)
694
+ def with_target(self, target: int | str) -> "TargetedTagCache":
695
+ target_info = self.get_target(target)
665
696
  if target_info is None:
666
- raise ValueError(f"target with name '{name}' not found in cache")
697
+ raise ValueError(f"target '{target}' not found in cache")
667
698
  return TargetedTagCache(cache=self, target_info=target_info)
668
699
 
669
700
  def iter_tags(
@@ -957,7 +988,7 @@ class TargetedTagCache(object):
957
988
  props: JsonType | None = None,
958
989
  tagset_namespace: str | None = None,
959
990
  tagset_version: str | None = None,
960
- ) -> Self:
991
+ ) -> Chainable[Self, TagRecordTable]:
961
992
  """
962
993
  Add a tag record to the cache for the specified time range. If ``begin_dt`` or ``end_dt`` is None, it will
963
994
  default to the target's ``begin_dt`` or ``end_dt`` respectively.
@@ -984,10 +1015,12 @@ class TargetedTagCache(object):
984
1015
  tag=tag.name if isinstance(tag, Tag) else tag,
985
1016
  props=props,
986
1017
  )
987
- session.add(clone_sequence_model_instance(TagRecordTable, tag_record))
1018
+ db_tag_record = clone_sequence_model_instance(TagRecordTable, tag_record)
1019
+ session.add(db_tag_record)
988
1020
  session.commit()
989
1021
 
990
- return self
1022
+ session.refresh(db_tag_record)
1023
+ return chainable(self, db_tag_record)
991
1024
 
992
1025
  def add_tag(
993
1026
  self,
@@ -995,7 +1028,7 @@ class TargetedTagCache(object):
995
1028
  props: JsonType | None = None,
996
1029
  tagset_namespace: str | None = None,
997
1030
  tagset_version: str | None = None,
998
- ) -> Self:
1031
+ ) -> Chainable[Self, TagRecordTable]:
999
1032
  """
1000
1033
  Add a tag record to the cache for the entire target range.
1001
1034
 
@@ -1019,6 +1052,61 @@ class TargetedTagCache(object):
1019
1052
  props=props,
1020
1053
  )
1021
1054
 
1055
+ def update_tag(
1056
+ self,
1057
+ sqn: int,
1058
+ *,
1059
+ begin_dt: datetime.datetime | None = None,
1060
+ end_dt: datetime.datetime | None = None,
1061
+ tag: str | Tag | BoundTag | None = None,
1062
+ props: JsonType | None = None,
1063
+ tagset_namespace: str | None = None,
1064
+ tagset_version: str | None = None,
1065
+ flags: int | None = None,
1066
+ ) -> Chainable[Self, TagRecordTable]:
1067
+ """
1068
+ Update a tag record in the cache by its sequence number.
1069
+
1070
+ :param sqn: Sequence number of the tag record to be updated
1071
+ :param begin_dt: New begin datetime of the tag record (optional)
1072
+ :param end_dt: New end datetime of the tag record (optional)
1073
+ :param tag: New tag name or ``Tag``/``BoundTag`` instance to be updated (optional). If ``Tag``/``BoundTag``
1074
+ instance is provided, its name will be used.
1075
+ :param props: New additional properties of the tag record in JSON format (optional)
1076
+ :param tagset_namespace: New namespace of the tagset that the tag belongs to (optional). If the ``tag``
1077
+ parameter is a ``BoundTag`` instance, this parameter will be ignored and the namespace
1078
+ from the instance will be used.
1079
+ :param tagset_version: New version of the tagset that the tag belongs to (optional). If the ``tag`` parameter
1080
+ is a ``BoundTag`` instance, this parameter will be ignored and the version from the
1081
+ instance will be used.
1082
+ :param flags: New integer bitmask storing status or metadata flags for this tag record (optional)
1083
+ :return: Self instance for chaining
1084
+ """
1085
+ with self.make_session() as session:
1086
+ db_tag_record = session.query(TagRecordTable).filter(TagRecordTable.sqn == sqn).one_or_none()
1087
+ if not db_tag_record:
1088
+ raise ValueError(f"tag record with sqn '{sqn}' not found in cache")
1089
+
1090
+ if begin_dt is not None:
1091
+ db_tag_record.begin_dt = begin_dt
1092
+ if end_dt is not None:
1093
+ db_tag_record.end_dt = end_dt
1094
+ if tagset_namespace is not None:
1095
+ db_tag_record.tagset_namespace = tag.namespace if isinstance(tag, BoundTag) else tagset_namespace
1096
+ if tagset_version is not None:
1097
+ db_tag_record.tagset_version = tag.version if isinstance(tag, BoundTag) else tagset_version
1098
+ if tag is not None:
1099
+ db_tag_record.tag = tag.name if isinstance(tag, Tag) else tag
1100
+ if props is not None:
1101
+ db_tag_record.props = props
1102
+ if flags is not None:
1103
+ db_tag_record.flags = flags
1104
+
1105
+ session.commit()
1106
+
1107
+ session.refresh(db_tag_record)
1108
+ return chainable(self, db_tag_record)
1109
+
1022
1110
  def remove_tags(
1023
1111
  self,
1024
1112
  begin_dt: datetime.datetime | None = None,
@@ -1093,3 +1181,50 @@ def tag_cache(*, identifier: str | None = None, file_path: str | None = None) ->
1093
1181
  if file_path is not None:
1094
1182
  return TagCache(file_path=file_path)
1095
1183
  return TagCache(file_path=tag_cache_file_path())
1184
+
1185
+
1186
+ @singleton
1187
+ def standard_clip_duration_us() -> int:
1188
+ """Duration of clips to split samples into, in microseconds, which is fixed to 20 seconds for now."""
1189
+ return 20 * 1_000_000 # 20 seconds
1190
+
1191
+
1192
+ def populate_clip_ranges(
1193
+ data_begin_dt: datetime.datetime,
1194
+ data_end_dt: datetime.datetime,
1195
+ *,
1196
+ clip_duration_us: int = None
1197
+ ) -> Generator[tuple[datetime.datetime, datetime.datetime, datetime.datetime, datetime.datetime]]:
1198
+ """
1199
+ Generate clip begin datetime, clip data begin datetime, and clip data end datetime for each clip slot that overlaps
1200
+ with the given data begin and end datetimes.
1201
+ The clip begin datetime is the beginning of the clip slot, the clip data begin datetime is the maximum of the clip
1202
+ begin datetime and the data begin datetime, and the clip data end datetime is the minimum of the clip end datetime
1203
+ and the data end datetime.
1204
+
1205
+ :param data_begin_dt: The beginning datetime of the data.
1206
+ :param data_end_dt: The end datetime of the data.
1207
+ :param clip_duration_us: The duration of each clip in microseconds. If not provided,
1208
+ the standard clip duration will be used.
1209
+ :return: A generator of tuples containing the clip begin datetime, clip data begin datetime,
1210
+ and clip data end datetime
1211
+ """
1212
+ clip_duration_us = clip_duration_us or standard_clip_duration_us()
1213
+
1214
+ if data_begin_dt == data_end_dt:
1215
+ clip_slot = math.floor(dt_to_ts_us(data_begin_dt) / clip_duration_us)
1216
+ clip_begin_dt = dt_from_ts_us(clip_slot * clip_duration_us)
1217
+ clip_end_dt = dt_from_ts_us((clip_slot + 1) * clip_duration_us)
1218
+ yield clip_begin_dt, clip_end_dt, data_begin_dt, data_end_dt
1219
+ return
1220
+
1221
+ begin_clip_slot = math.floor(dt_to_ts_us(data_begin_dt) / clip_duration_us)
1222
+ end_clip_slot = math.ceil(dt_to_ts_us(data_end_dt) / clip_duration_us)
1223
+
1224
+ for clip_slot in range(begin_clip_slot, end_clip_slot):
1225
+ clip_begin_dt = dt_from_ts_us(clip_slot * clip_duration_us)
1226
+ clip_end_dt = dt_from_ts_us((clip_slot + 1) * clip_duration_us)
1227
+ clip_data_begin_dt = max(clip_begin_dt, data_begin_dt)
1228
+ clip_data_end_dt = min(clip_end_dt, data_end_dt)
1229
+
1230
+ yield clip_begin_dt, clip_end_dt, clip_data_begin_dt, clip_data_end_dt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.63
3
+ Version: 1.0.65
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -10,6 +10,7 @@ from iker.common.utils.iterutils import last
10
10
  from iker.common.utils.jsonutils import JsonObject
11
11
 
12
12
  from plexus.common.utils.tagutils import MutableTagset, Tag, TagCache, Tagset
13
+ from plexus.common.utils.tagutils import populate_clip_ranges
13
14
  from plexus.common.utils.tagutils import predefined_tagsets, render_tagset_markdown_readme
14
15
  from plexus.common.utils.tagutils import tag_cache_file_path
15
16
 
@@ -497,3 +498,177 @@ class TagUtilsTest(unittest.TestCase):
497
498
  tag_records_count * 2 * (tags_count - tagset_tags_count) // tags_count)
498
499
  self.assertEqual(len(list(dst_target_cache.iter_tags(tagsets=[tagset], tagset_inverted=False))),
499
500
  tag_records_count * 2 * tagset_tags_count // tags_count)
501
+
502
+ data_populate_clip_ranges = [
503
+ (
504
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
505
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
506
+ [
507
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
508
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
509
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
510
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
511
+ ],
512
+ ),
513
+ (
514
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
515
+ dt_parse_iso("2023-01-01T00:00:50.000000+00:00"),
516
+ [
517
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
518
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
519
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
520
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
521
+ (dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
522
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
523
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
524
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00")),
525
+ (dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
526
+ dt_parse_iso("2023-01-01T00:01:00.000000+00:00"),
527
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
528
+ dt_parse_iso("2023-01-01T00:00:50.000000+00:00")),
529
+ ],
530
+ ),
531
+ (
532
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
533
+ dt_parse_iso("2023-01-01T00:00:15.000000+00:00"),
534
+ [
535
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
536
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
537
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
538
+ dt_parse_iso("2023-01-01T00:00:15.000000+00:00")),
539
+ ],
540
+ ),
541
+ (
542
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
543
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
544
+ [
545
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
546
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
547
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
548
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
549
+ (dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
550
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
551
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
552
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00")),
553
+ ],
554
+ ),
555
+ (
556
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
557
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
558
+ [
559
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
560
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
561
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
562
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00")),
563
+ ],
564
+ ),
565
+ (
566
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
567
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
568
+ [
569
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
570
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
571
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
572
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00")),
573
+ ],
574
+ )
575
+ ]
576
+
577
+ @ddt.idata(data_populate_clip_ranges)
578
+ @ddt.unpack
579
+ def test_populate_clip_ranges(self, data_begin_dt, data_end_dt, expected):
580
+ result = list(populate_clip_ranges(data_begin_dt, data_end_dt))
581
+ self.assertEqual(result, expected)
582
+
583
+ data_populate_clip_ranges__customized_duration = [
584
+ (
585
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
586
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
587
+ [
588
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
589
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
590
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
591
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00")),
592
+ (dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
593
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
594
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
595
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
596
+ ],
597
+ ),
598
+ (
599
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
600
+ dt_parse_iso("2023-01-01T00:00:50.000000+00:00"),
601
+ [
602
+ (dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
603
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
604
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
605
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
606
+ (dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
607
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
608
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
609
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00")),
610
+ (dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
611
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
612
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
613
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00")),
614
+ (dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
615
+ dt_parse_iso("2023-01-01T00:00:50.000000+00:00"),
616
+ dt_parse_iso("2023-01-01T00:00:40.000000+00:00"),
617
+ dt_parse_iso("2023-01-01T00:00:50.000000+00:00")),
618
+ ],
619
+ ),
620
+ (
621
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
622
+ dt_parse_iso("2023-01-01T00:00:15.000000+00:00"),
623
+ [
624
+ (dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
625
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
626
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
627
+ dt_parse_iso("2023-01-01T00:00:15.000000+00:00")),
628
+ ],
629
+ ),
630
+ (
631
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
632
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
633
+ [
634
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
635
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
636
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
637
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00")),
638
+ (dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
639
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
640
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
641
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00")),
642
+ (dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
643
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00"),
644
+ dt_parse_iso("2023-01-01T00:00:20.000000+00:00"),
645
+ dt_parse_iso("2023-01-01T00:00:30.000000+00:00")),
646
+ ],
647
+ ),
648
+ (
649
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
650
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
651
+ [
652
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
653
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
654
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
655
+ dt_parse_iso("2023-01-01T00:00:00.000000+00:00")),
656
+ ],
657
+ ),
658
+ (
659
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
660
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
661
+ [
662
+ (dt_parse_iso("2023-01-01T00:00:00.000000+00:00"),
663
+ dt_parse_iso("2023-01-01T00:00:10.000000+00:00"),
664
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00"),
665
+ dt_parse_iso("2023-01-01T00:00:05.000000+00:00")),
666
+ ],
667
+ )
668
+ ]
669
+
670
+ @ddt.idata(data_populate_clip_ranges__customized_duration)
671
+ @ddt.unpack
672
+ def test_populate_clip_ranges__customized_duration(self, data_begin_dt, data_end_dt, expected):
673
+ result = list(populate_clip_ranges(data_begin_dt, data_end_dt, clip_duration_us=10_000_000))
674
+ self.assertEqual(result, expected)