plexus-python-pulse-demeter 1.1.126__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.
- plexus/pulse/demeter/__init__.py +6 -0
- plexus/pulse/demeter/app/__init__.py +0 -0
- plexus/pulse/demeter/app/api/__init__.py +44 -0
- plexus/pulse/demeter/app/api/archive_internal_endpoints.py +316 -0
- plexus/pulse/demeter/app/api/archive_public_endpoints.py +255 -0
- plexus/pulse/demeter/app/api/dataset_internal_endpoints.py +1043 -0
- plexus/pulse/demeter/app/api/dataset_public_endpoints.py +2161 -0
- plexus/pulse/demeter/app/api/dataset_workflow_endpoints.py +532 -0
- plexus/pulse/demeter/app/api/tagging_endpoints.py +323 -0
- plexus/pulse/demeter/app/api/user_endpoints.py +78 -0
- plexus/pulse/demeter/app/core/__init__.py +0 -0
- plexus/pulse/demeter/app/core/database.py +43 -0
- plexus/pulse/demeter/app/core/vault.py +29 -0
- plexus/pulse/demeter/app/main.py +16 -0
- plexus/pulse/demeter/app/models/__init__.py +33 -0
- plexus/pulse/demeter/app/models/local.py +249 -0
- plexus/pulse/demeter/app/models/registry/__init__.py +112 -0
- plexus/pulse/demeter/app/models/registry/archive.py +529 -0
- plexus/pulse/demeter/app/models/registry/dataset.py +1531 -0
- plexus/pulse/demeter/app/models/registry/tagging.py +478 -0
- plexus/pulse/demeter/app/models/registry/user.py +79 -0
- plexus/pulse/demeter/tools/__init__.py +0 -0
- plexus/pulse/demeter/tools/main.py +306 -0
- plexus/pulse/demeter/utils/__init__.py +0 -0
- plexus/pulse/demeter/utils/config.py +25 -0
- plexus/pulse/demeter/utils/datautils.py +436 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/METADATA +36 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/RECORD +31 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/WHEEL +5 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/entry_points.txt +2 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import fnmatch
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import uuid as py_uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
from cloudpathlib import AnyPath, CloudPath
|
|
10
|
+
from plexus.common.utils.funcutils import singleton
|
|
11
|
+
from returns.result import Failure, Result, Success
|
|
12
|
+
|
|
13
|
+
from plexus.pulse.demeter.app.models.local import DatasetMetafile, SampleMetafile, StandaloneDataset, StandaloneSample
|
|
14
|
+
from plexus.pulse.demeter.app.models.registry.dataset import SampleLayout
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"dataset_metafile_filename",
|
|
18
|
+
"sample_metafile_filename",
|
|
19
|
+
"walk_standalone_sample",
|
|
20
|
+
"validate_standalone_sample",
|
|
21
|
+
"collect_standalone_dataset_samples",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@singleton
|
|
26
|
+
def dataset_metafile_filename() -> str:
|
|
27
|
+
return "pulse-demeter-dataset.json"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@singleton
|
|
31
|
+
def sample_metafile_filename() -> str:
|
|
32
|
+
return "pulse-demeter-sample.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass(frozen=True)
|
|
36
|
+
class ValidationFailure(object):
|
|
37
|
+
path: str
|
|
38
|
+
reason: str
|
|
39
|
+
exception: Exception | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def find_layout(
|
|
43
|
+
layout_reference: str | py_uuid.UUID,
|
|
44
|
+
layouts: list[SampleLayout],
|
|
45
|
+
) -> SampleLayout | None:
|
|
46
|
+
for layout in layouts:
|
|
47
|
+
if isinstance(layout_reference, py_uuid.UUID):
|
|
48
|
+
if layout.uuid == layout_reference:
|
|
49
|
+
return layout
|
|
50
|
+
else:
|
|
51
|
+
if layout.name == layout_reference:
|
|
52
|
+
return layout
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def retrieve_dataset_metafile(
|
|
57
|
+
dataset_path: Path | CloudPath,
|
|
58
|
+
) -> Result[DatasetMetafile | None, ValidationFailure]:
|
|
59
|
+
"""
|
|
60
|
+
Retrieve the dataset metafile for a given dataset path.
|
|
61
|
+
|
|
62
|
+
:param dataset_path: absolute path to the dataset root
|
|
63
|
+
|
|
64
|
+
:return: Result containing optional ``DatasetMetafile``, or a ``ValidationFailure`` on error.
|
|
65
|
+
"""
|
|
66
|
+
dataset_metafile_path = dataset_path / dataset_metafile_filename()
|
|
67
|
+
|
|
68
|
+
if dataset_metafile_path.is_file():
|
|
69
|
+
try:
|
|
70
|
+
with dataset_metafile_path.open("r", encoding="utf-8") as fh:
|
|
71
|
+
metafile = DatasetMetafile.model_validate(json.load(fh))
|
|
72
|
+
return Success(metafile)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return Failure(
|
|
75
|
+
ValidationFailure("", f"error reading and parsing dataset metafile '{dataset_metafile_filename()}'", e))
|
|
76
|
+
else:
|
|
77
|
+
# no dataset metafile found, considered as non-fatal
|
|
78
|
+
return Success(None)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def retrieve_sample_metafile_and_layout(
|
|
82
|
+
sample_path: Path | CloudPath,
|
|
83
|
+
layouts: list[SampleLayout],
|
|
84
|
+
) -> Result[tuple[SampleMetafile | None, SampleLayout | None], ValidationFailure]:
|
|
85
|
+
"""
|
|
86
|
+
Retrieve the sample metafile and corresponding layout for a given sample path.
|
|
87
|
+
|
|
88
|
+
:param sample_path: absolute path to the sample root
|
|
89
|
+
:param layouts: list of available ``SampleLayout`` objects
|
|
90
|
+
|
|
91
|
+
:return: Result containing optional tuple of ``SampleMetafile`` and ``SampleLayout``, or a ``ValidationFailure``
|
|
92
|
+
on error.
|
|
93
|
+
"""
|
|
94
|
+
sample_metafile_path = sample_path / sample_metafile_filename()
|
|
95
|
+
|
|
96
|
+
if sample_metafile_path.is_file():
|
|
97
|
+
try:
|
|
98
|
+
with sample_metafile_path.open("r", encoding="utf-8") as fh:
|
|
99
|
+
metafile = SampleMetafile.model_validate(json.load(fh))
|
|
100
|
+
except Exception as e:
|
|
101
|
+
return Failure(
|
|
102
|
+
ValidationFailure("", f"error reading and parsing sample metafile '{sample_metafile_filename()}'", e))
|
|
103
|
+
else:
|
|
104
|
+
# no sample metafile found, considered as non-fatal
|
|
105
|
+
return Success((None, None))
|
|
106
|
+
|
|
107
|
+
layout_reference = metafile.layout_uuid or metafile.layout_name
|
|
108
|
+
if layout_reference is None:
|
|
109
|
+
return Failure(ValidationFailure("", f"missing 'layout_uuid' or 'layout_name' in sample metafile"))
|
|
110
|
+
|
|
111
|
+
layout = find_layout(layout_reference, layouts)
|
|
112
|
+
if layout is None:
|
|
113
|
+
return Failure(ValidationFailure("", f"layout of reference '{layout_reference}' is missing"))
|
|
114
|
+
|
|
115
|
+
return Success((metafile, layout))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@singleton
|
|
119
|
+
def anonymous_layout_uuid() -> py_uuid.UUID:
|
|
120
|
+
"""UUID used for anonymous layouts in sample validation."""
|
|
121
|
+
return py_uuid.UUID("00000000-0000-0000-0000-000000000000")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def walk_standalone_sample(
|
|
125
|
+
dataset_path: Path | CloudPath,
|
|
126
|
+
sample_path: Path | CloudPath,
|
|
127
|
+
layout: SampleLayout,
|
|
128
|
+
layouts: list[SampleLayout],
|
|
129
|
+
*,
|
|
130
|
+
ignore_metafile: bool = False,
|
|
131
|
+
) -> Iterator[Result[StandaloneSample, ValidationFailure]]:
|
|
132
|
+
"""
|
|
133
|
+
Walk and validate a standalone sample against its layout.
|
|
134
|
+
|
|
135
|
+
:param dataset_path: absolute path to the dataset root
|
|
136
|
+
:param sample_path: absolute path to the sample root
|
|
137
|
+
:param layout: layout to validate against
|
|
138
|
+
:param layouts: list of available ``SampleLayout`` objects
|
|
139
|
+
:param ignore_metafile: whether to ignore the sample metafile during validation
|
|
140
|
+
|
|
141
|
+
:return: Iterator of Results containing ``StandaloneSample`` or ``ValidationFailure``
|
|
142
|
+
"""
|
|
143
|
+
if not dataset_path.is_absolute():
|
|
144
|
+
raise ValueError(f"'{dataset_path}' is not an absolute path")
|
|
145
|
+
if not sample_path.is_absolute():
|
|
146
|
+
raise ValueError(f"'{sample_path}' is not an absolute path")
|
|
147
|
+
|
|
148
|
+
relpath = str(sample_path.relative_to(dataset_path))
|
|
149
|
+
|
|
150
|
+
if not sample_path.exists():
|
|
151
|
+
yield Failure(ValidationFailure(relpath, f"missing required path '{relpath}'"))
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
if layout is None:
|
|
155
|
+
yield Failure(ValidationFailure(relpath, "missing layout for sample validation"))
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
metafile = None
|
|
159
|
+
metafile_layout = None
|
|
160
|
+
if not ignore_metafile:
|
|
161
|
+
match retrieve_sample_metafile_and_layout(sample_path, layouts):
|
|
162
|
+
case Success((metafile, metafile_layout)):
|
|
163
|
+
pass
|
|
164
|
+
case Failure(failure):
|
|
165
|
+
yield Failure(ValidationFailure(relpath, failure.reason, failure.exception))
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
if not ignore_metafile:
|
|
169
|
+
if layout.uuid != anonymous_layout_uuid():
|
|
170
|
+
if metafile_layout is None:
|
|
171
|
+
yield Failure(ValidationFailure(relpath, "missing sample metafile for layout validation"))
|
|
172
|
+
return
|
|
173
|
+
elif layout.uuid != metafile_layout.uuid:
|
|
174
|
+
yield Failure(ValidationFailure(relpath,
|
|
175
|
+
f"layout UUID '{layout.uuid}' does not match sample meta layout UUID '{metafile_layout.uuid}'"))
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
layout_spec = layout.layout_spec.get("layout")
|
|
179
|
+
layout_spec_type = layout_spec.get("type")
|
|
180
|
+
|
|
181
|
+
if layout_spec_type == "uuid":
|
|
182
|
+
# If the layout spec references a layout by UUID, validate against the layout specified in the sample metafile
|
|
183
|
+
try:
|
|
184
|
+
layout = find_layout(py_uuid.UUID(layout_spec.get("value")), layouts)
|
|
185
|
+
except ValueError as e:
|
|
186
|
+
yield Failure(ValidationFailure(relpath, "invalid UUID in layout spec", e))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
yield from walk_standalone_sample(dataset_path,
|
|
190
|
+
sample_path,
|
|
191
|
+
layout,
|
|
192
|
+
layouts,
|
|
193
|
+
ignore_metafile=ignore_metafile)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
if layout_spec_type == "name":
|
|
197
|
+
# If the layout spec references a layout by name, validate against the layout specified in the sample metafile
|
|
198
|
+
layout = find_layout(layout_spec.get("value"), layouts)
|
|
199
|
+
|
|
200
|
+
yield from walk_standalone_sample(dataset_path,
|
|
201
|
+
sample_path,
|
|
202
|
+
layout,
|
|
203
|
+
layouts,
|
|
204
|
+
ignore_metafile=ignore_metafile)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
if layout_spec_type == "file" and not sample_path.is_file():
|
|
208
|
+
yield Failure(ValidationFailure(relpath, "expected file but not a file"))
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
if layout_spec_type == "directory" and not sample_path.is_dir():
|
|
212
|
+
yield Failure(ValidationFailure(relpath, "expected directory but not a directory"))
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
if layout_spec_type == "file":
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
layout_spec_children = layout_spec.get("children", [])
|
|
219
|
+
allow_extra = layout_spec.get("allow_extra", True)
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
entries = sorted(list(sample_path.iterdir()))
|
|
223
|
+
except NotADirectoryError as e:
|
|
224
|
+
yield Failure(ValidationFailure(relpath, "expected directory to list children", e))
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
child_samples: list[StandaloneSample] = []
|
|
228
|
+
matched_entries: set[Path | CloudPath] = set()
|
|
229
|
+
|
|
230
|
+
for idx, layout_spec_child in enumerate(layout_spec_children):
|
|
231
|
+
child_matcher = layout_spec_child.get("matcher")
|
|
232
|
+
if child_matcher is None:
|
|
233
|
+
yield Failure(ValidationFailure(relpath, f"child spec at index {idx} missing 'matcher' key"))
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
child_layout = SampleLayout(uuid=anonymous_layout_uuid(),
|
|
237
|
+
name="anonymous",
|
|
238
|
+
description="",
|
|
239
|
+
layout_spec={"layout": layout_spec_child.get("layout")},
|
|
240
|
+
tags=[],
|
|
241
|
+
props={},
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
child_matcher_type = child_matcher.get("type")
|
|
245
|
+
child_matcher_pattern = child_matcher.get("pattern")
|
|
246
|
+
|
|
247
|
+
if child_matcher_type == "exact":
|
|
248
|
+
def matcher_func(p: Path | CloudPath) -> bool:
|
|
249
|
+
return p.name == child_matcher_pattern
|
|
250
|
+
elif child_matcher_type == "regex":
|
|
251
|
+
def matcher_func(p: Path | CloudPath) -> bool:
|
|
252
|
+
return re.match(child_matcher_pattern, p.name) is not None
|
|
253
|
+
elif child_matcher_type == "glob":
|
|
254
|
+
def matcher_func(p: Path | CloudPath) -> bool:
|
|
255
|
+
return fnmatch.fnmatch(p.name, child_matcher_pattern)
|
|
256
|
+
else:
|
|
257
|
+
def matcher_func(p: Path | CloudPath) -> bool:
|
|
258
|
+
return True # match all if no type specified
|
|
259
|
+
|
|
260
|
+
matches_count = 0
|
|
261
|
+
for entry in entries:
|
|
262
|
+
if not matcher_func(entry):
|
|
263
|
+
continue
|
|
264
|
+
|
|
265
|
+
if entry in matched_entries:
|
|
266
|
+
yield Failure(ValidationFailure(relpath, f"entry '{entry.name}' matched multiple times"))
|
|
267
|
+
|
|
268
|
+
matched_entries.add(entry)
|
|
269
|
+
matches_count += 1
|
|
270
|
+
|
|
271
|
+
for result in walk_standalone_sample(dataset_path,
|
|
272
|
+
entry,
|
|
273
|
+
child_layout,
|
|
274
|
+
layouts,
|
|
275
|
+
ignore_metafile=ignore_metafile):
|
|
276
|
+
match result:
|
|
277
|
+
case Success(sample):
|
|
278
|
+
child_samples.append(sample)
|
|
279
|
+
case _:
|
|
280
|
+
pass
|
|
281
|
+
yield result
|
|
282
|
+
|
|
283
|
+
min_items = child_matcher.get("min_items")
|
|
284
|
+
if min_items is not None and matches_count < min_items:
|
|
285
|
+
yield Failure(ValidationFailure(relpath,
|
|
286
|
+
f"found {matches_count} matches for '{child_matcher_pattern}' < min_items {min_items}"))
|
|
287
|
+
|
|
288
|
+
max_items = child_matcher.get("max_items")
|
|
289
|
+
if max_items is not None and matches_count > max_items:
|
|
290
|
+
yield Failure(ValidationFailure(relpath,
|
|
291
|
+
f"found {matches_count} matches for '{child_matcher_pattern}' > max_items {max_items}"))
|
|
292
|
+
|
|
293
|
+
if not allow_extra:
|
|
294
|
+
for entry in entries:
|
|
295
|
+
if entry in matched_entries:
|
|
296
|
+
continue
|
|
297
|
+
if entry.is_file() and entry.name == sample_metafile_filename():
|
|
298
|
+
continue # allow sample metafile by default
|
|
299
|
+
yield Failure(ValidationFailure(relpath, f"unexpected extra entry '{entry.name}'"))
|
|
300
|
+
|
|
301
|
+
if metafile is not None:
|
|
302
|
+
try:
|
|
303
|
+
sample = StandaloneSample(path=relpath,
|
|
304
|
+
layout_uuid=layout.uuid,
|
|
305
|
+
bag_name=metafile.bag_name,
|
|
306
|
+
vehicle_name=metafile.vehicle_name,
|
|
307
|
+
begin_frame_dt=metafile.begin_frame_dt,
|
|
308
|
+
end_frame_dt=metafile.end_frame_dt,
|
|
309
|
+
child_sample_paths=[child_sample.path for child_sample in child_samples],
|
|
310
|
+
tags=metafile.tags,
|
|
311
|
+
props=metafile.props,
|
|
312
|
+
)
|
|
313
|
+
yield Success(sample)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
yield Failure(ValidationFailure(relpath, "error constructing 'StandaloneSample'", e))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def validate_standalone_sample(
|
|
319
|
+
dataset_root: str | Path | CloudPath,
|
|
320
|
+
sample_root: str | Path | CloudPath,
|
|
321
|
+
layout_reference: str | py_uuid.UUID,
|
|
322
|
+
layouts: list[SampleLayout],
|
|
323
|
+
) -> list[ValidationFailure]:
|
|
324
|
+
"""
|
|
325
|
+
Validate a standalone sample against its layout.
|
|
326
|
+
|
|
327
|
+
:param dataset_root: absolute path to the dataset root
|
|
328
|
+
:param sample_root: absolute or relative path to the sample root
|
|
329
|
+
:param layout_reference: layout reference (name or UUID) to validate against
|
|
330
|
+
:param layouts: list of available ``SampleLayout`` objects
|
|
331
|
+
|
|
332
|
+
:return: list of ``ValidationFailure`` objects encountered during validation
|
|
333
|
+
"""
|
|
334
|
+
dataset_root_path = AnyPath(dataset_root)
|
|
335
|
+
if not dataset_root_path.is_absolute():
|
|
336
|
+
raise ValueError(f"'{dataset_root_path}' is not an absolute path")
|
|
337
|
+
|
|
338
|
+
sample_root_path = AnyPath(sample_root)
|
|
339
|
+
if not sample_root_path.is_absolute():
|
|
340
|
+
sample_root_path = dataset_root_path / sample_root_path
|
|
341
|
+
|
|
342
|
+
relpath = str(sample_root_path.relative_to(dataset_root_path))
|
|
343
|
+
|
|
344
|
+
referred_layout = find_layout(layout_reference, layouts)
|
|
345
|
+
if referred_layout is None:
|
|
346
|
+
return [ValidationFailure(relpath, f"layout of reference '{layout_reference}' is missing")]
|
|
347
|
+
|
|
348
|
+
failures: list[ValidationFailure] = []
|
|
349
|
+
for result in walk_standalone_sample(dataset_root_path,
|
|
350
|
+
sample_root_path,
|
|
351
|
+
referred_layout,
|
|
352
|
+
layouts,
|
|
353
|
+
ignore_metafile=True):
|
|
354
|
+
match result:
|
|
355
|
+
case Success(_):
|
|
356
|
+
pass
|
|
357
|
+
case Failure(failure):
|
|
358
|
+
failures.append(failure)
|
|
359
|
+
|
|
360
|
+
return failures
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def collect_standalone_dataset_samples(
|
|
364
|
+
dataset_root: str | Path | CloudPath,
|
|
365
|
+
layouts: list[SampleLayout],
|
|
366
|
+
) -> tuple[StandaloneDataset | None, list[StandaloneSample], list[ValidationFailure]]:
|
|
367
|
+
"""
|
|
368
|
+
Collect standalone samples from a dataset root.
|
|
369
|
+
|
|
370
|
+
:param dataset_root: absolute path to the dataset root
|
|
371
|
+
:param layouts: list of available ``SampleLayout`` objects
|
|
372
|
+
|
|
373
|
+
:return: tuple containing optional ``StandaloneDataset``, list of ``StandaloneSample``, and list of
|
|
374
|
+
``ValidationFailure`` objects
|
|
375
|
+
"""
|
|
376
|
+
dataset_root_path = AnyPath(dataset_root)
|
|
377
|
+
if not dataset_root_path.is_absolute():
|
|
378
|
+
raise ValueError(f"'{dataset_root_path}' is not an absolute path")
|
|
379
|
+
|
|
380
|
+
metafile = None
|
|
381
|
+
match retrieve_dataset_metafile(dataset_root_path):
|
|
382
|
+
case Success(metafile):
|
|
383
|
+
pass
|
|
384
|
+
case Failure(failure):
|
|
385
|
+
return None, [], [failure]
|
|
386
|
+
|
|
387
|
+
if metafile is None:
|
|
388
|
+
return None, [], [ValidationFailure("", f"missing dataset metafile in '{dataset_root_path}'")]
|
|
389
|
+
try:
|
|
390
|
+
dataset = StandaloneDataset(name=metafile.name,
|
|
391
|
+
description=metafile.description,
|
|
392
|
+
contributor=metafile.contributor,
|
|
393
|
+
tags=metafile.tags,
|
|
394
|
+
props=metafile.props,
|
|
395
|
+
)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
return None, [], [ValidationFailure("", f"error constructing 'StandaloneDataset'", e)]
|
|
398
|
+
|
|
399
|
+
samples: list[StandaloneSample] = []
|
|
400
|
+
failures: list[ValidationFailure] = []
|
|
401
|
+
|
|
402
|
+
relpath_dedupe: set[str] = set()
|
|
403
|
+
for sample_path in dataset_root_path.rglob("*"):
|
|
404
|
+
if not sample_path.is_dir():
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
relpath = str(sample_path.relative_to(dataset_root_path))
|
|
408
|
+
|
|
409
|
+
if relpath in relpath_dedupe:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
match retrieve_sample_metafile_and_layout(sample_path, layouts):
|
|
413
|
+
case Failure(failure):
|
|
414
|
+
failures.append(ValidationFailure(relpath, failure.reason, failure.exception))
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
case Success((_, metafile_layout)):
|
|
418
|
+
if metafile_layout is None:
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
sample_relpath_dedupe: set[str] = set()
|
|
422
|
+
|
|
423
|
+
for result in walk_standalone_sample(dataset_root_path, sample_path, metafile_layout, layouts):
|
|
424
|
+
match result:
|
|
425
|
+
case Success(sample):
|
|
426
|
+
if sample.path not in relpath_dedupe:
|
|
427
|
+
sample_relpath_dedupe.add(sample.path)
|
|
428
|
+
samples.append(sample)
|
|
429
|
+
case Failure(failure):
|
|
430
|
+
if failure.path not in relpath_dedupe:
|
|
431
|
+
sample_relpath_dedupe.add(failure.path)
|
|
432
|
+
failures.append(failure)
|
|
433
|
+
|
|
434
|
+
relpath_dedupe.update(sample_relpath_dedupe)
|
|
435
|
+
|
|
436
|
+
return dataset, samples, failures
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plexus-python-pulse-demeter
|
|
3
|
+
Version: 1.1.126
|
|
4
|
+
Classifier: Programming Language :: Python :: 3
|
|
5
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
7
|
+
Requires-Python: <3.14,>=3.12
|
|
8
|
+
Requires-Dist: cloudpathlib>=0.21
|
|
9
|
+
Requires-Dist: jsonschema>=4.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.12
|
|
11
|
+
Requires-Dist: returns>=0.26
|
|
12
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
13
|
+
Requires-Dist: sqlmodel
|
|
14
|
+
Requires-Dist: textcase
|
|
15
|
+
Requires-Dist: plexus-python-common[all]>=1.1
|
|
16
|
+
Requires-Dist: plexus-python-pulse-common[all]>=1.1
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Requires-Dist: plexus-python-pulse-demeter[api,cli]; extra == "all"
|
|
19
|
+
Provides-Extra: api
|
|
20
|
+
Requires-Dist: alembic; extra == "api"
|
|
21
|
+
Requires-Dist: fastapi; extra == "api"
|
|
22
|
+
Requires-Dist: httpx; extra == "api"
|
|
23
|
+
Requires-Dist: psycopg; extra == "api"
|
|
24
|
+
Requires-Dist: python-multipart; extra == "api"
|
|
25
|
+
Requires-Dist: ujson; extra == "api"
|
|
26
|
+
Requires-Dist: uvicorn; extra == "api"
|
|
27
|
+
Provides-Extra: cli
|
|
28
|
+
Requires-Dist: argcomplete; extra == "cli"
|
|
29
|
+
Provides-Extra: test
|
|
30
|
+
Requires-Dist: ddt>=1.7; extra == "test"
|
|
31
|
+
Requires-Dist: moto[s3,server]>=5.1; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
33
|
+
Requires-Dist: pytest-httpx>=0.35; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-order>=1.3; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-postgresql>=6.1; extra == "test"
|
|
36
|
+
Requires-Dist: pytest>=8.3; extra == "test"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
plexus/pulse/demeter/__init__.py,sha256=vxHFsMrvJXWVyXVYFsDiQ_IUElGstGTasjWpGAwWHzA,184
|
|
2
|
+
plexus/pulse/demeter/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
plexus/pulse/demeter/app/main.py,sha256=bd67HvdGV71zDxzJJkViUxTq4IhesJ18VfQUV2jzAws,325
|
|
4
|
+
plexus/pulse/demeter/app/api/__init__.py,sha256=0TeS_lTsRpOGJH-ubKiE2V4pd-rIrCU1e25qvoUKSEs,1637
|
|
5
|
+
plexus/pulse/demeter/app/api/archive_internal_endpoints.py,sha256=Qzhg16ZPdtgCehF1ilUHOkcgS_DskzL7bBkIivz1fSU,11750
|
|
6
|
+
plexus/pulse/demeter/app/api/archive_public_endpoints.py,sha256=uyoGCtVeGpzOJpnbdv0oFa-borp9YNLH-tUx-gVPmuY,9295
|
|
7
|
+
plexus/pulse/demeter/app/api/dataset_internal_endpoints.py,sha256=oNS61daRi3sa2yt_vGAkmZ22CY3Tvam6kpOygiLOKK0,40321
|
|
8
|
+
plexus/pulse/demeter/app/api/dataset_public_endpoints.py,sha256=ghhyOMh25scwQmJB3_G5RKSGMTZbJgaUTPCk3v03ysE,98645
|
|
9
|
+
plexus/pulse/demeter/app/api/dataset_workflow_endpoints.py,sha256=sYdDVy6Vi5NeSjczEpXs-fa3I3JxOenApBSB6t6F20k,23028
|
|
10
|
+
plexus/pulse/demeter/app/api/tagging_endpoints.py,sha256=ur56K0jNuMn2CNn97mC7cPA7HqFWQP7IHKeRvHqQfK4,12652
|
|
11
|
+
plexus/pulse/demeter/app/api/user_endpoints.py,sha256=zhm407bH7rJBidBRRYSfmMUNaF7I8-beGADA_AlLlFA,2716
|
|
12
|
+
plexus/pulse/demeter/app/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
plexus/pulse/demeter/app/core/database.py,sha256=gzZTxNG2Jyhvceem4E5ea1ttN_f9ldSugPEYFvTh7WE,1296
|
|
14
|
+
plexus/pulse/demeter/app/core/vault.py,sha256=zVVXwqk8Wva_S7RsgIAp7a1hz9xSiGGd5kwYiDzV3GA,1073
|
|
15
|
+
plexus/pulse/demeter/app/models/__init__.py,sha256=vTaUkrM74ECgdzXY5BhN9RLm4jpBgrmBK7vdckS0MrM,750
|
|
16
|
+
plexus/pulse/demeter/app/models/local.py,sha256=ApmHBD5RYgtoiwKw8oWXOPsmk7o9ifPm8IbQhT1zjxY,7641
|
|
17
|
+
plexus/pulse/demeter/app/models/registry/__init__.py,sha256=HV1Gf27ARIbD6ix7Ebif_3kDg-65mPpjxDWAStdZ9ac,2923
|
|
18
|
+
plexus/pulse/demeter/app/models/registry/archive.py,sha256=iU1niJQHfFKeoQgu-6Sx_30YlfP5K5EA0uL7LLvS4MA,19588
|
|
19
|
+
plexus/pulse/demeter/app/models/registry/dataset.py,sha256=5u7aJ3HmgDG6lsiHhQtIzvpjwGEn-XZkH4ip46Qn-7E,66679
|
|
20
|
+
plexus/pulse/demeter/app/models/registry/tagging.py,sha256=BZD7xql9uzTUwBJLKndENDNHvilzm8mCAOlNGNrQx_0,18364
|
|
21
|
+
plexus/pulse/demeter/app/models/registry/user.py,sha256=lDE2H8kzZ5Kwp2QbAOxj4A-29ybX7A5xTek6MzEHnu4,2532
|
|
22
|
+
plexus/pulse/demeter/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
plexus/pulse/demeter/tools/main.py,sha256=SOL6pYvHciYl698kmZtdr_lYue7J_8U4ILP6ZaxwTpg,12142
|
|
24
|
+
plexus/pulse/demeter/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
|
+
plexus/pulse/demeter/utils/config.py,sha256=Ij9wkGpSGIN2NwQzeB3eewftZrdMsAJQfBjp2PuFARs,947
|
|
26
|
+
plexus/pulse/demeter/utils/datautils.py,sha256=gOUsii1ZrkStjAOxcmN7tuhhTz3dGZdizo23VJTVAAo,17250
|
|
27
|
+
plexus_python_pulse_demeter-1.1.126.dist-info/METADATA,sha256=-8P8pzIP8ZrDl-WJ_P__wrBVc9xGQPbiceo23HeSSas,1375
|
|
28
|
+
plexus_python_pulse_demeter-1.1.126.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
29
|
+
plexus_python_pulse_demeter-1.1.126.dist-info/entry_points.txt,sha256=XGj43U0GF9wwHdrVvsUXNiwF0su7RVjCJgC2kD2saSY,78
|
|
30
|
+
plexus_python_pulse_demeter-1.1.126.dist-info/top_level.txt,sha256=ug_g7CVwaMQuas5UzAXbHUrQvKGCn8ezc6ZNvvRlJOE,7
|
|
31
|
+
plexus_python_pulse_demeter-1.1.126.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
plexus
|