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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kleinkram
3
- Version: 0.0.117
3
+ Version: 0.0.119
4
4
  Summary: A CLI for the ETH project kleinkram
5
5
  Project-URL: Homepage, https://github.com/leggedrobotics/kleinkram
6
6
  Project-URL: Issues, https://github.com/leggedrobotics/kleinkram/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kleinkram"
7
- version = "0.0.117"
7
+ version = "0.0.119"
8
8
  authors = [
9
9
  { name="Johann Schwabe", email="jschwab@ethz.ch" },
10
10
  ]
@@ -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: Annotated[str, typer.Option()] = None,
143
- open_browser: Annotated[bool, typer.Option()] = True,
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(endpoint: Annotated[str, typer.Argument()]):
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(key: Annotated[str, typer.Argument()]):
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] = {}
@@ -0,0 +1,2 @@
1
+ API_URL = "http://localhost:3000"
2
+ # API_URL = "https://api.datasets.leggedrobotics.com"
@@ -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: Annotated[str, typer.Option()] = None,
44
- mission: Annotated[str, typer.Option()] = None,
45
- topics: Annotated[List[str], typer.Option()] = None,
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: Annotated[str, typer.Option()] = None,
108
- verbose: Annotated[bool, typer.Option()] = False,
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
- print(f"mission: {data['name']}")
162
- print(f"Creator: {data['creator']['name']}")
163
- table = Table("Filename", "Size", "date")
164
- for file in data["files"]:
165
- table.add_row(file["filename"], f"{file['size']}", file["date"])
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()] = None,
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(name: Annotated[str, typer.Option()]):
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.raise_for_status()
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