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.
Files changed (31) hide show
  1. plexus/pulse/demeter/__init__.py +6 -0
  2. plexus/pulse/demeter/app/__init__.py +0 -0
  3. plexus/pulse/demeter/app/api/__init__.py +44 -0
  4. plexus/pulse/demeter/app/api/archive_internal_endpoints.py +316 -0
  5. plexus/pulse/demeter/app/api/archive_public_endpoints.py +255 -0
  6. plexus/pulse/demeter/app/api/dataset_internal_endpoints.py +1043 -0
  7. plexus/pulse/demeter/app/api/dataset_public_endpoints.py +2161 -0
  8. plexus/pulse/demeter/app/api/dataset_workflow_endpoints.py +532 -0
  9. plexus/pulse/demeter/app/api/tagging_endpoints.py +323 -0
  10. plexus/pulse/demeter/app/api/user_endpoints.py +78 -0
  11. plexus/pulse/demeter/app/core/__init__.py +0 -0
  12. plexus/pulse/demeter/app/core/database.py +43 -0
  13. plexus/pulse/demeter/app/core/vault.py +29 -0
  14. plexus/pulse/demeter/app/main.py +16 -0
  15. plexus/pulse/demeter/app/models/__init__.py +33 -0
  16. plexus/pulse/demeter/app/models/local.py +249 -0
  17. plexus/pulse/demeter/app/models/registry/__init__.py +112 -0
  18. plexus/pulse/demeter/app/models/registry/archive.py +529 -0
  19. plexus/pulse/demeter/app/models/registry/dataset.py +1531 -0
  20. plexus/pulse/demeter/app/models/registry/tagging.py +478 -0
  21. plexus/pulse/demeter/app/models/registry/user.py +79 -0
  22. plexus/pulse/demeter/tools/__init__.py +0 -0
  23. plexus/pulse/demeter/tools/main.py +306 -0
  24. plexus/pulse/demeter/utils/__init__.py +0 -0
  25. plexus/pulse/demeter/utils/config.py +25 -0
  26. plexus/pulse/demeter/utils/datautils.py +436 -0
  27. plexus_python_pulse_demeter-1.1.126.dist-info/METADATA +36 -0
  28. plexus_python_pulse_demeter-1.1.126.dist-info/RECORD +31 -0
  29. plexus_python_pulse_demeter-1.1.126.dist-info/WHEEL +5 -0
  30. plexus_python_pulse_demeter-1.1.126.dist-info/entry_points.txt +2 -0
  31. 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ plexus-demeter-tools = plexus.pulse.demeter.tools.main:main