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.
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/PKG-INFO +4 -3
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/README.md +2 -2
- kleinkram-0.56.0.dev20251201085236/kleinkram/api/deser.py +337 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/file_transfer.py +23 -14
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/pagination.py +11 -2
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/query.py +8 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/routes.py +198 -25
- kleinkram-0.56.0.dev20251201085236/kleinkram/auth.py +210 -0
- kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_action.py +140 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_endpoint.py +1 -1
- kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_file_validator.py +130 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_mission.py +16 -4
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_project.py +2 -2
- kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_run.py +233 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_upload.py +63 -20
- kleinkram-0.56.0.dev20251201085236/kleinkram/cli/_verify.py +106 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/app.py +45 -4
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/core.py +17 -5
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/errors.py +12 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/models.py +49 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/printing.py +225 -2
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/utils.py +2 -4
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/PKG-INFO +4 -3
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/SOURCES.txt +5 -2
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/requires.txt +1 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/top_level.txt +0 -1
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/requirements.txt +1 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/setup.cfg +1 -1
- {kleinkram-0.49.0.dev20250728101614/testing → kleinkram-0.56.0.dev20251201085236/tests}/backend_fixtures.py +28 -3
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/conftest.py +1 -1
- kleinkram-0.56.0.dev20251201085236/tests/generate_test_data.py +89 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_core.py +1 -1
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_end_to_end.py +2 -2
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_fixtures.py +2 -2
- kleinkram-0.49.0.dev20250728101614/kleinkram/api/deser.py +0 -162
- kleinkram-0.49.0.dev20250728101614/kleinkram/auth.py +0 -95
- kleinkram-0.49.0.dev20250728101614/kleinkram/cli/_verify.py +0 -66
- kleinkram-0.49.0.dev20250728101614/tests/__init__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/__init__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/__main__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/_version.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/__init__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/client.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/__init__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_download.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_file.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/_list.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/cli/error_handling.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/config.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/main.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/py.typed +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/types.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/wrappers.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/dependency_links.txt +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram.egg-info/entry_points.txt +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/pyproject.toml +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/setup.py +0 -0
- {kleinkram-0.49.0.dev20250728101614/testing → kleinkram-0.56.0.dev20251201085236/tests}/__init__.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_config.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_error_handling.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_printing.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_query.py +0 -0
- {kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/tests/test_utils.py +0 -0
- {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.
|
|
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
|
|
122
|
-
|
|
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
|
|
96
|
-
|
|
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(
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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"])
|
{kleinkram-0.49.0.dev20250728101614 → kleinkram-0.56.0.dev20251201085236}/kleinkram/api/query.py
RENAMED
|
@@ -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
|