plexus-python-pulse-common 1.1.76__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.
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/PKG-INFO +2 -1
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/pyproject.toml +2 -0
- plexus_python_pulse_common-1.1.78/src/plexus/pulse/common/utils/flowutils.py +291 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/ormutils.py +38 -1
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/tagutils.py +36 -1
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/PKG-INFO +2 -1
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/SOURCES.txt +2 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/requires.txt +1 -0
- plexus_python_pulse_common-1.1.78/test/plexus_tests/pulse/common/utils/flowutils_test.py +48 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.editorconfig +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.github/workflows/pr.yml +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.github/workflows/push.yml +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.gitignore +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/MANIFEST.in +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/README.md +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/VERSION +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/0-dummy +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/1-dummy +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/2-dummy +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.0.vol-0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.0.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.1.vol-1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.1.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.2.vol-2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.2.jsonl +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part0 +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part1 +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.csv.part2 +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/pathutils/dummy.txt +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.76 → 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
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/setup.cfg +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/setup.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMFile.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMNode.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMTags.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/OSMWay.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/apiutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/bagutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/config.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/datautils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/dockerutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/gisutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/jsonutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/pathutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/s3utils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/sqlutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/strutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus/pulse/common/utils/testutils.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/dependency_links.txt +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/not-zip-safe +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/src/plexus_python_pulse_common.egg-info/top_level.txt +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/osm_file_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/carto/osm_tags_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/__init__.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/bagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/datautils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/dockerutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/gisutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/jsonutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/ormutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/pathutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/s3utils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/strutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/tagutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/test/plexus_tests/pulse/common/utils/testutils_test.py +0 -0
- {plexus_python_pulse_common-1.1.76 → 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.
|
|
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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import uuid as py_uuid
|
|
2
3
|
from collections.abc import Iterable, Sequence
|
|
3
4
|
from typing import Protocol, Self
|
|
4
5
|
|
|
@@ -28,8 +29,10 @@ __all__ = [
|
|
|
28
29
|
"ChangingModelMixinProtocol",
|
|
29
30
|
"SnapshotModelMixinProtocol",
|
|
30
31
|
"RevisionModelMixinProtocol",
|
|
32
|
+
"SQLiteUUID",
|
|
31
33
|
"SQLiteDateTime",
|
|
32
34
|
"model_sqn_type",
|
|
35
|
+
"model_uuid_type",
|
|
33
36
|
"model_datetime_tz_type",
|
|
34
37
|
"model_revision_type",
|
|
35
38
|
"SequenceModelMixin",
|
|
@@ -395,11 +398,29 @@ class RevisionModelMixinProtocol(SequenceModelMixinProtocol):
|
|
|
395
398
|
...
|
|
396
399
|
|
|
397
400
|
|
|
401
|
+
class SQLiteUUID(sa.TypeDecorator):
|
|
402
|
+
"""
|
|
403
|
+
Custom SQLAlchemy type decorator for handling UUIDs in SQLite.
|
|
404
|
+
"""
|
|
405
|
+
impl = sa.CHAR(36)
|
|
406
|
+
cache_ok = True
|
|
407
|
+
|
|
408
|
+
def process_bind_param(self, value: py_uuid.UUID | None, dialect) -> str | None:
|
|
409
|
+
if value is None:
|
|
410
|
+
return None
|
|
411
|
+
return str(value)
|
|
412
|
+
|
|
413
|
+
def process_result_value(self, value: str | None, dialect) -> py_uuid.UUID | None:
|
|
414
|
+
if value is None:
|
|
415
|
+
return None
|
|
416
|
+
return py_uuid.UUID(value)
|
|
417
|
+
|
|
418
|
+
|
|
398
419
|
class SQLiteDateTime(sa.TypeDecorator):
|
|
399
420
|
"""
|
|
400
421
|
Custom SQLAlchemy type decorator for handling timezone-aware datetimes in SQLite.
|
|
401
422
|
"""
|
|
402
|
-
impl = sa.
|
|
423
|
+
impl = sa.DATETIME
|
|
403
424
|
cache_ok = True
|
|
404
425
|
|
|
405
426
|
def process_bind_param(self, value: datetime.datetime | None, dialect) -> datetime.datetime | None:
|
|
@@ -433,6 +454,22 @@ def model_sqn_type(dialect: str | None = None) -> sa.types.TypeEngine[int]:
|
|
|
433
454
|
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
434
455
|
|
|
435
456
|
|
|
457
|
+
def model_uuid_type(dialect: str | None = None) -> sa.types.TypeEngine[py_uuid.UUID]:
|
|
458
|
+
"""
|
|
459
|
+
Returns the appropriate SQLAlchemy column type for a UUID based on the specified database dialect.
|
|
460
|
+
|
|
461
|
+
:param dialect: The database dialect to determine the column type for. If None, defaults to PostgreSQL.
|
|
462
|
+
:return: The SQLAlchemy column type to use for the UUID field.
|
|
463
|
+
"""
|
|
464
|
+
if dialect is None:
|
|
465
|
+
dialect = dialects.postgresql
|
|
466
|
+
if dialect == dialects.postgresql:
|
|
467
|
+
return sa_pg.UUID(as_uuid=True)
|
|
468
|
+
if dialect == dialects.sqlite:
|
|
469
|
+
return SQLiteUUID
|
|
470
|
+
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
471
|
+
|
|
472
|
+
|
|
436
473
|
def model_datetime_tz_type(dialect: str | None = None) -> sa.types.TypeEngine[datetime.datetime]:
|
|
437
474
|
"""
|
|
438
475
|
Returns the appropriate SQLAlchemy column type for a timezone-aware datetime field based on the specified database
|
|
@@ -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.
|
|
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
|
|
@@ -54,6 +54,7 @@ src/plexus/pulse/common/utils/bagutils.py
|
|
|
54
54
|
src/plexus/pulse/common/utils/config.py
|
|
55
55
|
src/plexus/pulse/common/utils/datautils.py
|
|
56
56
|
src/plexus/pulse/common/utils/dockerutils.py
|
|
57
|
+
src/plexus/pulse/common/utils/flowutils.py
|
|
57
58
|
src/plexus/pulse/common/utils/gisutils.py
|
|
58
59
|
src/plexus/pulse/common/utils/jsonutils.py
|
|
59
60
|
src/plexus/pulse/common/utils/ormutils.py
|
|
@@ -80,6 +81,7 @@ test/plexus_tests/pulse/common/utils/__init__.py
|
|
|
80
81
|
test/plexus_tests/pulse/common/utils/bagutils_test.py
|
|
81
82
|
test/plexus_tests/pulse/common/utils/datautils_test.py
|
|
82
83
|
test/plexus_tests/pulse/common/utils/dockerutils_test.py
|
|
84
|
+
test/plexus_tests/pulse/common/utils/flowutils_test.py
|
|
83
85
|
test/plexus_tests/pulse/common/utils/gisutils_test.py
|
|
84
86
|
test/plexus_tests/pulse/common/utils/jsonutils_test.py
|
|
85
87
|
test/plexus_tests/pulse/common/utils/ormutils_test.py
|
|
@@ -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"))
|
|
File without changes
|
{plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.github/workflows/pr.yml
RENAMED
|
File without changes
|
{plexus_python_pulse_common-1.1.76 → plexus_python_pulse_common-1.1.78}/.github/workflows/push.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|