plexus-python-pulse-common 1.1.77__py3-none-any.whl → 1.1.79__py3-none-any.whl

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.
@@ -9,28 +9,62 @@ from collections.abc import Generator
9
9
  from typing import Self
10
10
 
11
11
  import pydantic as pdt
12
+ import requests
12
13
  import sqlalchemy as sa
13
14
  import sqlalchemy.dialects.sqlite as sa_sqlite
14
15
  import sqlalchemy.orm as sa_orm
15
16
  from plexus.common.utils.dbutils import ConnectionMaker
16
17
  from plexus.common.utils.funcutils import Chainable
17
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
18
21
  from sqlmodel import Field
19
22
 
20
23
  from plexus.pulse.common.utils.ormutils import SQLiteUUID, SequenceModelMixinProtocol
21
24
  from plexus.pulse.common.utils.ormutils import clone_sequence_model_instance, make_base_model, make_sequence_model_mixin
22
25
 
23
26
  __all__ = [
27
+ "flow_workspace_root_path",
28
+ "current_flow_run_id",
29
+ "current_flow_workspace_root_path",
24
30
  "FlowStageEnum",
25
31
  "current_flow_stage",
26
- "current_flow_run_id",
32
+ "stage_can_resolve_dataset_ref",
33
+ "make_dataset_ref_model_mixin",
27
34
  "DatasetRef",
28
35
  "DatasetRefTable",
29
- "flow_context_file_path",
36
+ "current_flow_context_file_path",
30
37
  "FlowContext",
31
- "context",
38
+ "flow_context",
39
+ "flow_api_url",
40
+ "resolve_dataset_ref",
41
+ "resolve_dataset_storage_location",
42
+ "resolve",
32
43
  ]
33
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
+
34
68
 
35
69
  class FlowStageEnum(enum.Enum):
36
70
  Preprocessing = "preprocessing"
@@ -40,20 +74,19 @@ class FlowStageEnum(enum.Enum):
40
74
 
41
75
 
42
76
  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'")
77
+ value = os.environ.get(flow_stage_env)
78
+ if value is None:
79
+ raise ValueError(f"Missing required environment variable '{flow_stage_env}'")
46
80
  for stage in list(FlowStageEnum):
47
- if stage_str == stage.value:
81
+ if value == stage.value:
48
82
  return stage
49
- raise ValueError(f"Illegal 'PULSE_FLOW_STAGE' value '{stage_str}'")
83
+ raise ValueError(f"Illegal '{flow_stage_env}' value '{value}'")
50
84
 
51
85
 
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
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}
57
90
 
58
91
 
59
92
  def make_dataset_ref_model_mixin() -> type[pdt.BaseModel]:
@@ -62,10 +95,16 @@ def make_dataset_ref_model_mixin() -> type[pdt.BaseModel]:
62
95
  sa_column=sa.Column(SQLiteUUID(), nullable=False),
63
96
  description="UUID of the dataset",
64
97
  )
65
- dataset_name: str | None = Field(
66
- sa_column=sa.Column(sa_sqlite.VARCHAR(128), nullable=True),
98
+ dataset_name: str = Field(
99
+ sa_column=sa.Column(sa_sqlite.VARCHAR(128), nullable=False),
67
100
  description="Name of the dataset",
68
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
+ )
69
108
 
70
109
  return ModelMixin
71
110
 
@@ -84,30 +123,16 @@ class DatasetRefTable(DatasetRef, make_sequence_model_mixin("sqlite"), table=Tru
84
123
  if typing.TYPE_CHECKING:
85
124
  class DatasetRef(BaseModel):
86
125
  dataset_uuid: sa_orm.Mapped[py_uuid.UUID] = ...
87
- dataset_name: sa_orm.Mapped[str | None] = ...
126
+ dataset_name: sa_orm.Mapped[str] = ...
127
+ dataset_props: sa_orm.Mapped[JsonObject | None] = ...
88
128
 
89
129
 
90
130
  class DatasetRefTable(DatasetRef, SequenceModelMixinProtocol):
91
131
  pass
92
132
 
93
133
 
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"
134
+ def current_flow_context_file_path() -> pathlib.Path:
135
+ return current_flow_workspace_root_path() / "flow_context.db"
111
136
 
112
137
 
113
138
  class FlowContext(object):
@@ -117,7 +142,7 @@ class FlowContext(object):
117
142
  file_path: str | os.PathLike[str] | None = None,
118
143
  fail_if_exists: bool = False,
119
144
  ):
120
- self.file_path = pathlib.Path(file_path or flow_context_file_path())
145
+ self.file_path = pathlib.Path(file_path or current_flow_context_file_path())
121
146
  if fail_if_exists and self.file_path.exists():
122
147
  raise FileExistsError(f"flow context file '{self.file_path}' already exists")
123
148
  self.file_path.parent.mkdir(parents=True, exist_ok=True)
@@ -148,13 +173,32 @@ class FlowContext(object):
148
173
  with self.make_session() as session:
149
174
  return session.query(DatasetRefTable).all()
150
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
+
151
194
  def add_dataset_ref(
152
195
  self,
153
196
  dataset_uuid: py_uuid.UUID,
154
- dataset_name: str | None = None,
197
+ dataset_name: str,
198
+ dataset_props: JsonObject | None = None,
155
199
  ) -> Chainable[Self, DatasetRefTable]:
156
200
  with self.make_session() as session:
157
- dataset_ref = DatasetRef(dataset_uuid=dataset_uuid, dataset_name=dataset_name)
201
+ dataset_ref = DatasetRef(dataset_uuid=dataset_uuid, dataset_name=dataset_name, dataset_props=dataset_props)
158
202
  db_dataset_ref = clone_sequence_model_instance(DatasetRefTable, dataset_ref)
159
203
  session.add(db_dataset_ref)
160
204
  session.commit()
@@ -163,5 +207,85 @@ class FlowContext(object):
163
207
 
164
208
 
165
209
  @singleton
166
- def context() -> FlowContext:
210
+ def flow_context() -> FlowContext:
167
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.79
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -16,12 +16,14 @@ Requires-Dist: jsonschema>=4.0
16
16
  Requires-Dist: lxml>=6.0
17
17
  Requires-Dist: markdown-it-py>=4.0
18
18
  Requires-Dist: numpy>=2.3
19
+ Requires-Dist: packaging>=25.0
19
20
  Requires-Dist: pydantic>=2.8
20
21
  Requires-Dist: pygeodesy>=24.10
21
22
  Requires-Dist: pyparsing>=3.2
22
23
  Requires-Dist: pyproj>=3.6
23
24
  Requires-Dist: pyquaternion>=0.9
24
25
  Requires-Dist: pyyaml>=6.0
26
+ Requires-Dist: requests>=2.0
25
27
  Requires-Dist: sqlalchemy>=2.0
26
28
  Requires-Dist: s3fs>=2025.10
27
29
  Requires-Dist: rich>=13.9
@@ -14,7 +14,7 @@ plexus/pulse/common/utils/bagutils.py,sha256=5JGnMgR8Q9OciW5dE7_2U7n1FGVEXMZXv1S
14
14
  plexus/pulse/common/utils/config.py,sha256=YPtFK5QHTVsXwcFBGV0rlZad0GFtsgwysQNyKYjdzvk,2409
15
15
  plexus/pulse/common/utils/datautils.py,sha256=OWlnyFSa6UaPbSSG3HzCJHxdiHyfHAE-FUVHaLfNZoM,9335
16
16
  plexus/pulse/common/utils/dockerutils.py,sha256=7DF9yFGz8uHwpYk115xBWqOjs5etz0UE7yh66eDvBzs,5793
17
- plexus/pulse/common/utils/flowutils.py,sha256=VmbVcCVztH8DH47s_5JbdOZchsEc7hZbq1am95amJzs,5307
17
+ plexus/pulse/common/utils/flowutils.py,sha256=MGu15KL9mw-LyGGLceVmY-c2FGer5PURnMZWFSJN--I,10182
18
18
  plexus/pulse/common/utils/gisutils.py,sha256=UX_IHPeAx9mbA9p_SsbPFwdsdp7bGeaQhWaCTCzvrBM,11466
19
19
  plexus/pulse/common/utils/jsonutils.py,sha256=zSbW94klN2jrkMdBECu4RNJX6zzNaSxauQ3FuihuB6E,3342
20
20
  plexus/pulse/common/utils/ormutils.py,sha256=JD-hKLEGkFWy7ojc3dC_90qtexe2UnLEvD90tEOHtCg,61897
@@ -22,9 +22,9 @@ plexus/pulse/common/utils/pathutils.py,sha256=S5xk2WEmxP8MiuSKIU3cAEtk4Nk5Lz7IqC
22
22
  plexus/pulse/common/utils/s3utils.py,sha256=RArl95rWoyBEFyWU9X0c8WgoQBjP_jOK3OkPoKxIMnA,36025
23
23
  plexus/pulse/common/utils/sqlutils.py,sha256=D6kTBjhO5YlNRt3uFlPt6z3uH61m9ajEzPYmsI6NoFc,231
24
24
  plexus/pulse/common/utils/strutils.py,sha256=PkmMHCcHN9vALCy_8fJTf00Dg7I2N4QTCXdBmB3t7rk,16440
25
- plexus/pulse/common/utils/tagutils.py,sha256=smM79dyRCiswKTTcI-At7FLxDH54VN4Id3aFHbDxzYo,61696
25
+ plexus/pulse/common/utils/tagutils.py,sha256=-7cZWVpmtI7R8bB-SBtHPw4WyYJKp8AptH_Rfc-5sk4,62938
26
26
  plexus/pulse/common/utils/testutils.py,sha256=hKPRvoN4mg8QGQJH121l59a0uo8jN5rntGQAVYplGBw,6166
27
- plexus_python_pulse_common-1.1.77.dist-info/METADATA,sha256=Aei_-DjiA9AuGRzaDCZ5Vp3R8dJxcjXJSKFZ7SZwRHQ,1495
28
- plexus_python_pulse_common-1.1.77.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
- plexus_python_pulse_common-1.1.77.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
30
- plexus_python_pulse_common-1.1.77.dist-info/RECORD,,
27
+ plexus_python_pulse_common-1.1.79.dist-info/METADATA,sha256=yWNshrrquuxOYcZVF9KyXRMxqqTUZVOviSYLaapI_Co,1555
28
+ plexus_python_pulse_common-1.1.79.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
+ plexus_python_pulse_common-1.1.79.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
30
+ plexus_python_pulse_common-1.1.79.dist-info/RECORD,,