kleinkram 0.37.0.dev20241113182530__py3-none-any.whl → 0.37.0.dev20241118113347__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.
Potentially problematic release.
This version of kleinkram might be problematic. Click here for more details.
- kleinkram/__init__.py +6 -0
- kleinkram/__main__.py +6 -0
- kleinkram/_version.py +6 -0
- kleinkram/api/__init__.py +0 -0
- kleinkram/api/client.py +65 -0
- kleinkram/api/file_transfer.py +337 -0
- kleinkram/api/routes.py +460 -0
- kleinkram/app.py +180 -0
- kleinkram/auth.py +96 -0
- kleinkram/commands/__init__.py +1 -0
- kleinkram/commands/download.py +103 -0
- kleinkram/commands/endpoint.py +62 -0
- kleinkram/commands/list.py +93 -0
- kleinkram/commands/mission.py +57 -0
- kleinkram/commands/project.py +24 -0
- kleinkram/commands/upload.py +138 -0
- kleinkram/commands/verify.py +117 -0
- kleinkram/config.py +171 -0
- kleinkram/consts.py +8 -1
- kleinkram/core.py +14 -0
- kleinkram/enums.py +10 -0
- kleinkram/errors.py +59 -0
- kleinkram/main.py +6 -489
- kleinkram/models.py +186 -0
- kleinkram/utils.py +179 -0
- {kleinkram-0.37.0.dev20241113182530.dist-info/licenses → kleinkram-0.37.0.dev20241118113347.dist-info}/LICENSE +1 -1
- kleinkram-0.37.0.dev20241118113347.dist-info/METADATA +113 -0
- kleinkram-0.37.0.dev20241118113347.dist-info/RECORD +33 -0
- {kleinkram-0.37.0.dev20241113182530.dist-info → kleinkram-0.37.0.dev20241118113347.dist-info}/WHEEL +2 -1
- kleinkram-0.37.0.dev20241118113347.dist-info/entry_points.txt +2 -0
- kleinkram-0.37.0.dev20241118113347.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_utils.py +153 -0
- kleinkram/api_client.py +0 -63
- kleinkram/auth/auth.py +0 -160
- kleinkram/endpoint/endpoint.py +0 -58
- kleinkram/error_handling.py +0 -177
- kleinkram/file/file.py +0 -144
- kleinkram/helper.py +0 -272
- kleinkram/mission/mission.py +0 -310
- kleinkram/project/project.py +0 -138
- kleinkram/queue/queue.py +0 -8
- kleinkram/tag/tag.py +0 -71
- kleinkram/topic/topic.py +0 -55
- kleinkram/user/user.py +0 -75
- kleinkram-0.37.0.dev20241113182530.dist-info/METADATA +0 -24
- kleinkram-0.37.0.dev20241113182530.dist-info/RECORD +0 -20
- kleinkram-0.37.0.dev20241113182530.dist-info/entry_points.txt +0 -2
kleinkram/main.py
CHANGED
|
@@ -1,495 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os
|
|
3
|
-
from datetime import datetime, timedelta
|
|
4
|
-
from enum import Enum
|
|
1
|
+
from __future__ import annotations
|
|
5
2
|
|
|
6
|
-
import
|
|
7
|
-
import typer
|
|
8
|
-
from rich import print
|
|
9
|
-
from rich.table import Table
|
|
10
|
-
from typer.core import TyperGroup
|
|
11
|
-
from typer.models import Context
|
|
12
|
-
from typing_extensions import Annotated, List, Optional
|
|
3
|
+
from kleinkram.app import app
|
|
13
4
|
|
|
14
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
15
|
-
from kleinkram.auth.auth import login, setCliKey, logout
|
|
16
|
-
from kleinkram.endpoint.endpoint import endpoint
|
|
17
|
-
from kleinkram.error_handling import (
|
|
18
|
-
ErrorHandledTyper,
|
|
19
|
-
AccessDeniedException,
|
|
20
|
-
)
|
|
21
|
-
from kleinkram.file.file import file
|
|
22
|
-
from kleinkram.mission.mission import missionCommands
|
|
23
|
-
from kleinkram.project.project import project
|
|
24
|
-
from kleinkram.queue.queue import queue
|
|
25
|
-
from kleinkram.tag.tag import tag
|
|
26
|
-
from kleinkram.topic.topic import topic
|
|
27
|
-
from kleinkram.user.user import user
|
|
28
|
-
from .helper import (
|
|
29
|
-
is_valid_UUIDv4,
|
|
30
|
-
canUploadMission,
|
|
31
|
-
promptForTags,
|
|
32
|
-
expand_and_match,
|
|
33
|
-
uploadFiles,
|
|
34
|
-
)
|
|
35
5
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Commands = "COMMANDS"
|
|
40
|
-
AdditionalCommands = "ADDITIONAL COMMANDS"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def version_callback(value: bool):
|
|
44
|
-
if value:
|
|
45
|
-
try:
|
|
46
|
-
_version = importlib.metadata.version("kleinkram")
|
|
47
|
-
except importlib.metadata.PackageNotFoundError:
|
|
48
|
-
_version = "local"
|
|
49
|
-
typer.echo(f"CLI Version: {_version}")
|
|
50
|
-
raise typer.Exit()
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class OrderCommands(TyperGroup):
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
The following code snippet is taken from https://github.com/tiangolo/typer/discussions/855 (see comment
|
|
57
|
-
https://github.com/tiangolo/typer/discussions/855#discussioncomment-9824582) and adapted to our use case.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
def list_commands(self, _ctx: Context) -> List[str]:
|
|
61
|
-
order = list(CommandPanel)
|
|
62
|
-
grouped_commands = {
|
|
63
|
-
name: getattr(command, "rich_help_panel")
|
|
64
|
-
for name, command in sorted(self.commands.items())
|
|
65
|
-
if getattr(command, "rich_help_panel") in order
|
|
66
|
-
}
|
|
67
|
-
ungrouped_command_names = [
|
|
68
|
-
command.name
|
|
69
|
-
for command in self.commands.values()
|
|
70
|
-
if command.name not in grouped_commands
|
|
71
|
-
]
|
|
72
|
-
return [
|
|
73
|
-
name
|
|
74
|
-
for name, command in sorted(
|
|
75
|
-
grouped_commands.items(),
|
|
76
|
-
key=lambda item: order.index(item[1]),
|
|
77
|
-
)
|
|
78
|
-
] + sorted(ungrouped_command_names)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
app = ErrorHandledTyper(
|
|
82
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
83
|
-
no_args_is_help=True,
|
|
84
|
-
cls=OrderCommands,
|
|
85
|
-
help=f"Kleinkram CLI\n\nThe Kleinkram CLI is a command line interface for Kleinkram. "
|
|
86
|
-
f"For a list of available commands, run 'klein --help' or visit "
|
|
87
|
-
f"https://docs.datasets.leggedrobotics.com/usage/cli/cli-getting-started.html for more information.",
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@app.callback()
|
|
92
|
-
def version(
|
|
93
|
-
version: bool = typer.Option(
|
|
94
|
-
None,
|
|
95
|
-
"--version",
|
|
96
|
-
"-v",
|
|
97
|
-
callback=version_callback,
|
|
98
|
-
is_eager=True,
|
|
99
|
-
help="Print the version and exit",
|
|
100
|
-
)
|
|
101
|
-
):
|
|
102
|
-
pass
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
app.add_typer(project, rich_help_panel=CommandPanel.Commands)
|
|
106
|
-
app.add_typer(missionCommands, rich_help_panel=CommandPanel.Commands)
|
|
107
|
-
|
|
108
|
-
app.add_typer(topic, rich_help_panel=CommandPanel.Commands)
|
|
109
|
-
app.add_typer(file, rich_help_panel=CommandPanel.Commands)
|
|
110
|
-
app.add_typer(queue, rich_help_panel=CommandPanel.Commands)
|
|
111
|
-
app.add_typer(user, rich_help_panel=CommandPanel.Commands)
|
|
112
|
-
app.add_typer(tag, rich_help_panel=CommandPanel.Commands)
|
|
113
|
-
app.add_typer(endpoint, rich_help_panel=CommandPanel.AdditionalCommands)
|
|
114
|
-
|
|
115
|
-
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(login)
|
|
116
|
-
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(logout)
|
|
117
|
-
app.command(hidden=True)(setCliKey)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@app.command("download", rich_help_panel=CommandPanel.CoreCommands)
|
|
121
|
-
def download():
|
|
122
|
-
print(
|
|
123
|
-
"Not implemented yet. Consider using the 'klein file download' or 'klein mission download' commands."
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands, no_args_is_help=True)
|
|
128
|
-
def upload(
|
|
129
|
-
path: Annotated[
|
|
130
|
-
List[str],
|
|
131
|
-
typer.Option(help="Path to files to upload, Regex supported"),
|
|
132
|
-
],
|
|
133
|
-
project: Annotated[str, typer.Option(help="Name or UUID of a Project")],
|
|
134
|
-
mission: Annotated[str, typer.Option(help="Name of UUID Mission to create")],
|
|
135
|
-
tags: Annotated[
|
|
136
|
-
Optional[List[str]],
|
|
137
|
-
typer.Option(help="Tags to add to the mission"),
|
|
138
|
-
] = None,
|
|
139
|
-
fix_filenames: Annotated[
|
|
140
|
-
bool,
|
|
141
|
-
typer.Option(help="Automatically fix filenames such that they are valid"),
|
|
142
|
-
] = False,
|
|
143
|
-
create_project: Annotated[
|
|
144
|
-
bool,
|
|
145
|
-
typer.Option(help="Allows adding files to an existing mission"),
|
|
146
|
-
] = False,
|
|
147
|
-
create_mission: Annotated[
|
|
148
|
-
bool,
|
|
149
|
-
typer.Option(help="Allows adding files to an existing mission"),
|
|
150
|
-
] = False,
|
|
151
|
-
overwrite: Annotated[
|
|
152
|
-
bool,
|
|
153
|
-
typer.Option(
|
|
154
|
-
help="Overwrite files with the same name.\n\n*WARNING:* This cannot be undone! This command will NOT delete"
|
|
155
|
-
"converted files, i.g. if the file is of type 'some-name.bag' the converted 'some-name.mcap' file will not "
|
|
156
|
-
"be deleted."
|
|
157
|
-
),
|
|
158
|
-
] = False,
|
|
159
|
-
overwrite_all: Annotated[
|
|
160
|
-
bool,
|
|
161
|
-
typer.Option(
|
|
162
|
-
help="Overwrite files with the same name.\n\n*WARNING:* This cannot be undone! This command WILL "
|
|
163
|
-
"automatically delete converted files, i.g. if the file is of type 'some-name.bag' the converted "
|
|
164
|
-
"'some-name.mcap' file will be deleted."
|
|
165
|
-
),
|
|
166
|
-
] = False,
|
|
167
|
-
ignore_tags: Annotated[
|
|
168
|
-
bool,
|
|
169
|
-
typer.Option(help="Ignore required tags for the mission."),
|
|
170
|
-
] = False,
|
|
171
|
-
):
|
|
172
|
-
"""
|
|
173
|
-
Upload files matching the path to a mission in a project.
|
|
174
|
-
|
|
175
|
-
The mission name must be unique within the project and not yet created.\n
|
|
176
|
-
Multiple paths can be given by using the option multiple times.\n
|
|
177
|
-
Examples:\n
|
|
178
|
-
- 'klein upload --path "~/data/**/*.bag" --project "Project_1" --mission "Mission_1" --tags "0700946d-1d6a-4520-b263-0e177f49c35b:LEE-H" --tags "1565118d-593c-4517-8c2d-9658452d9319:Dodo"'\n
|
|
179
|
-
|
|
180
|
-
"""
|
|
181
|
-
|
|
182
|
-
client = AuthenticatedClient()
|
|
183
|
-
|
|
184
|
-
##############################
|
|
185
|
-
# Check if project exists
|
|
186
|
-
##############################
|
|
187
|
-
if is_valid_UUIDv4(project):
|
|
188
|
-
get_project_url = "/project/one"
|
|
189
|
-
project_response = client.get(get_project_url, params={"uuid": project})
|
|
190
|
-
else:
|
|
191
|
-
get_project_url = "/project/byName"
|
|
192
|
-
project_response = client.get(get_project_url, params={"name": project})
|
|
193
|
-
|
|
194
|
-
if project_response.status_code >= 400:
|
|
195
|
-
if not create_project and not is_valid_UUIDv4(project):
|
|
196
|
-
raise AccessDeniedException(
|
|
197
|
-
f"The project '{project}' does not exist or you do not have access to it.\n"
|
|
198
|
-
f"Consider using the following command to create a project: 'klein project create' "
|
|
199
|
-
f"or consider passing the flag '--create-project' to create the project automatically.",
|
|
200
|
-
f"{project_response.json()['message']} ({project_response.status_code})",
|
|
201
|
-
)
|
|
202
|
-
elif is_valid_UUIDv4(project):
|
|
203
|
-
raise ValueError(
|
|
204
|
-
f"Project '{project}' does not exist. UUIDs cannot be used to create projects.\n"
|
|
205
|
-
f"Please provide a valid project name or consider creating the project using the"
|
|
206
|
-
f" following command: 'klein project create'"
|
|
207
|
-
)
|
|
208
|
-
else:
|
|
209
|
-
print(f"Project '{project}' does not exist. Creating it now.")
|
|
210
|
-
create_project_url = "/project/create"
|
|
211
|
-
project_response = client.post(
|
|
212
|
-
create_project_url,
|
|
213
|
-
json={
|
|
214
|
-
"name": project,
|
|
215
|
-
"description": "autogenerated with kleinkram CLI",
|
|
216
|
-
"requiredTags": [],
|
|
217
|
-
},
|
|
218
|
-
)
|
|
219
|
-
if project_response.status_code >= 400:
|
|
220
|
-
msg = str(project_response.json()["message"])
|
|
221
|
-
raise ValueError(
|
|
222
|
-
f"Failed to create project. Status Code: "
|
|
223
|
-
f"{str(project_response.status_code)}\n"
|
|
224
|
-
f"{msg}"
|
|
225
|
-
)
|
|
226
|
-
print("Project created successfully.")
|
|
227
|
-
|
|
228
|
-
project_json = project_response.json()
|
|
229
|
-
if not project_json["uuid"]:
|
|
230
|
-
print(f"Project not found: '{project}'")
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
can_upload = canUploadMission(client, project_json["uuid"])
|
|
234
|
-
if not can_upload:
|
|
235
|
-
raise AccessDeniedException(
|
|
236
|
-
f"You do not have the required permissions to upload to project '{project}'\n",
|
|
237
|
-
"Access Denied",
|
|
238
|
-
)
|
|
239
|
-
|
|
240
|
-
##############################
|
|
241
|
-
# Check if mission exists
|
|
242
|
-
##############################
|
|
243
|
-
if is_valid_UUIDv4(mission):
|
|
244
|
-
get_mission_url = "/mission/one"
|
|
245
|
-
mission_response = client.get(get_mission_url, params={"uuid": mission})
|
|
246
|
-
else:
|
|
247
|
-
get_mission_url = "/mission/byName"
|
|
248
|
-
mission_response = client.get(
|
|
249
|
-
get_mission_url,
|
|
250
|
-
params={"name": mission, "projectUUID": project_json["uuid"]},
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
if mission_response.status_code >= 400:
|
|
254
|
-
if not create_mission:
|
|
255
|
-
raise AccessDeniedException(
|
|
256
|
-
f"The mission '{mission}' does not exist or you do not have access to it.\n"
|
|
257
|
-
f"Consider using the following command to create a mission: 'klein mission create' "
|
|
258
|
-
f"or consider passing the flag '--create-mission' to create the mission automatically.",
|
|
259
|
-
f"{mission_response.json()['message']} ({mission_response.status_code})",
|
|
260
|
-
)
|
|
261
|
-
else:
|
|
262
|
-
print(f"Mission '{mission}' does not exist. Creating it now.")
|
|
263
|
-
create_mission_url = "/mission/create"
|
|
264
|
-
if not tags:
|
|
265
|
-
tags = []
|
|
266
|
-
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
267
|
-
required_tags = (
|
|
268
|
-
project_json["requiredTags"] if "requiredTags" in project_json else []
|
|
269
|
-
)
|
|
270
|
-
missing_tags = [
|
|
271
|
-
tag_key
|
|
272
|
-
for tag_key in required_tags
|
|
273
|
-
if (tag_key["uuid"] not in tags_dict)
|
|
274
|
-
]
|
|
275
|
-
if not ignore_tags:
|
|
276
|
-
if missing_tags and not ignore_tags:
|
|
277
|
-
promptForTags(tags_dict, required_tags)
|
|
278
|
-
else:
|
|
279
|
-
print("Ignoring required tags for the mission:")
|
|
280
|
-
for tag_key in missing_tags:
|
|
281
|
-
print(f" - {tag_key}")
|
|
282
|
-
|
|
283
|
-
mission_response = client.post(
|
|
284
|
-
create_mission_url,
|
|
285
|
-
json={
|
|
286
|
-
"name": mission,
|
|
287
|
-
"projectUUID": project_json["uuid"],
|
|
288
|
-
"tags": tags_dict,
|
|
289
|
-
"ignoreTags": ignore_tags,
|
|
290
|
-
},
|
|
291
|
-
)
|
|
292
|
-
if mission_response.status_code >= 400:
|
|
293
|
-
raise ValueError(
|
|
294
|
-
f"Failed to create mission. Status Code: "
|
|
295
|
-
f"{str(mission_response.status_code)}\n"
|
|
296
|
-
f"{mission_response.json()['message'][0]}"
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
mission_json = mission_response.json()
|
|
300
|
-
|
|
301
|
-
files = []
|
|
302
|
-
for p in path:
|
|
303
|
-
files.extend(expand_and_match(p))
|
|
304
|
-
|
|
305
|
-
print(
|
|
306
|
-
f"Uploading the following files to mission '{mission_json['name']}' in project '{project_json['name']}':"
|
|
307
|
-
)
|
|
308
|
-
filename_filepaths_map = {}
|
|
309
|
-
for path in files:
|
|
310
|
-
if not os.path.isdir(path):
|
|
311
|
-
|
|
312
|
-
filename = path.split("/")[-1]
|
|
313
|
-
filename_without_extension, extension = os.path.splitext(filename)
|
|
314
|
-
if fix_filenames:
|
|
315
|
-
|
|
316
|
-
# replace all non-alphanumeric characters with underscores
|
|
317
|
-
filename_without_extension = "".join(
|
|
318
|
-
char if char.isalnum() else "_"
|
|
319
|
-
for char in filename_without_extension
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
# trim filename to 40 characters
|
|
323
|
-
filename_without_extension = filename_without_extension[:40]
|
|
324
|
-
filename = f"{filename_without_extension}{extension}"
|
|
325
|
-
|
|
326
|
-
if (
|
|
327
|
-
not filename.replace(".", "")
|
|
328
|
-
.replace("_", "")
|
|
329
|
-
.replace("-", "")
|
|
330
|
-
.isalnum()
|
|
331
|
-
):
|
|
332
|
-
raise ValueError(
|
|
333
|
-
f"Filename '{filename}' is not valid. It must only contain alphanumeric characters, underscores and "
|
|
334
|
-
f"hyphens. Consider using the '--fix-filenames' option to automatically fix the filenames."
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
if not 3 <= len(filename_without_extension) <= 50:
|
|
338
|
-
raise ValueError(
|
|
339
|
-
f"Filename '{filename}' is not valid. It must be between 3 and 40 characters long. Consider using "
|
|
340
|
-
f"the '--fix-filenames' option to automatically fix the filenames."
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
filename_filepaths_map[filename] = path
|
|
344
|
-
typer.secho(f" - {filename}", fg=typer.colors.RESET)
|
|
345
|
-
print("\n\n")
|
|
346
|
-
|
|
347
|
-
filenames = list(filename_filepaths_map.keys())
|
|
348
|
-
|
|
349
|
-
if not filenames:
|
|
350
|
-
raise ValueError("No files found matching the given path.")
|
|
351
|
-
|
|
352
|
-
# validate filenames
|
|
353
|
-
if len(filenames) != len(set(filenames)):
|
|
354
|
-
raise ValueError(
|
|
355
|
-
"Filenames must be unique. Please check the files you are trying to upload. This can happen if you have "
|
|
356
|
-
"multiple files with the same name in different directories or use the '--fix-filenames' option."
|
|
357
|
-
)
|
|
358
|
-
|
|
359
|
-
# check if files already exist
|
|
360
|
-
get_files_url = "/file/ofMission"
|
|
361
|
-
response = client.get(
|
|
362
|
-
get_files_url,
|
|
363
|
-
params={"uuid": mission_json["uuid"]},
|
|
364
|
-
)
|
|
365
|
-
if response.status_code >= 400:
|
|
366
|
-
raise ValueError(
|
|
367
|
-
"Failed to check for existing files. Status Code: "
|
|
368
|
-
+ str(response.status_code)
|
|
369
|
-
+ "\n"
|
|
370
|
-
+ response.json()["message"]
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
existing_files = response.json()[0]
|
|
374
|
-
conflicting_files = [
|
|
375
|
-
file for file in existing_files if file["filename"] in filenames
|
|
376
|
-
]
|
|
377
|
-
|
|
378
|
-
if conflicting_files and len(conflicting_files):
|
|
379
|
-
print("The following files already exist in the mission:")
|
|
380
|
-
for file in conflicting_files:
|
|
381
|
-
typer.secho(f" - {file['filename']}", fg=typer.colors.RED, nl=False)
|
|
382
|
-
if overwrite or overwrite_all:
|
|
383
|
-
# delete existing files
|
|
384
|
-
delete_files_url = f"/file/{file['uuid']}"
|
|
385
|
-
response = client.delete(delete_files_url)
|
|
386
|
-
if response.status_code >= 400:
|
|
387
|
-
raise ValueError(
|
|
388
|
-
"Failed to delete existing files. Status Code: "
|
|
389
|
-
+ str(response.status_code)
|
|
390
|
-
+ "\n"
|
|
391
|
-
+ response.json()["message"]
|
|
392
|
-
)
|
|
393
|
-
print(" » deleted")
|
|
394
|
-
|
|
395
|
-
# check if converted files exist
|
|
396
|
-
mcap_file = file["filename"].replace(".bag", ".mcap")
|
|
397
|
-
|
|
398
|
-
if mcap_file == file["filename"]:
|
|
399
|
-
continue
|
|
400
|
-
|
|
401
|
-
mcap_uuid = next(
|
|
402
|
-
(
|
|
403
|
-
file["uuid"]
|
|
404
|
-
for file in existing_files
|
|
405
|
-
if file["filename"] == mcap_file
|
|
406
|
-
),
|
|
407
|
-
None,
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
if mcap_uuid and overwrite_all:
|
|
411
|
-
typer.secho(f" {mcap_file}", fg=typer.colors.RED, nl=False)
|
|
412
|
-
delete_files_url = f"/file/{mcap_uuid}"
|
|
413
|
-
response = client.delete(delete_files_url)
|
|
414
|
-
if response.status_code >= 400:
|
|
415
|
-
raise ValueError(
|
|
416
|
-
"Failed to delete existing files. Status Code: "
|
|
417
|
-
+ str(response.status_code)
|
|
418
|
-
+ "\n"
|
|
419
|
-
+ response.json()["message"]
|
|
420
|
-
)
|
|
421
|
-
print(" » deleted")
|
|
422
|
-
elif mcap_uuid and not overwrite_all:
|
|
423
|
-
print(
|
|
424
|
-
f" {mcap_file} » skipped (consider using '--overwrite-all' to delete this file)"
|
|
425
|
-
)
|
|
426
|
-
else:
|
|
427
|
-
print(" » not found")
|
|
428
|
-
|
|
429
|
-
else:
|
|
430
|
-
print("")
|
|
431
|
-
|
|
432
|
-
if not overwrite and not overwrite_all:
|
|
433
|
-
print(
|
|
434
|
-
"\nYou may use the '--overwrite' or '--overwrite-all' flag to overwrite existing files."
|
|
435
|
-
)
|
|
436
|
-
print("")
|
|
437
|
-
|
|
438
|
-
get_temporary_credentials = "/file/temporaryAccess"
|
|
439
|
-
response = client.post(
|
|
440
|
-
get_temporary_credentials,
|
|
441
|
-
json={"filenames": filenames, "missionUUID": mission_json["uuid"]},
|
|
442
|
-
)
|
|
443
|
-
if response.status_code >= 400:
|
|
444
|
-
raise ValueError(
|
|
445
|
-
"Failed to upload data. Status Code: "
|
|
446
|
-
+ str(response.status_code)
|
|
447
|
-
+ "\n"
|
|
448
|
-
+ response.json()["message"][0]
|
|
449
|
-
)
|
|
450
|
-
|
|
451
|
-
uploadFiles(response.json(), filename_filepaths_map, 4)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
@queue.command("list")
|
|
455
|
-
def list_queue():
|
|
456
|
-
"""List current Queue entities"""
|
|
457
|
-
try:
|
|
458
|
-
url = "/queue/active"
|
|
459
|
-
startDate = datetime.now().date() - timedelta(days=1)
|
|
460
|
-
client = AuthenticatedClient()
|
|
461
|
-
response = client.get(url, params={"startDate": startDate})
|
|
462
|
-
response.raise_for_status()
|
|
463
|
-
data = response.json()
|
|
464
|
-
table = Table("UUID", "filename", "mission", "state", "origin", "createdAt")
|
|
465
|
-
for topic in data:
|
|
466
|
-
table.add_row(
|
|
467
|
-
topic["uuid"],
|
|
468
|
-
topic["filename"],
|
|
469
|
-
topic["mission"]["name"],
|
|
470
|
-
topic["state"],
|
|
471
|
-
topic["location"],
|
|
472
|
-
topic["createdAt"],
|
|
473
|
-
)
|
|
474
|
-
print(table)
|
|
475
|
-
|
|
476
|
-
except httpx.HTTPError as e:
|
|
477
|
-
print(e)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
@app.command("claim", hidden=True)
|
|
481
|
-
def claim():
|
|
482
|
-
"""
|
|
483
|
-
Claim admin rights as the first user
|
|
484
|
-
|
|
485
|
-
Only works if no other user has claimed admin rights before.
|
|
486
|
-
"""
|
|
487
|
-
|
|
488
|
-
client = AuthenticatedClient()
|
|
489
|
-
response = client.post("/user/claimAdmin")
|
|
490
|
-
response.raise_for_status()
|
|
491
|
-
print("Admin claimed.")
|
|
6
|
+
def main() -> int:
|
|
7
|
+
app()
|
|
8
|
+
return 0
|
|
492
9
|
|
|
493
10
|
|
|
494
11
|
if __name__ == "__main__":
|
|
495
|
-
|
|
12
|
+
raise SystemExit(main())
|
kleinkram/models.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import List
|
|
7
|
+
from typing import NamedTuple
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
from typing import Union
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, eq=True)
|
|
18
|
+
class Project:
|
|
19
|
+
id: UUID
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
missions: List[Mission] = field(default_factory=list)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, eq=True)
|
|
26
|
+
class Mission:
|
|
27
|
+
id: UUID
|
|
28
|
+
name: str
|
|
29
|
+
project_id: UUID
|
|
30
|
+
project_name: str
|
|
31
|
+
files: List[File] = field(default_factory=list)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FileState(str, Enum):
|
|
35
|
+
OK = "OK"
|
|
36
|
+
CORRUPTED = "CORRUPTED"
|
|
37
|
+
UPLOADING = "UPLOADING"
|
|
38
|
+
ERROR = "ERROR"
|
|
39
|
+
CONVERSION_ERROR = "CONVERSION_ERROR"
|
|
40
|
+
LOST = "LOST"
|
|
41
|
+
FOUND = "FOUND"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
FILE_STATE_COLOR = {
|
|
45
|
+
FileState.OK: "green",
|
|
46
|
+
FileState.CORRUPTED: "red",
|
|
47
|
+
FileState.UPLOADING: "yellow",
|
|
48
|
+
FileState.ERROR: "red",
|
|
49
|
+
FileState.CONVERSION_ERROR: "red",
|
|
50
|
+
FileState.LOST: "bold red",
|
|
51
|
+
FileState.FOUND: "yellow",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, eq=True)
|
|
56
|
+
class File:
|
|
57
|
+
id: UUID
|
|
58
|
+
name: str
|
|
59
|
+
hash: str
|
|
60
|
+
size: int
|
|
61
|
+
mission_id: UUID
|
|
62
|
+
mission_name: str
|
|
63
|
+
project_id: UUID
|
|
64
|
+
project_name: str
|
|
65
|
+
state: FileState = FileState.OK
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DataType(str, Enum):
|
|
69
|
+
LOCATION = "LOCATION"
|
|
70
|
+
STRING = "STRING"
|
|
71
|
+
LINK = "LINK"
|
|
72
|
+
BOOLEAN = "BOOLEAN"
|
|
73
|
+
NUMBER = "NUMBER"
|
|
74
|
+
DATE = "DATE"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, eq=True)
|
|
78
|
+
class TagType:
|
|
79
|
+
name: str
|
|
80
|
+
id: UUID
|
|
81
|
+
data_type: DataType
|
|
82
|
+
description: Optional[str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def delimiter_row(
|
|
86
|
+
*lengths: int, delimiter: str = "-", cols: list[int] | None = None
|
|
87
|
+
) -> List[str]:
|
|
88
|
+
ret = []
|
|
89
|
+
for i, col_len in enumerate(lengths):
|
|
90
|
+
if cols is None or i in cols:
|
|
91
|
+
ret.append(delimiter * col_len)
|
|
92
|
+
else:
|
|
93
|
+
ret.append("")
|
|
94
|
+
return ret
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def projects_to_table(projects: List[Project]) -> Table:
|
|
98
|
+
table = Table(title="projects")
|
|
99
|
+
table.add_column("id")
|
|
100
|
+
table.add_column("name")
|
|
101
|
+
table.add_column("description")
|
|
102
|
+
|
|
103
|
+
for project in projects:
|
|
104
|
+
table.add_row(str(project.id), project.name, project.description)
|
|
105
|
+
|
|
106
|
+
return table
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def missions_to_table(missions: List[Mission]) -> Table:
|
|
110
|
+
table = Table(title="missions")
|
|
111
|
+
table.add_column("project")
|
|
112
|
+
table.add_column("name")
|
|
113
|
+
table.add_column("id")
|
|
114
|
+
|
|
115
|
+
# order by project, name
|
|
116
|
+
missions_tp: List[Tuple[str, str, Mission]] = []
|
|
117
|
+
for mission in missions:
|
|
118
|
+
missions_tp.append((mission.project_name, mission.name, mission))
|
|
119
|
+
missions_tp.sort()
|
|
120
|
+
|
|
121
|
+
if not missions_tp:
|
|
122
|
+
return table
|
|
123
|
+
last_project: Optional[str] = None
|
|
124
|
+
for project, _, mission in missions_tp:
|
|
125
|
+
# add delimiter row if project changes
|
|
126
|
+
if last_project is not None and last_project != project:
|
|
127
|
+
table.add_row()
|
|
128
|
+
last_project = project
|
|
129
|
+
|
|
130
|
+
table.add_row(mission.project_name, mission.name, str(mission.id))
|
|
131
|
+
|
|
132
|
+
return table
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def files_to_table(
|
|
136
|
+
files: List[File], *, title: str = "files", delimiters: bool = True
|
|
137
|
+
) -> Table:
|
|
138
|
+
table = Table(title=title)
|
|
139
|
+
table.add_column("project")
|
|
140
|
+
table.add_column("mission")
|
|
141
|
+
table.add_column("name")
|
|
142
|
+
table.add_column("id")
|
|
143
|
+
table.add_column("state")
|
|
144
|
+
|
|
145
|
+
# order by project, mission, name
|
|
146
|
+
files_tp: List[Tuple[str, str, str, File]] = []
|
|
147
|
+
for file in files:
|
|
148
|
+
files_tp.append((file.project_name, file.mission_name, file.name, file))
|
|
149
|
+
files_tp.sort()
|
|
150
|
+
|
|
151
|
+
if not files_tp:
|
|
152
|
+
return table
|
|
153
|
+
|
|
154
|
+
last_mission: Optional[str] = None
|
|
155
|
+
for _, mission, _, file in files_tp:
|
|
156
|
+
if last_mission is not None and last_mission != mission and delimiters:
|
|
157
|
+
table.add_row()
|
|
158
|
+
last_mission = mission
|
|
159
|
+
|
|
160
|
+
table.add_row(
|
|
161
|
+
file.project_name,
|
|
162
|
+
file.mission_name,
|
|
163
|
+
file.name,
|
|
164
|
+
Text(str(file.id), style="green"),
|
|
165
|
+
Text(file.state.value, style=FILE_STATE_COLOR[file.state]),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return table
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class FilesById(NamedTuple):
|
|
172
|
+
ids: List[UUID]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class FilesByMission(NamedTuple):
|
|
176
|
+
mission: MissionById | MissionByName
|
|
177
|
+
files: List[Union[str, UUID]]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MissionById(NamedTuple):
|
|
181
|
+
id: UUID
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MissionByName(NamedTuple):
|
|
185
|
+
name: str
|
|
186
|
+
project: Union[str, UUID]
|