plexus-python-pulse-common 1.1.77__tar.gz → 1.1.78__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 (96) hide show
  1. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/PKG-INFO +2 -1
  2. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/pyproject.toml +2 -0
  3. plexus_python_pulse_common-1.1.78/src/plexus/pulse/common/utils/flowutils.py +291 -0
  4. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/tagutils.py +36 -1
  5. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/PKG-INFO +2 -1
  6. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/requires.txt +1 -0
  7. plexus_python_pulse_common-1.1.78/test/plexus_tests/pulse/common/utils/flowutils_test.py +48 -0
  8. plexus_python_pulse_common-1.1.77/src/plexus/pulse/common/utils/flowutils.py +0 -167
  9. plexus_python_pulse_common-1.1.77/test/plexus_tests/pulse/common/utils/flowutils_test.py +0 -36
  10. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/.editorconfig +0 -0
  11. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/.github/workflows/pr.yml +0 -0
  12. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/.github/workflows/push.yml +0 -0
  13. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/.gitignore +0 -0
  14. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/MANIFEST.in +0 -0
  15. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/README.md +0 -0
  16. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/VERSION +0 -0
  17. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  18. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  19. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  20. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/0-dummy +0 -0
  21. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/1-dummy +0 -0
  22. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/2-dummy +0 -0
  23. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
  24. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
  25. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
  26. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
  27. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
  28. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
  29. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
  30. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
  31. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
  32. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
  33. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
  34. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
  35. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.txt +0 -0
  36. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  37. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  38. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  39. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  40. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  41. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  42. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  43. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  44. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  45. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  46. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  47. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  48. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/setup.cfg +0 -0
  49. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/setup.py +0 -0
  50. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/__init__.py +0 -0
  51. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMFile.py +0 -0
  52. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMNode.py +0 -0
  53. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMTags.py +0 -0
  54. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMWay.py +0 -0
  55. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/__init__.py +0 -0
  56. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/__init__.py +0 -0
  57. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/__init__.py +0 -0
  58. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -0
  59. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +0 -0
  60. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/__init__.py +0 -0
  61. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/apiutils.py +0 -0
  62. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/bagutils.py +0 -0
  63. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/config.py +0 -0
  64. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/datautils.py +0 -0
  65. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/dockerutils.py +0 -0
  66. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/gisutils.py +0 -0
  67. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/jsonutils.py +0 -0
  68. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/ormutils.py +0 -0
  69. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/pathutils.py +0 -0
  70. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/s3utils.py +0 -0
  71. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/sqlutils.py +0 -0
  72. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/strutils.py +0 -0
  73. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/testutils.py +0 -0
  74. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/SOURCES.txt +0 -0
  75. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/dependency_links.txt +0 -0
  76. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/not-zip-safe +0 -0
  77. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/top_level.txt +0 -0
  78. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/__init__.py +0 -0
  79. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/__init__.py +0 -0
  80. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/__init__.py +0 -0
  81. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/__init__.py +0 -0
  82. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/osm_file_test.py +0 -0
  83. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/osm_tags_test.py +0 -0
  84. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/__init__.py +0 -0
  85. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/bagutils_test.py +0 -0
  86. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/datautils_test.py +0 -0
  87. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/dockerutils_test.py +0 -0
  88. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/gisutils_test.py +0 -0
  89. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/jsonutils_test.py +0 -0
  90. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/ormutils_test.py +0 -0
  91. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/pathutils_test.py +0 -0
  92. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/s3utils_test.py +0 -0
  93. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/strutils_test.py +0 -0
  94. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/tagutils_test.py +0 -0
  95. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/testutils_test.py +0 -0
  96. {plexus_python_pulse_common-1.1.77 → plexus_python_pulse_common-1.1.78}/test/testenv.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-pulse-common
3
- Version: 1.1.77
3
+ Version: 1.1.78
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -22,6 +22,7 @@ Requires-Dist: pyparsing>=3.2
22
22
  Requires-Dist: pyproj>=3.6
23
23
  Requires-Dist: pyquaternion>=0.9
24
24
  Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: requests>=2.0
25
26
  Requires-Dist: sqlalchemy>=2.0
26
27
  Requires-Dist: s3fs>=2025.10
27
28
  Requires-Dist: rich>=13.9
@@ -24,6 +24,7 @@ dev = [
24
24
  "pyproj>=3.6",
25
25
  "pyquaternion>=0.9",
26
26
  "pyyaml>=6.0",
27
+ "requests>=2.0",
27
28
  "sqlalchemy>=2.0",
28
29
  "s3fs>=2025.10",
29
30
  "rich>=13.9",
@@ -71,6 +72,7 @@ dependencies = [
71
72
  "pyproj>=3.6",
72
73
  "pyquaternion>=0.9",
73
74
  "pyyaml>=6.0",
75
+ "requests>=2.0",
74
76
  "sqlalchemy>=2.0",
75
77
  "s3fs>=2025.10",
76
78
  "rich>=13.9",
@@ -0,0 +1,291 @@
1
+ import contextlib
2
+ import enum
3
+ import os
4
+ import pathlib
5
+ import threading
6
+ import typing
7
+ import uuid as py_uuid
8
+ from collections.abc import Generator
9
+ from typing import Self
10
+
11
+ import pydantic as pdt
12
+ import requests
13
+ import sqlalchemy as sa
14
+ import sqlalchemy.dialects.sqlite as sa_sqlite
15
+ import sqlalchemy.orm as sa_orm
16
+ from plexus.common.utils.dbutils import ConnectionMaker
17
+ from plexus.common.utils.funcutils import Chainable
18
+ from plexus.common.utils.funcutils import chainable, singleton
19
+ from plexus.common.utils.jsonutils import JsonObject
20
+ from plexus.common.utils.retry import retry
21
+ from sqlmodel import Field
22
+
23
+ from plexus.pulse.common.utils.ormutils import SQLiteUUID, SequenceModelMixinProtocol
24
+ from plexus.pulse.common.utils.ormutils import clone_sequence_model_instance, make_base_model, make_sequence_model_mixin
25
+
26
+ __all__ = [
27
+ "flow_workspace_root_path",
28
+ "current_flow_run_id",
29
+ "current_flow_workspace_root_path",
30
+ "FlowStageEnum",
31
+ "current_flow_stage",
32
+ "stage_can_resolve_dataset_ref",
33
+ "make_dataset_ref_model_mixin",
34
+ "DatasetRef",
35
+ "DatasetRefTable",
36
+ "current_flow_context_file_path",
37
+ "FlowContext",
38
+ "flow_context",
39
+ "flow_api_url",
40
+ "resolve_dataset_ref",
41
+ "resolve_dataset_storage_location",
42
+ "resolve",
43
+ ]
44
+
45
+ flow_stage_env = "PULSE_FLOW_STAGE"
46
+ flow_run_id_env = "PULSE_FLOW_RUN_ID"
47
+ flow_workspace_root_dir_env = "PULSE_FLOW_WORKSPACE_ROOT_DIR"
48
+ flow_api_url_env = "PULSE_FLOW_API_URL"
49
+
50
+
51
+ def flow_workspace_root_path() -> pathlib.Path:
52
+ value = os.environ.get(flow_workspace_root_dir_env)
53
+ if value is None:
54
+ raise ValueError(f"Missing required environment variable '{flow_workspace_root_dir_env}'")
55
+ return pathlib.Path(value).absolute()
56
+
57
+
58
+ def current_flow_run_id() -> str:
59
+ value = os.environ.get(flow_run_id_env)
60
+ if value is None:
61
+ raise ValueError(f"Missing required environment variable '{flow_run_id_env}'")
62
+ return value
63
+
64
+
65
+ def current_flow_workspace_root_path() -> pathlib.Path:
66
+ return flow_workspace_root_path() / current_flow_run_id()
67
+
68
+
69
+ class FlowStageEnum(enum.Enum):
70
+ Preprocessing = "preprocessing"
71
+ Annotation = "annotation"
72
+ Curation = "curation"
73
+ Training = "training"
74
+
75
+
76
+ def current_flow_stage() -> FlowStageEnum:
77
+ value = os.environ.get(flow_stage_env)
78
+ if value is None:
79
+ raise ValueError(f"Missing required environment variable '{flow_stage_env}'")
80
+ for stage in list(FlowStageEnum):
81
+ if value == stage.value:
82
+ return stage
83
+ raise ValueError(f"Illegal '{flow_stage_env}' value '{value}'")
84
+
85
+
86
+ def stage_can_resolve_dataset_ref(stage: FlowStageEnum | None = None) -> bool:
87
+ if stage is None:
88
+ stage = current_flow_stage()
89
+ return stage in {FlowStageEnum.Curation}
90
+
91
+
92
+ def make_dataset_ref_model_mixin() -> type[pdt.BaseModel]:
93
+ class ModelMixin(pdt.BaseModel):
94
+ dataset_uuid: py_uuid.UUID = Field(
95
+ sa_column=sa.Column(SQLiteUUID(), nullable=False),
96
+ description="UUID of the dataset",
97
+ )
98
+ dataset_name: str = Field(
99
+ sa_column=sa.Column(sa_sqlite.VARCHAR(128), nullable=False),
100
+ description="Name of the dataset",
101
+ )
102
+ dataset_props: JsonObject | None = Field(
103
+ sa_column=sa.Column(sa_sqlite.JSON),
104
+ default=None,
105
+ description="Additional properties stored as JSON",
106
+ schema_extra=dict(examples=[{"example_prop_key": "example_prop_value"}]),
107
+ )
108
+
109
+ return ModelMixin
110
+
111
+
112
+ BaseModel = make_base_model()
113
+
114
+
115
+ class DatasetRef(BaseModel, make_dataset_ref_model_mixin()):
116
+ pass
117
+
118
+
119
+ class DatasetRefTable(DatasetRef, make_sequence_model_mixin("sqlite"), table=True):
120
+ __tablename__ = "dataset_ref"
121
+
122
+
123
+ if typing.TYPE_CHECKING:
124
+ class DatasetRef(BaseModel):
125
+ dataset_uuid: sa_orm.Mapped[py_uuid.UUID] = ...
126
+ dataset_name: sa_orm.Mapped[str] = ...
127
+ dataset_props: sa_orm.Mapped[JsonObject | None] = ...
128
+
129
+
130
+ class DatasetRefTable(DatasetRef, SequenceModelMixinProtocol):
131
+ pass
132
+
133
+
134
+ def current_flow_context_file_path() -> pathlib.Path:
135
+ return current_flow_workspace_root_path() / "flow_context.db"
136
+
137
+
138
+ class FlowContext(object):
139
+ def __init__(
140
+ self,
141
+ *,
142
+ file_path: str | os.PathLike[str] | None = None,
143
+ fail_if_exists: bool = False,
144
+ ):
145
+ self.file_path = pathlib.Path(file_path or current_flow_context_file_path())
146
+ if fail_if_exists and self.file_path.exists():
147
+ raise FileExistsError(f"flow context file '{self.file_path}' already exists")
148
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
149
+
150
+ self.conn_maker = ConnectionMaker.from_url(f"sqlite:///{self.file_path.absolute()}",
151
+ engine_opts=dict(connect_args={"check_same_thread": False}))
152
+
153
+ self.thread_lock = threading.Lock()
154
+
155
+ BaseModel.metadata.create_all(self.conn_maker.engine)
156
+
157
+ @contextlib.contextmanager
158
+ def make_session(self) -> Generator[sa_orm.Session, None, None]:
159
+ with self.thread_lock:
160
+ with self.conn_maker.make_session(expire_on_commit=False) as session:
161
+ try:
162
+ yield session
163
+ session.commit()
164
+ except Exception:
165
+ session.rollback()
166
+ raise
167
+
168
+ def count_dataset_ref(self) -> int:
169
+ with self.make_session() as session:
170
+ return session.query(DatasetRefTable).count()
171
+
172
+ def list_dataset_refs(self) -> list[DatasetRefTable]:
173
+ with self.make_session() as session:
174
+ return session.query(DatasetRefTable).all()
175
+
176
+ def query_dataset_ref(
177
+ self,
178
+ dataset_uuid: py_uuid.UUID | None = None,
179
+ dataset_name: str | None = None,
180
+ ) -> Chainable[Self, DatasetRefTable | None]:
181
+ if dataset_uuid is None and dataset_name is None:
182
+ raise ValueError("either 'dataset_uuid' or 'dataset_name' is required")
183
+
184
+ with self.make_session() as session:
185
+ query_stmt = session.query(DatasetRefTable)
186
+ if dataset_uuid is not None:
187
+ query_stmt = query_stmt.filter(DatasetRefTable.dataset_uuid == dataset_uuid)
188
+ if dataset_name is not None:
189
+ query_stmt = query_stmt.filter(DatasetRefTable.dataset_name == dataset_name)
190
+ db_dataset_ref = query_stmt.one_or_none()
191
+
192
+ return chainable(self, db_dataset_ref)
193
+
194
+ def add_dataset_ref(
195
+ self,
196
+ dataset_uuid: py_uuid.UUID,
197
+ dataset_name: str,
198
+ dataset_props: JsonObject | None = None,
199
+ ) -> Chainable[Self, DatasetRefTable]:
200
+ with self.make_session() as session:
201
+ dataset_ref = DatasetRef(dataset_uuid=dataset_uuid, dataset_name=dataset_name, dataset_props=dataset_props)
202
+ db_dataset_ref = clone_sequence_model_instance(DatasetRefTable, dataset_ref)
203
+ session.add(db_dataset_ref)
204
+ session.commit()
205
+
206
+ return chainable(self, db_dataset_ref)
207
+
208
+
209
+ @singleton
210
+ def flow_context() -> FlowContext:
211
+ return FlowContext()
212
+
213
+
214
+ def flow_api_url() -> str:
215
+ value = os.environ.get(flow_api_url_env)
216
+ if value is None:
217
+ raise ValueError(f"Missing required environment variable '{flow_api_url_env}'")
218
+ return value
219
+
220
+
221
+ @retry(wait=10, retrials=3)
222
+ def fetch_dataset_ref(
223
+ dataset_uuid: py_uuid.UUID | None = None,
224
+ dataset_name: str | None = None,
225
+ ) -> DatasetRef | None:
226
+ response = requests.get(f"{flow_api_url()}/pulse_demeter/dataset_public/query_dataset",
227
+ headers={"accept": "application/json"},
228
+ params={
229
+ "dataset_uuid": str(dataset_uuid),
230
+ "dataset_name": dataset_name,
231
+ })
232
+ response.raise_for_status()
233
+
234
+ response_data = response.json()
235
+
236
+ try:
237
+ uuid = py_uuid.UUID(response_data["dataset"]["uuid"])
238
+ name = response_data["dataset"]["name"]
239
+ props = response_data["dataset"].get("props", None)
240
+ return DatasetRef(dataset_uuid=uuid, dataset_name=name, dataset_props=props)
241
+ except KeyError:
242
+ return None
243
+
244
+
245
+ def resolve_dataset_ref(
246
+ dataset_uuid: py_uuid.UUID | None = None,
247
+ dataset_name: str | None = None,
248
+ *,
249
+ context: FlowContext | None = None,
250
+ ) -> DatasetRefTable:
251
+ if context is None:
252
+ context = flow_context()
253
+
254
+ if dataset_uuid is None and dataset_name is None:
255
+ raise ValueError("either 'dataset_uuid' or 'dataset_name' is required")
256
+
257
+ _, db_dataset_ref = context.query_dataset_ref(dataset_uuid=dataset_uuid, dataset_name=dataset_name)
258
+ if db_dataset_ref is None:
259
+ if not stage_can_resolve_dataset_ref():
260
+ # In some stages, the dataset ref should have been resolved and stored in the flow context.
261
+ # If not found, it's an error.
262
+ raise ValueError(f"cannot resolve dataset ref at flow stage '{current_flow_stage()}'")
263
+
264
+ dataset_ref = fetch_dataset_ref(dataset_uuid=dataset_uuid, dataset_name=dataset_name)
265
+ if dataset_ref is None:
266
+ raise ValueError(f"cannot found dataset with uuid '{str(dataset_uuid)}' or name '{dataset_name}'")
267
+
268
+ _, db_dataset_ref = context.add_dataset_ref(dataset_ref.dataset_uuid,
269
+ dataset_ref.dataset_name,
270
+ dataset_ref.dataset_props)
271
+
272
+ return db_dataset_ref
273
+
274
+
275
+ def resolve_dataset_storage_location(
276
+ dataset_uuid: str | py_uuid.UUID | None = None,
277
+ dataset_name: str | None = None,
278
+ *,
279
+ context: FlowContext | None = None,
280
+ ) -> pathlib.Path:
281
+ if isinstance(dataset_uuid, str):
282
+ return resolve_dataset_storage_location(py_uuid.UUID(dataset_uuid), dataset_name, context=context)
283
+
284
+ db_dataset_ref = resolve_dataset_ref(dataset_uuid, dataset_name, context=context)
285
+ if "storage_location" in (db_dataset_ref.dataset_props or {}):
286
+ return pathlib.Path(db_dataset_ref.dataset_props["storage_location"]).absolute()
287
+
288
+ return current_flow_workspace_root_path() / "datasets" / str(db_dataset_ref.dataset_uuid)
289
+
290
+
291
+ resolve = resolve_dataset_storage_location
@@ -26,7 +26,7 @@ from plexus.common.utils.iterutils import dicttree_add, dicttree_remove
26
26
  from plexus.common.utils.iterutils import dicttree_children, dicttree_lineage, dicttree_subtree
27
27
  from plexus.common.utils.jsonutils import JsonObject, JsonType
28
28
  from plexus.common.utils.randutils import randomizer
29
- from plexus.common.utils.strutils import is_blank
29
+ from plexus.common.utils.strutils import is_blank, parse_bool_or
30
30
  from sqlmodel import Field
31
31
 
32
32
  from plexus.pulse.common.resources.tags import predefined_tagset_specs
@@ -47,6 +47,7 @@ __all__ = [
47
47
  "MutableTagset",
48
48
  "populate_tagset",
49
49
  "predefined_tagsets",
50
+ "seek_predefined_tagset_of",
50
51
  "render_tagset_markdown_readme",
51
52
  "make_tag_target_model_mixin",
52
53
  "make_tag_record_model_mixin",
@@ -356,6 +357,23 @@ def predefined_tagsets() -> dict[str, Tagset]:
356
357
  return tagsets
357
358
 
358
359
 
360
+ def seek_predefined_tagset_of(
361
+ tag: str | Tag | BoundTag,
362
+ *,
363
+ tagset_namespace: str | None = None,
364
+ tagset_version: str | None = None,
365
+ ) -> Tagset | None:
366
+ if isinstance(tag, BoundTag):
367
+ versioned_namespace = versioned_identifier(tagset_namespace or tag.namespace, tagset_version or tag.version)
368
+ else:
369
+ versioned_namespace = versioned_identifier(tagset_namespace, tagset_version)
370
+
371
+ tagset = predefined_tagsets().get(versioned_namespace)
372
+ if tagset is None or tag not in tagset:
373
+ return None
374
+ return tagset
375
+
376
+
359
377
  def render_tagset_markdown_readme(tagset: Tagset) -> str:
360
378
  def render_desc(desc: str | RichDesc | None) -> str:
361
379
  if desc is None:
@@ -628,6 +646,16 @@ def tag_cache_file_path() -> pathlib.Path:
628
646
  return pathlib.Path.home() / ".local" / "plexus" / "tag_cache" / f"{randomizer().random_alphanumeric(7)}.db"
629
647
 
630
648
 
649
+ tag_cache_strict_mode_env = "PULSE_TAG_CACHE_STRICT_MODE"
650
+
651
+
652
+ def is_tag_cache_strict_mode() -> bool:
653
+ value = os.environ.get(tag_cache_strict_mode_env)
654
+ if value is None:
655
+ return False
656
+ return parse_bool_or(value) or False
657
+
658
+
631
659
  class TagCache(object):
632
660
  def __init__(
633
661
  self,
@@ -1089,6 +1117,13 @@ class TagCache(object):
1089
1117
  instance, this parameter will be ignored and the version from the instance will be used.
1090
1118
  :return: Self instance for chaining along with the added ``TagRecordTable`` instance
1091
1119
  """
1120
+ if is_tag_cache_strict_mode():
1121
+ tagset = seek_predefined_tagset_of(tag, tagset_namespace=tagset_namespace, tagset_version=tagset_version)
1122
+ if tagset is None:
1123
+ raise ValueError(
1124
+ f"cannot find in the predefined tagset the specified tag '{tag}', with namespace '{tagset_namespace}' and version '{tagset_version}'"
1125
+ )
1126
+
1092
1127
  with self.make_session() as session:
1093
1128
  tag_record = TagRecord(
1094
1129
  target_sqn=target_sqn,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-pulse-common
3
- Version: 1.1.77
3
+ Version: 1.1.78
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -22,6 +22,7 @@ Requires-Dist: pyparsing>=3.2
22
22
  Requires-Dist: pyproj>=3.6
23
23
  Requires-Dist: pyquaternion>=0.9
24
24
  Requires-Dist: pyyaml>=6.0
25
+ Requires-Dist: requests>=2.0
25
26
  Requires-Dist: sqlalchemy>=2.0
26
27
  Requires-Dist: s3fs>=2025.10
27
28
  Requires-Dist: rich>=13.9
@@ -14,6 +14,7 @@ pyparsing>=3.2
14
14
  pyproj>=3.6
15
15
  pyquaternion>=0.9
16
16
  pyyaml>=6.0
17
+ requests>=2.0
17
18
  sqlalchemy>=2.0
18
19
  s3fs>=2025.10
19
20
  rich>=13.9
@@ -0,0 +1,48 @@
1
+ import os
2
+ import tempfile
3
+ import unittest
4
+ import uuid as pyuuid
5
+ from unittest.mock import patch
6
+
7
+ import ddt
8
+
9
+ from plexus.pulse.common.utils.flowutils import FlowContext
10
+
11
+
12
+ @ddt.ddt
13
+ class FlowUtilsTest(unittest.TestCase):
14
+
15
+ def test_flow_context(self):
16
+ with tempfile.TemporaryDirectory(delete=True) as temp_dir:
17
+ with patch.dict(os.environ,
18
+ {
19
+ "PULSE_FLOW_WORKSPACE_ROOT_DIR": temp_dir,
20
+ "PULSE_FLOW_RUN_ID": "01234567-89ab-cdef-0123-456789abcdef",
21
+ },
22
+ clear=True):
23
+ context = (
24
+ FlowContext()
25
+ .add_dataset_ref(pyuuid.UUID("01234567-89ab-cdef-0123-456789abcdef"), "one_dataset")
26
+ .add_dataset_ref(pyuuid.UUID("89abcdef-0123-4567-89ab-cdef01234567"), "another_dataset")
27
+ .add_dataset_ref(pyuuid.UUID("00000000-0000-0000-0000-000000000000"), "zero_dataset")
28
+ )
29
+
30
+ self.assertEqual(context.count_dataset_ref(), 3)
31
+
32
+ one_dataset, another_dataset, zero_dataset = context.list_dataset_refs()
33
+
34
+ self.assertEqual(one_dataset.dataset_uuid, pyuuid.UUID("01234567-89ab-cdef-0123-456789abcdef"))
35
+ self.assertEqual(another_dataset.dataset_uuid, pyuuid.UUID("89abcdef-0123-4567-89ab-cdef01234567"))
36
+ self.assertEqual(zero_dataset.dataset_uuid, pyuuid.UUID("00000000-0000-0000-0000-000000000000"))
37
+
38
+ # This is the same flow context file as above, but we are creating a new `FlowContext` instance
39
+ # via explicit file path which is deducted from the mocked environment variables
40
+ context = FlowContext(file_path=f"{temp_dir}/01234567-89ab-cdef-0123-456789abcdef/flow_context.db")
41
+
42
+ self.assertEqual(context.count_dataset_ref(), 3)
43
+
44
+ one_dataset, another_dataset, zero_dataset = context.list_dataset_refs()
45
+
46
+ self.assertEqual(one_dataset.dataset_uuid, pyuuid.UUID("01234567-89ab-cdef-0123-456789abcdef"))
47
+ self.assertEqual(another_dataset.dataset_uuid, pyuuid.UUID("89abcdef-0123-4567-89ab-cdef01234567"))
48
+ self.assertEqual(zero_dataset.dataset_uuid, pyuuid.UUID("00000000-0000-0000-0000-000000000000"))
@@ -1,167 +0,0 @@
1
- import contextlib
2
- import enum
3
- import os
4
- import pathlib
5
- import threading
6
- import typing
7
- import uuid as py_uuid
8
- from collections.abc import Generator
9
- from typing import Self
10
-
11
- import pydantic as pdt
12
- import sqlalchemy as sa
13
- import sqlalchemy.dialects.sqlite as sa_sqlite
14
- import sqlalchemy.orm as sa_orm
15
- from plexus.common.utils.dbutils import ConnectionMaker
16
- from plexus.common.utils.funcutils import Chainable
17
- from plexus.common.utils.funcutils import chainable, singleton
18
- from sqlmodel import Field
19
-
20
- from plexus.pulse.common.utils.ormutils import SQLiteUUID, SequenceModelMixinProtocol
21
- from plexus.pulse.common.utils.ormutils import clone_sequence_model_instance, make_base_model, make_sequence_model_mixin
22
-
23
- __all__ = [
24
- "FlowStageEnum",
25
- "current_flow_stage",
26
- "current_flow_run_id",
27
- "DatasetRef",
28
- "DatasetRefTable",
29
- "flow_context_file_path",
30
- "FlowContext",
31
- "context",
32
- ]
33
-
34
-
35
- class FlowStageEnum(enum.Enum):
36
- Preprocessing = "preprocessing"
37
- Annotation = "annotation"
38
- Curation = "curation"
39
- Training = "training"
40
-
41
-
42
- def current_flow_stage() -> FlowStageEnum:
43
- stage_str = os.environ.get("PULSE_FLOW_STAGE")
44
- if stage_str is None:
45
- raise ValueError("Missing required environment variable 'PULSE_FLOW_STAGE'")
46
- for stage in list(FlowStageEnum):
47
- if stage_str == stage.value:
48
- return stage
49
- raise ValueError(f"Illegal 'PULSE_FLOW_STAGE' value '{stage_str}'")
50
-
51
-
52
- def current_flow_run_id() -> str:
53
- run_id_str = os.environ.get("PULSE_FLOW_RUN_ID")
54
- if run_id_str is None:
55
- raise ValueError("Missing required environment variable 'PULSE_FLOW_RUN_ID'")
56
- return run_id_str
57
-
58
-
59
- def make_dataset_ref_model_mixin() -> type[pdt.BaseModel]:
60
- class ModelMixin(pdt.BaseModel):
61
- dataset_uuid: py_uuid.UUID = Field(
62
- sa_column=sa.Column(SQLiteUUID(), nullable=False),
63
- description="UUID of the dataset",
64
- )
65
- dataset_name: str | None = Field(
66
- sa_column=sa.Column(sa_sqlite.VARCHAR(128), nullable=True),
67
- description="Name of the dataset",
68
- )
69
-
70
- return ModelMixin
71
-
72
-
73
- BaseModel = make_base_model()
74
-
75
-
76
- class DatasetRef(BaseModel, make_dataset_ref_model_mixin()):
77
- pass
78
-
79
-
80
- class DatasetRefTable(DatasetRef, make_sequence_model_mixin("sqlite"), table=True):
81
- __tablename__ = "dataset_ref"
82
-
83
-
84
- if typing.TYPE_CHECKING:
85
- class DatasetRef(BaseModel):
86
- dataset_uuid: sa_orm.Mapped[py_uuid.UUID] = ...
87
- dataset_name: sa_orm.Mapped[str | None] = ...
88
-
89
-
90
- class DatasetRefTable(DatasetRef, SequenceModelMixinProtocol):
91
- pass
92
-
93
-
94
- def is_resolve_dataset_ref_stage(stage: FlowStageEnum | None) -> bool:
95
- if stage is None:
96
- stage = current_flow_stage()
97
- return stage in {FlowStageEnum.Curation}
98
-
99
-
100
- def is_construct_dataset_ref_stage(stage: FlowStageEnum | None) -> bool:
101
- if stage is None:
102
- stage = current_flow_stage()
103
- return stage in {FlowStageEnum.Training}
104
-
105
-
106
- def flow_context_file_path() -> pathlib.Path:
107
- root_dir = os.environ.get("PULSE_FLOW_CONTEXT_ROOT_DIR")
108
- if root_dir is None:
109
- raise ValueError("Missing required environment variable 'PULSE_FLOW_CONTEXT_ROOT_DIR'")
110
- return pathlib.Path(root_dir) / current_flow_run_id() / "flow_context.db"
111
-
112
-
113
- class FlowContext(object):
114
- def __init__(
115
- self,
116
- *,
117
- file_path: str | os.PathLike[str] | None = None,
118
- fail_if_exists: bool = False,
119
- ):
120
- self.file_path = pathlib.Path(file_path or flow_context_file_path())
121
- if fail_if_exists and self.file_path.exists():
122
- raise FileExistsError(f"flow context file '{self.file_path}' already exists")
123
- self.file_path.parent.mkdir(parents=True, exist_ok=True)
124
-
125
- self.conn_maker = ConnectionMaker.from_url(f"sqlite:///{self.file_path.absolute()}",
126
- engine_opts=dict(connect_args={"check_same_thread": False}))
127
-
128
- self.thread_lock = threading.Lock()
129
-
130
- BaseModel.metadata.create_all(self.conn_maker.engine)
131
-
132
- @contextlib.contextmanager
133
- def make_session(self) -> Generator[sa_orm.Session, None, None]:
134
- with self.thread_lock:
135
- with self.conn_maker.make_session(expire_on_commit=False) as session:
136
- try:
137
- yield session
138
- session.commit()
139
- except Exception:
140
- session.rollback()
141
- raise
142
-
143
- def count_dataset_ref(self) -> int:
144
- with self.make_session() as session:
145
- return session.query(DatasetRefTable).count()
146
-
147
- def list_dataset_refs(self) -> list[DatasetRefTable]:
148
- with self.make_session() as session:
149
- return session.query(DatasetRefTable).all()
150
-
151
- def add_dataset_ref(
152
- self,
153
- dataset_uuid: py_uuid.UUID,
154
- dataset_name: str | None = None,
155
- ) -> Chainable[Self, DatasetRefTable]:
156
- with self.make_session() as session:
157
- dataset_ref = DatasetRef(dataset_uuid=dataset_uuid, dataset_name=dataset_name)
158
- db_dataset_ref = clone_sequence_model_instance(DatasetRefTable, dataset_ref)
159
- session.add(db_dataset_ref)
160
- session.commit()
161
-
162
- return chainable(self, db_dataset_ref)
163
-
164
-
165
- @singleton
166
- def context() -> FlowContext:
167
- return FlowContext()
@@ -1,36 +0,0 @@
1
- import os
2
- import tempfile
3
- import unittest
4
- import uuid as pyuuid
5
- from unittest.mock import patch
6
-
7
- import ddt
8
-
9
- from plexus.pulse.common.utils.flowutils import FlowContext
10
-
11
-
12
- @ddt.ddt
13
- class FlowUtilsTest(unittest.TestCase):
14
-
15
- def test_flow_context(self):
16
- with tempfile.TemporaryDirectory(delete=True) as temp_dir:
17
- with patch.dict(os.environ,
18
- {
19
- "PULSE_FLOW_CONTEXT_ROOT_DIR": temp_dir,
20
- "PULSE_FLOW_RUN_ID": "01234567-89ab-cdef-0123-456789abcdef",
21
- },
22
- clear=True):
23
- context = (
24
- FlowContext()
25
- .add_dataset_ref(pyuuid.UUID("01234567-89ab-cdef-0123-456789abcdef"), "test_dataset")
26
- .add_dataset_ref(pyuuid.UUID("89abcdef-0123-4567-89ab-cdef01234567"), "another_test_dataset")
27
- .add_dataset_ref(pyuuid.UUID("00000000-0000-0000-0000-000000000000"))
28
- )
29
-
30
- self.assertEqual(context.count_dataset_ref(), 3)
31
-
32
- dataset, another_dataset, anonymous_dataset = context.list_dataset_refs()
33
-
34
- self.assertEqual(dataset.dataset_uuid, pyuuid.UUID("01234567-89ab-cdef-0123-456789abcdef"))
35
- self.assertEqual(another_dataset.dataset_uuid, pyuuid.UUID("89abcdef-0123-4567-89ab-cdef01234567"))
36
- self.assertEqual(anonymous_dataset.dataset_uuid, pyuuid.UUID("00000000-0000-0000-0000-000000000000"))