kleinkram 0.0.117__tar.gz → 0.0.118__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.118}/PKG-INFO +1 -1
- {kleinkram-0.0.117 → kleinkram-0.0.118}/pyproject.toml +1 -1
- {kleinkram-0.0.117 → kleinkram-0.0.118}/src/kleinkram/auth.py +35 -5
- kleinkram-0.0.118/src/kleinkram/consts.py +2 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/src/kleinkram/main.py +100 -35
- kleinkram-0.0.117/src/kleinkram/consts.py +0 -1
- {kleinkram-0.0.117 → kleinkram-0.0.118}/.gitignore +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/LICENSE +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/README.md +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/deploy.sh +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/dev.sh +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/requirements.txt +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/src/kleinkram/__init__.py +0 -0
- {kleinkram-0.0.117 → kleinkram-0.0.118}/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,19 @@ 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
|
+
print(f"Failed to create project: {response.json()["message"]}")
|
|
245
|
+
return
|
|
205
246
|
print("Project created")
|
|
206
247
|
|
|
207
248
|
except httpx.HTTPError as e:
|
|
@@ -210,12 +251,23 @@ def create_project(name: Annotated[str, typer.Option()]):
|
|
|
210
251
|
|
|
211
252
|
@app.command("upload")
|
|
212
253
|
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)],
|
|
254
|
+
path: Annotated[str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")],
|
|
255
|
+
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
256
|
+
mission: Annotated[str, typer.Option(prompt=True, help="Name of Mission to create")],
|
|
216
257
|
):
|
|
258
|
+
"""
|
|
259
|
+
Upload files matching the path to a mission in a project.
|
|
260
|
+
|
|
261
|
+
The mission name must be unique within the project and not yet created.\n
|
|
262
|
+
Examples:\n
|
|
263
|
+
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1"'\n
|
|
264
|
+
|
|
265
|
+
"""
|
|
217
266
|
files = expand_and_match(path)
|
|
218
267
|
filenames = list(map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x),files)))
|
|
268
|
+
if not filenames:
|
|
269
|
+
print("No files found")
|
|
270
|
+
return
|
|
219
271
|
filepaths = {}
|
|
220
272
|
for path in files:
|
|
221
273
|
if not os.path.isdir(path):
|
|
@@ -247,7 +299,7 @@ def upload(
|
|
|
247
299
|
|
|
248
300
|
create_mission_url = "/mission/create"
|
|
249
301
|
new_mission = client.post(
|
|
250
|
-
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"]}
|
|
302
|
+
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"], "tags": []}
|
|
251
303
|
)
|
|
252
304
|
new_mission.raise_for_status()
|
|
253
305
|
new_mission_data = new_mission.json()
|
|
@@ -322,7 +374,7 @@ def clear_queue():
|
|
|
322
374
|
print("Operation cancelled.")
|
|
323
375
|
|
|
324
376
|
|
|
325
|
-
@app.command("wipe")
|
|
377
|
+
@app.command("wipe", hidden=True)
|
|
326
378
|
def wipe():
|
|
327
379
|
"""Wipe all data"""
|
|
328
380
|
# Prompt the user for confirmation
|
|
@@ -362,8 +414,13 @@ def wipe():
|
|
|
362
414
|
print("Operation cancelled.")
|
|
363
415
|
|
|
364
416
|
|
|
365
|
-
@app.command("claim")
|
|
417
|
+
@app.command("claim", hidden=True)
|
|
366
418
|
def claim():
|
|
419
|
+
"""
|
|
420
|
+
Claim admin rights as the first user
|
|
421
|
+
|
|
422
|
+
Only works if no other user has claimed admin rights before.
|
|
423
|
+
"""
|
|
367
424
|
response = client.post("/user/claimAdmin")
|
|
368
425
|
response.raise_for_status()
|
|
369
426
|
print("Admin claimed.")
|
|
@@ -371,6 +428,7 @@ def claim():
|
|
|
371
428
|
|
|
372
429
|
@user.command("list")
|
|
373
430
|
def users():
|
|
431
|
+
"""List all users"""
|
|
374
432
|
response = client.get("/user/all")
|
|
375
433
|
response.raise_for_status()
|
|
376
434
|
data = response.json()
|
|
@@ -382,6 +440,7 @@ def users():
|
|
|
382
440
|
|
|
383
441
|
@user.command("info")
|
|
384
442
|
def user_info():
|
|
443
|
+
"""Get logged in user info"""
|
|
385
444
|
response = client.get("/user/me")
|
|
386
445
|
response.raise_for_status()
|
|
387
446
|
data = response.json()
|
|
@@ -390,6 +449,7 @@ def user_info():
|
|
|
390
449
|
|
|
391
450
|
@user.command("promote")
|
|
392
451
|
def promote(email: Annotated[str, typer.Option()]):
|
|
452
|
+
"""Promote another user to admin"""
|
|
393
453
|
response = client.post("/user/promote", json={"email": email})
|
|
394
454
|
response.raise_for_status()
|
|
395
455
|
print("User promoted.")
|
|
@@ -397,6 +457,7 @@ def promote(email: Annotated[str, typer.Option()]):
|
|
|
397
457
|
|
|
398
458
|
@user.command("demote")
|
|
399
459
|
def demote(email: Annotated[str, typer.Option()]):
|
|
460
|
+
"""Demote another user from admin"""
|
|
400
461
|
response = client.post("/user/demote", json={"email": email})
|
|
401
462
|
response.raise_for_status()
|
|
402
463
|
print("User demoted.")
|
|
@@ -406,6 +467,7 @@ def demote(email: Annotated[str, typer.Option()]):
|
|
|
406
467
|
def download(
|
|
407
468
|
missionuuid: Annotated[str, typer.Argument()],
|
|
408
469
|
):
|
|
470
|
+
"""Download file"""
|
|
409
471
|
try:
|
|
410
472
|
response = client.get("/file/downloadWithToken", params={"uuid": missionuuid})
|
|
411
473
|
response.raise_for_status()
|
|
@@ -419,6 +481,7 @@ def addTag(
|
|
|
419
481
|
tagtypeuuid: Annotated[str, typer.Argument()],
|
|
420
482
|
value: Annotated[str, typer.Argument()],
|
|
421
483
|
):
|
|
484
|
+
"""Tag a mission"""
|
|
422
485
|
try:
|
|
423
486
|
response = client.post("/tag/addTag", json={"mission": missionuuid, "tagType": tagtypeuuid, "value": value})
|
|
424
487
|
if response.status_code < 400:
|
|
@@ -436,6 +499,7 @@ def addTag(
|
|
|
436
499
|
def tagTypes(
|
|
437
500
|
verbose: Annotated[bool, typer.Option()] = False,
|
|
438
501
|
):
|
|
502
|
+
"""List all tagtypes"""
|
|
439
503
|
try:
|
|
440
504
|
response = client.get("/tag/all")
|
|
441
505
|
response.raise_for_status()
|
|
@@ -456,6 +520,7 @@ def tagTypes(
|
|
|
456
520
|
def deleteTag(
|
|
457
521
|
taguuid: Annotated[str, typer.Argument()],
|
|
458
522
|
):
|
|
523
|
+
"""Delete a tag"""
|
|
459
524
|
try:
|
|
460
525
|
response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
|
|
461
526
|
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
|