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,306 @@
1
+ # This magic comment is used by `argcomplete` to identify that this script requires
2
+ # argument completion support when using shell autocompletion features.
3
+
4
+ # PYTHON_ARGCOMPLETE_OK
5
+
6
+ # `argcomplete` is a Python library that provides support for command-line argument autocompletion in
7
+ # shells like bash and zsh.
8
+ # By including the `PYTHON_ARGCOMPLETE_OK` comment at the top of the script, it signals to `argcomplete` that
9
+ # this script is compatible with its autocompletion features. When users set up `argcomplete` in their shell,
10
+ # they can enjoy autocompletion for the arguments defined in this script, enhancing the user experience when
11
+ # running the command-line tool.
12
+
13
+ # !!! IMPORTANT: DO NOT import heavy dependencies at the top level of this script and the initialization
14
+ # code of the classes derived from `CommandTool`.
15
+ #
16
+ # `argcomplete` works by analyzing the argument parser defined in the script and generating the necessary
17
+ # autocompletion scripts for the shell. Thus, to reduce the cost of loading this script when autocompletion
18
+ # is triggered, we should avoid importing heavy dependencies at the top level of the script. Instead, we can
19
+ # import them within the functions or methods where they are actually used. This way, when `argcomplete` loads
20
+ # the script for autocompletion, it won't incur the overhead of importing unnecessary modules, leading to
21
+ # faster autocompletion performance.
22
+
23
+ import abc
24
+ import argparse
25
+ import logging
26
+
27
+
28
+ class CommandTool(abc.ABC):
29
+ @classmethod
30
+ @abc.abstractmethod
31
+ def commands(cls) -> list[str]:
32
+ pass
33
+
34
+ @classmethod
35
+ @abc.abstractmethod
36
+ def desc(cls) -> str:
37
+ pass
38
+
39
+ @classmethod
40
+ @abc.abstractmethod
41
+ def make_parser(cls, parser: argparse.ArgumentParser) -> None:
42
+ pass
43
+
44
+ @abc.abstractmethod
45
+ def run(self, args: argparse.Namespace):
46
+ pass
47
+
48
+
49
+ class TagCacheUploadCommandTool(CommandTool):
50
+
51
+ @classmethod
52
+ def commands(cls) -> list[str]:
53
+ return ["tag-cache", "upload"]
54
+
55
+ @classmethod
56
+ def desc(cls) -> str:
57
+ return "Upload the tag cache SQLite DB file to the Demeter API server for ingestion."
58
+
59
+ @classmethod
60
+ def make_parser(cls, parser: argparse.ArgumentParser) -> None:
61
+ parser.add_argument("tag_cache_file", type=str, help="Path to the tag cache SQLite DB file.")
62
+ parser.add_argument("--api-url",
63
+ type=str,
64
+ default="http://localhost:8000",
65
+ help="URL of the Demeter API server.")
66
+
67
+ def run(self, args: argparse.Namespace) -> int:
68
+ import shutil
69
+ import tempfile
70
+ from pathlib import Path
71
+
72
+ import requests
73
+ from plexus.common.utils.pathutils import make_path
74
+ from plexus.pulse.common.utils.tagutils import TagCache
75
+ from plexus.pulse.common.utils.tagutils import predefined_tagsets
76
+
77
+ from plexus.pulse.demeter.app.api.tagging_endpoints import router as tagging_router
78
+
79
+ tag_cache_file_path = make_path(args.tag_cache_file)
80
+ if not tag_cache_file_path.is_file():
81
+ raise ValueError(f"Tag cache file <'{args.tag_cache_file}'> does not exist or is not a file")
82
+
83
+ api_url = args.api_url.rstrip("/")
84
+
85
+ logging.info(f"Uploading tag cache from file <'%s'> to API at <'%s'>", tag_cache_file_path, api_url)
86
+
87
+ with tempfile.NamedTemporaryFile(suffix=tag_cache_file_path.suffix, delete=False) as temp_file:
88
+ temp_file_path = Path(temp_file.name)
89
+
90
+ try:
91
+
92
+ shutil.copy2(tag_cache_file_path, temp_file_path)
93
+ upload_file_path = temp_file_path
94
+
95
+ tag_cache = TagCache(file_path=upload_file_path)
96
+ if args.prune_undefined:
97
+ tag_cache.remove_records(tagsets=list(predefined_tagsets().values()), tagset_inverted=True)
98
+
99
+ with open(upload_file_path, "rb") as fh:
100
+ response = requests.post(f"{api_url}/{tagging_router.prefix}/upload_tag_cache/",
101
+ files={"tag_cache_file": fh})
102
+ if response.status_code != 200:
103
+ logging.error("Failed to upload tag cache. Status code <%s>, Response:\n%s",
104
+ response.status_code,
105
+ response.text)
106
+ return -1
107
+
108
+ logging.info(
109
+ "Tag cache uploaded successfully to API at <'%s'> with <%d> tag targets and <%d> tags records were uploaded.",
110
+ api_url,
111
+ len(tag_cache.query_targets()),
112
+ len(list(tag_cache.iter_record_and_targets())),
113
+ )
114
+
115
+ return 0
116
+
117
+ finally:
118
+ if temp_file_path is not None and temp_file_path.exists():
119
+ temp_file_path.unlink()
120
+
121
+
122
+ class TagCacheValidateCommandTool(CommandTool):
123
+
124
+ @classmethod
125
+ def commands(cls) -> list[str]:
126
+ return ["tag-cache", "validate"]
127
+
128
+ @classmethod
129
+ def desc(cls) -> str:
130
+ return "Validate the tag cache SQLite DB file and print its contents in a human-readable format."
131
+
132
+ @classmethod
133
+ def make_parser(cls, parser: argparse.ArgumentParser) -> None:
134
+ parser.add_argument("tag_cache_file", type=str, help="Path to the tag cache SQLite DB file.")
135
+
136
+ def run(self, args: argparse.Namespace) -> int:
137
+ import uuid as py_uuid
138
+
139
+ import rich.console
140
+ from plexus.common.utils.pathutils import make_path
141
+ from plexus.pulse.common.utils.ormutils import print_model_results
142
+ from plexus.pulse.common.utils.tagutils import TagCache
143
+ from plexus.pulse.common.utils.tagutils import predefined_tagsets
144
+ from plexus.pulse.common.utils.tagutils import versioned_identifier
145
+
146
+ from plexus.pulse.demeter.app.models.registry.tagging import DataTagRecord, DataTagTarget
147
+
148
+ console = rich.console.Console()
149
+
150
+ tag_cache_file_path = make_path(args.tag_cache_file)
151
+ if not tag_cache_file_path.is_file():
152
+ raise ValueError(f"Tag cache file <'{args.tag_cache_file}'> does not exist or is not a file")
153
+
154
+ logging.info(f"Loading tag cache from file <'%s'>", tag_cache_file_path)
155
+
156
+ tag_cache = TagCache(file_path=tag_cache_file_path)
157
+
158
+ errors_count = 0
159
+
160
+ for target in tag_cache.query_targets():
161
+ try:
162
+ DataTagTarget(
163
+ uuid=py_uuid.uuid4(),
164
+ tagger_name=target.tagger_name,
165
+ tagger_version=target.tagger_version,
166
+ vehicle_name=target.vehicle_name,
167
+ begin_dt=target.begin_dt,
168
+ end_dt=target.end_dt,
169
+ )
170
+ except Exception as e:
171
+ errors_count += 1
172
+ console.print(
173
+ f"[red]Error:[/red] Failed to parse tag target [bold]{target}[/bold]. Exception: {str(e)}")
174
+
175
+ for record in tag_cache.with_target(target.identifier).iter_records():
176
+ versioned_namespace = versioned_identifier(record.tagset_namespace, record.tagset_version)
177
+ if versioned_namespace not in predefined_tagsets():
178
+ errors_count += 1
179
+ console.print(
180
+ f"[red]Error:[/red] Tagset [bold]{versioned_namespace}[/bold] is not predefined.")
181
+
182
+ try:
183
+ DataTagRecord(
184
+ target_uuid=py_uuid.uuid4(),
185
+ vehicle_name=target.vehicle_name,
186
+ begin_dt=record.begin_dt,
187
+ end_dt=record.end_dt,
188
+ tagset_namespace=record.tagset_namespace,
189
+ tagset_version=record.tagset_version or
190
+ (
191
+ predefined_tagsets().get(record.tagset_namespace).version
192
+ if record.tagset_namespace in predefined_tagsets()
193
+ else None
194
+ ),
195
+ tag=record.tag,
196
+ props=record.props,
197
+ )
198
+ except Exception as e:
199
+ errors_count += 1
200
+ console.print(
201
+ f"[red]Error:[/red] Failed to parse tag record [bold]{record}[/bold]. Exception: {str(e)}")
202
+
203
+ logging.info("Tag cache validation completed with <%d> error(s).", errors_count)
204
+
205
+ print_model_results(tag_cache.query_targets())
206
+ print_model_results(tag_cache.iter_record_and_targets())
207
+
208
+ return 0 if errors_count == 0 else -1
209
+
210
+
211
+ class DatasetValidateCommandTool(CommandTool):
212
+
213
+ @classmethod
214
+ def commands(cls) -> list[str]:
215
+ return ["dataset", "validate"]
216
+
217
+ @classmethod
218
+ def desc(cls) -> str:
219
+ return "Validate standalone samples from a specified dataset source directory."
220
+
221
+ @classmethod
222
+ def make_parser(cls, parser: argparse.ArgumentParser) -> None:
223
+ parser.add_argument("dataset_root", type=str, help="Root directory of the dataset.")
224
+ parser.add_argument("--api-url",
225
+ type=str,
226
+ default="http://localhost:8000",
227
+ help="URL of the Demeter API server.")
228
+
229
+ def run(self, args: argparse.Namespace) -> int:
230
+ from pathlib import Path
231
+
232
+ import requests
233
+ from cloudpathlib import AnyPath, S3Path
234
+
235
+ from plexus.pulse.demeter.app.api.dataset_internal_endpoints import router as internal_router
236
+ from plexus.pulse.demeter.app.core.vault import make_progressed_client
237
+ from plexus.pulse.demeter.app.models.registry.dataset import SampleLayout
238
+ from plexus.pulse.demeter.utils.datautils import collect_standalone_dataset_samples
239
+
240
+ dataset_root = args.dataset_root
241
+
242
+ logging.info(f"Starting sample collection from dataset root <'%s'>.", dataset_root)
243
+
244
+ api_url = args.api_url.rstrip("/")
245
+
246
+ logging.info(f"Fetching sample layouts from API at <'%s'>.", api_url)
247
+
248
+ response = requests.get(f"{api_url}/{internal_router.prefix}/layouts")
249
+ response.raise_for_status()
250
+
251
+ layouts = [SampleLayout.model_validate(item) for item in response.json()]
252
+
253
+ logging.info(f"Retrieved <%d> sample layouts from API.", len(layouts))
254
+
255
+ if isinstance(AnyPath(dataset_root), S3Path):
256
+ with make_progressed_client() as s3_client:
257
+ dataset_root = s3_client.CloudPath(dataset_root)
258
+ _, _, errors = collect_standalone_dataset_samples(dataset_root, layouts)
259
+ else:
260
+ _, _, errors = collect_standalone_dataset_samples(Path(dataset_root).absolute(), layouts)
261
+
262
+ if len(errors) > 0:
263
+ print("Validation errors found:")
264
+ for error in errors:
265
+ print(f"- {error}")
266
+ return 1
267
+
268
+ logging.info("Sample collection completed successfully with no errors.")
269
+
270
+ return 0
271
+
272
+
273
+ def main():
274
+ import argcomplete
275
+ from plexus.common.utils.argutils import ParserTree
276
+
277
+ tools: list[type[CommandTool]] = [
278
+ TagCacheUploadCommandTool,
279
+ TagCacheValidateCommandTool,
280
+ DatasetValidateCommandTool,
281
+ ]
282
+
283
+ parser_tree = ParserTree(argparse.ArgumentParser(description="Pulse Demeter Python tools"))
284
+ for tool in tools:
285
+ parser = parser_tree.add_subcommand_parser(tool.commands(), help=tool.desc())
286
+ tool.make_parser(parser)
287
+
288
+ argcomplete.autocomplete(parser_tree.root_node.parser)
289
+
290
+ from plexus.pulse.demeter.utils.config import config
291
+
292
+ logging.basicConfig(level=config().get("plexus", "logging.level"),
293
+ format=config().get("plexus", "logging.format"))
294
+
295
+ commands, args = parser_tree.parse_args()
296
+ for tool in tools:
297
+ if commands == tool.commands():
298
+ try:
299
+ SystemExit(tool().run(args))
300
+ except Exception as e:
301
+ logging.exception(f"An error occurred while executing the command '%s': %s", " ".join(commands), str(e))
302
+ SystemExit(-1)
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()
File without changes
@@ -0,0 +1,25 @@
1
+ from plexus.common.utils.config import Config
2
+ from plexus.common.utils.funcutils import singleton
3
+ from plexus.common.utils.pathutils import make_path
4
+
5
+
6
+ @singleton
7
+ def config() -> Config:
8
+ default_items: list[tuple[str, str, str]] = [
9
+ ("pulse.apps.database", "host", ""),
10
+ ("pulse.apps.database", "port", "5432"),
11
+ ("pulse.apps.database", "username", ""),
12
+ ("pulse.apps.database", "password", ""),
13
+ ("pulse.apps.database", "database", "pulse-apps"),
14
+ ("pulse.apps.vault", "access_key_id", ""),
15
+ ("pulse.apps.vault", "secret_access_key", ""),
16
+ ("pulse.apps.vault", "endpoint_url", "http://localhost:9000"),
17
+ ]
18
+
19
+ from plexus.pulse.common.utils.config import config as pulse_config
20
+
21
+ instance = pulse_config(make_path("~/.plexus-pulse.cfg", expand=True, normalize=True, absolute=True))
22
+ instance.restore()
23
+ instance.update(default_items, overwrite=False)
24
+
25
+ return instance