kleinkram 0.43.2.dev20250331124109__py3-none-any.whl → 0.58.0.dev20260110152317__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.
Files changed (44) hide show
  1. kleinkram/api/client.py +6 -18
  2. kleinkram/api/deser.py +152 -1
  3. kleinkram/api/file_transfer.py +202 -101
  4. kleinkram/api/pagination.py +11 -2
  5. kleinkram/api/query.py +10 -10
  6. kleinkram/api/routes.py +192 -59
  7. kleinkram/auth.py +108 -7
  8. kleinkram/cli/_action.py +131 -0
  9. kleinkram/cli/_download.py +8 -19
  10. kleinkram/cli/_endpoint.py +2 -4
  11. kleinkram/cli/_file.py +6 -18
  12. kleinkram/cli/_file_validator.py +125 -0
  13. kleinkram/cli/_list.py +5 -15
  14. kleinkram/cli/_mission.py +24 -28
  15. kleinkram/cli/_project.py +10 -26
  16. kleinkram/cli/_run.py +220 -0
  17. kleinkram/cli/_upload.py +58 -26
  18. kleinkram/cli/_verify.py +59 -16
  19. kleinkram/cli/app.py +56 -17
  20. kleinkram/cli/error_handling.py +1 -3
  21. kleinkram/config.py +6 -21
  22. kleinkram/core.py +53 -43
  23. kleinkram/errors.py +12 -0
  24. kleinkram/models.py +51 -1
  25. kleinkram/printing.py +229 -18
  26. kleinkram/utils.py +10 -24
  27. kleinkram/wrappers.py +54 -30
  28. {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/METADATA +6 -4
  29. kleinkram-0.58.0.dev20260110152317.dist-info/RECORD +53 -0
  30. {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/WHEEL +1 -1
  31. {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/top_level.txt +0 -1
  32. {testing → tests}/backend_fixtures.py +27 -3
  33. tests/conftest.py +1 -1
  34. tests/generate_test_data.py +314 -0
  35. tests/test_config.py +2 -6
  36. tests/test_core.py +11 -31
  37. tests/test_end_to_end.py +3 -5
  38. tests/test_fixtures.py +3 -5
  39. tests/test_printing.py +9 -11
  40. tests/test_utils.py +1 -3
  41. tests/test_wrappers.py +9 -27
  42. kleinkram-0.43.2.dev20250331124109.dist-info/RECORD +0 -50
  43. testing/__init__.py +0 -0
  44. {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/entry_points.txt +0 -0
kleinkram/api/client.py CHANGED
@@ -39,15 +39,11 @@ ListData = Sequence[Data]
39
39
  QueryParams = Mapping[str, Union[Data, NestedData, ListData]]
40
40
 
41
41
 
42
- def _convert_nested_data_query_params_values(
43
- key: str, values: NestedData
44
- ) -> List[Tuple[str, Data]]:
42
+ def _convert_nested_data_query_params_values(key: str, values: NestedData) -> List[Tuple[str, Data]]:
45
43
  return [(f"{key}[{k}]", v) for k, v in values.items()]
46
44
 
47
45
 
48
- def _convert_list_data_query_params_values(
49
- key: str, values: ListData
50
- ) -> List[Tuple[str, Data]]:
46
+ def _convert_list_data_query_params_values(key: str, values: ListData) -> List[Tuple[str, Data]]:
51
47
  return [(key, value) for value in values]
52
48
 
53
49
 
@@ -71,9 +67,7 @@ class AuthenticatedClient(httpx.Client):
71
67
  _config: Config
72
68
  _config_lock: Lock
73
69
 
74
- def __init__(
75
- self, config_path: Path = CONFIG_PATH, *args: Any, **kwargs: Any
76
- ) -> None:
70
+ def __init__(self, config_path: Path = CONFIG_PATH, *args: Any, **kwargs: Any) -> None:
77
71
  super().__init__(*args, **kwargs)
78
72
 
79
73
  self._config = get_config(path=config_path)
@@ -116,9 +110,7 @@ class AuthenticatedClient(httpx.Client):
116
110
 
117
111
  self.cookies.set(COOKIE_AUTH_TOKEN, new_access_token)
118
112
 
119
- def _send_request_with_kleinkram_headers(
120
- self, *args: Any, **kwargs: Any
121
- ) -> httpx.Response:
113
+ def _send_request_with_kleinkram_headers(self, *args: Any, **kwargs: Any) -> httpx.Response:
122
114
  # add the cli version to the headers
123
115
  headers = kwargs.get("headers") or {}
124
116
  headers.setdefault(CLI_VERSION_HEADER, __version__)
@@ -150,9 +142,7 @@ class AuthenticatedClient(httpx.Client):
150
142
  logger.info(f"requesting {method} {full_url}")
151
143
 
152
144
  httpx_params = _convert_query_params_to_httpx_format(params or {})
153
- response = self._send_request_with_kleinkram_headers(
154
- method, full_url, params=httpx_params, *args, **kwargs
155
- )
145
+ response = self._send_request_with_kleinkram_headers(method, full_url, params=httpx_params, *args, **kwargs)
156
146
 
157
147
  logger.info(f"got response {response}")
158
148
 
@@ -170,9 +160,7 @@ class AuthenticatedClient(httpx.Client):
170
160
  raise NotAuthenticated
171
161
 
172
162
  logger.info(f"retrying request {method} {full_url}")
173
- response = self._send_request_with_kleinkram_headers(
174
- method, full_url, params=httpx_params, *args, **kwargs
175
- )
163
+ response = self._send_request_with_kleinkram_headers(method, full_url, params=httpx_params, *args, **kwargs)
176
164
  logger.info(f"got response {response}")
177
165
  return response
178
166
  else:
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
@@ -12,10 +13,14 @@ from uuid import UUID
12
13
  import dateutil.parser
13
14
 
14
15
  from kleinkram.errors import ParsingError
16
+ from kleinkram.models import ActionTemplate
15
17
  from kleinkram.models import File
16
18
  from kleinkram.models import FileState
19
+ from kleinkram.models import LogEntry
20
+ from kleinkram.models import MetadataValue
17
21
  from kleinkram.models import Mission
18
22
  from kleinkram.models import Project
23
+ from kleinkram.models import Run
19
24
 
20
25
  __all__ = [
21
26
  "_parse_project",
@@ -27,6 +32,7 @@ __all__ = [
27
32
  ProjectObject = NewType("ProjectObject", Dict[str, Any])
28
33
  MissionObject = NewType("MissionObject", Dict[str, Any])
29
34
  FileObject = NewType("FileObject", Dict[str, Any])
35
+ RunObject = NewType("RunObject", Dict[str, Any])
30
36
 
31
37
  MISSION = "mission"
32
38
  PROJECT = "project"
@@ -51,6 +57,9 @@ class MissionObjectKeys(str, Enum):
51
57
  DESCRIPTION = "description"
52
58
  CREATED_AT = "createdAt"
53
59
  UPDATED_AT = "updatedAt"
60
+ TAGS = "tags"
61
+ FILESIZE = "size"
62
+ FILECOUNT = "filesCount"
54
63
 
55
64
 
56
65
  class ProjectObjectKeys(str, Enum):
@@ -59,6 +68,40 @@ class ProjectObjectKeys(str, Enum):
59
68
  DESCRIPTION = "description"
60
69
  CREATED_AT = "createdAt"
61
70
  UPDATED_AT = "updatedAt"
71
+ REQUIRED_TAGS = "requiredTags"
72
+
73
+
74
+ class RunObjectKeys(str, Enum):
75
+ UUID = "uuid"
76
+ STATE = "state"
77
+ STATE_CAUSE = "stateCause"
78
+ CREATED_AT = "createdAt"
79
+ MISSION = "mission"
80
+ TEMPLATE = "template"
81
+ UPDATED_AT = "updatedAt"
82
+ LOGS = "logs"
83
+ ARTIFACT_URL = "artifactUrl"
84
+
85
+
86
+ class TemplateObjectKeys(str, Enum):
87
+ UUID = "uuid"
88
+ NAME = "name"
89
+ ACCESS_RIGHTS = "accessRights"
90
+ COMMAND = "command"
91
+ CPU_CORES = "cpuCores"
92
+ CPU_MEMORY_GB = "cpuMemory"
93
+ ENTRYPOINT = "entrypoint"
94
+ GPU_MEMORY_GB = "gpuMemory"
95
+ IMAGE_NAME = "imageName"
96
+ MAX_RUNTIME_MINUTES = "maxRuntime"
97
+ CREATED_AT = "createdAt"
98
+ VERSION = "version"
99
+
100
+
101
+ class LogEntryObjectKeys(str, Enum):
102
+ TIMESTAMP = "timestamp"
103
+ LEVEL = "type"
104
+ MESSAGE = "message"
62
105
 
63
106
 
64
107
  def _get_nested_info(data, key: Literal["mission", "project"]) -> Tuple[UUID, str]:
@@ -83,6 +126,21 @@ def _parse_file_state(state: str) -> FileState:
83
126
  raise ParsingError(f"error parsing file state: {state}") from e
84
127
 
85
128
 
129
+ def _parse_metadata(tags: List[Dict]) -> Dict[str, MetadataValue]:
130
+ result = {}
131
+ try:
132
+ for tag in tags:
133
+ entry = {tag.get("name"): MetadataValue(tag.get("valueAsString"), tag.get("datatype"))}
134
+ result.update(entry)
135
+ return result
136
+ except ValueError as e:
137
+ raise ParsingError(f"error parsing metadata: {e}") from e
138
+
139
+
140
+ def _parse_required_tags(tags: List[Dict]) -> list[str]:
141
+ return list(_parse_metadata(tags).keys())
142
+
143
+
86
144
  def _parse_project(project_object: ProjectObject) -> Project:
87
145
  try:
88
146
  id_ = UUID(project_object[ProjectObjectKeys.UUID], version=4)
@@ -90,6 +148,7 @@ def _parse_project(project_object: ProjectObject) -> Project:
90
148
  description = project_object[ProjectObjectKeys.DESCRIPTION]
91
149
  created_at = _parse_datetime(project_object[ProjectObjectKeys.CREATED_AT])
92
150
  updated_at = _parse_datetime(project_object[ProjectObjectKeys.UPDATED_AT])
151
+ required_tags = _parse_required_tags(project_object[ProjectObjectKeys.REQUIRED_TAGS])
93
152
  except Exception as e:
94
153
  raise ParsingError(f"error parsing project: {project_object}") from e
95
154
  return Project(
@@ -98,6 +157,7 @@ def _parse_project(project_object: ProjectObject) -> Project:
98
157
  description=description,
99
158
  created_at=created_at,
100
159
  updated_at=updated_at,
160
+ required_tags=required_tags,
101
161
  )
102
162
 
103
163
 
@@ -107,7 +167,9 @@ def _parse_mission(mission: MissionObject) -> Mission:
107
167
  name = mission[MissionObjectKeys.NAME]
108
168
  created_at = _parse_datetime(mission[MissionObjectKeys.CREATED_AT])
109
169
  updated_at = _parse_datetime(mission[MissionObjectKeys.UPDATED_AT])
110
- metadata = {} # TODO: this crap is really bad to parse
170
+ metadata = _parse_metadata(mission[MissionObjectKeys.TAGS])
171
+ file_count = mission[MissionObjectKeys.FILECOUNT]
172
+ filesize = mission[MissionObjectKeys.FILESIZE]
111
173
 
112
174
  project_id, project_name = _get_nested_info(mission, PROJECT)
113
175
 
@@ -119,6 +181,8 @@ def _parse_mission(mission: MissionObject) -> Mission:
119
181
  metadata=metadata,
120
182
  project_id=project_id,
121
183
  project_name=project_name,
184
+ number_of_files=file_count,
185
+ size=filesize,
122
186
  )
123
187
  except Exception as e:
124
188
  raise ParsingError(f"error parsing mission: {mission}") from e
@@ -160,3 +224,90 @@ def _parse_file(file: FileObject) -> File:
160
224
  except Exception as e:
161
225
  raise ParsingError(f"error parsing file: {file}") from e
162
226
  return parsed
227
+
228
+
229
+ def _parse_action_template(run_object: RunObject) -> ActionTemplate:
230
+ try:
231
+ uuid_ = UUID(run_object[TemplateObjectKeys.UUID], version=4)
232
+ access_rights = run_object[TemplateObjectKeys.ACCESS_RIGHTS]
233
+ command = run_object[TemplateObjectKeys.COMMAND]
234
+ cpu_cores = run_object[TemplateObjectKeys.CPU_CORES]
235
+ cpu_memory_gb = run_object[TemplateObjectKeys.CPU_MEMORY_GB]
236
+ entrypoint = run_object[TemplateObjectKeys.ENTRYPOINT]
237
+ gpu_memory_gb = run_object[TemplateObjectKeys.GPU_MEMORY_GB]
238
+ image_name = run_object[TemplateObjectKeys.IMAGE_NAME]
239
+ max_runtime_minutes = run_object[TemplateObjectKeys.MAX_RUNTIME_MINUTES]
240
+ created_at = _parse_datetime(run_object[TemplateObjectKeys.CREATED_AT])
241
+ name = run_object[TemplateObjectKeys.NAME]
242
+ version = run_object[TemplateObjectKeys.VERSION]
243
+
244
+ except Exception as e:
245
+ raise ParsingError(f"error parsing action template: {run_object}") from e
246
+
247
+ return ActionTemplate(
248
+ uuid=uuid_,
249
+ access_rights=access_rights,
250
+ command=command,
251
+ cpu_cores=cpu_cores,
252
+ cpu_memory_gb=cpu_memory_gb,
253
+ entrypoint=entrypoint,
254
+ gpu_memory_gb=gpu_memory_gb,
255
+ image_name=image_name,
256
+ max_runtime_minutes=max_runtime_minutes,
257
+ created_at=created_at,
258
+ name=name,
259
+ version=version,
260
+ )
261
+
262
+
263
+ def _parse_run(run_object: RunObject) -> Run:
264
+ try:
265
+ uuid_ = UUID(run_object[RunObjectKeys.UUID], version=4)
266
+ state = run_object[RunObjectKeys.STATE]
267
+ state_cause = run_object[RunObjectKeys.STATE_CAUSE]
268
+ artifact_url = run_object.get(RunObjectKeys.ARTIFACT_URL)
269
+ created_at = _parse_datetime(run_object[RunObjectKeys.CREATED_AT])
270
+ updated_at = (
271
+ _parse_datetime(run_object[RunObjectKeys.UPDATED_AT]) if run_object.get(RunObjectKeys.UPDATED_AT) else None
272
+ )
273
+
274
+ mission_dict = run_object[RunObjectKeys.MISSION]
275
+ mission_id = UUID(mission_dict[MissionObjectKeys.UUID], version=4)
276
+ mission_name = mission_dict[MissionObjectKeys.NAME]
277
+
278
+ project_dict = mission_dict[PROJECT]
279
+ project_name = project_dict[ProjectObjectKeys.NAME]
280
+
281
+ template_dict = run_object[RunObjectKeys.TEMPLATE]
282
+ template_id = UUID(template_dict[TemplateObjectKeys.UUID], version=4)
283
+ template_name = template_dict[TemplateObjectKeys.NAME]
284
+ logs = []
285
+ for log_entry in run_object.get(RunObjectKeys.LOGS, []):
286
+ log_timestamp = _parse_datetime(log_entry[LogEntryObjectKeys.TIMESTAMP])
287
+ log_level = log_entry[LogEntryObjectKeys.LEVEL]
288
+ log_message = log_entry[LogEntryObjectKeys.MESSAGE]
289
+ logs.append(
290
+ LogEntry(
291
+ timestamp=log_timestamp,
292
+ level=log_level,
293
+ message=log_message,
294
+ )
295
+ )
296
+
297
+ except Exception as e:
298
+ raise ParsingError(f"error parsing run: {run_object}") from e
299
+
300
+ return Run(
301
+ uuid=uuid_,
302
+ state=state,
303
+ state_cause=state_cause,
304
+ artifact_url=artifact_url,
305
+ created_at=created_at,
306
+ updated_at=updated_at,
307
+ mission_id=mission_id,
308
+ mission_name=mission_name,
309
+ project_name=project_name,
310
+ template_id=template_id,
311
+ template_name=template_name,
312
+ logs=logs,
313
+ )