kleinkram 0.0.117__tar.gz → 0.0.119__tar.gz
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-0.0.117 → kleinkram-0.0.119}/PKG-INFO +1 -1
- {kleinkram-0.0.117 → kleinkram-0.0.119}/pyproject.toml +1 -1
- {kleinkram-0.0.117 → kleinkram-0.0.119}/src/kleinkram/auth.py +35 -5
- kleinkram-0.0.119/src/kleinkram/consts.py +2 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/src/kleinkram/main.py +101 -35
- kleinkram-0.0.117/src/kleinkram/consts.py +0 -1
- {kleinkram-0.0.117 → kleinkram-0.0.119}/.gitignore +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/LICENSE +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/README.md +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/deploy.sh +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/dev.sh +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/requirements.txt +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/src/kleinkram/__init__.py +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.119}/src/kleinkram/helper.py +0 -0
|
@@ -3,6 +3,8 @@ import os
|
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import webbrowser
|
|
5
5
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
6
8
|
from typing_extensions import Annotated
|
|
7
9
|
|
|
8
10
|
from pathlib import Path
|
|
@@ -139,9 +141,13 @@ client = AuthenticatedClient()
|
|
|
139
141
|
|
|
140
142
|
|
|
141
143
|
def login(
|
|
142
|
-
key:
|
|
143
|
-
open_browser:
|
|
144
|
+
key: Optional[str] = typer.Option(None, help="CLI Key", hidden=True),
|
|
145
|
+
open_browser: Optional[bool] = typer.Option(True, help="Open browser for authentication"),
|
|
144
146
|
):
|
|
147
|
+
"""
|
|
148
|
+
Login into the currently set endpoint.\n
|
|
149
|
+
By default, it will open the browser for authentication. On machines without a browser, you can manually open the URL provided and paste the tokens back.
|
|
150
|
+
"""
|
|
145
151
|
tokenfile = TokenFile()
|
|
146
152
|
if key:
|
|
147
153
|
tokenfile.saveTokens(key)
|
|
@@ -181,7 +187,18 @@ def login(
|
|
|
181
187
|
return
|
|
182
188
|
|
|
183
189
|
|
|
184
|
-
def setEndpoint(
|
|
190
|
+
def setEndpoint(
|
|
191
|
+
endpoint: Optional[str] = typer.Argument(None, help="API endpoint to use")
|
|
192
|
+
):
|
|
193
|
+
"""
|
|
194
|
+
Set the current endpoint
|
|
195
|
+
|
|
196
|
+
Use this command to switch between different API endpoints.\n
|
|
197
|
+
Standard endpoints are:\n
|
|
198
|
+
- http://localhost:3000\n
|
|
199
|
+
- https://api.datasets.leggedrobotics.com\n
|
|
200
|
+
- https://api.datasets.dev.leggedrobotics.com
|
|
201
|
+
"""
|
|
185
202
|
tokenfile = TokenFile()
|
|
186
203
|
tokenfile.endpoint = endpoint
|
|
187
204
|
tokenfile.writeToFile()
|
|
@@ -191,14 +208,27 @@ def setEndpoint(endpoint: Annotated[str, typer.Argument()]):
|
|
|
191
208
|
|
|
192
209
|
|
|
193
210
|
def endpoint():
|
|
211
|
+
"""
|
|
212
|
+
Get the current endpoint
|
|
213
|
+
|
|
214
|
+
Also displays all endpoints with saved tokens.
|
|
215
|
+
"""
|
|
194
216
|
tokenfile = TokenFile()
|
|
195
217
|
print("Current: " + tokenfile.endpoint)
|
|
196
218
|
print("Saved Tokens found for:")
|
|
197
219
|
for _endpoint, _ in tokenfile.tokens.items():
|
|
198
|
-
print(_endpoint)
|
|
220
|
+
print("- " + _endpoint)
|
|
199
221
|
|
|
200
222
|
|
|
201
|
-
def setCliKey(
|
|
223
|
+
def setCliKey(
|
|
224
|
+
key: Annotated[str, typer.Argument(help="CLI Key")]
|
|
225
|
+
):
|
|
226
|
+
"""
|
|
227
|
+
Set the CLI key (Actions Only)
|
|
228
|
+
|
|
229
|
+
Same as login with the --key option.
|
|
230
|
+
Should never be used by the user, only in docker containers launched from within Kleinkram.
|
|
231
|
+
"""
|
|
202
232
|
tokenfile = TokenFile()
|
|
203
233
|
if not tokenfile.endpoint in tokenfile.tokens:
|
|
204
234
|
tokenfile.tokens[tokenfile.endpoint] = {}
|
|
@@ -14,14 +14,14 @@ from .helper import uploadFiles, expand_and_match
|
|
|
14
14
|
from .auth import login, client, endpoint, setCliKey, setEndpoint
|
|
15
15
|
|
|
16
16
|
app = typer.Typer()
|
|
17
|
-
projects = typer.Typer(name="projects")
|
|
18
|
-
missions = typer.Typer(name="missions")
|
|
19
|
-
files = typer.Typer(name="files")
|
|
20
|
-
topics = typer.Typer(name="topics")
|
|
21
|
-
queue = typer.Typer(name="queue")
|
|
22
|
-
user = typer.Typer(name="users")
|
|
23
|
-
tagtypes = typer.Typer(name="tagtypes")
|
|
24
|
-
tag = typer.Typer(name="tag")
|
|
17
|
+
projects = typer.Typer(name="projects", help="Project operations")
|
|
18
|
+
missions = typer.Typer(name="missions", help="Mission operations")
|
|
19
|
+
files = typer.Typer(name="files", help="File operations")
|
|
20
|
+
topics = typer.Typer(name="topics", help="Topic operations")
|
|
21
|
+
queue = typer.Typer(name="queue", help="Queue operations")
|
|
22
|
+
user = typer.Typer(name="users", help="User operations")
|
|
23
|
+
tagtypes = typer.Typer(name="tagtypes", help="TagType operations")
|
|
24
|
+
tag = typer.Typer(name="tag", help="Tag operations")
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
app.add_typer(projects)
|
|
@@ -35,27 +35,38 @@ app.add_typer(tag)
|
|
|
35
35
|
app.command()(login)
|
|
36
36
|
app.command()(endpoint)
|
|
37
37
|
app.command()(setEndpoint)
|
|
38
|
-
app.command()(setCliKey)
|
|
38
|
+
app.command(hidden=True)(setCliKey)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
@files.command("list")
|
|
42
42
|
def list_files(
|
|
43
|
-
project:
|
|
44
|
-
mission:
|
|
45
|
-
topics:
|
|
43
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
44
|
+
mission: Optional[str] = typer.Option(None, help="Name of Mission"),
|
|
45
|
+
topics: Optional[List[str]] = typer.Option(None, help="Comma separated list of topics")
|
|
46
46
|
):
|
|
47
47
|
"""
|
|
48
48
|
List all files with optional filters for project, mission, or topics.
|
|
49
|
+
|
|
50
|
+
Can list files of a project, mission, or with specific topics (Logical AND).
|
|
51
|
+
Examples:\n
|
|
52
|
+
- 'klein files list'\n
|
|
53
|
+
- 'klein files list --project "Project 1"'\n
|
|
54
|
+
- 'klein files list --mission "Mission 1"'\n
|
|
55
|
+
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw"'\n
|
|
56
|
+
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw" --mission "Mission A"'
|
|
49
57
|
"""
|
|
50
58
|
try:
|
|
51
59
|
url = f"/file/filteredByNames"
|
|
60
|
+
params = {}
|
|
61
|
+
if project:
|
|
62
|
+
params["projectName"] = project
|
|
63
|
+
if mission:
|
|
64
|
+
params["missionName"] = mission
|
|
65
|
+
if topics:
|
|
66
|
+
params["topics"] = topics
|
|
52
67
|
response = client.get(
|
|
53
68
|
url,
|
|
54
|
-
params=
|
|
55
|
-
"projectName": project,
|
|
56
|
-
"missionName": mission,
|
|
57
|
-
"topics": topics,
|
|
58
|
-
},
|
|
69
|
+
params=params,
|
|
59
70
|
)
|
|
60
71
|
response.raise_for_status()
|
|
61
72
|
data = response.json()
|
|
@@ -104,11 +115,12 @@ def list_projects():
|
|
|
104
115
|
|
|
105
116
|
@missions.command("list")
|
|
106
117
|
def list_missions(
|
|
107
|
-
project:
|
|
108
|
-
verbose:
|
|
118
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
119
|
+
verbose: Optional[bool] = typer.Option(False, help="Outputs a table with more information"),
|
|
109
120
|
):
|
|
110
121
|
"""
|
|
111
122
|
List all missions with optional filter for project.
|
|
123
|
+
|
|
112
124
|
"""
|
|
113
125
|
try:
|
|
114
126
|
url = "/mission"
|
|
@@ -152,26 +164,47 @@ def list_missions(
|
|
|
152
164
|
@missions.command("byUUID")
|
|
153
165
|
def mission_by_uuid(
|
|
154
166
|
uuid: Annotated[str, typer.Argument()],
|
|
167
|
+
json: Optional[bool] = typer.Option(False, help="Output as JSON"),
|
|
155
168
|
):
|
|
169
|
+
"""
|
|
170
|
+
Get mission name, project name, creator and table of its files given a Mission UUID
|
|
171
|
+
|
|
172
|
+
Use the JSON flag to output the full JSON response instead.
|
|
173
|
+
|
|
174
|
+
Can be run with API Key or with login.
|
|
175
|
+
"""
|
|
156
176
|
try:
|
|
157
177
|
url = "/mission/byUUID"
|
|
158
178
|
response = client.get(url, params={"uuid": uuid})
|
|
159
179
|
response.raise_for_status()
|
|
160
180
|
data = response.json()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
if json:
|
|
182
|
+
print(data)
|
|
183
|
+
else:
|
|
184
|
+
print(f"mission: {data['name']}")
|
|
185
|
+
print(f"Creator: {data['creator']['name']}")
|
|
186
|
+
print("Project: " + data["project"]["name"])
|
|
187
|
+
table = Table("Filename", "Size", "date")
|
|
188
|
+
for file in data["files"]:
|
|
189
|
+
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
190
|
+
print(table)
|
|
166
191
|
except httpx.HTTPError as e:
|
|
167
192
|
print(f"Failed to fetch missions: {e}")
|
|
168
193
|
|
|
169
194
|
|
|
170
195
|
@topics.command("list")
|
|
171
196
|
def topics(
|
|
172
|
-
file: Annotated[str, typer.Option()]
|
|
173
|
-
full: Annotated[bool, typer.Option()] = False,
|
|
197
|
+
file: Annotated[str, typer.Option(help="Name of File")],
|
|
198
|
+
full: Annotated[bool, typer.Option(help="As a table with additional parameters")] = False,
|
|
199
|
+
# Todo add mission / project as optional argument as filenames are not unique
|
|
174
200
|
):
|
|
201
|
+
"""
|
|
202
|
+
List topics for a file
|
|
203
|
+
|
|
204
|
+
Only makes sense with MCAP files as we don't associate topics with BAGs.
|
|
205
|
+
"""
|
|
206
|
+
if file.endswith(".bag"):
|
|
207
|
+
print("BAG files generally do not have topics")
|
|
175
208
|
try:
|
|
176
209
|
url = "/file/byName"
|
|
177
210
|
response = client.get(url, params={"name": file})
|
|
@@ -197,11 +230,20 @@ def topics(
|
|
|
197
230
|
|
|
198
231
|
|
|
199
232
|
@projects.command("create")
|
|
200
|
-
def create_project(
|
|
233
|
+
def create_project(
|
|
234
|
+
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
235
|
+
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
236
|
+
):
|
|
237
|
+
"""
|
|
238
|
+
Create a new project
|
|
239
|
+
"""
|
|
201
240
|
try:
|
|
202
241
|
url = "/project/create"
|
|
203
|
-
response = client.post(url, json={"name": name})
|
|
204
|
-
response.
|
|
242
|
+
response = client.post(url, json={"name": name, "description": description, "requiredTags": []}) # TODO: Add required tags as option
|
|
243
|
+
if response.status_code >= 400:
|
|
244
|
+
response_json = response.json()
|
|
245
|
+
print(f"Failed to create project: {response_json["message"]}")
|
|
246
|
+
return
|
|
205
247
|
print("Project created")
|
|
206
248
|
|
|
207
249
|
except httpx.HTTPError as e:
|
|
@@ -210,12 +252,23 @@ def create_project(name: Annotated[str, typer.Option()]):
|
|
|
210
252
|
|
|
211
253
|
@app.command("upload")
|
|
212
254
|
def upload(
|
|
213
|
-
path: Annotated[str, typer.Option(prompt=True)],
|
|
214
|
-
project: Annotated[str, typer.Option(prompt=True)],
|
|
215
|
-
mission: Annotated[str, typer.Option(prompt=True)],
|
|
255
|
+
path: Annotated[str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")],
|
|
256
|
+
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
257
|
+
mission: Annotated[str, typer.Option(prompt=True, help="Name of Mission to create")],
|
|
216
258
|
):
|
|
259
|
+
"""
|
|
260
|
+
Upload files matching the path to a mission in a project.
|
|
261
|
+
|
|
262
|
+
The mission name must be unique within the project and not yet created.\n
|
|
263
|
+
Examples:\n
|
|
264
|
+
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1"'\n
|
|
265
|
+
|
|
266
|
+
"""
|
|
217
267
|
files = expand_and_match(path)
|
|
218
268
|
filenames = list(map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x),files)))
|
|
269
|
+
if not filenames:
|
|
270
|
+
print("No files found")
|
|
271
|
+
return
|
|
219
272
|
filepaths = {}
|
|
220
273
|
for path in files:
|
|
221
274
|
if not os.path.isdir(path):
|
|
@@ -247,7 +300,7 @@ def upload(
|
|
|
247
300
|
|
|
248
301
|
create_mission_url = "/mission/create"
|
|
249
302
|
new_mission = client.post(
|
|
250
|
-
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"]}
|
|
303
|
+
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"], "tags": []}
|
|
251
304
|
)
|
|
252
305
|
new_mission.raise_for_status()
|
|
253
306
|
new_mission_data = new_mission.json()
|
|
@@ -322,7 +375,7 @@ def clear_queue():
|
|
|
322
375
|
print("Operation cancelled.")
|
|
323
376
|
|
|
324
377
|
|
|
325
|
-
@app.command("wipe")
|
|
378
|
+
@app.command("wipe", hidden=True)
|
|
326
379
|
def wipe():
|
|
327
380
|
"""Wipe all data"""
|
|
328
381
|
# Prompt the user for confirmation
|
|
@@ -362,8 +415,13 @@ def wipe():
|
|
|
362
415
|
print("Operation cancelled.")
|
|
363
416
|
|
|
364
417
|
|
|
365
|
-
@app.command("claim")
|
|
418
|
+
@app.command("claim", hidden=True)
|
|
366
419
|
def claim():
|
|
420
|
+
"""
|
|
421
|
+
Claim admin rights as the first user
|
|
422
|
+
|
|
423
|
+
Only works if no other user has claimed admin rights before.
|
|
424
|
+
"""
|
|
367
425
|
response = client.post("/user/claimAdmin")
|
|
368
426
|
response.raise_for_status()
|
|
369
427
|
print("Admin claimed.")
|
|
@@ -371,6 +429,7 @@ def claim():
|
|
|
371
429
|
|
|
372
430
|
@user.command("list")
|
|
373
431
|
def users():
|
|
432
|
+
"""List all users"""
|
|
374
433
|
response = client.get("/user/all")
|
|
375
434
|
response.raise_for_status()
|
|
376
435
|
data = response.json()
|
|
@@ -382,6 +441,7 @@ def users():
|
|
|
382
441
|
|
|
383
442
|
@user.command("info")
|
|
384
443
|
def user_info():
|
|
444
|
+
"""Get logged in user info"""
|
|
385
445
|
response = client.get("/user/me")
|
|
386
446
|
response.raise_for_status()
|
|
387
447
|
data = response.json()
|
|
@@ -390,6 +450,7 @@ def user_info():
|
|
|
390
450
|
|
|
391
451
|
@user.command("promote")
|
|
392
452
|
def promote(email: Annotated[str, typer.Option()]):
|
|
453
|
+
"""Promote another user to admin"""
|
|
393
454
|
response = client.post("/user/promote", json={"email": email})
|
|
394
455
|
response.raise_for_status()
|
|
395
456
|
print("User promoted.")
|
|
@@ -397,6 +458,7 @@ def promote(email: Annotated[str, typer.Option()]):
|
|
|
397
458
|
|
|
398
459
|
@user.command("demote")
|
|
399
460
|
def demote(email: Annotated[str, typer.Option()]):
|
|
461
|
+
"""Demote another user from admin"""
|
|
400
462
|
response = client.post("/user/demote", json={"email": email})
|
|
401
463
|
response.raise_for_status()
|
|
402
464
|
print("User demoted.")
|
|
@@ -406,6 +468,7 @@ def demote(email: Annotated[str, typer.Option()]):
|
|
|
406
468
|
def download(
|
|
407
469
|
missionuuid: Annotated[str, typer.Argument()],
|
|
408
470
|
):
|
|
471
|
+
"""Download file"""
|
|
409
472
|
try:
|
|
410
473
|
response = client.get("/file/downloadWithToken", params={"uuid": missionuuid})
|
|
411
474
|
response.raise_for_status()
|
|
@@ -419,6 +482,7 @@ def addTag(
|
|
|
419
482
|
tagtypeuuid: Annotated[str, typer.Argument()],
|
|
420
483
|
value: Annotated[str, typer.Argument()],
|
|
421
484
|
):
|
|
485
|
+
"""Tag a mission"""
|
|
422
486
|
try:
|
|
423
487
|
response = client.post("/tag/addTag", json={"mission": missionuuid, "tagType": tagtypeuuid, "value": value})
|
|
424
488
|
if response.status_code < 400:
|
|
@@ -436,6 +500,7 @@ def addTag(
|
|
|
436
500
|
def tagTypes(
|
|
437
501
|
verbose: Annotated[bool, typer.Option()] = False,
|
|
438
502
|
):
|
|
503
|
+
"""List all tagtypes"""
|
|
439
504
|
try:
|
|
440
505
|
response = client.get("/tag/all")
|
|
441
506
|
response.raise_for_status()
|
|
@@ -456,6 +521,7 @@ def tagTypes(
|
|
|
456
521
|
def deleteTag(
|
|
457
522
|
taguuid: Annotated[str, typer.Argument()],
|
|
458
523
|
):
|
|
524
|
+
"""Delete a tag"""
|
|
459
525
|
try:
|
|
460
526
|
response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
|
|
461
527
|
if response.status_code < 400:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
API_URL='https://api.datasets.leggedrobotics.com'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|