kleinkram 0.49.0.dev20250728101614__tar.gz → 0.56.0.dev20251201085236__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.
Files changed (64) hide show
  1. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/PKG-INFO +4 -3
  2. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/README.md +2 -2
  3. kleinkram-0.56.0.dev20251201085236/kleinkram/api/deser.py +337 -0
  4. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/file_transfer.py +23 -14
  5. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/pagination.py +11 -2
  6. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/query.py +8 -0
  7. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/routes.py +198 -25
  8. kleinkram-0.56.0.dev20251201085236/kleinkram/auth.py +210 -0
  9. kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_action.py +140 -0
  10. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_endpoint.py +1 -1
  11. kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_file_validator.py +130 -0
  12. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_mission.py +16 -4
  13. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_project.py +2 -2
  14. kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_run.py +233 -0
  15. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_upload.py +63 -20
  16. kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_verify.py +106 -0
  17. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/app.py +45 -4
  18. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/core.py +17 -5
  19. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/errors.py +12 -0
  20. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/models.py +49 -0
  21. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/printing.py +225 -2
  22. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/utils.py +2 -4
  23. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/PKG-INFO +4 -3
  24. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/SOURCES.txt +5 -2
  25. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/requires.txt +1 -0
  26. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/top_level.txt +0 -1
  27. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/requirements.txt +1 -0
  28. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/setup.cfg +1 -1
  29. {kleinkram-0.49.0.dev20250728101614/testing → kleinkram-0.56.0.dev20251201085236/tests}/backend_fixtures.py +28 -3
  30. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/conftest.py +1 -1
  31. kleinkram-0.56.0.dev20251201085236/tests/generate_test_data.py +89 -0
  32. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_core.py +1 -1
  33. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_end_to_end.py +2 -2
  34. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_fixtures.py +2 -2
  35. kleinkram-0.49.0.dev20250728101614/kleinkram/api/deser.py +0 -162
  36. kleinkram-0.49.0.dev20250728101614/kleinkram/auth.py +0 -95
  37. kleinkram-0.49.0.dev20250728101614/kleinkram/cli/_verify.py +0 -66
  38. kleinkram-0.49.0.dev20250728101614/tests/__init__.py +0 -0
  39. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/__init__.py +0 -0
  40. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/__main__.py +0 -0
  41. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/_version.py +0 -0
  42. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/__init__.py +0 -0
  43. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/client.py +0 -0
  44. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/__init__.py +0 -0
  45. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_download.py +0 -0
  46. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_file.py +0 -0
  47. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_list.py +0 -0
  48. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/error_handling.py +0 -0
  49. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/config.py +0 -0
  50. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/main.py +0 -0
  51. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/py.typed +0 -0
  52. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/types.py +0 -0
  53. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/wrappers.py +0 -0
  54. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/dependency_links.txt +0 -0
  55. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/entry_points.txt +0 -0
  56. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/pyproject.toml +0 -0
  57. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/setup.py +0 -0
  58. {kleinkram-0.49.0.dev20250728101614/testing → kleinkram-0.56.0.dev20251201085236/tests}/__init__.py +0 -0
  59. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_config.py +0 -0
  60. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_error_handling.py +0 -0
  61. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_printing.py +0 -0
  62. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_query.py +0 -0
  63. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_utils.py +0 -0
  64. {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kleinkram
3
- Version: 0.49.0.dev20250728101614
3
+ Version: 0.56.0.dev20251201085236
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
@@ -23,6 +23,7 @@ Requires-Dist: rich
23
23
  Requires-Dist: tqdm
24
24
  Requires-Dist: typer
25
25
  Requires-Dist: click
26
+ Requires-Dist: requests
26
27
 
27
28
  # Kleinkram: CLI
28
29
 
@@ -118,7 +119,7 @@ pytest
118
119
  ```
119
120
  For the latter you need to have an instance of the backend running locally.
120
121
  See instructions in the root of the repository for this.
121
- On top of that these tests require particular files to be present in the `cli/data/testing` directory.
122
- To see the exact files that are required, see `cli/testing/backend_fixtures.py`.
122
+ On top of that these tests require particular files to be present in the `cli/tests/data` directory.
123
+ These files are automatically generated by the `cli/tests/generate_test_data.py` script.
123
124
 
124
125
  You also need to make sure to be logged in with the cli with `klein login`.
@@ -92,7 +92,7 @@ pytest
92
92
  ```
93
93
  For the latter you need to have an instance of the backend running locally.
94
94
  See instructions in the root of the repository for this.
95
- On top of that these tests require particular files to be present in the `cli/data/testing` directory.
96
- To see the exact files that are required, see `cli/testing/backend_fixtures.py`.
95
+ On top of that these tests require particular files to be present in the `cli/tests/data` directory.
96
+ These files are automatically generated by the `cli/tests/generate_test_data.py` script.
97
97
 
98
98
  You also need to make sure to be logged in with the cli with `klein login`.
@@ -0,0 +1,337 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any
6
+ from typing import Dict
7
+ from typing import List
8
+ from typing import Literal
9
+ from typing import NewType
10
+ from typing import Tuple
11
+ from uuid import UUID
12
+
13
+ import dateutil.parser
14
+
15
+ from kleinkram.errors import ParsingError
16
+ from kleinkram.models import File, Run, LogEntry, ActionTemplate
17
+ from kleinkram.models import FileState
18
+ from kleinkram.models import MetadataValue
19
+ from kleinkram.models import Mission
20
+ from kleinkram.models import Project
21
+
22
+ __all__ = [
23
+ "_parse_project",
24
+ "_parse_mission",
25
+ "_parse_file",
26
+ ]
27
+
28
+
29
+ ProjectObject = NewType("ProjectObject", Dict[str, Any])
30
+ MissionObject = NewType("MissionObject", Dict[str, Any])
31
+ FileObject = NewType("FileObject", Dict[str, Any])
32
+ RunObject = NewType("RunObject", Dict[str, Any])
33
+
34
+ MISSION = "mission"
35
+ PROJECT = "project"
36
+
37
+
38
+ class FileObjectKeys(str, Enum):
39
+ UUID = "uuid"
40
+ FILENAME = "filename"
41
+ DATE = "date" # at some point this will become a metadata
42
+ CREATED_AT = "createdAt"
43
+ UPDATED_AT = "updatedAt"
44
+ STATE = "state"
45
+ SIZE = "size"
46
+ HASH = "hash"
47
+ TYPE = "type"
48
+ CATEGORIES = "categories"
49
+
50
+
51
+ class MissionObjectKeys(str, Enum):
52
+ UUID = "uuid"
53
+ NAME = "name"
54
+ DESCRIPTION = "description"
55
+ CREATED_AT = "createdAt"
56
+ UPDATED_AT = "updatedAt"
57
+ TAGS = "tags"
58
+ FILESIZE = "size"
59
+ FILECOUNT = "filesCount"
60
+
61
+
62
+ class ProjectObjectKeys(str, Enum):
63
+ UUID = "uuid"
64
+ NAME = "name"
65
+ DESCRIPTION = "description"
66
+ CREATED_AT = "createdAt"
67
+ UPDATED_AT = "updatedAt"
68
+ REQUIRED_TAGS = "requiredTags"
69
+
70
+
71
+ class RunObjectKeys(str, Enum):
72
+ UUID = "uuid"
73
+ STATE = "state"
74
+ STATE_CAUSE = "stateCause"
75
+ CREATED_AT = "createdAt"
76
+ MISSION = "mission"
77
+ TEMPLATE = "template"
78
+ UPDATED_AT = "updatedAt"
79
+ LOGS = "logs"
80
+ ARTIFACT_URL = "artifactUrl"
81
+
82
+
83
+ class TemplateObjectKeys(str, Enum):
84
+ UUID = "uuid"
85
+ NAME = "name"
86
+ ACCESS_RIGHTS = "accessRights"
87
+ COMMAND = "command"
88
+ CPU_CORES = "cpuCores"
89
+ CPU_MEMORY_GB = "cpuMemory"
90
+ ENTRYPOINT = "entrypoint"
91
+ GPU_MEMORY_GB = "gpuMemory"
92
+ IMAGE_NAME = "imageName"
93
+ MAX_RUNTIME_MINUTES = "maxRuntime"
94
+ CREATED_AT = "createdAt"
95
+ VERSION = "version"
96
+
97
+
98
+ class LogEntryObjectKeys(str, Enum):
99
+ TIMESTAMP = "timestamp"
100
+ LEVEL = "type"
101
+ MESSAGE = "message"
102
+
103
+
104
+ def _get_nested_info(data, key: Literal["mission", "project"]) -> Tuple[UUID, str]:
105
+ nested_data = data[key]
106
+ return (
107
+ UUID(nested_data[ProjectObjectKeys.UUID], version=4),
108
+ nested_data[ProjectObjectKeys.NAME],
109
+ )
110
+
111
+
112
+ def _parse_datetime(date: str) -> datetime:
113
+ try:
114
+ return dateutil.parser.isoparse(date)
115
+ except ValueError as e:
116
+ raise ParsingError(f"error parsing date: {date}") from e
117
+
118
+
119
+ def _parse_file_state(state: str) -> FileState:
120
+ try:
121
+ return FileState(state)
122
+ except ValueError as e:
123
+ raise ParsingError(f"error parsing file state: {state}") from e
124
+
125
+
126
+ def _parse_metadata(tags: List[Dict]) -> Dict[str, MetadataValue]:
127
+ result = {}
128
+ try:
129
+ for tag in tags:
130
+ entry = {
131
+ tag.get("name"): MetadataValue(
132
+ tag.get("valueAsString"), tag.get("datatype")
133
+ )
134
+ }
135
+ result.update(entry)
136
+ return result
137
+ except ValueError as e:
138
+ raise ParsingError(f"error parsing metadata: {e}") from e
139
+
140
+
141
+ def _parse_required_tags(tags: List[Dict]) -> list[str]:
142
+ return list(_parse_metadata(tags).keys())
143
+
144
+
145
+ def _parse_project(project_object: ProjectObject) -> Project:
146
+ try:
147
+ id_ = UUID(project_object[ProjectObjectKeys.UUID], version=4)
148
+ name = project_object[ProjectObjectKeys.NAME]
149
+ description = project_object[ProjectObjectKeys.DESCRIPTION]
150
+ created_at = _parse_datetime(project_object[ProjectObjectKeys.CREATED_AT])
151
+ updated_at = _parse_datetime(project_object[ProjectObjectKeys.UPDATED_AT])
152
+ required_tags = _parse_required_tags(
153
+ project_object[ProjectObjectKeys.REQUIRED_TAGS]
154
+ )
155
+ except Exception as e:
156
+ raise ParsingError(f"error parsing project: {project_object}") from e
157
+ return Project(
158
+ id=id_,
159
+ name=name,
160
+ description=description,
161
+ created_at=created_at,
162
+ updated_at=updated_at,
163
+ required_tags=required_tags,
164
+ )
165
+
166
+
167
+ def _parse_mission(mission: MissionObject) -> Mission:
168
+ try:
169
+ id_ = UUID(mission[MissionObjectKeys.UUID], version=4)
170
+ name = mission[MissionObjectKeys.NAME]
171
+ created_at = _parse_datetime(mission[MissionObjectKeys.CREATED_AT])
172
+ updated_at = _parse_datetime(mission[MissionObjectKeys.UPDATED_AT])
173
+ metadata = _parse_metadata(mission[MissionObjectKeys.TAGS])
174
+ file_count = mission[MissionObjectKeys.FILECOUNT]
175
+ filesize = mission[MissionObjectKeys.FILESIZE]
176
+
177
+ project_id, project_name = _get_nested_info(mission, PROJECT)
178
+
179
+ parsed = Mission(
180
+ id=id_,
181
+ name=name,
182
+ created_at=created_at,
183
+ updated_at=updated_at,
184
+ metadata=metadata,
185
+ project_id=project_id,
186
+ project_name=project_name,
187
+ number_of_files=file_count,
188
+ size=filesize,
189
+ )
190
+ except Exception as e:
191
+ raise ParsingError(f"error parsing mission: {mission}") from e
192
+ return parsed
193
+
194
+
195
+ def _parse_file(file: FileObject) -> File:
196
+ try:
197
+ name = file[FileObjectKeys.FILENAME]
198
+ id_ = UUID(file[FileObjectKeys.UUID], version=4)
199
+ fsize = file[FileObjectKeys.SIZE]
200
+ fhash = file[FileObjectKeys.HASH]
201
+ ftype = file[FileObjectKeys.TYPE].split(".")[-1]
202
+ fdate = file[FileObjectKeys.DATE]
203
+ created_at = _parse_datetime(file[FileObjectKeys.CREATED_AT])
204
+ updated_at = _parse_datetime(file[FileObjectKeys.UPDATED_AT])
205
+ state = _parse_file_state(file[FileObjectKeys.STATE])
206
+ categories = file[FileObjectKeys.CATEGORIES]
207
+
208
+ mission_id, mission_name = _get_nested_info(file, MISSION)
209
+ project_id, project_name = _get_nested_info(file[MISSION], PROJECT)
210
+
211
+ parsed = File(
212
+ id=id_,
213
+ name=name,
214
+ hash=fhash,
215
+ size=fsize,
216
+ type_=ftype,
217
+ date=fdate,
218
+ categories=categories,
219
+ state=state,
220
+ created_at=created_at,
221
+ updated_at=updated_at,
222
+ mission_id=mission_id,
223
+ mission_name=mission_name,
224
+ project_id=project_id,
225
+ project_name=project_name,
226
+ )
227
+ except Exception as e:
228
+ raise ParsingError(f"error parsing file: {file}") from e
229
+ return parsed
230
+
231
+
232
+ """
233
+ @dataclass(frozen=True)
234
+ class ActionTemplate:
235
+ uuid: UUID
236
+ access_rights: int
237
+ command: str
238
+ cpu_cores: int
239
+ cpu_memory_gb: int
240
+ entrypoint: str
241
+ gpu_memory_gb: int
242
+ image_name: str
243
+ max_runtime_minutes: int
244
+ created_at: datetime
245
+ name: str
246
+ version: str
247
+
248
+ """
249
+
250
+
251
+ def _parse_action_template(run_object: RunObject) -> ActionTemplate:
252
+ try:
253
+ uuid_ = UUID(run_object[TemplateObjectKeys.UUID], version=4)
254
+ access_rights = run_object[TemplateObjectKeys.ACCESS_RIGHTS]
255
+ command = run_object[TemplateObjectKeys.COMMAND]
256
+ cpu_cores = run_object[TemplateObjectKeys.CPU_CORES]
257
+ cpu_memory_gb = run_object[TemplateObjectKeys.CPU_MEMORY_GB]
258
+ entrypoint = run_object[TemplateObjectKeys.ENTRYPOINT]
259
+ gpu_memory_gb = run_object[TemplateObjectKeys.GPU_MEMORY_GB]
260
+ image_name = run_object[TemplateObjectKeys.IMAGE_NAME]
261
+ max_runtime_minutes = run_object[TemplateObjectKeys.MAX_RUNTIME_MINUTES]
262
+ created_at = _parse_datetime(run_object[TemplateObjectKeys.CREATED_AT])
263
+ name = run_object[TemplateObjectKeys.NAME]
264
+ version = run_object[TemplateObjectKeys.VERSION]
265
+
266
+ except Exception as e:
267
+ raise ParsingError(f"error parsing action template: {run_object}") from e
268
+
269
+ return ActionTemplate(
270
+ uuid=uuid_,
271
+ access_rights=access_rights,
272
+ command=command,
273
+ cpu_cores=cpu_cores,
274
+ cpu_memory_gb=cpu_memory_gb,
275
+ entrypoint=entrypoint,
276
+ gpu_memory_gb=gpu_memory_gb,
277
+ image_name=image_name,
278
+ max_runtime_minutes=max_runtime_minutes,
279
+ created_at=created_at,
280
+ name=name,
281
+ version=version,
282
+ )
283
+
284
+
285
+ def _parse_run(run_object: RunObject) -> Run:
286
+ try:
287
+ uuid_ = UUID(run_object[RunObjectKeys.UUID], version=4)
288
+ state = run_object[RunObjectKeys.STATE]
289
+ state_cause = run_object[RunObjectKeys.STATE_CAUSE]
290
+ artifact_url = run_object.get(RunObjectKeys.ARTIFACT_URL)
291
+ created_at = _parse_datetime(run_object[RunObjectKeys.CREATED_AT])
292
+ updated_at = (
293
+ _parse_datetime(run_object[RunObjectKeys.UPDATED_AT])
294
+ if run_object.get(RunObjectKeys.UPDATED_AT)
295
+ else None
296
+ )
297
+
298
+ mission_dict = run_object[RunObjectKeys.MISSION]
299
+ mission_id = UUID(mission_dict[MissionObjectKeys.UUID], version=4)
300
+ mission_name = mission_dict[MissionObjectKeys.NAME]
301
+
302
+ project_dict = mission_dict[PROJECT]
303
+ project_name = project_dict[ProjectObjectKeys.NAME]
304
+
305
+ template_dict = run_object[RunObjectKeys.TEMPLATE]
306
+ template_id = UUID(template_dict[TemplateObjectKeys.UUID], version=4)
307
+ template_name = template_dict[TemplateObjectKeys.NAME]
308
+ logs = []
309
+ for log_entry in run_object.get(RunObjectKeys.LOGS, []):
310
+ log_timestamp = _parse_datetime(log_entry[LogEntryObjectKeys.TIMESTAMP])
311
+ log_level = log_entry[LogEntryObjectKeys.LEVEL]
312
+ log_message = log_entry[LogEntryObjectKeys.MESSAGE]
313
+ logs.append(
314
+ LogEntry(
315
+ timestamp=log_timestamp,
316
+ level=log_level,
317
+ message=log_message,
318
+ )
319
+ )
320
+
321
+ except Exception as e:
322
+ raise ParsingError(f"error parsing run: {run_object}") from e
323
+
324
+ return Run(
325
+ uuid=uuid_,
326
+ state=state,
327
+ state_cause=state_cause,
328
+ artifact_url=artifact_url,
329
+ created_at=created_at,
330
+ updated_at=updated_at,
331
+ mission_id=mission_id,
332
+ mission_name=mission_name,
333
+ project_name=project_name,
334
+ template_id=template_id,
335
+ template_name=template_name,
336
+ logs=logs,
337
+ )
@@ -62,6 +62,7 @@ def _confirm_file_upload(
62
62
  data = {
63
63
  "uuid": str(file_id),
64
64
  "md5": file_hash,
65
+ "source": "CLI",
65
66
  }
66
67
  resp = client.post(UPLOAD_CONFIRM, json=data)
67
68
  resp.raise_for_status()
@@ -96,6 +97,7 @@ def _get_upload_creditials(
96
97
  dct = {
97
98
  "filenames": [internal_filename],
98
99
  "missionUUID": str(mission_id),
100
+ "source": "CLI",
99
101
  }
100
102
  resp = client.post(UPLOAD_CREDS, json=dct)
101
103
  resp.raise_for_status()
@@ -251,7 +253,9 @@ def _get_file_download(client: AuthenticatedClient, id: UUID) -> str:
251
253
  """\
252
254
  get the download url for a file by file id
253
255
  """
254
- resp = client.get(DOWNLOAD_URL, params={"uuid": str(id), "expires": True})
256
+ resp = client.get(
257
+ DOWNLOAD_URL, params={"uuid": str(id), "expires": True, "preview_only": False}
258
+ )
255
259
 
256
260
  if 400 <= resp.status_code < 500:
257
261
  raise AccessDenied(
@@ -406,6 +410,10 @@ def download_file(
406
410
 
407
411
  observed_hash = b64_md5(path)
408
412
  if file.hash is not None and observed_hash != file.hash:
413
+ print(
414
+ f"HASH MISMATCH: {path} expected={file.hash} observed={observed_hash}",
415
+ file=sys.stderr,
416
+ )
409
417
  # Download completed but hash failed
410
418
  return (
411
419
  DownloadState.DOWNLOADED_INVALID_HASH,
@@ -572,19 +580,20 @@ def upload_files(
572
580
 
573
581
  avg_speed_bps = total_uploaded_bytes / elapsed_time if elapsed_time > 0 else 0
574
582
 
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
- )
583
+ if verbose:
584
+ console.print(f"Upload took {elapsed_time:.2f} seconds")
585
+ console.print(f"Total uploaded: {format_bytes(total_uploaded_bytes)}")
586
+ console.print(f"Average speed: {format_bytes(avg_speed_bps, speed=True)}")
587
+
588
+ if failed_files > 0:
589
+ console.print(
590
+ f"\nUploaded {len(files) - failed_files - skipped_files} files, {skipped_files} skipped, {failed_files} uploads failed",
591
+ style="red",
592
+ )
593
+ else:
594
+ console.print(
595
+ f"\nUploaded {len(files) - skipped_files} files, {skipped_files} skipped"
596
+ )
588
597
 
589
598
 
590
599
  def download_files(
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import Enum
4
3
  from typing import Any
5
4
  from typing import Dict
6
5
  from typing import Generator
@@ -17,6 +16,7 @@ DataPage = Dict[str, Any]
17
16
  PAGE_SIZE = 128
18
17
  SKIP = "skip"
19
18
  TAKE = "take"
19
+ EXACT_MATCH = "exactMatch"
20
20
 
21
21
 
22
22
  def paginated_request(
@@ -25,6 +25,7 @@ def paginated_request(
25
25
  params: Optional[Mapping[str, Any]] = None,
26
26
  max_entries: Optional[int] = None,
27
27
  page_size: int = PAGE_SIZE,
28
+ exact_match: bool = False,
28
29
  ) -> Generator[DataPage, None, None]:
29
30
  total_entries_count = 0
30
31
 
@@ -32,10 +33,18 @@ def paginated_request(
32
33
 
33
34
  params[TAKE] = page_size
34
35
  params[SKIP] = 0
36
+ if exact_match:
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)
38
- resp.raise_for_status() # TODO: this is fine for now
41
+
42
+ # explicitly handle 404 if json contains message
43
+ if resp.status_code == 404 and "message" in resp.json():
44
+ raise ValueError(resp.json()["message"])
45
+
46
+ # raise for other errors
47
+ resp.raise_for_status()
39
48
 
40
49
  paged_data = resp.json()
41
50
  data_page = cast(List[DataPage], paged_data["data"])
@@ -41,6 +41,14 @@ class FileQuery:
41
41
  mission_query: MissionQuery = field(default_factory=MissionQuery)
42
42
 
43
43
 
44
+ @dataclass
45
+ class RunQuery:
46
+ mission_ids: List[UUID] = field(default_factory=list)
47
+ mission_patterns: List[str] = field(default_factory=list)
48
+ project_ids: List[UUID] = field(default_factory=list)
49
+ project_patterns: List[str] = field(default_factory=list)
50
+
51
+
44
52
  def check_mission_query_is_creatable(query: MissionQuery) -> str:
45
53
  """\
46
54
  check if a query is unique and can be used to create a mission