kleinkram 0.51.0.dev20251002112719__tar.gz → 0.51.0.dev20251003120233__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.

Files changed (57) hide show
  1. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/PKG-INFO +1 -1
  2. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/deser.py +20 -2
  3. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/file_transfer.py +14 -13
  4. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/routes.py +97 -15
  5. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_mission.py +16 -4
  6. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_upload.py +1 -2
  7. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/core.py +5 -2
  8. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/errors.py +2 -0
  9. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/models.py +1 -0
  10. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/printing.py +2 -1
  11. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/utils.py +1 -8
  12. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/PKG-INFO +1 -1
  13. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/setup.cfg +1 -1
  14. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/README.md +0 -0
  15. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/__init__.py +0 -0
  16. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/__main__.py +0 -0
  17. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/_version.py +0 -0
  18. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/__init__.py +0 -0
  19. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/client.py +0 -0
  20. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/pagination.py +0 -0
  21. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/api/query.py +0 -0
  22. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/auth.py +0 -0
  23. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/__init__.py +0 -0
  24. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_download.py +0 -0
  25. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_endpoint.py +0 -0
  26. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_file.py +0 -0
  27. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_list.py +0 -0
  28. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_project.py +0 -0
  29. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/_verify.py +0 -0
  30. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/app.py +0 -0
  31. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/cli/error_handling.py +0 -0
  32. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/config.py +0 -0
  33. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/main.py +0 -0
  34. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/py.typed +0 -0
  35. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/types.py +0 -0
  36. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram/wrappers.py +0 -0
  37. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/SOURCES.txt +0 -0
  38. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/dependency_links.txt +0 -0
  39. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/entry_points.txt +0 -0
  40. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/requires.txt +0 -0
  41. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/kleinkram.egg-info/top_level.txt +0 -0
  42. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/pyproject.toml +0 -0
  43. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/requirements.txt +0 -0
  44. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/setup.py +0 -0
  45. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/testing/__init__.py +0 -0
  46. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/testing/backend_fixtures.py +0 -0
  47. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/__init__.py +0 -0
  48. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/conftest.py +0 -0
  49. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_config.py +0 -0
  50. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_core.py +0 -0
  51. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_end_to_end.py +0 -0
  52. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_error_handling.py +0 -0
  53. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_fixtures.py +0 -0
  54. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_printing.py +0 -0
  55. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_query.py +0 -0
  56. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_utils.py +0 -0
  57. {kleinkram-0.51.0.dev20251002112719 → kleinkram-0.51.0.dev20251003120233}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.51.0.dev20251002112719
3
+ Version: 0.51.0.dev20251003120233
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Author-email: pucyril@ethz.ch, dgarmier@ethz.ch, jschwab@ethz.ch
@@ -7,12 +7,13 @@ from typing import Dict
7
7
  from typing import Literal
8
8
  from typing import NewType
9
9
  from typing import Tuple
10
+ from typing import List
10
11
  from uuid import UUID
11
12
 
12
13
  import dateutil.parser
13
14
 
14
15
  from kleinkram.errors import ParsingError
15
- from kleinkram.models import File
16
+ from kleinkram.models import File, MetadataValue
16
17
  from kleinkram.models import FileState
17
18
  from kleinkram.models import Mission
18
19
  from kleinkram.models import Project
@@ -51,6 +52,7 @@ class MissionObjectKeys(str, Enum):
51
52
  DESCRIPTION = "description"
52
53
  CREATED_AT = "createdAt"
53
54
  UPDATED_AT = "updatedAt"
55
+ TAGS = "tags"
54
56
 
55
57
 
56
58
  class ProjectObjectKeys(str, Enum):
@@ -59,6 +61,7 @@ class ProjectObjectKeys(str, Enum):
59
61
  DESCRIPTION = "description"
60
62
  CREATED_AT = "createdAt"
61
63
  UPDATED_AT = "updatedAt"
64
+ REQUIRED_TAGS = "requiredTags"
62
65
 
63
66
 
64
67
  def _get_nested_info(data, key: Literal["mission", "project"]) -> Tuple[UUID, str]:
@@ -82,6 +85,19 @@ def _parse_file_state(state: str) -> FileState:
82
85
  except ValueError as e:
83
86
  raise ParsingError(f"error parsing file state: {state}") from e
84
87
 
88
+ def _parse_metadata(tags: List[Dict]) -> Dict[str, MetadataValue]:
89
+ result = {}
90
+ try:
91
+ for tag in tags:
92
+ entry = {tag.get("name"): MetadataValue(tag.get("valueAsString"), tag.get("datatype"))}
93
+ result.update(entry)
94
+ return result
95
+ except ValueError as e:
96
+ raise ParsingError(f"error parsing metadata: {e}") from e
97
+
98
+ def _parse_required_tags(tags: List[Dict]) -> list[str]:
99
+ return list(_parse_metadata(tags).keys())
100
+
85
101
 
86
102
  def _parse_project(project_object: ProjectObject) -> Project:
87
103
  try:
@@ -90,6 +106,7 @@ def _parse_project(project_object: ProjectObject) -> Project:
90
106
  description = project_object[ProjectObjectKeys.DESCRIPTION]
91
107
  created_at = _parse_datetime(project_object[ProjectObjectKeys.CREATED_AT])
92
108
  updated_at = _parse_datetime(project_object[ProjectObjectKeys.UPDATED_AT])
109
+ required_tags = _parse_required_tags(project_object[ProjectObjectKeys.REQUIRED_TAGS])
93
110
  except Exception as e:
94
111
  raise ParsingError(f"error parsing project: {project_object}") from e
95
112
  return Project(
@@ -98,6 +115,7 @@ def _parse_project(project_object: ProjectObject) -> Project:
98
115
  description=description,
99
116
  created_at=created_at,
100
117
  updated_at=updated_at,
118
+ required_tags=required_tags,
101
119
  )
102
120
 
103
121
 
@@ -107,7 +125,7 @@ def _parse_mission(mission: MissionObject) -> Mission:
107
125
  name = mission[MissionObjectKeys.NAME]
108
126
  created_at = _parse_datetime(mission[MissionObjectKeys.CREATED_AT])
109
127
  updated_at = _parse_datetime(mission[MissionObjectKeys.UPDATED_AT])
110
- metadata = {} # TODO: this crap is really bad to parse
128
+ metadata = _parse_metadata(mission[MissionObjectKeys.TAGS])
111
129
 
112
130
  project_id, project_name = _get_nested_info(mission, PROJECT)
113
131
 
@@ -572,19 +572,20 @@ def upload_files(
572
572
 
573
573
  avg_speed_bps = total_uploaded_bytes / elapsed_time if elapsed_time > 0 else 0
574
574
 
575
- console.print(f"Upload took {elapsed_time:.2f} seconds")
576
- console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
577
- console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
578
-
579
- if failed_files > 0:
580
- console.print(
581
- f"\nUploaded {len(files) - failed_files - skipped_files} files, {skipped_files} skipped, {failed_files} uploads failed",
582
- style="red",
583
- )
584
- else:
585
- console.print(
586
- f"\nUploaded {len(files) - skipped_files} files, {skipped_files} skipped"
587
- )
575
+ if verbose:
576
+ console.print(f"Upload took {elapsed_time:.2f} seconds")
577
+ console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
578
+ console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
579
+
580
+ if failed_files > 0:
581
+ console.print(
582
+ f"\nUploaded {len(files) - failed_files - skipped_files} files, {skipped_files} skipped, {failed_files} uploads failed",
583
+ style="red",
584
+ )
585
+ else:
586
+ console.print(
587
+ f"\nUploaded {len(files) - skipped_files} files, {skipped_files} skipped"
588
+ )
588
589
 
589
590
 
590
591
  def download_files(
@@ -1,7 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import tempfile
4
5
  from enum import Enum
6
+ from pathlib import Path
5
7
  from typing import Any
6
8
  from typing import Dict
7
9
  from typing import Generator
@@ -40,10 +42,11 @@ from kleinkram.errors import MissionExists
40
42
  from kleinkram.errors import MissionNotFound
41
43
  from kleinkram.errors import ProjectExists
42
44
  from kleinkram.errors import ProjectNotFound
45
+ from kleinkram.errors import MissionValidationError
43
46
  from kleinkram.models import File
44
47
  from kleinkram.models import Mission
45
48
  from kleinkram.models import Project
46
- from kleinkram.utils import is_valid_uuid4
49
+ from kleinkram.utils import is_valid_uuid4, split_args
47
50
 
48
51
  __all__ = [
49
52
  "_get_api_version",
@@ -220,6 +223,28 @@ def _mission_name_is_available(
220
223
  return False
221
224
 
222
225
 
226
+ def _validate_mission_name(
227
+ client: AuthenticatedClient, project_id: UUID, mission_name: str
228
+ ) -> None:
229
+ if not _mission_name_is_available(client, mission_name, project_id):
230
+ raise MissionExists(
231
+ f"Mission with name: `{mission_name}` already exists"
232
+ f" in project: {project_id}"
233
+ )
234
+
235
+ if is_valid_uuid4(mission_name):
236
+ raise ValueError(
237
+ f"Mission name: `{mission_name}` is a valid UUIDv4, "
238
+ "mission names must not be valid UUIDv4's"
239
+ )
240
+
241
+ if mission_name.endswith(" "):
242
+ raise ValueError(
243
+ "A mission name cannot end with a whitespace. "
244
+ f"The given mission name was \'{mission_name}\'"
245
+ )
246
+
247
+
223
248
  def _project_name_is_available(client: AuthenticatedClient, project_name: str) -> bool:
224
249
  project_query = ProjectQuery(patterns=[project_name])
225
250
  try:
@@ -228,6 +253,47 @@ def _project_name_is_available(client: AuthenticatedClient, project_name: str) -
228
253
  return True
229
254
  return False
230
255
 
256
+ def _validate_mission_created(client: AuthenticatedClient, project_id: str, mission_name: str) -> None:
257
+ """
258
+ validate that a mission is successfully created
259
+ """
260
+ mission_ids, mission_patterns = split_args([mission_name])
261
+ project_ids, project_patterns = split_args([project_id])
262
+
263
+ project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
264
+ mission_query = MissionQuery(
265
+ ids=mission_ids,
266
+ patterns=mission_patterns,
267
+ project_query=project_query,
268
+ )
269
+ try:
270
+ with tempfile.NamedTemporaryFile(suffix=".mcap", delete=False) as tmp:
271
+ tmp.write(b"dummy content")
272
+ tmp_path = Path(tmp.name)
273
+
274
+ kleinkram.core.upload(
275
+ client=client,
276
+ query=mission_query,
277
+ file_paths=[tmp_path],
278
+ verbose=False,
279
+ )
280
+
281
+ file_query = FileQuery(
282
+ ids=[],
283
+ patterns=[tmp_path.name],
284
+ mission_query=mission_query,
285
+ )
286
+ file_parsed = get_file(client, file_query)
287
+
288
+ kleinkram.core.delete_files(client=client, file_ids=[file_parsed.id])
289
+
290
+ except Exception as e:
291
+ raise MissionValidationError(f"Mission validation failed: {e}")
292
+
293
+ finally:
294
+ if tmp_path.exists():
295
+ tmp_path.unlink()
296
+
231
297
 
232
298
  def _create_mission(
233
299
  client: AuthenticatedClient,
@@ -236,6 +302,7 @@ def _create_mission(
236
302
  *,
237
303
  metadata: Optional[Dict[str, str]] = None,
238
304
  ignore_missing_tags: bool = False,
305
+ required_tags: Optional[List[str]] = None,
239
306
  ) -> UUID:
240
307
  """\
241
308
  creates a new mission with the given name and project_id
@@ -246,16 +313,11 @@ def _create_mission(
246
313
  if metadata is None:
247
314
  metadata = {}
248
315
 
249
- if not _mission_name_is_available(client, mission_name, project_id):
250
- raise MissionExists(
251
- f"Mission with name: `{mission_name}` already exists"
252
- f" in project: {project_id}"
253
- )
316
+ _validate_mission_name(client, project_id, mission_name)
254
317
 
255
- if is_valid_uuid4(mission_name):
256
- raise ValueError(
257
- f"Mission name: `{mission_name}` is a valid UUIDv4, "
258
- "mission names must not be valid UUIDv4's"
318
+ if required_tags and not set(required_tags).issubset(metadata.keys()):
319
+ raise InvalidMissionMetadata(
320
+ f"Mission tags `{required_tags}` are required but missing from metadata: {metadata}"
259
321
  )
260
322
 
261
323
  # we need to translate tag keys to tag type ids
@@ -267,9 +329,9 @@ def _create_mission(
267
329
  "tags": {str(k): v for k, v in tags.items()},
268
330
  "ignoreTags": ignore_missing_tags,
269
331
  }
270
-
271
332
  resp = client.post(CREATE_MISSION, json=payload)
272
333
  resp.raise_for_status()
334
+ _validate_mission_created(client, str(project_id), mission_name)
273
335
 
274
336
  return UUID(resp.json()["uuid"], version=4)
275
337
 
@@ -288,18 +350,37 @@ def _create_project(
288
350
  return UUID(resp.json()["uuid"], version=4)
289
351
 
290
352
 
353
+ def _validate_tag_value(
354
+ tag_value, tag_datatype
355
+ ) -> None:
356
+ if tag_datatype == "NUMBER":
357
+ try:
358
+ float(tag_value)
359
+ except:
360
+ raise InvalidMissionMetadata(f"Value '{tag_value}' is not a valid NUMBER")
361
+ elif tag_datatype == "BOOLEAN":
362
+ if tag_value.lower() not in {"true", "false"}:
363
+ raise InvalidMissionMetadata(f"Value '{tag_value}' is not a valid BOOLEAN (expected 'true' or 'false')")
364
+ else:
365
+ pass # any string is fine
366
+ #TODO: add check for LOCATION tag datatype
367
+
368
+
291
369
  def _get_metadata_type_id_by_name(
292
370
  client: AuthenticatedClient, tag_name: str
293
- ) -> Optional[UUID]:
371
+ ) -> Tuple[Optional[UUID], str]:
294
372
  resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
295
373
 
296
374
  if resp.status_code in (403, 404):
297
375
  return None
298
376
 
299
377
  resp.raise_for_status()
378
+ try:
379
+ data = resp.json()['data'][0]
380
+ except IndexError:
381
+ return None, None
300
382
 
301
- data = resp.json()[0]
302
- return UUID(data["uuid"], version=4)
383
+ return UUID(data["uuid"], version=4), data["datatype"]
303
384
 
304
385
 
305
386
  def _get_tags_map(
@@ -309,9 +390,10 @@ def _get_tags_map(
309
390
  # why are we using metadata type ids as keys???
310
391
  ret = {}
311
392
  for key, val in metadata.items():
312
- metadata_type_id = _get_metadata_type_id_by_name(client, key)
393
+ metadata_type_id, tag_datatype = _get_metadata_type_id_by_name(client, key)
313
394
  if metadata_type_id is None:
314
395
  raise InvalidMissionMetadata(f"metadata field: {key} does not exist")
396
+ _validate_tag_value(val, tag_datatype)
315
397
  ret[metadata_type_id] = val
316
398
  return ret
317
399
 
@@ -13,6 +13,7 @@ from kleinkram.api.query import ProjectQuery
13
13
  from kleinkram.api.routes import get_mission
14
14
  from kleinkram.api.routes import get_project
15
15
  from kleinkram.config import get_shared_state
16
+ from kleinkram.errors import InvalidMissionQuery
16
17
  from kleinkram.printing import print_mission_info
17
18
  from kleinkram.utils import load_metadata
18
19
  from kleinkram.utils import split_args
@@ -45,13 +46,16 @@ def create(
45
46
  metadata_dct = load_metadata(Path(metadata)) if metadata else {} # noqa
46
47
 
47
48
  client = AuthenticatedClient()
48
- project_id = get_project(client, project_query).id
49
+ project = get_project(client, project_query)
50
+ project_id = project.id
51
+ project_required_tags = project.required_tags
49
52
  mission_id = kleinkram.api.routes._create_mission(
50
53
  client,
51
54
  project_id,
52
55
  mission_name,
53
56
  metadata=metadata_dct,
54
57
  ignore_missing_tags=ignore_missing_tags,
58
+ required_tags=project_required_tags,
55
59
  )
56
60
 
57
61
  mission_parsed = get_mission(client, MissionQuery(ids=[mission_id]))
@@ -120,9 +124,6 @@ def delete(
120
124
  False, "--confirm", "-y", "--yes", help="confirm deletion"
121
125
  ),
122
126
  ) -> None:
123
- if not confirm:
124
- typer.confirm(f"delete {project} {mission}", abort=True)
125
-
126
127
  project_ids, project_patterns = split_args([project] if project else [])
127
128
  project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
128
129
 
@@ -132,9 +133,20 @@ def delete(
132
133
  patterns=mission_patterns,
133
134
  project_query=project_query,
134
135
  )
136
+ if mission_patterns and not (project_patterns or project_ids):
137
+ raise InvalidMissionQuery(
138
+ "Mission query does not uniquely determine mission. "
139
+ "Project name or id must be specified when deleting by mission name"
140
+ )
135
141
 
136
142
  client = AuthenticatedClient()
137
143
  mission_parsed = get_mission(client, mission_query)
144
+ if not confirm:
145
+ if project:
146
+ typer.confirm(f"delete {project} {mission}", abort=True)
147
+ else:
148
+ typer.confirm(f"delete {mission_parsed.name} {mission}", abort=True)
149
+
138
150
  kleinkram.core.delete_mission(client=client, mission_id=mission_parsed.id)
139
151
 
140
152
 
@@ -45,8 +45,7 @@ def upload(
45
45
  help="fix filenames before upload, this does not change the filenames locally",
46
46
  ),
47
47
  experimental_datatypes: bool = typer.Option(
48
- False,
49
- help="allow experimental datatypes (yaml, svo2, db3, tum)"
48
+ False, help="allow experimental datatypes (yaml, svo2, db3, tum)"
50
49
  ),
51
50
  ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
52
51
  ) -> None:
@@ -107,9 +107,11 @@ def upload(
107
107
 
108
108
  if create and mission is None:
109
109
  # check if project exists and get its id at the same time
110
- project_id = kleinkram.api.routes.get_project(
110
+ project = kleinkram.api.routes.get_project(
111
111
  client, query=query.project_query
112
- ).id
112
+ )
113
+ project_id = project.id
114
+ project_required_tags = project.required_tags
113
115
  mission_name = check_mission_query_is_creatable(query)
114
116
  kleinkram.api.routes._create_mission(
115
117
  client,
@@ -117,6 +119,7 @@ def upload(
117
119
  mission_name,
118
120
  metadata=metadata or {},
119
121
  ignore_missing_tags=ignore_missing_metadata,
122
+ required_tags=project_required_tags,
120
123
  )
121
124
  mission = kleinkram.api.routes.get_mission(client, query)
122
125
 
@@ -45,6 +45,8 @@ class FileNameNotSupported(Exception): ...
45
45
 
46
46
  class InvalidMissionMetadata(Exception): ...
47
47
 
48
+ class MissionValidationError(Exception): ...
49
+
48
50
 
49
51
  class NotAuthenticated(Exception):
50
52
  def __init__(self) -> None:
@@ -41,6 +41,7 @@ class Project:
41
41
  description: str
42
42
  created_at: datetime
43
43
  updated_at: datetime
44
+ required_tags: List[str]
44
45
 
45
46
 
46
47
  @dataclass(frozen=True)
@@ -233,7 +233,7 @@ def file_info_table(file: File) -> Table:
233
233
 
234
234
 
235
235
  def mission_info_table(
236
- mission: Mission, print_metadata: bool = False
236
+ mission: Mission, print_metadata: bool = True
237
237
  ) -> Tuple[Table, ...]:
238
238
  table = Table("k", "v", title=f"mission info: {mission.name}", show_header=False)
239
239
 
@@ -269,6 +269,7 @@ def project_info_table(project: Project) -> Table:
269
269
  table.add_row("description", project.description)
270
270
  table.add_row("created", str(project.created_at))
271
271
  table.add_row("updated", str(project.updated_at))
272
+ table.add_row("required tags", ", ".join(project.required_tags))
272
273
 
273
274
  return table
274
275
 
@@ -28,14 +28,7 @@ from kleinkram.types import IdLike
28
28
  from kleinkram.types import PathLike
29
29
 
30
30
  INTERNAL_ALLOWED_CHARS = string.ascii_letters + string.digits + "_" + "-"
31
- SUPPORT_FILE_TYPES = [
32
- ".bag",
33
- ".mcap",
34
- ".db3",
35
- ".svo2",
36
- ".tum",
37
- ".yaml"
38
- ]
31
+ SUPPORT_FILE_TYPES = [".bag", ".mcap", ".db3", ".svo2", ".tum", ".yaml"]
39
32
 
40
33
 
41
34
  def file_paths_from_files(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.51.0.dev20251002112719
3
+ Version: 0.51.0.dev20251003120233
4
4
  Summary: give me your bags
5
5
  Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
6
  Author-email: pucyril@ethz.ch, dgarmier@ethz.ch, jschwab@ethz.ch
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = kleinkram
3
- version = 0.51.0-dev20251002112719
3
+ version = 0.51.0-dev20251003120233
4
4
  description = give me your bags
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown