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