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,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
|