kleinkram 0.50.2.dev20250915073003__py3-none-any.whl → 0.51.0__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.
- kleinkram/api/deser.py +28 -1
- kleinkram/api/file_transfer.py +14 -13
- kleinkram/api/pagination.py +3 -0
- kleinkram/api/routes.py +110 -17
- kleinkram/cli/_mission.py +16 -4
- kleinkram/cli/_project.py +1 -1
- kleinkram/cli/_upload.py +15 -0
- kleinkram/core.py +7 -4
- kleinkram/errors.py +6 -0
- kleinkram/models.py +1 -0
- kleinkram/printing.py +2 -1
- kleinkram/utils.py +1 -4
- {kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/METADATA +1 -1
- {kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/RECORD +17 -17
- {kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/WHEEL +0 -0
- {kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/top_level.txt +0 -0
kleinkram/api/deser.py
CHANGED
|
@@ -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 =
|
|
137
|
+
metadata = _parse_metadata(mission[MissionObjectKeys.TAGS])
|
|
111
138
|
|
|
112
139
|
project_id, project_name = _get_nested_info(mission, PROJECT)
|
|
113
140
|
|
kleinkram/api/file_transfer.py
CHANGED
|
@@ -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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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(
|
kleinkram/api/pagination.py
CHANGED
|
@@ -17,6 +17,7 @@ DataPage = Dict[str, Any]
|
|
|
17
17
|
PAGE_SIZE = 128
|
|
18
18
|
SKIP = "skip"
|
|
19
19
|
TAKE = "take"
|
|
20
|
+
EXACT_MATCH = "exactMatch"
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def paginated_request(
|
|
@@ -25,6 +26,7 @@ def paginated_request(
|
|
|
25
26
|
params: Optional[Mapping[str, Any]] = None,
|
|
26
27
|
max_entries: Optional[int] = None,
|
|
27
28
|
page_size: int = PAGE_SIZE,
|
|
29
|
+
exact_match: bool = False,
|
|
28
30
|
) -> Generator[DataPage, None, None]:
|
|
29
31
|
total_entries_count = 0
|
|
30
32
|
|
|
@@ -32,6 +34,7 @@ def paginated_request(
|
|
|
32
34
|
|
|
33
35
|
params[TAKE] = page_size
|
|
34
36
|
params[SKIP] = 0
|
|
37
|
+
params[EXACT_MATCH] = str(exact_match).lower() # pass string rather than bool
|
|
35
38
|
|
|
36
39
|
while True:
|
|
37
40
|
resp = client.get(endpoint, params=params)
|
kleinkram/api/routes.py
CHANGED
|
@@ -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",
|
|
@@ -159,15 +163,22 @@ def get_projects(
|
|
|
159
163
|
client: AuthenticatedClient,
|
|
160
164
|
project_query: ProjectQuery,
|
|
161
165
|
max_entries: Optional[int] = None,
|
|
166
|
+
exact_match: bool = False,
|
|
162
167
|
) -> Generator[Project, None, None]:
|
|
163
168
|
params = _project_query_to_params(project_query)
|
|
164
169
|
response_stream = paginated_request(
|
|
165
|
-
client,
|
|
170
|
+
client,
|
|
171
|
+
PROJECT_ENDPOINT,
|
|
172
|
+
params=params,
|
|
173
|
+
max_entries=max_entries,
|
|
174
|
+
exact_match=exact_match,
|
|
166
175
|
)
|
|
167
176
|
yield from map(lambda p: _parse_project(ProjectObject(p)), response_stream)
|
|
168
177
|
|
|
169
178
|
|
|
170
|
-
def get_project(
|
|
179
|
+
def get_project(
|
|
180
|
+
client: AuthenticatedClient, query: ProjectQuery, exact_match: bool = False
|
|
181
|
+
) -> Project:
|
|
171
182
|
"""\
|
|
172
183
|
get a unique project by specifying a project spec
|
|
173
184
|
"""
|
|
@@ -176,7 +187,7 @@ def get_project(client: AuthenticatedClient, query: ProjectQuery) -> Project:
|
|
|
176
187
|
f"Project query does not uniquely determine project: {query}"
|
|
177
188
|
)
|
|
178
189
|
try:
|
|
179
|
-
return next(get_projects(client, query))
|
|
190
|
+
return next(get_projects(client, query, exact_match=exact_match))
|
|
180
191
|
except StopIteration:
|
|
181
192
|
raise ProjectNotFound(f"Project not found: {query}")
|
|
182
193
|
|
|
@@ -220,6 +231,28 @@ def _mission_name_is_available(
|
|
|
220
231
|
return False
|
|
221
232
|
|
|
222
233
|
|
|
234
|
+
def _validate_mission_name(
|
|
235
|
+
client: AuthenticatedClient, project_id: UUID, mission_name: str
|
|
236
|
+
) -> None:
|
|
237
|
+
if not _mission_name_is_available(client, mission_name, project_id):
|
|
238
|
+
raise MissionExists(
|
|
239
|
+
f"Mission with name: `{mission_name}` already exists"
|
|
240
|
+
f" in project: {project_id}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if is_valid_uuid4(mission_name):
|
|
244
|
+
raise ValueError(
|
|
245
|
+
f"Mission name: `{mission_name}` is a valid UUIDv4, "
|
|
246
|
+
"mission names must not be valid UUIDv4's"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if mission_name.endswith(" "):
|
|
250
|
+
raise ValueError(
|
|
251
|
+
"A mission name cannot end with a whitespace. "
|
|
252
|
+
f"The given mission name was '{mission_name}'"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
223
256
|
def _project_name_is_available(client: AuthenticatedClient, project_name: str) -> bool:
|
|
224
257
|
project_query = ProjectQuery(patterns=[project_name])
|
|
225
258
|
try:
|
|
@@ -229,6 +262,50 @@ def _project_name_is_available(client: AuthenticatedClient, project_name: str) -
|
|
|
229
262
|
return False
|
|
230
263
|
|
|
231
264
|
|
|
265
|
+
def _validate_mission_created(
|
|
266
|
+
client: AuthenticatedClient, project_id: str, mission_name: str
|
|
267
|
+
) -> None:
|
|
268
|
+
"""
|
|
269
|
+
validate that a mission is successfully created
|
|
270
|
+
"""
|
|
271
|
+
mission_ids, mission_patterns = split_args([mission_name])
|
|
272
|
+
project_ids, project_patterns = split_args([project_id])
|
|
273
|
+
|
|
274
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
275
|
+
mission_query = MissionQuery(
|
|
276
|
+
ids=mission_ids,
|
|
277
|
+
patterns=mission_patterns,
|
|
278
|
+
project_query=project_query,
|
|
279
|
+
)
|
|
280
|
+
try:
|
|
281
|
+
with tempfile.NamedTemporaryFile(suffix=".mcap", delete=False) as tmp:
|
|
282
|
+
tmp.write(b"dummy content")
|
|
283
|
+
tmp_path = Path(tmp.name)
|
|
284
|
+
|
|
285
|
+
kleinkram.core.upload(
|
|
286
|
+
client=client,
|
|
287
|
+
query=mission_query,
|
|
288
|
+
file_paths=[tmp_path],
|
|
289
|
+
verbose=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
file_query = FileQuery(
|
|
293
|
+
ids=[],
|
|
294
|
+
patterns=[tmp_path.name],
|
|
295
|
+
mission_query=mission_query,
|
|
296
|
+
)
|
|
297
|
+
file_parsed = get_file(client, file_query)
|
|
298
|
+
|
|
299
|
+
kleinkram.core.delete_files(client=client, file_ids=[file_parsed.id])
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
raise MissionValidationError(f"Mission validation failed: {e}")
|
|
303
|
+
|
|
304
|
+
finally:
|
|
305
|
+
if tmp_path.exists():
|
|
306
|
+
tmp_path.unlink()
|
|
307
|
+
|
|
308
|
+
|
|
232
309
|
def _create_mission(
|
|
233
310
|
client: AuthenticatedClient,
|
|
234
311
|
project_id: UUID,
|
|
@@ -236,6 +313,7 @@ def _create_mission(
|
|
|
236
313
|
*,
|
|
237
314
|
metadata: Optional[Dict[str, str]] = None,
|
|
238
315
|
ignore_missing_tags: bool = False,
|
|
316
|
+
required_tags: Optional[List[str]] = None,
|
|
239
317
|
) -> UUID:
|
|
240
318
|
"""\
|
|
241
319
|
creates a new mission with the given name and project_id
|
|
@@ -246,16 +324,11 @@ def _create_mission(
|
|
|
246
324
|
if metadata is None:
|
|
247
325
|
metadata = {}
|
|
248
326
|
|
|
249
|
-
|
|
250
|
-
raise MissionExists(
|
|
251
|
-
f"Mission with name: `{mission_name}` already exists"
|
|
252
|
-
f" in project: {project_id}"
|
|
253
|
-
)
|
|
327
|
+
_validate_mission_name(client, project_id, mission_name)
|
|
254
328
|
|
|
255
|
-
if
|
|
256
|
-
raise
|
|
257
|
-
f"Mission
|
|
258
|
-
"mission names must not be valid UUIDv4's"
|
|
329
|
+
if required_tags and not set(required_tags).issubset(metadata.keys()):
|
|
330
|
+
raise InvalidMissionMetadata(
|
|
331
|
+
f"Mission tags `{required_tags}` are required but missing from metadata: {metadata}"
|
|
259
332
|
)
|
|
260
333
|
|
|
261
334
|
# we need to translate tag keys to tag type ids
|
|
@@ -267,9 +340,9 @@ def _create_mission(
|
|
|
267
340
|
"tags": {str(k): v for k, v in tags.items()},
|
|
268
341
|
"ignoreTags": ignore_missing_tags,
|
|
269
342
|
}
|
|
270
|
-
|
|
271
343
|
resp = client.post(CREATE_MISSION, json=payload)
|
|
272
344
|
resp.raise_for_status()
|
|
345
|
+
_validate_mission_created(client, str(project_id), mission_name)
|
|
273
346
|
|
|
274
347
|
return UUID(resp.json()["uuid"], version=4)
|
|
275
348
|
|
|
@@ -288,18 +361,37 @@ def _create_project(
|
|
|
288
361
|
return UUID(resp.json()["uuid"], version=4)
|
|
289
362
|
|
|
290
363
|
|
|
364
|
+
def _validate_tag_value(tag_value, tag_datatype) -> None:
|
|
365
|
+
if tag_datatype == "NUMBER":
|
|
366
|
+
try:
|
|
367
|
+
float(tag_value)
|
|
368
|
+
except ValueError:
|
|
369
|
+
raise InvalidMissionMetadata(f"Value '{tag_value}' is not a valid NUMBER")
|
|
370
|
+
elif tag_datatype == "BOOLEAN":
|
|
371
|
+
if tag_value.lower() not in {"true", "false"}:
|
|
372
|
+
raise InvalidMissionMetadata(
|
|
373
|
+
f"Value '{tag_value}' is not a valid BOOLEAN (expected 'true' or 'false')"
|
|
374
|
+
)
|
|
375
|
+
else:
|
|
376
|
+
pass # any string is fine
|
|
377
|
+
# TODO: add check for LOCATION tag datatype
|
|
378
|
+
|
|
379
|
+
|
|
291
380
|
def _get_metadata_type_id_by_name(
|
|
292
381
|
client: AuthenticatedClient, tag_name: str
|
|
293
|
-
) -> Optional[UUID]:
|
|
382
|
+
) -> Tuple[Optional[UUID], str]:
|
|
294
383
|
resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
|
|
295
384
|
|
|
296
385
|
if resp.status_code in (403, 404):
|
|
297
386
|
return None
|
|
298
387
|
|
|
299
388
|
resp.raise_for_status()
|
|
389
|
+
try:
|
|
390
|
+
data = resp.json()["data"][0]
|
|
391
|
+
except IndexError:
|
|
392
|
+
return None, None
|
|
300
393
|
|
|
301
|
-
data =
|
|
302
|
-
return UUID(data["uuid"], version=4)
|
|
394
|
+
return UUID(data["uuid"], version=4), data["datatype"]
|
|
303
395
|
|
|
304
396
|
|
|
305
397
|
def _get_tags_map(
|
|
@@ -309,9 +401,10 @@ def _get_tags_map(
|
|
|
309
401
|
# why are we using metadata type ids as keys???
|
|
310
402
|
ret = {}
|
|
311
403
|
for key, val in metadata.items():
|
|
312
|
-
metadata_type_id = _get_metadata_type_id_by_name(client, key)
|
|
404
|
+
metadata_type_id, tag_datatype = _get_metadata_type_id_by_name(client, key)
|
|
313
405
|
if metadata_type_id is None:
|
|
314
406
|
raise InvalidMissionMetadata(f"metadata field: {key} does not exist")
|
|
407
|
+
_validate_tag_value(val, tag_datatype)
|
|
315
408
|
ret[metadata_type_id] = val
|
|
316
409
|
return ret
|
|
317
410
|
|
kleinkram/cli/_mission.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
kleinkram/cli/_project.py
CHANGED
|
@@ -90,7 +90,7 @@ def delete(
|
|
|
90
90
|
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
91
91
|
|
|
92
92
|
client = AuthenticatedClient()
|
|
93
|
-
project_id = get_project(client=client, query=project_query).id
|
|
93
|
+
project_id = get_project(client=client, query=project_query, exact_match=True).id
|
|
94
94
|
kleinkram.core.delete_project(client=client, project_id=project_id)
|
|
95
95
|
|
|
96
96
|
|
kleinkram/cli/_upload.py
CHANGED
|
@@ -13,6 +13,7 @@ from kleinkram.api.query import MissionQuery
|
|
|
13
13
|
from kleinkram.api.query import ProjectQuery
|
|
14
14
|
from kleinkram.config import get_shared_state
|
|
15
15
|
from kleinkram.errors import FileNameNotSupported
|
|
16
|
+
from kleinkram.errors import DatatypeNotSupported
|
|
16
17
|
from kleinkram.errors import MissionNotFound
|
|
17
18
|
from kleinkram.utils import load_metadata
|
|
18
19
|
from kleinkram.utils import split_args
|
|
@@ -44,6 +45,9 @@ def upload(
|
|
|
44
45
|
False,
|
|
45
46
|
help="fix filenames before upload, this does not change the filenames locally",
|
|
46
47
|
),
|
|
48
|
+
experimental_datatypes: bool = typer.Option(
|
|
49
|
+
False, help="allow experimental datatypes (yaml, svo2, db3, tum)"
|
|
50
|
+
),
|
|
47
51
|
ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
|
|
48
52
|
) -> None:
|
|
49
53
|
# get filepaths
|
|
@@ -61,6 +65,17 @@ def upload(
|
|
|
61
65
|
|
|
62
66
|
if not fix_filenames:
|
|
63
67
|
for file in file_paths:
|
|
68
|
+
|
|
69
|
+
# check for experimental datatypes and throw an exception if not allowed
|
|
70
|
+
EXPERIMENTAL_DATATYPES = {".yaml", ".svo2", ".db3", ".tum"}
|
|
71
|
+
|
|
72
|
+
if not experimental_datatypes:
|
|
73
|
+
if file.suffix.lower() in EXPERIMENTAL_DATATYPES:
|
|
74
|
+
raise DatatypeNotSupported(
|
|
75
|
+
f"Datatype '{file.suffix}' for file {file} is not supported without the "
|
|
76
|
+
f"`--experimental-datatypes` flag. "
|
|
77
|
+
)
|
|
78
|
+
|
|
64
79
|
if not kleinkram.utils.check_filename_is_sanatized(file.stem):
|
|
65
80
|
raise FileNameNotSupported(
|
|
66
81
|
f"Only `{''.join(kleinkram.utils.INTERNAL_ALLOWED_CHARS)}` are "
|
kleinkram/core.py
CHANGED
|
@@ -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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
|
@@ -281,7 +282,9 @@ def delete_mission(*, client: AuthenticatedClient, mission_id: UUID) -> None:
|
|
|
281
282
|
|
|
282
283
|
def delete_project(*, client: AuthenticatedClient, project_id: UUID) -> None:
|
|
283
284
|
pquery = ProjectQuery(ids=[project_id])
|
|
284
|
-
_ = kleinkram.api.routes.get_project(
|
|
285
|
+
_ = kleinkram.api.routes.get_project(
|
|
286
|
+
client, pquery, exact_match=True
|
|
287
|
+
) # check if project exists
|
|
285
288
|
|
|
286
289
|
# delete all missions and files
|
|
287
290
|
missions = list(
|
kleinkram/errors.py
CHANGED
|
@@ -43,9 +43,15 @@ class FileTypeNotSupported(Exception): ...
|
|
|
43
43
|
class FileNameNotSupported(Exception): ...
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
class DatatypeNotSupported(Exception): ...
|
|
47
|
+
|
|
48
|
+
|
|
46
49
|
class InvalidMissionMetadata(Exception): ...
|
|
47
50
|
|
|
48
51
|
|
|
52
|
+
class MissionValidationError(Exception): ...
|
|
53
|
+
|
|
54
|
+
|
|
49
55
|
class NotAuthenticated(Exception):
|
|
50
56
|
def __init__(self) -> None:
|
|
51
57
|
super().__init__(LOGIN_MESSAGE)
|
kleinkram/models.py
CHANGED
kleinkram/printing.py
CHANGED
|
@@ -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 =
|
|
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
|
|
kleinkram/utils.py
CHANGED
|
@@ -28,10 +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
|
-
]
|
|
31
|
+
SUPPORT_FILE_TYPES = [".bag", ".mcap", ".db3", ".svo2", ".tum", ".yaml"]
|
|
35
32
|
|
|
36
33
|
|
|
37
34
|
def file_paths_from_files(
|
|
@@ -3,30 +3,30 @@ kleinkram/__main__.py,sha256=B9RiZxfO4jpCmWPUHyKJ7_EoZlEG4sPpH-nz7T_YhhQ,125
|
|
|
3
3
|
kleinkram/_version.py,sha256=QYJyRTcqFcJj4qWYpqs7WcoOP6jxDMqyvxLY-cD6KcE,129
|
|
4
4
|
kleinkram/auth.py,sha256=PdSYZZO8AauNLZbn9PBgPM3o-O_nwoOKTj94EGnPRE8,3003
|
|
5
5
|
kleinkram/config.py,sha256=nx6uSM5nLP4SKe8b9VAx4KDtCCwtyshXmzbEJcUwpsY,7411
|
|
6
|
-
kleinkram/core.py,sha256=
|
|
7
|
-
kleinkram/errors.py,sha256=
|
|
6
|
+
kleinkram/core.py,sha256=N91W_IRw7yH9pR-_wAmaVjcGgiz0xF7QwG20863oOiY,9760
|
|
7
|
+
kleinkram/errors.py,sha256=C0P7Clw-wLIo9v03aRP2B5_2GjctzlinIFIFe7qZ9OQ,1057
|
|
8
8
|
kleinkram/main.py,sha256=BTE0mZN__xd46wBhFi6iBlK9eGGQvJ1LdUMsbnysLi0,172
|
|
9
|
-
kleinkram/models.py,sha256=
|
|
10
|
-
kleinkram/printing.py,sha256=
|
|
9
|
+
kleinkram/models.py,sha256=0C_TharLDHA4RCe6Plas9N_uO_teN1Z4iP70WljOAfs,1899
|
|
10
|
+
kleinkram/printing.py,sha256=9o4UQq9MYkGwMIlTchbdMLjUROdJWB100Lq1b3OFfko,12280
|
|
11
11
|
kleinkram/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
kleinkram/types.py,sha256=nfDjj8TB1Jn5vqO0Xg6qhLOuKom9DDhe62BrngqnVGM,185
|
|
13
|
-
kleinkram/utils.py,sha256=
|
|
13
|
+
kleinkram/utils.py,sha256=AtaTvEQ0TrGaQtZylwniE9l1u7_IRYigLT2bc_jc-lQ,6790
|
|
14
14
|
kleinkram/wrappers.py,sha256=ZScoEov5Q6D2rvaJJ8E-4f58P_NGWrGc9mRPYxSqOC0,13127
|
|
15
15
|
kleinkram/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
16
|
kleinkram/api/client.py,sha256=VwuT97_WdbDpcVGwMXB0fRnUoQnUSf7BOP5eXUFokfI,5932
|
|
17
|
-
kleinkram/api/deser.py,sha256=
|
|
18
|
-
kleinkram/api/file_transfer.py,sha256=
|
|
19
|
-
kleinkram/api/pagination.py,sha256=
|
|
17
|
+
kleinkram/api/deser.py,sha256=6ar6_WbgvTIkx1rNRzvVP9YNa5BrFD4181q1fml1KwU,5637
|
|
18
|
+
kleinkram/api/file_transfer.py,sha256=Ija34JXaszZR7_hvb08aVzq-DB2KG3ze-qqb7zjrchQ,19985
|
|
19
|
+
kleinkram/api/pagination.py,sha256=VqjIPMzcD2FY3yeBmP76S7vprUGnuFfTLOzbskqnl0U,1511
|
|
20
20
|
kleinkram/api/query.py,sha256=9Exi4hJR7Ml38_zjAcOvSEoIAxZLlpM6QwwzO9fs5Gk,3293
|
|
21
|
-
kleinkram/api/routes.py,sha256=
|
|
21
|
+
kleinkram/api/routes.py,sha256=buWu4BKdAF1Tk3fmT-MiuG_otJ2Sv4lIcEVE9cZ8isg,15264
|
|
22
22
|
kleinkram/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
23
|
kleinkram/cli/_download.py,sha256=e0fDyp_CFOdbKIUGKmtITvAVINa6STYJk5w5QlElXSs,2394
|
|
24
24
|
kleinkram/cli/_endpoint.py,sha256=oY0p4bnuHLEDJCXtTmir4AHswcKAygZ8I4IWC3RFcKc,1796
|
|
25
25
|
kleinkram/cli/_file.py,sha256=Q2fLDdUyfHFmdGC6wIxMqgEl0F76qszhzWJrRV5rTBM,2973
|
|
26
26
|
kleinkram/cli/_list.py,sha256=5gI3aIUeKC0_eWPQqdFXSBBFvpkTTJSm31TamHa197c,3090
|
|
27
|
-
kleinkram/cli/_mission.py,sha256=
|
|
28
|
-
kleinkram/cli/_project.py,sha256=
|
|
29
|
-
kleinkram/cli/_upload.py,sha256=
|
|
27
|
+
kleinkram/cli/_mission.py,sha256=3ZMPRlPZIvJwmFQqeXu6N8DcmYtSVGj4xWHuAdKAlsc,5845
|
|
28
|
+
kleinkram/cli/_project.py,sha256=tvVwcNaBYKZhIh6KjPcdyyTmaep6y-GvG_sV7O49Ov0,3416
|
|
29
|
+
kleinkram/cli/_upload.py,sha256=EFxbf4SNLuse_lPt0EIpfQLB-1pw7-oHNLdjxLrdMYE,3491
|
|
30
30
|
kleinkram/cli/_verify.py,sha256=n9QThY0JnqaIqw6udYXdRQGcpUl2lIbFXGQIgpTnDPE,2112
|
|
31
31
|
kleinkram/cli/app.py,sha256=Yetkt2jd6cggor7mPpV9Lcp6aLd45rACdF1nBW0uy9k,7546
|
|
32
32
|
kleinkram/cli/error_handling.py,sha256=wK3tzeKVSrZm-xmiyzGLnGT2E4TRpyxhaak6GWGP7P8,1921
|
|
@@ -43,8 +43,8 @@ tests/test_printing.py,sha256=kPzpIQOtQJ9yQ32mM8cMGDVOGsbrZZLQhfsXN1Pe68Q,2231
|
|
|
43
43
|
tests/test_query.py,sha256=fExmCKXLA7-9j2S2sF_sbvRX_2s6Cp3a7OTcqE25q9g,3864
|
|
44
44
|
tests/test_utils.py,sha256=eUBYrn3xrcgcaxm1X4fqZaX4tRvkbI6rh6BUbNbu9T0,4784
|
|
45
45
|
tests/test_wrappers.py,sha256=TbcTyO2L7fslbzgfDdcVZkencxNQ8cGPZm_iB6c9d6Q,2673
|
|
46
|
-
kleinkram-0.
|
|
47
|
-
kleinkram-0.
|
|
48
|
-
kleinkram-0.
|
|
49
|
-
kleinkram-0.
|
|
50
|
-
kleinkram-0.
|
|
46
|
+
kleinkram-0.51.0.dist-info/METADATA,sha256=MimUnV5qvmx7LuY0cfDjS4FV3wc4pO6k4zIQT20CnWY,2828
|
|
47
|
+
kleinkram-0.51.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
48
|
+
kleinkram-0.51.0.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
|
|
49
|
+
kleinkram-0.51.0.dist-info/top_level.txt,sha256=N3-sJagEHu1Tk1X6Dx1X1q0pLDNbDZpLzRxVftvepds,24
|
|
50
|
+
kleinkram-0.51.0.dist-info/RECORD,,
|
|
File without changes
|
{kleinkram-0.50.2.dev20250915073003.dist-info → kleinkram-0.51.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|