kleinkram 0.38.1.dev20241120100707__py3-none-any.whl → 0.38.1.dev20241212075157__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.

@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ from typing import Dict
5
+ from typing import Optional
6
+ from uuid import UUID
7
+
8
+ from kleinkram.errors import ParsingError
9
+ from kleinkram.models import File
10
+ from kleinkram.models import FileState
11
+ from kleinkram.models import Mission
12
+ from kleinkram.models import Project
13
+
14
+ __all__ = [
15
+ "_parse_project",
16
+ "_parse_mission",
17
+ "_parse_file",
18
+ ]
19
+
20
+
21
+ def _parse_project(project: Dict[str, Any]) -> Project:
22
+ try:
23
+ project_id = UUID(project["uuid"], version=4)
24
+ project_name = project["name"]
25
+ project_description = project["description"]
26
+
27
+ parsed = Project(
28
+ id=project_id, name=project_name, description=project_description
29
+ )
30
+ except Exception:
31
+ raise ParsingError(f"error parsing project: {project}")
32
+ return parsed
33
+
34
+
35
+ def _parse_mission(
36
+ mission: Dict[str, Any], project: Optional[Project] = None
37
+ ) -> Mission:
38
+ try:
39
+ mission_id = UUID(mission["uuid"], version=4)
40
+ mission_name = mission["name"]
41
+
42
+ project_id = (
43
+ project.id if project else UUID(mission["project"]["uuid"], version=4)
44
+ )
45
+ project_name = project.name if project else mission["project"]["name"]
46
+
47
+ parsed = Mission(
48
+ id=mission_id,
49
+ name=mission_name,
50
+ project_id=project_id,
51
+ project_name=project_name,
52
+ )
53
+ except Exception:
54
+ raise ParsingError(f"error parsing mission: {mission}")
55
+ return parsed
56
+
57
+
58
+ def _parse_file(file: Dict[str, Any], mission: Optional[Mission] = None) -> File:
59
+ try:
60
+ filename = file["filename"]
61
+ file_id = UUID(file["uuid"], version=4)
62
+ file_size = file["size"]
63
+ file_hash = file["hash"]
64
+
65
+ project_id = (
66
+ mission.project_id if mission else UUID(file["project"]["uuid"], version=4)
67
+ )
68
+ project_name = mission.project_name if mission else file["project"]["name"]
69
+
70
+ mission_id = mission.id if mission else UUID(file["mission"]["uuid"], version=4)
71
+ mission_name = mission.name if mission else file["mission"]["name"]
72
+
73
+ parsed = File(
74
+ id=file_id,
75
+ name=filename,
76
+ size=file_size,
77
+ hash=file_hash,
78
+ project_id=project_id,
79
+ project_name=project_name,
80
+ mission_id=mission_id,
81
+ mission_name=mission_name,
82
+ state=FileState(file["state"]),
83
+ )
84
+ except Exception:
85
+ raise ParsingError(f"error parsing file: {file}")
86
+ return parsed
kleinkram/api/routes.py CHANGED
@@ -1,209 +1,129 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
- from typing import Any
5
- from typing import cast
6
3
  from typing import Dict
7
4
  from typing import List
8
5
  from typing import Optional
9
6
  from typing import Tuple
10
- from typing import Union
11
7
  from uuid import UUID
12
8
 
13
9
  import httpx
14
10
  from kleinkram.api.client import AuthenticatedClient
11
+ from kleinkram.api.parsing import _parse_file
12
+ from kleinkram.api.parsing import _parse_mission
13
+ from kleinkram.api.parsing import _parse_project
15
14
  from kleinkram.config import Config
16
- from kleinkram.errors import MissionDoesNotExist
15
+ from kleinkram.errors import AccessDenied
17
16
  from kleinkram.errors import MissionExists
18
- from kleinkram.errors import NoPermission
17
+ from kleinkram.errors import MissionNotFound
19
18
  from kleinkram.models import DataType
20
19
  from kleinkram.models import File
21
- from kleinkram.models import FilesById
22
- from kleinkram.models import FilesByMission
23
- from kleinkram.models import FileState
24
20
  from kleinkram.models import Mission
25
- from kleinkram.models import MissionById
26
- from kleinkram.models import MissionByName
27
21
  from kleinkram.models import Project
28
22
  from kleinkram.models import TagType
29
- from kleinkram.utils import filtered_by_patterns
30
23
  from kleinkram.utils import is_valid_uuid4
31
24
 
32
- logger = logging.getLogger(__name__)
25
+ __all__ = [
26
+ "_get_projects",
27
+ "_get_missions_by_project",
28
+ "_get_files_by_mission",
29
+ "_create_mission",
30
+ "_update_mission_metadata",
31
+ "_get_api_version",
32
+ "_claim_admin",
33
+ ]
33
34
 
34
35
 
35
36
  MAX_PAGINATION = 10_000
36
37
 
37
- TEMP_CREDS = "/file/temporaryAccess"
38
38
  CLAIM_ADMIN = "/user/claimAdmin"
39
-
40
- PROJECT_BY_NAME = "/project/byName"
41
- PROJECT_BY_ID = "/project/one"
42
- PROJECT_CREATE = "/project/create"
43
39
  PROJECT_ALL = "/project/filtered"
44
-
40
+ MISSIONS_BY_PROJECT = "/mission/filtered"
45
41
  MISSION_BY_NAME = "/mission/byName"
46
- MISSION_BY_ID = "/mission/one"
47
42
  MISSION_CREATE = "/mission/create"
48
- MISSION_BY_PROJECT_NAME = "/mission/filteredByProjectName"
49
43
  MISSION_UPDATE_METADATA = "/mission/tags"
50
-
51
- ALL_USERS = "/user/all"
52
- USER_INFO = "/user/me"
53
- PROMOTE_USER = "/user/promote"
54
- DEMOTE_USER = "/user/demote"
55
-
56
- FILE_DOWNLOAD = "/file/download"
57
- FILE_QUERY = "/file/filteredByNames"
58
- FILE_ONE = "/file/one"
59
44
  FILE_OF_MISSION = "/file/ofMission"
60
-
61
45
  TAG_TYPE_BY_NAME = "/tag/filtered"
62
-
63
46
  GET_STATUS = "/user/me"
64
47
 
65
48
 
66
- def claim_admin(client: AuthenticatedClient) -> None:
67
- """\
68
- the first user on the system could call this
69
- """
70
- response = client.post(CLAIM_ADMIN)
71
- response.raise_for_status()
72
- return
73
-
74
-
75
- def get_project(
76
- client: AuthenticatedClient, identifier: Union[str, UUID]
77
- ) -> Union[tuple[UUID, Dict[str, Any]], tuple[None, None]]:
78
-
79
- if isinstance(identifier, UUID):
80
- params = {"uuid": str(identifier)}
81
- else:
82
- params = {"name": identifier}
83
-
84
- resp = client.get("/missions", params=params)
49
+ def _get_projects(client: AuthenticatedClient) -> list[Project]:
50
+ resp = client.get(PROJECT_ALL)
85
51
 
86
52
  if resp.status_code in (403, 404):
87
- return None, None
53
+ return []
88
54
 
89
- # TODO: handle other status codes
90
55
  resp.raise_for_status()
91
56
 
92
- details = resp.json()
93
- return UUID(details["uuid"], version=4), details
94
-
95
-
96
- def create_project(
97
- client: AuthenticatedClient,
98
- project_name: str,
99
- *,
100
- description: str | None = None,
101
- check_exists: bool = False,
102
- ) -> UUID:
103
- """\
104
- creates a new mission with the given name and project_id
105
-
106
- if check_exists is True, the function will return the existing mission_id,
107
- otherwise if the mission already exists an error will be raised
108
- """
109
- if description is None:
110
- description = "autogenerated by CLI"
111
-
112
- if check_exists:
113
- project_id, _ = get_project(client, project_name)
114
- if project_id is not None:
115
- return project_id
116
-
117
- if is_valid_uuid4(project_name):
118
- raise ValueError(
119
- f"Project name: `{project_name}` is a valid UUIDv4, "
120
- "project names must not be valid UUIDv4's"
121
- )
122
-
123
- resp = client.post(
124
- MISSION_CREATE,
125
- json={
126
- "name": project_name,
127
- "description": description,
128
- "requiredTags": [],
129
- },
130
- )
57
+ ret = []
58
+ for pr in resp.json()[0]:
59
+ ret.append(_parse_project(pr))
131
60
 
132
- if resp.status_code >= 400:
133
- raise ValueError(
134
- f"Failed to create project. Status Code: "
135
- f"{str(resp.status_code)}\n"
136
- f"{resp.json()['message'][0]}"
137
- )
61
+ return ret
138
62
 
139
- return UUID(resp.json()["uuid"], version=4)
140
63
 
64
+ def _get_missions_by_project(
65
+ client: AuthenticatedClient, project: Project
66
+ ) -> List[Mission]:
67
+ params = {"uuid": str(project.id), "take": MAX_PAGINATION}
141
68
 
142
- def get_mission_id_by_name(
143
- client: AuthenticatedClient, mission_name, project_id: UUID
144
- ) -> Optional[UUID]:
145
- params = {"name": mission_name, "projectUUID": str(project_id)}
146
- resp = client.get(MISSION_BY_NAME, params=params)
69
+ resp = client.get(MISSIONS_BY_PROJECT, params=params)
147
70
 
148
71
  if resp.status_code in (403, 404):
149
- return None
72
+ return []
150
73
 
151
- # TODO: handle other status codes
152
74
  resp.raise_for_status()
153
75
 
154
76
  data = resp.json()
77
+ missions = []
78
+
79
+ for mission in data[0]:
80
+ missions.append(_parse_mission(mission, project))
81
+
82
+ return missions
155
83
 
156
- return UUID(data["uuid"], version=4)
157
84
 
85
+ def _get_files_by_mission(client: AuthenticatedClient, mission: Mission) -> List[File]:
86
+ params = {"uuid": str(mission.id), "take": MAX_PAGINATION}
158
87
 
159
- def get_mission_by_id(
160
- client: AuthenticatedClient, mission_id: UUID
161
- ) -> Optional[Mission]:
162
- params = {"uuid": str(mission_id), "take": MAX_PAGINATION}
163
88
  resp = client.get(FILE_OF_MISSION, params=params)
164
89
 
165
90
  if resp.status_code in (403, 404):
166
- return None
91
+ return []
167
92
 
168
93
  resp.raise_for_status()
169
- data = resp.json()[0]
170
- files = [_parse_file(file) for file in data]
171
94
 
172
- resp = client.get(MISSION_BY_ID, params={"uuid": str(mission_id)})
173
- resp.raise_for_status()
95
+ data = resp.json()
174
96
 
175
- mission_data = resp.json()
176
- mission = Mission(
177
- id=mission_id,
178
- name=mission_data["name"],
179
- project_id=UUID(mission_data["project"]["uuid"], version=4),
180
- project_name=mission_data["project"]["name"],
181
- files=files,
182
- )
97
+ files = []
98
+ for file in data[0]:
99
+ files.append(_parse_file(file, mission))
183
100
 
184
- return mission
101
+ return files
185
102
 
186
103
 
187
- def get_project_id_by_name(
188
- client: AuthenticatedClient, project_name: str
104
+ def _get_mission_id_by_name(
105
+ client: AuthenticatedClient, mission_name, project_id: UUID
189
106
  ) -> Optional[UUID]:
190
- params = {"name": project_name}
191
- resp = client.get(PROJECT_BY_NAME, params=params)
107
+ params = {"name": mission_name, "projectUUID": str(project_id)}
108
+ resp = client.get(MISSION_BY_NAME, params=params)
192
109
 
193
110
  if resp.status_code in (403, 404):
194
111
  return None
195
112
 
113
+ # TODO: handle other status codes
196
114
  resp.raise_for_status()
197
115
 
198
- return UUID(resp.json()["uuid"], version=4)
116
+ data = resp.json()
117
+
118
+ return UUID(data["uuid"], version=4)
199
119
 
200
120
 
201
- def create_mission(
121
+ def _create_mission(
202
122
  client: AuthenticatedClient,
203
123
  project_id: UUID,
204
124
  mission_name: str,
205
125
  *,
206
- tags: Optional[Dict[UUID, str]] = None,
126
+ metadata: Optional[Dict[str, str]] = None,
207
127
  ignore_missing_tags: bool = False,
208
128
  ) -> UUID:
209
129
  """\
@@ -212,8 +132,10 @@ def create_mission(
212
132
  if check_exists is True, the function will return the existing mission_id,
213
133
  otherwise if the mission already exists an error will be raised
214
134
  """
135
+ if metadata is None:
136
+ metadata = {}
215
137
 
216
- if get_mission_id_by_name(client, mission_name, project_id) is not None:
138
+ if _get_mission_id_by_name(client, mission_name, project_id) is not None:
217
139
  raise MissionExists(f"Mission with name: `{mission_name}` already exists")
218
140
 
219
141
  if is_valid_uuid4(mission_name):
@@ -222,10 +144,13 @@ def create_mission(
222
144
  "mission names must not be valid UUIDv4's"
223
145
  )
224
146
 
147
+ # we need to translate tag keys to tag type ids
148
+ tags = _get_tags_map(client, metadata)
149
+
225
150
  payload = {
226
151
  "name": mission_name,
227
152
  "projectUUID": str(project_id),
228
- "tags": {str(k): v for k, v in tags.items()} if tags else {},
153
+ "tags": {str(k): v for k, v in tags.items()},
229
154
  "ignoreTags": ignore_missing_tags,
230
155
  }
231
156
 
@@ -235,175 +160,7 @@ def create_mission(
235
160
  return UUID(resp.json()["uuid"], version=4)
236
161
 
237
162
 
238
- def get_project_permission_level(client: AuthenticatedClient, project_id: UUID) -> int:
239
- """\
240
- we need this to check if a user has the permissions to
241
- create a mission in an existing project
242
- """
243
-
244
- resp = client.get("/user/permissions")
245
- resp.raise_for_status()
246
-
247
- project_group: List[Dict[str, Union[str, int]]] = resp.json().get("projects", [])
248
- filtered_by_id = filter(lambda x: x.get("uuid") == str(project_id), project_group)
249
-
250
- # it is possilbe that a user has access to a project via multiple groups
251
- # in this case we take the highest permission level
252
- return cast(int, max(map(lambda x: x.get("access", 0), filtered_by_id)))
253
-
254
-
255
- def _parse_file(file: Dict[str, Any]) -> File:
256
- project_id = UUID(file["mission"]["project"]["uuid"], version=4)
257
- project_name = file["mission"]["project"]["name"]
258
-
259
- mission_id = UUID(file["mission"]["uuid"], version=4)
260
- mission_name = file["mission"]["name"]
261
-
262
- filename = file["filename"]
263
- file_id = UUID(file["uuid"], version=4)
264
- file_size = file["size"]
265
- file_hash = file["hash"]
266
-
267
- parsed = File(
268
- id=file_id,
269
- name=filename,
270
- size=file_size,
271
- hash=file_hash,
272
- project_id=project_id,
273
- project_name=project_name,
274
- mission_id=mission_id,
275
- mission_name=mission_name,
276
- state=FileState(file["state"]),
277
- )
278
- return parsed
279
-
280
-
281
- def get_file(client: AuthenticatedClient, id: UUID) -> File:
282
- resp = client.get(FILE_ONE, params={"uuid": str(id)})
283
- resp.raise_for_status()
284
-
285
- return _parse_file(resp.json())
286
-
287
-
288
- def get_files(
289
- client: AuthenticatedClient,
290
- name: Optional[str] = None,
291
- project: Optional[str] = None,
292
- mission: Optional[str] = None,
293
- topics: Optional[List[str]] = None,
294
- tags: Optional[Dict[str, str]] = None,
295
- ) -> List[File]:
296
- # TODO: allow to search by id
297
-
298
- params: Dict[str, Any] = {"take": MAX_PAGINATION}
299
- if name is not None:
300
- params["name"] = name
301
- if project is not None:
302
- params["projectName"] = project
303
- if mission is not None:
304
- params["missionName"] = mission
305
- if topics:
306
- params["topics"] = ",".join(topics)
307
- if tags:
308
- params["tags"] = tags
309
-
310
- resp = client.get(FILE_QUERY, params=params)
311
- resp.raise_for_status()
312
-
313
- files = []
314
- data = resp.json()
315
-
316
- for file in data:
317
- try:
318
- parsed = _parse_file(file)
319
- files.append(parsed)
320
- except Exception:
321
- print(f"Error parsing file: {file}")
322
- return files
323
-
324
-
325
- def get_missions(
326
- client: AuthenticatedClient,
327
- project: Optional[str] = None,
328
- tags: Optional[Dict[str, str]] = None,
329
- ) -> list[Mission]:
330
- # TODO: use a better endpoint once this exists
331
- matching_files = get_files(client, project=project, tags=tags)
332
-
333
- ret = {}
334
- for file in matching_files:
335
- ret[file.mission_id] = Mission(
336
- id=file.mission_id,
337
- name=file.mission_name,
338
- project_id=file.project_id,
339
- project_name=file.project_name,
340
- )
341
-
342
- return list(ret.values())
343
-
344
-
345
- def get_projects(client: AuthenticatedClient) -> list[Project]:
346
- resp = client.get(PROJECT_ALL)
347
- resp.raise_for_status()
348
-
349
- ret = []
350
- for pr in resp.json()[0]:
351
- id = UUID(pr["uuid"], version=4)
352
- name = pr["name"]
353
- description = pr["description"]
354
- ret.append(Project(id=id, name=name, description=description))
355
-
356
- return ret
357
-
358
-
359
- def get_mission_by_spec(
360
- client: AuthenticatedClient, spec: Union[MissionById, MissionByName]
361
- ) -> Optional[Mission]:
362
- if isinstance(spec, MissionById):
363
- return get_mission_by_id(client, spec.id)
364
-
365
- if isinstance(spec.project, UUID):
366
- project_id = spec.project
367
- else:
368
- project_id = get_project_id_by_name(client, spec.project)
369
- if project_id is None:
370
- return None
371
-
372
- mission_id = get_mission_id_by_name(client, spec.name, project_id)
373
- if mission_id is None:
374
- return None
375
-
376
- return get_mission_by_id(client, mission_id)
377
-
378
-
379
- def get_files_by_file_spec(
380
- client: AuthenticatedClient, spec: Union[FilesByMission, FilesById]
381
- ) -> List[File]:
382
- if isinstance(spec, FilesById):
383
- return [get_file(client, file_id) for file_id in spec.ids]
384
-
385
- parsed_mission = get_mission_by_spec(client, spec.mission)
386
- if parsed_mission is None:
387
- raise ValueError("mission not found")
388
-
389
- if spec.files:
390
- file_ids = [id_ for id_ in spec.files if isinstance(id_, UUID)]
391
- file_names = filtered_by_patterns(
392
- [file.name for file in parsed_mission.files],
393
- [name for name in spec.files if isinstance(name, str)],
394
- )
395
-
396
- filtered = [
397
- file
398
- for file in parsed_mission.files
399
- if file.id in file_ids or file.name in file_names
400
- ]
401
- return filtered
402
-
403
- return parsed_mission.files
404
-
405
-
406
- def get_tag_type_by_name(
163
+ def _get_tag_type_by_name(
407
164
  client: AuthenticatedClient, tag_name: str
408
165
  ) -> Optional[TagType]:
409
166
  resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
@@ -423,13 +180,13 @@ def get_tag_type_by_name(
423
180
  return tag_type
424
181
 
425
182
 
426
- def get_tags_map(
183
+ def _get_tags_map(
427
184
  client: AuthenticatedClient, metadata: Dict[str, str]
428
185
  ) -> Dict[UUID, str]:
429
186
  # TODO: this needs a better endpoint
430
187
  ret = {}
431
188
  for key, val in metadata.items():
432
- tag_type = get_tag_type_by_name(client, key)
189
+ tag_type = _get_tag_type_by_name(client, key)
433
190
 
434
191
  if tag_type is None:
435
192
  print(f"tag: {key} not found")
@@ -440,10 +197,10 @@ def get_tags_map(
440
197
  return ret
441
198
 
442
199
 
443
- def update_mission_metadata(
444
- client: AuthenticatedClient, mission_id: UUID, metadata: Dict[str, str]
200
+ def _update_mission_metadata(
201
+ client: AuthenticatedClient, mission_id: UUID, *, metadata: Dict[str, str]
445
202
  ) -> None:
446
- tags_dct = get_tags_map(client, metadata)
203
+ tags_dct = _get_tags_map(client, metadata)
447
204
  payload = {
448
205
  "missionUUID": str(mission_id),
449
206
  "tags": {str(k): v for k, v in tags_dct.items()},
@@ -451,15 +208,15 @@ def update_mission_metadata(
451
208
  resp = client.post(MISSION_UPDATE_METADATA, json=payload)
452
209
 
453
210
  if resp.status_code == 404:
454
- raise MissionDoesNotExist
211
+ raise MissionNotFound
455
212
 
456
213
  if resp.status_code == 403:
457
- raise NoPermission
214
+ raise AccessDenied(f"cannot update mission: {mission_id}")
458
215
 
459
216
  resp.raise_for_status()
460
217
 
461
218
 
462
- def get_api_version() -> Tuple[int, int, int]:
219
+ def _get_api_version() -> Tuple[int, int, int]:
463
220
  config = Config()
464
221
  client = httpx.Client()
465
222
 
@@ -467,3 +224,12 @@ def get_api_version() -> Tuple[int, int, int]:
467
224
  vers = resp.headers["kleinkram-version"].split(".")
468
225
 
469
226
  return tuple(map(int, vers)) # type: ignore
227
+
228
+
229
+ def _claim_admin(client: AuthenticatedClient) -> None:
230
+ """\
231
+ the first user on the system could call this
232
+ """
233
+ response = client.post(CLAIM_ADMIN)
234
+ response.raise_for_status()
235
+ return