kleinkram 0.32.3.dev20241023090650__py3-none-any.whl → 0.33.0.dev20241024121528__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.

@@ -13,6 +13,12 @@ ExceptionType = "typing.Type[Exception]"
13
13
  ErrorHandlingCallback = typing.Callable[[Exception], int]
14
14
 
15
15
 
16
+ class AbortException(Exception):
17
+
18
+ def __init__(self, message: str):
19
+ self.message = message
20
+
21
+
16
22
  class AccessDeniedException(Exception):
17
23
 
18
24
  def __init__(self, message: str, api_error: str):
@@ -105,6 +111,20 @@ def remote_down_handler(e: Exception):
105
111
  print()
106
112
 
107
113
 
114
+ def abort_handler(e: AbortException):
115
+ console = Console(file=sys.stderr)
116
+ panel = Panel(
117
+ f"{e.message}",
118
+ title="Command Aborted",
119
+ style="yellow",
120
+ padding=(1, 2),
121
+ highlight=True,
122
+ )
123
+ print()
124
+ console.print(panel)
125
+ print()
126
+
127
+
108
128
  class ErrorHandledTyper(Typer):
109
129
  error_handlers: typing.Dict[ExceptionType, ErrorHandlingCallback] = {
110
130
  NotAuthenticatedException: not_authenticated_handler,
@@ -114,6 +134,7 @@ class ErrorHandledTyper(Typer):
114
134
  ValueError: value_error_handler,
115
135
  RemoteProtocolError: remote_down_handler,
116
136
  ReadError: remote_down_handler,
137
+ AbortException: abort_handler,
117
138
  }
118
139
 
119
140
  def __init__(self, *args, **kwargs):
@@ -138,11 +159,16 @@ class ErrorHandledTyper(Typer):
138
159
  f"An unhanded error of type {type(e).__name__} occurred.",
139
160
  fg=typer.colors.RED,
140
161
  )
141
- typer.secho(str(e), fg=typer.colors.RED)
162
+
142
163
  typer.secho(
143
164
  " » Please report this error to the developers.",
144
165
  fg=typer.colors.RED,
145
166
  )
167
+
168
+ typer.secho(f"\n\n{e}:", fg=typer.colors.RED)
169
+ console = Console()
170
+ console.print_exception(show_locals=True)
171
+
146
172
  else:
147
173
  self.error_handlers[type(e)](e)
148
174
 
kleinkram/helper.py CHANGED
@@ -1,20 +1,20 @@
1
1
  import glob
2
2
  import os
3
3
  import queue
4
+ import sys
4
5
  import threading
5
6
  from datetime import datetime
6
7
  from functools import partial
7
8
 
8
- import typer
9
- from botocore.config import Config
10
- from botocore.utils import calculate_md5
11
- from typing_extensions import Dict, List
12
9
  import boto3
13
-
14
10
  import tqdm
11
+ import typer
15
12
  from boto3.s3.transfer import TransferConfig
16
- from botocore.client import BaseClient
13
+ from botocore.config import Config
14
+ from botocore.utils import calculate_md5
17
15
  from rich import print
16
+ from rich.console import Console
17
+ from typing_extensions import Dict
18
18
 
19
19
  from kleinkram.api_client import AuthenticatedClient
20
20
 
@@ -106,84 +106,101 @@ def expand_and_match(path_pattern):
106
106
  return file_list
107
107
 
108
108
 
109
- def uploadFiles(files: Dict[str, str], credentials: Dict[str, str], nrThreads: int):
109
+ def uploadFiles(
110
+ files_with_access: Dict[str, object], paths: Dict[str, str], nrThreads: int
111
+ ):
110
112
  client = AuthenticatedClient()
111
113
 
112
- session = boto3.Session(
113
- aws_access_key_id=credentials["accessKey"],
114
- aws_secret_access_key=credentials["secretKey"],
115
- aws_session_token=credentials["sessionToken"],
116
- )
117
114
  api_endpoint = client.tokenfile.endpoint
118
115
  if api_endpoint == "http://localhost:3000":
119
116
  minio_endpoint = "http://localhost:9000"
120
117
  else:
121
118
  minio_endpoint = api_endpoint.replace("api", "minio")
122
119
 
123
- config = Config(retries={"max_attempts": 10, "mode": "standard"})
124
- s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
125
-
126
120
  _queue = queue.Queue()
127
- for file in files.items():
128
- _queue.put(file)
121
+ for file_with_access in files_with_access:
122
+ _queue.put((file_with_access, str(paths[file_with_access["fileName"]])))
123
+
129
124
  threads = []
130
- transferCallback = TransferCallback()
131
- failed_uploads = []
125
+ transfer_callback = TransferCallback()
132
126
 
133
127
  for i in range(nrThreads):
134
128
  thread = threading.Thread(
135
- target=uploadFile, args=(_queue, s3, transferCallback, failed_uploads)
129
+ target=uploadFile,
130
+ args=(_queue, minio_endpoint, transfer_callback),
136
131
  )
137
132
  thread.start()
138
133
  threads.append(thread)
139
134
  for thread in threads:
140
135
  thread.join()
141
- if len(failed_uploads) > 0:
142
- print("Failed to upload the following files:")
143
- for file in failed_uploads:
144
- print(file)
145
136
 
146
137
 
147
138
  def uploadFile(
148
139
  _queue: queue.Queue,
149
- s3: BaseClient,
150
- transferCallback: TransferCallback,
151
- failed_uploads: List[str],
140
+ minio_endpoint: str,
141
+ transfer_callback: TransferCallback,
152
142
  ):
143
+ config = Config(retries={"max_attempts": 10, "mode": "standard"})
144
+
153
145
  while True:
154
146
  try:
155
- filename, _file = _queue.get(timeout=3)
156
- queueUUID = _file["queueUUID"]
157
- filepath = _file["filepath"]
158
- bucket = _file["bucket"]
159
- target_location = _file["location"]
160
- config = TransferConfig(
161
- multipart_chunksize=10 * 1024 * 1024, max_concurrency=5
147
+ file_with_access, filepath = _queue.get(timeout=3)
148
+
149
+ if "error" in file_with_access and (
150
+ file_with_access["error"] is not None or file_with_access["error"] != ""
151
+ ):
152
+ console = Console(file=sys.stderr, style="red")
153
+ console.print(
154
+ f"Error uploading file: {file_with_access['fileName']} ({filepath}): {file_with_access['error']}"
155
+ )
156
+ _queue.task_done()
157
+ continue
158
+
159
+ access_key = file_with_access["accessCredentials"]["accessKey"]
160
+ secret_key = file_with_access["accessCredentials"]["secretKey"]
161
+ session_token = file_with_access["accessCredentials"]["sessionToken"]
162
+
163
+ session = boto3.Session(
164
+ aws_access_key_id=access_key,
165
+ aws_secret_access_key=secret_key,
166
+ aws_session_token=session_token,
167
+ )
168
+
169
+ s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
170
+
171
+ fileu_uid = file_with_access["fileUUID"]
172
+ bucket = file_with_access["bucket"]
173
+
174
+ transfer_config = TransferConfig(
175
+ multipart_chunksize=10 * 1024 * 1024,
176
+ max_concurrency=5,
162
177
  )
163
178
  with open(filepath, "rb") as f:
164
179
  md5_checksum = calculate_md5(f)
165
180
  file_size = os.path.getsize(filepath)
166
- transferCallback.add_file(filename, file_size)
167
- callback_function = create_transfer_callback(transferCallback, filename)
181
+ transfer_callback.add_file(filepath, file_size)
182
+ callback_function = create_transfer_callback(
183
+ transfer_callback, filepath
184
+ )
168
185
  s3.Bucket(bucket).upload_file(
169
186
  filepath,
170
- target_location,
171
- Config=config,
187
+ fileu_uid,
188
+ Config=transfer_config,
172
189
  Callback=callback_function,
173
190
  )
174
191
 
175
192
  client = AuthenticatedClient()
176
193
  res = client.post(
177
194
  "/queue/confirmUpload",
178
- json={"uuid": queueUUID, "md5": md5_checksum},
195
+ json={"uuid": fileu_uid, "md5": md5_checksum},
179
196
  )
180
197
  res.raise_for_status()
181
198
  _queue.task_done()
182
199
  except queue.Empty:
183
200
  break
184
201
  except Exception as e:
185
- print(f"Error uploading {filename}: {e}")
186
- failed_uploads.append(filepath)
202
+ print("Error uploading file: " + filepath)
203
+ print(e)
187
204
  _queue.task_done()
188
205
 
189
206
 
kleinkram/main.py CHANGED
@@ -1,9 +1,9 @@
1
+ import importlib.metadata
1
2
  import os
2
3
  from datetime import datetime, timedelta
3
4
  from enum import Enum
4
5
 
5
6
  import httpx
6
- import importlib.metadata
7
7
  import typer
8
8
  from rich import print
9
9
  from rich.table import Table
@@ -14,7 +14,10 @@ from typing_extensions import Annotated, List, Optional
14
14
  from kleinkram.api_client import AuthenticatedClient
15
15
  from kleinkram.auth.auth import login, setCliKey, logout
16
16
  from kleinkram.endpoint.endpoint import endpoint
17
- from kleinkram.error_handling import ErrorHandledTyper, AccessDeniedException
17
+ from kleinkram.error_handling import (
18
+ ErrorHandledTyper,
19
+ AccessDeniedException,
20
+ )
18
21
  from kleinkram.file.file import file
19
22
  from kleinkram.mission.mission import missionCommands
20
23
  from kleinkram.project.project import project
@@ -73,6 +76,9 @@ app = ErrorHandledTyper(
73
76
  context_settings={"help_option_names": ["-h", "--help"]},
74
77
  no_args_is_help=True,
75
78
  cls=OrderCommands,
79
+ help=f"Kleinkram CLI\n\nThe Kleinkram CLI is a command line interface for Kleinkram. "
80
+ f"For a list of available commands, run 'klein --help' or visit "
81
+ f"https://docs.datasets.leggedrobotics.com/usage/cli/cli-getting-started.html for more information.",
76
82
  )
77
83
 
78
84
 
@@ -110,20 +116,30 @@ def download():
110
116
  raise NotImplementedError("Not implemented yet.")
111
117
 
112
118
 
113
- @app.command("upload", rich_help_panel=CommandPanel.CoreCommands)
119
+ @app.command("upload", rich_help_panel=CommandPanel.CoreCommands, no_args_is_help=True)
114
120
  def upload(
115
121
  path: Annotated[
116
122
  List[str],
117
- typer.Option(prompt=True, help="Path to files to upload, Regex supported"),
118
- ],
119
- project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
120
- mission: Annotated[
121
- str, typer.Option(prompt=True, help="Name of Mission to create")
123
+ typer.Option(help="Path to files to upload, Regex supported"),
122
124
  ],
125
+ project: Annotated[str, typer.Option(help="Name of Project")],
126
+ mission: Annotated[str, typer.Option(help="Name of Mission to create")],
123
127
  tags: Annotated[
124
128
  Optional[List[str]],
125
- typer.Option(prompt=False, help="Tags to add to the mission"),
129
+ typer.Option(help="Tags to add to the mission"),
126
130
  ] = None,
131
+ fix_filenames: Annotated[
132
+ bool,
133
+ typer.Option(help="Automatically fix filenames such that they are valid"),
134
+ ] = False,
135
+ create_project: Annotated[
136
+ bool,
137
+ typer.Option(help="Allows adding files to an existing mission"),
138
+ ] = False,
139
+ create_mission: Annotated[
140
+ bool,
141
+ typer.Option(help="Allows adding files to an existing mission"),
142
+ ] = False,
127
143
  ):
128
144
  """
129
145
  Upload files matching the path to a mission in a project.
@@ -137,107 +153,157 @@ def upload(
137
153
  files = []
138
154
  for p in path:
139
155
  files.extend(expand_and_match(p))
140
- filenames = list(
141
- map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
142
- )
143
- if not filenames:
144
- raise ValueError("No files found matching the given path.")
145
156
 
146
157
  print(
147
158
  f"Uploading the following files to mission '{mission}' in project '{project}':"
148
159
  )
149
- filepaths = {}
160
+ filename_filepaths_map = {}
150
161
  for path in files:
151
162
  if not os.path.isdir(path):
152
- filepaths[path.split("/")[-1]] = path
153
- typer.secho(f" - {path}", fg=typer.colors.RESET)
154
163
 
155
- try:
156
- client = AuthenticatedClient()
164
+ filename = path.split("/")[-1]
165
+ filename_without_extension, extension = os.path.splitext(filename)
166
+ if fix_filenames:
167
+
168
+ # replace all non-alphanumeric characters with underscores
169
+ filename_without_extension = "".join(
170
+ char if char.isalnum() else "_"
171
+ for char in filename_without_extension
172
+ )
173
+
174
+ # trim filename to 40 characters
175
+ filename_without_extension = filename_without_extension[:40]
176
+ filename = f"{filename_without_extension}{extension}"
177
+
178
+ if (
179
+ not filename.replace(".", "")
180
+ .replace("_", "")
181
+ .replace("-", "")
182
+ .isalnum()
183
+ ):
184
+ raise ValueError(
185
+ f"Filename '{filename}' is not valid. It must only contain alphanumeric characters, underscores and "
186
+ f"hyphens. Consider using the '--fix-filenames' option to automatically fix the filenames."
187
+ )
188
+
189
+ if not 3 <= len(filename_without_extension) <= 40:
190
+ raise ValueError(
191
+ f"Filename '{filename}' is not valid. It must be between 3 and 40 characters long. Consider using "
192
+ f"the '--fix-filenames' option to automatically fix the filenames."
193
+ )
194
+
195
+ filename_filepaths_map[filename] = path
196
+ typer.secho(f" - {filename}", fg=typer.colors.RESET)
197
+ print("\n\n")
198
+
199
+ filenames = list(filename_filepaths_map.keys())
200
+
201
+ if not filenames:
202
+ raise ValueError("No files found matching the given path.")
157
203
 
158
- get_project_url = "/project/byName"
159
- project_response = client.get(get_project_url, params={"name": project})
160
- if project_response.status_code >= 400:
204
+ # validate filenames
205
+ if len(filenames) != len(set(filenames)):
206
+ raise ValueError(
207
+ "Filenames must be unique. Please check the files you are trying to upload. This can happen if you have "
208
+ "multiple files with the same name in different directories or use the '--fix-filenames' option."
209
+ )
210
+
211
+ client = AuthenticatedClient()
212
+
213
+ get_project_url = "/project/byName"
214
+ project_response = client.get(get_project_url, params={"name": project})
215
+ if project_response.status_code >= 400:
216
+ if not create_project:
161
217
  raise AccessDeniedException(
162
218
  f"The project '{project}' does not exist or you do not have access to it.\n"
163
- f"Consider using the following command to create a project: 'klein project create'\n",
219
+ f"Consider using the following command to create a project: 'klein project create' "
220
+ f"or consider passing the flag '--create-project' to create the project automatically.",
164
221
  f"{project_response.json()['message']} ({project_response.status_code})",
165
222
  )
166
-
167
- project_json = project_response.json()
168
- if not project_json["uuid"]:
169
- print(f"Project not found: '{project}'")
170
- return
171
-
172
- can_upload = canUploadMission(client, project_json["uuid"])
173
- if not can_upload:
174
- raise AccessDeniedException(
175
- f"You do not have the required permissions to upload to project '{project}'\n",
176
- "Access Denied",
223
+ else:
224
+ print(f"Project '{project}' does not exist. Creating it now.")
225
+ create_project_url = "/project/create"
226
+ project_response = client.post(
227
+ create_project_url,
228
+ json={
229
+ "name": project,
230
+ "description": "Autogenerated by klein CLI",
231
+ "requiredTags": [],
232
+ },
177
233
  )
234
+ if project_response.status_code >= 400:
235
+ raise ValueError(
236
+ f"Failed to create project. Status Code: "
237
+ f"{str(project_response.status_code)}\n"
238
+ f"{project_response.json()['message'][0]}"
239
+ )
240
+ print("Project created successfully.")
241
+
242
+ project_json = project_response.json()
243
+ if not project_json["uuid"]:
244
+ print(f"Project not found: '{project}'")
245
+ return
246
+
247
+ can_upload = canUploadMission(client, project_json["uuid"])
248
+ if not can_upload:
249
+ raise AccessDeniedException(
250
+ f"You do not have the required permissions to upload to project '{project}'\n",
251
+ "Access Denied",
252
+ )
178
253
 
179
- if not tags:
180
- tags = []
181
- tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
254
+ if not tags:
255
+ tags = []
256
+ tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
182
257
 
183
- promptForTags(tags_dict, project_json["requiredTags"])
258
+ required_tags = (
259
+ project_json["requiredTags"] if "requiredTags" in project_json else []
260
+ )
261
+ promptForTags(tags_dict, required_tags)
184
262
 
185
- get_mission_url = "/mission/byName"
186
- mission_response = client.get(get_mission_url, params={"name": mission})
187
- mission_response.raise_for_status()
188
- if mission_response.content:
189
- mission_json = mission_response.json()
190
- if mission_json["uuid"]:
263
+ get_mission_url = "/mission/byName"
264
+ mission_response = client.get(get_mission_url, params={"name": mission})
265
+ if mission_response.status_code >= 400:
266
+ if not create_mission:
267
+ raise AccessDeniedException(
268
+ f"The mission '{mission}' does not exist or you do not have access to it.\n"
269
+ f"Consider using the following command to create a mission: 'klein mission create' "
270
+ f"or consider passing the flag '--create-mission' to create the mission automatically.",
271
+ f"{mission_response.json()['message']} ({mission_response.status_code})",
272
+ )
273
+ else:
274
+ print(f"Mission '{mission}' does not exist. Creating it now.")
275
+ create_mission_url = "/mission/create"
276
+ mission_response = client.post(
277
+ create_mission_url,
278
+ json={
279
+ "name": mission,
280
+ "projectUUID": project_json["uuid"],
281
+ "tags": tags_dict,
282
+ },
283
+ )
284
+ if mission_response.status_code >= 400:
191
285
  raise ValueError(
192
- f"Mission {mission_json['name']} ({mission_json['uuid']}) already exists. Delete it or select "
193
- f"another name."
286
+ f"Failed to create mission. Status Code: "
287
+ f"{str(mission_response.status_code)}\n"
288
+ f"{mission_response.json()['message'][0]}"
194
289
  )
195
- raise Exception(f"Something failed, should not happen")
196
-
197
- create_mission_url = "/mission/create"
198
- new_mission = client.post(
199
- create_mission_url,
200
- json={
201
- "name": mission,
202
- "projectUUID": project_json["uuid"],
203
- "tags": tags_dict,
204
- },
205
- )
206
- if new_mission.status_code >= 400:
207
- raise ValueError(
208
- "Failed to create mission. Status Code: "
209
- + str(new_mission.status_code)
210
- + "\n"
211
- + new_mission.json()["message"]
212
- )
213
- new_mission_data = new_mission.json()
214
290
 
215
- get_temporary_credentials = "/file/temporaryAccess"
216
- response_2 = client.post(
217
- get_temporary_credentials,
218
- json={"filenames": filenames, "missionUUID": new_mission_data["uuid"]},
291
+ mission_json = mission_response.json()
292
+
293
+ get_temporary_credentials = "/file/temporaryAccess"
294
+ response = client.post(
295
+ get_temporary_credentials,
296
+ json={"filenames": filenames, "missionUUID": mission_json["uuid"]},
297
+ )
298
+ if response.status_code >= 400:
299
+ raise ValueError(
300
+ "Failed to get temporary credentials. Status Code: "
301
+ + str(response.status_code)
302
+ + "\n"
303
+ + response.json()["message"][0]
219
304
  )
220
- if response_2.status_code >= 400:
221
- raise ValueError(
222
- "Failed to get temporary credentials. Status Code: "
223
- + str(response_2.status_code)
224
- + "\n"
225
- + response_2.json()["message"]
226
- )
227
- temp_credentials = response_2.json()
228
- credential = temp_credentials["credentials"]
229
- confirmed_files = temp_credentials["files"]
230
- for _file in filenames:
231
- if not _file in confirmed_files.keys():
232
- raise Exception(
233
- "Could not upload File '" + _file + "'. Is the filename unique? "
234
- )
235
- confirmed_files[_file]["filepath"] = filepaths[_file]
236
- if len(confirmed_files.keys()) > 0:
237
- uploadFiles(confirmed_files, credential, 4)
238
305
 
239
- except httpx.HTTPError as e:
240
- print(e)
306
+ uploadFiles(response.json(), filename_filepaths_map, 4)
241
307
 
242
308
 
243
309
  @queue.command("list")
@@ -1,7 +1,8 @@
1
- from typing_extensions import Annotated
2
-
3
1
  import httpx
4
2
  import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from typing_extensions import Annotated
5
6
 
6
7
  from kleinkram.api_client import AuthenticatedClient
7
8
 
@@ -13,22 +14,87 @@ project = typer.Typer(
13
14
  )
14
15
 
15
16
 
16
- @project.command("list")
17
+ @project.command("list", help="List all projects")
17
18
  def list_projects():
18
19
  """
19
20
  List all projects.
20
21
  """
21
- try:
22
- client = AuthenticatedClient()
23
- response = client.get("/project/filtered")
24
- response.raise_for_status()
25
- projects = response.json()[0]
26
- print("Projects:")
27
- for _project in projects:
28
- print(f"- {_project['name']}")
22
+ client = AuthenticatedClient()
23
+ response = client.get("/project/filtered")
24
+ response.raise_for_status()
25
+ projects = response.json()[0]
29
26
 
30
- except httpx.HTTPError as e:
31
- print(f"Failed to fetch projects: {e}")
27
+ stdout_console = Console(stderr=False)
28
+ stderr_console = Console(stderr=True)
29
+ stderr_console.print(f"\nfound {len(projects)} projects with the following UUIDs:")
30
+
31
+ # print the uuids to stdout for simple piping
32
+ for p in projects:
33
+ stderr_console.print(" - ", end="")
34
+ stdout_console.print(p["uuid"])
35
+ stderr_console.print("\n")
36
+
37
+ # Print a summary table using rich to stderr
38
+ table = Table(title="Projects", expand=True)
39
+ table.add_column("Project UUID", width=10)
40
+ table.add_column("Project Name", width=12)
41
+ table.add_column("Description")
42
+ for p in projects:
43
+ table.add_row(p["uuid"], p["name"], p["description"])
44
+
45
+ stderr_console.print(table)
46
+ stderr_console.print("\n")
47
+
48
+
49
+ @project.command("details", help="Get details of a project")
50
+ def project_details(
51
+ project_uuid: Annotated[
52
+ str, typer.Argument(help="UUID of the project to get details of")
53
+ ]
54
+ ):
55
+ """
56
+ Get details of a project
57
+ """
58
+ client = AuthenticatedClient()
59
+ response = client.get(f"/project/one?uuid={project_uuid}")
60
+ response.raise_for_status()
61
+ project = response.json()
62
+
63
+ stdout_console = Console(stderr=False)
64
+ stderr_console = Console(stderr=True)
65
+ stderr_console.print(
66
+ f"\nDetails of project with UUID {project_uuid}:", highlight=False
67
+ )
68
+
69
+ # Print the details to stderr using rich
70
+ table = Table(title="Project Details", expand=True)
71
+ table.add_column("Key", width=16)
72
+ table.add_column("Value")
73
+ for key, value in project.items():
74
+
75
+ access_name_map = {0: "READ", 10: "CREATE", 20: "WRITE", 30: "DELETE"}
76
+
77
+ if key == "project_accesses":
78
+ value = ", ".join(
79
+ [
80
+ f"'{access['accessGroup']['name']}' ({access_name_map[access['rights']]})"
81
+ for access in value
82
+ ]
83
+ )
84
+
85
+ if key == "missions":
86
+ value = ", ".join([f"'{mission['name']}'" for mission in value])
87
+
88
+ if key == "creator":
89
+ value = value["name"]
90
+
91
+ table.add_row(key, f"{value}")
92
+
93
+ stderr_console.print(table)
94
+ stderr_console.print("\nList of missions:")
95
+ for mission in project["missions"]:
96
+ stderr_console.print(" - ", end="")
97
+ stdout_console.print(mission["uuid"])
32
98
 
33
99
 
34
100
  @project.command("create")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kleinkram
3
- Version: 0.32.3.dev20241023090650
3
+ Version: 0.33.0.dev20241024121528
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
@@ -1,20 +1,20 @@
1
1
  kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  kleinkram/api_client.py,sha256=1GPsM-XFbPYEKP7RfWmzMTwxRqnVh4wtHVuW25KT8kA,2264
3
3
  kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
4
- kleinkram/error_handling.py,sha256=JKwQVKLu5VPkhpQbiJsCkhAgdNuzNuDbxlS85qTlvMU,4116
5
- kleinkram/helper.py,sha256=tBxZj4PzII0YHfHtrDhagKrb4gTdNYa31t4E3DRDK0c,8372
6
- kleinkram/main.py,sha256=9u_msMlPhphfaL92GNrm-cRUeSHxc_mOFPeXLDzv-ok,9642
4
+ kleinkram/error_handling.py,sha256=-Ai5xB_aGHDhzOhZyCJWijarSgU0_QJJrrvTFCAK6qI,4644
5
+ kleinkram/helper.py,sha256=mhRCztnkJ-Y9udpRKUY92ymVEZTlwzDrfLZkRupDqys,8953
6
+ kleinkram/main.py,sha256=cgDTfFQW91wlHZYg4_MmlERcFXDd1z--lUP6bGu8YnM,12536
7
7
  kleinkram/auth/auth.py,sha256=w3-TsxWxURzLQ3_p43zgV4Rlh4dVL_WqI6HG2aes-b4,4991
8
8
  kleinkram/endpoint/endpoint.py,sha256=uez5UrAnP7L5rVHUysA9tFkN3dB3dG1Ojt9g3w-UWuQ,1441
9
9
  kleinkram/file/file.py,sha256=gLCZDHHgQWq25OmeG-lwkIh4aRZaLK12xxLkbhZ_m-g,5390
10
10
  kleinkram/mission/mission.py,sha256=KI_r-DbaXr8uKi9rnSopj-G1N4Nq_ELEBn4mPJXMQzQ,8861
11
- kleinkram/project/project.py,sha256=yDygz9JJ4Td5VsoCoCLm36HccRyd7jl65Hq05uxEGts,1602
11
+ kleinkram/project/project.py,sha256=le85GN9RgrqJeAL5mS-PhowFDjv-HBCYhgkKeFAUcGs,3780
12
12
  kleinkram/queue/queue.py,sha256=MaLBjAu8asi9BkPvbbT-5AobCcpy3ex5rxM1kHpRINA,181
13
13
  kleinkram/tag/tag.py,sha256=JSHbDPVfsvP34MuQhw__DPQk-Bah5G9BgwYsj_K_JGc,1805
14
14
  kleinkram/topic/topic.py,sha256=IaXhrIHcJ3FSIr0WC-N7u9fkz-lAvSBgQklTX67t0Yc,1641
15
15
  kleinkram/user/user.py,sha256=hDrbWeFPPnh2sswDd445SwcIFGyAbfXXWpYq1VqrK0g,1379
16
- kleinkram-0.32.3.dev20241023090650.dist-info/METADATA,sha256=yBp013XYStuPOiuT0iz-R3KmqU3lxkI2sp9hF8VNMq4,845
17
- kleinkram-0.32.3.dev20241023090650.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
- kleinkram-0.32.3.dev20241023090650.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
19
- kleinkram-0.32.3.dev20241023090650.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
20
- kleinkram-0.32.3.dev20241023090650.dist-info/RECORD,,
16
+ kleinkram-0.33.0.dev20241024121528.dist-info/METADATA,sha256=_68yPVnug6xFs04N2uqmSzeGCBnTB13ziNb_4ma-sMA,845
17
+ kleinkram-0.33.0.dev20241024121528.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
18
+ kleinkram-0.33.0.dev20241024121528.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
19
+ kleinkram-0.33.0.dev20241024121528.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
20
+ kleinkram-0.33.0.dev20241024121528.dist-info/RECORD,,