kleinkram 0.51.0.dev20251002115350__tar.gz → 0.51.0.dev20251003125418__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.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/PKG-INFO +1 -1
  2. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/deser.py +28 -1
  3. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/file_transfer.py +14 -13
  4. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/routes.py +100 -14
  5. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_mission.py +16 -4
  6. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/core.py +4 -3
  7. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/errors.py +3 -0
  8. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/models.py +1 -0
  9. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/printing.py +2 -1
  10. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/PKG-INFO +1 -1
  11. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/setup.cfg +1 -1
  12. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/README.md +0 -0
  13. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/__init__.py +0 -0
  14. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/__main__.py +0 -0
  15. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/_version.py +0 -0
  16. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/__init__.py +0 -0
  17. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/client.py +0 -0
  18. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/pagination.py +0 -0
  19. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/api/query.py +0 -0
  20. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/auth.py +0 -0
  21. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/__init__.py +0 -0
  22. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_download.py +0 -0
  23. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_endpoint.py +0 -0
  24. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_file.py +0 -0
  25. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_list.py +0 -0
  26. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_project.py +0 -0
  27. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_upload.py +0 -0
  28. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/_verify.py +0 -0
  29. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/app.py +0 -0
  30. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/cli/error_handling.py +0 -0
  31. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/config.py +0 -0
  32. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/main.py +0 -0
  33. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/py.typed +0 -0
  34. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/types.py +0 -0
  35. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/utils.py +0 -0
  36. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram/wrappers.py +0 -0
  37. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/SOURCES.txt +0 -0
  38. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/dependency_links.txt +0 -0
  39. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/entry_points.txt +0 -0
  40. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/requires.txt +0 -0
  41. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/kleinkram.egg-info/top_level.txt +0 -0
  42. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/pyproject.toml +0 -0
  43. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/requirements.txt +0 -0
  44. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/setup.py +0 -0
  45. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/testing/__init__.py +0 -0
  46. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/testing/backend_fixtures.py +0 -0
  47. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/__init__.py +0 -0
  48. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/conftest.py +0 -0
  49. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_config.py +0 -0
  50. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_core.py +0 -0
  51. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_end_to_end.py +0 -0
  52. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_error_handling.py +0 -0
  53. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_fixtures.py +0 -0
  54. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_printing.py +0 -0
  55. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_query.py +0 -0
  56. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/tests/test_utils.py +0 -0
  57. {kleinkram-0.51.0.dev20251002115350 → kleinkram-0.51.0.dev20251003125418}/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.dev20251002115350
3
+ Version: 0.51.0.dev20251003125418
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
@@ -4,6 +4,7 @@ from datetime import datetime
4
4
  from enum import Enum
5
5
  from typing import Any
6
6
  from typing import Dict
7
+ from typing import List
7
8
  from typing import Literal
8
9
  from typing import NewType
9
10
  from typing import Tuple
@@ -14,6 +15,7 @@ import dateutil.parser
14
15
  from kleinkram.errors import ParsingError
15
16
  from kleinkram.models import File
16
17
  from kleinkram.models import FileState
18
+ from kleinkram.models import MetadataValue
17
19
  from kleinkram.models import Mission
18
20
  from kleinkram.models import Project
19
21
 
@@ -51,6 +53,7 @@ class MissionObjectKeys(str, Enum):
51
53
  DESCRIPTION = "description"
52
54
  CREATED_AT = "createdAt"
53
55
  UPDATED_AT = "updatedAt"
56
+ TAGS = "tags"
54
57
 
55
58
 
56
59
  class ProjectObjectKeys(str, Enum):
@@ -59,6 +62,7 @@ class ProjectObjectKeys(str, Enum):
59
62
  DESCRIPTION = "description"
60
63
  CREATED_AT = "createdAt"
61
64
  UPDATED_AT = "updatedAt"
65
+ REQUIRED_TAGS = "requiredTags"
62
66
 
63
67
 
64
68
  def _get_nested_info(data, key: Literal["mission", "project"]) -> Tuple[UUID, str]:
@@ -83,6 +87,25 @@ def _parse_file_state(state: str) -> FileState:
83
87
  raise ParsingError(f"error parsing file state: {state}") from e
84
88
 
85
89
 
90
+ def _parse_metadata(tags: List[Dict]) -> Dict[str, MetadataValue]:
91
+ result = {}
92
+ try:
93
+ for tag in tags:
94
+ entry = {
95
+ tag.get("name"): MetadataValue(
96
+ tag.get("valueAsString"), tag.get("datatype")
97
+ )
98
+ }
99
+ result.update(entry)
100
+ return result
101
+ except ValueError as e:
102
+ raise ParsingError(f"error parsing metadata: {e}") from e
103
+
104
+
105
+ def _parse_required_tags(tags: List[Dict]) -> list[str]:
106
+ return list(_parse_metadata(tags).keys())
107
+
108
+
86
109
  def _parse_project(project_object: ProjectObject) -> Project:
87
110
  try:
88
111
  id_ = UUID(project_object[ProjectObjectKeys.UUID], version=4)
@@ -90,6 +113,9 @@ def _parse_project(project_object: ProjectObject) -> Project:
90
113
  description = project_object[ProjectObjectKeys.DESCRIPTION]
91
114
  created_at = _parse_datetime(project_object[ProjectObjectKeys.CREATED_AT])
92
115
  updated_at = _parse_datetime(project_object[ProjectObjectKeys.UPDATED_AT])
116
+ required_tags = _parse_required_tags(
117
+ project_object[ProjectObjectKeys.REQUIRED_TAGS]
118
+ )
93
119
  except Exception as e:
94
120
  raise ParsingError(f"error parsing project: {project_object}") from e
95
121
  return Project(
@@ -98,6 +124,7 @@ def _parse_project(project_object: ProjectObject) -> Project:
98
124
  description=description,
99
125
  created_at=created_at,
100
126
  updated_at=updated_at,
127
+ required_tags=required_tags,
101
128
  )
102
129
 
103
130
 
@@ -107,7 +134,7 @@ def _parse_mission(mission: MissionObject) -> Mission:
107
134
  name = mission[MissionObjectKeys.NAME]
108
135
  created_at = _parse_datetime(mission[MissionObjectKeys.CREATED_AT])
109
136
  updated_at = _parse_datetime(mission[MissionObjectKeys.UPDATED_AT])
110
- metadata = {} # TODO: this crap is really bad to parse
137
+ metadata = _parse_metadata(mission[MissionObjectKeys.TAGS])
111
138
 
112
139
  project_id, project_name = _get_nested_info(mission, PROJECT)
113
140
 
@@ -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
@@ -38,12 +40,14 @@ from kleinkram.errors import InvalidMissionQuery
38
40
  from kleinkram.errors import InvalidProjectQuery
39
41
  from kleinkram.errors import MissionExists
40
42
  from kleinkram.errors import MissionNotFound
43
+ from kleinkram.errors import MissionValidationError
41
44
  from kleinkram.errors import ProjectExists
42
45
  from kleinkram.errors import ProjectNotFound
43
46
  from kleinkram.models import File
44
47
  from kleinkram.models import Mission
45
48
  from kleinkram.models import Project
46
49
  from kleinkram.utils import is_valid_uuid4
50
+ from kleinkram.utils import split_args
47
51
 
48
52
  __all__ = [
49
53
  "_get_api_version",
@@ -220,6 +224,28 @@ def _mission_name_is_available(
220
224
  return False
221
225
 
222
226
 
227
+ def _validate_mission_name(
228
+ client: AuthenticatedClient, project_id: UUID, mission_name: str
229
+ ) -> None:
230
+ if not _mission_name_is_available(client, mission_name, project_id):
231
+ raise MissionExists(
232
+ f"Mission with name: `{mission_name}` already exists"
233
+ f" in project: {project_id}"
234
+ )
235
+
236
+ if is_valid_uuid4(mission_name):
237
+ raise ValueError(
238
+ f"Mission name: `{mission_name}` is a valid UUIDv4, "
239
+ "mission names must not be valid UUIDv4's"
240
+ )
241
+
242
+ if mission_name.endswith(" "):
243
+ raise ValueError(
244
+ "A mission name cannot end with a whitespace. "
245
+ f"The given mission name was '{mission_name}'"
246
+ )
247
+
248
+
223
249
  def _project_name_is_available(client: AuthenticatedClient, project_name: str) -> bool:
224
250
  project_query = ProjectQuery(patterns=[project_name])
225
251
  try:
@@ -229,6 +255,50 @@ def _project_name_is_available(client: AuthenticatedClient, project_name: str) -
229
255
  return False
230
256
 
231
257
 
258
+ def _validate_mission_created(
259
+ client: AuthenticatedClient, project_id: str, mission_name: str
260
+ ) -> None:
261
+ """
262
+ validate that a mission is successfully created
263
+ """
264
+ mission_ids, mission_patterns = split_args([mission_name])
265
+ project_ids, project_patterns = split_args([project_id])
266
+
267
+ project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
268
+ mission_query = MissionQuery(
269
+ ids=mission_ids,
270
+ patterns=mission_patterns,
271
+ project_query=project_query,
272
+ )
273
+ try:
274
+ with tempfile.NamedTemporaryFile(suffix=".mcap", delete=False) as tmp:
275
+ tmp.write(b"dummy content")
276
+ tmp_path = Path(tmp.name)
277
+
278
+ kleinkram.core.upload(
279
+ client=client,
280
+ query=mission_query,
281
+ file_paths=[tmp_path],
282
+ verbose=False,
283
+ )
284
+
285
+ file_query = FileQuery(
286
+ ids=[],
287
+ patterns=[tmp_path.name],
288
+ mission_query=mission_query,
289
+ )
290
+ file_parsed = get_file(client, file_query)
291
+
292
+ kleinkram.core.delete_files(client=client, file_ids=[file_parsed.id])
293
+
294
+ except Exception as e:
295
+ raise MissionValidationError(f"Mission validation failed: {e}")
296
+
297
+ finally:
298
+ if tmp_path.exists():
299
+ tmp_path.unlink()
300
+
301
+
232
302
  def _create_mission(
233
303
  client: AuthenticatedClient,
234
304
  project_id: UUID,
@@ -236,6 +306,7 @@ def _create_mission(
236
306
  *,
237
307
  metadata: Optional[Dict[str, str]] = None,
238
308
  ignore_missing_tags: bool = False,
309
+ required_tags: Optional[List[str]] = None,
239
310
  ) -> UUID:
240
311
  """\
241
312
  creates a new mission with the given name and project_id
@@ -246,16 +317,11 @@ def _create_mission(
246
317
  if metadata is None:
247
318
  metadata = {}
248
319
 
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
- )
320
+ _validate_mission_name(client, project_id, mission_name)
254
321
 
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"
322
+ if required_tags and not set(required_tags).issubset(metadata.keys()):
323
+ raise InvalidMissionMetadata(
324
+ f"Mission tags `{required_tags}` are required but missing from metadata: {metadata}"
259
325
  )
260
326
 
261
327
  # we need to translate tag keys to tag type ids
@@ -267,9 +333,9 @@ def _create_mission(
267
333
  "tags": {str(k): v for k, v in tags.items()},
268
334
  "ignoreTags": ignore_missing_tags,
269
335
  }
270
-
271
336
  resp = client.post(CREATE_MISSION, json=payload)
272
337
  resp.raise_for_status()
338
+ _validate_mission_created(client, str(project_id), mission_name)
273
339
 
274
340
  return UUID(resp.json()["uuid"], version=4)
275
341
 
@@ -288,18 +354,37 @@ def _create_project(
288
354
  return UUID(resp.json()["uuid"], version=4)
289
355
 
290
356
 
357
+ def _validate_tag_value(tag_value, tag_datatype) -> None:
358
+ if tag_datatype == "NUMBER":
359
+ try:
360
+ float(tag_value)
361
+ except ValueError:
362
+ raise InvalidMissionMetadata(f"Value '{tag_value}' is not a valid NUMBER")
363
+ elif tag_datatype == "BOOLEAN":
364
+ if tag_value.lower() not in {"true", "false"}:
365
+ raise InvalidMissionMetadata(
366
+ f"Value '{tag_value}' is not a valid BOOLEAN (expected 'true' or 'false')"
367
+ )
368
+ else:
369
+ pass # any string is fine
370
+ # TODO: add check for LOCATION tag datatype
371
+
372
+
291
373
  def _get_metadata_type_id_by_name(
292
374
  client: AuthenticatedClient, tag_name: str
293
- ) -> Optional[UUID]:
375
+ ) -> Tuple[Optional[UUID], str]:
294
376
  resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
295
377
 
296
378
  if resp.status_code in (403, 404):
297
379
  return None
298
380
 
299
381
  resp.raise_for_status()
382
+ try:
383
+ data = resp.json()["data"][0]
384
+ except IndexError:
385
+ return None, None
300
386
 
301
- data = resp.json()[0]
302
- return UUID(data["uuid"], version=4)
387
+ return UUID(data["uuid"], version=4), data["datatype"]
303
388
 
304
389
 
305
390
  def _get_tags_map(
@@ -309,9 +394,10 @@ def _get_tags_map(
309
394
  # why are we using metadata type ids as keys???
310
395
  ret = {}
311
396
  for key, val in metadata.items():
312
- metadata_type_id = _get_metadata_type_id_by_name(client, key)
397
+ metadata_type_id, tag_datatype = _get_metadata_type_id_by_name(client, key)
313
398
  if metadata_type_id is None:
314
399
  raise InvalidMissionMetadata(f"metadata field: {key} does not exist")
400
+ _validate_tag_value(val, tag_datatype)
315
401
  ret[metadata_type_id] = val
316
402
  return ret
317
403
 
@@ -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
 
@@ -107,9 +107,9 @@ 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(
111
- client, query=query.project_query
112
- ).id
110
+ project = kleinkram.api.routes.get_project(client, query=query.project_query)
111
+ project_id = project.id
112
+ project_required_tags = project.required_tags
113
113
  mission_name = check_mission_query_is_creatable(query)
114
114
  kleinkram.api.routes._create_mission(
115
115
  client,
@@ -117,6 +117,7 @@ def upload(
117
117
  mission_name,
118
118
  metadata=metadata or {},
119
119
  ignore_missing_tags=ignore_missing_metadata,
120
+ required_tags=project_required_tags,
120
121
  )
121
122
  mission = kleinkram.api.routes.get_mission(client, query)
122
123
 
@@ -46,6 +46,9 @@ class FileNameNotSupported(Exception): ...
46
46
  class InvalidMissionMetadata(Exception): ...
47
47
 
48
48
 
49
+ class MissionValidationError(Exception): ...
50
+
51
+
49
52
  class NotAuthenticated(Exception):
50
53
  def __init__(self) -> None:
51
54
  super().__init__(LOGIN_MESSAGE)
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.51.0.dev20251002115350
3
+ Version: 0.51.0.dev20251003125418
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-dev20251002115350
3
+ version = 0.51.0-dev20251003125418
4
4
  description = give me your bags
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown