kleinkram 0.38.1.dev20241212075157__py3-none-any.whl → 0.38.1.dev20250207122632__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.

Files changed (58) hide show
  1. kleinkram/__init__.py +33 -2
  2. kleinkram/api/client.py +21 -16
  3. kleinkram/api/deser.py +165 -0
  4. kleinkram/api/file_transfer.py +13 -24
  5. kleinkram/api/pagination.py +56 -0
  6. kleinkram/api/query.py +111 -0
  7. kleinkram/api/routes.py +266 -97
  8. kleinkram/auth.py +21 -20
  9. kleinkram/cli/__init__.py +0 -0
  10. kleinkram/{commands/download.py → cli/_download.py} +18 -44
  11. kleinkram/cli/_endpoint.py +58 -0
  12. kleinkram/{commands/list.py → cli/_list.py} +25 -38
  13. kleinkram/cli/_mission.py +153 -0
  14. kleinkram/cli/_project.py +99 -0
  15. kleinkram/cli/_upload.py +84 -0
  16. kleinkram/cli/_verify.py +56 -0
  17. kleinkram/{app.py → cli/app.py} +57 -25
  18. kleinkram/cli/error_handling.py +67 -0
  19. kleinkram/config.py +141 -107
  20. kleinkram/core.py +251 -3
  21. kleinkram/errors.py +13 -45
  22. kleinkram/main.py +1 -1
  23. kleinkram/models.py +48 -149
  24. kleinkram/printing.py +325 -0
  25. kleinkram/py.typed +0 -0
  26. kleinkram/types.py +9 -0
  27. kleinkram/utils.py +88 -29
  28. kleinkram/wrappers.py +401 -0
  29. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/METADATA +3 -3
  30. kleinkram-0.38.1.dev20250207122632.dist-info/RECORD +49 -0
  31. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/WHEEL +1 -1
  32. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/top_level.txt +1 -0
  33. testing/__init__.py +0 -0
  34. testing/backend_fixtures.py +67 -0
  35. tests/conftest.py +7 -0
  36. tests/test_config.py +115 -0
  37. tests/test_core.py +165 -0
  38. tests/test_end_to_end.py +29 -39
  39. tests/test_error_handling.py +44 -0
  40. tests/test_fixtures.py +34 -0
  41. tests/test_printing.py +62 -0
  42. tests/test_query.py +138 -0
  43. tests/test_utils.py +46 -24
  44. tests/test_wrappers.py +71 -0
  45. kleinkram/api/parsing.py +0 -86
  46. kleinkram/commands/__init__.py +0 -1
  47. kleinkram/commands/endpoint.py +0 -62
  48. kleinkram/commands/mission.py +0 -69
  49. kleinkram/commands/project.py +0 -24
  50. kleinkram/commands/upload.py +0 -164
  51. kleinkram/commands/verify.py +0 -142
  52. kleinkram/consts.py +0 -8
  53. kleinkram/enums.py +0 -10
  54. kleinkram/resources.py +0 -158
  55. kleinkram-0.38.1.dev20241212075157.dist-info/LICENSE +0 -674
  56. kleinkram-0.38.1.dev20241212075157.dist-info/RECORD +0 -37
  57. tests/test_resources.py +0 -137
  58. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/entry_points.txt +0 -0
kleinkram/errors.py CHANGED
@@ -1,34 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any
4
- from typing import Callable
5
- from typing import OrderedDict
6
- from typing import Type
7
-
8
- import typer
9
-
10
3
  LOGIN_MESSAGE = "Please login using `klein login`."
11
- INVALID_CONFIG_MESSAGE = "Invalid config file."
12
4
 
13
5
 
14
6
  class ParsingError(Exception): ...
15
7
 
16
8
 
17
- class InvalidMissionSpec(Exception): ...
9
+ class InvalidFileQuery(Exception): ...
10
+
11
+
12
+ class InvalidMissionQuery(Exception): ...
18
13
 
19
14
 
20
- class InvalidProjectSpec(Exception): ...
15
+ class InvalidProjectQuery(Exception): ...
21
16
 
22
17
 
23
18
  class MissionExists(Exception): ...
24
19
 
25
20
 
21
+ class ProjectExists(Exception): ...
22
+
23
+
26
24
  class MissionNotFound(Exception): ...
27
25
 
28
26
 
29
27
  class ProjectNotFound(Exception): ...
30
28
 
31
29
 
30
+ class FileNotFound(Exception): ...
31
+
32
+
32
33
  class AccessDenied(Exception): ...
33
34
 
34
35
 
@@ -43,40 +44,7 @@ class InvalidCLIVersion(Exception): ...
43
44
  class FileTypeNotSupported(Exception): ...
44
45
 
45
46
 
46
- class InvalidConfigFile(Exception):
47
- def __init__(self) -> None:
48
- super().__init__(INVALID_CONFIG_MESSAGE)
49
-
50
-
51
- ExceptionHandler = Callable[[Exception], int]
52
-
53
-
54
- class ErrorHandledTyper(typer.Typer):
55
- """\
56
- error handlers that are last added will be used first
57
- """
58
-
59
- _error_handlers: OrderedDict[Type[Exception], ExceptionHandler]
60
-
61
- def error_handler(
62
- self, exc: type[Exception]
63
- ) -> Callable[[ExceptionHandler], ExceptionHandler]:
64
- def dec(func: ExceptionHandler) -> ExceptionHandler:
65
- self._error_handlers[exc] = func
66
- return func
67
-
68
- return dec
47
+ class FileNameNotSupported(Exception): ...
69
48
 
70
- def __init__(self, *args: Any, **kwargs: Any) -> None:
71
- super().__init__(*args, **kwargs)
72
- self._error_handlers = OrderedDict()
73
49
 
74
- def __call__(self, *args: Any, **kwargs: Any) -> int:
75
- try:
76
- return super().__call__(*args, **kwargs)
77
- except Exception as e:
78
- for tp, handler in reversed(self._error_handlers.items()):
79
- if isinstance(e, tp):
80
- exit_code = handler(e)
81
- raise SystemExit(exit_code)
82
- raise
50
+ class InvalidMissionMetadata(Exception): ...
kleinkram/main.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from kleinkram.app import app
3
+ from kleinkram.cli.app import app
4
4
 
5
5
 
6
6
  def main() -> int:
kleinkram/models.py CHANGED
@@ -2,33 +2,26 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from dataclasses import field
5
+ from datetime import datetime
5
6
  from enum import Enum
7
+ from typing import Dict
6
8
  from typing import List
7
- from typing import NamedTuple
8
- from typing import Optional
9
- from typing import Tuple
10
- from typing import Union
11
9
  from uuid import UUID
12
10
 
13
- from rich.table import Table
14
- from rich.text import Text
15
11
 
12
+ class MetadataValueType(str, Enum):
13
+ LOCATION = "LOCATION" # string
14
+ STRING = "STRING" # string
15
+ LINK = "LINK" # string
16
+ BOOLEAN = "BOOLEAN" # bool
17
+ NUMBER = "NUMBER" # float
18
+ DATE = "DATE" # datetime
16
19
 
17
- @dataclass(eq=True)
18
- class Project:
19
- id: UUID
20
- name: str
21
- description: str
22
- missions: List[Mission] = field(default_factory=list)
23
20
 
24
-
25
- @dataclass(eq=True)
26
- class Mission:
27
- id: UUID
28
- name: str
29
- project_id: UUID
30
- project_name: str
31
- files: List[File] = field(default_factory=list)
21
+ @dataclass(frozen=True)
22
+ class MetadataValue:
23
+ value: str
24
+ type_: MetadataValueType
32
25
 
33
26
 
34
27
  class FileState(str, Enum):
@@ -41,146 +34,52 @@ class FileState(str, Enum):
41
34
  FOUND = "FOUND"
42
35
 
43
36
 
44
- FILE_STATE_COLOR = {
45
- FileState.OK: "green",
46
- FileState.CORRUPTED: "red",
47
- FileState.UPLOADING: "yellow",
48
- FileState.ERROR: "red",
49
- FileState.CONVERSION_ERROR: "red",
50
- FileState.LOST: "bold red",
51
- FileState.FOUND: "yellow",
52
- }
37
+ @dataclass(frozen=True)
38
+ class Project:
39
+ id: UUID
40
+ name: str
41
+ description: str
42
+ created_at: datetime
43
+ updated_at: datetime
53
44
 
54
45
 
55
- @dataclass(frozen=True, eq=True)
46
+ @dataclass(frozen=True)
47
+ class Mission:
48
+ id: UUID
49
+ name: str
50
+ created_at: datetime
51
+ updated_at: datetime
52
+ project_id: UUID
53
+ project_name: str
54
+ metadata: Dict[str, MetadataValue] = field(default_factory=dict)
55
+ number_of_files: int = 0
56
+ size: int = 0
57
+
58
+
59
+ @dataclass(frozen=True)
56
60
  class File:
57
61
  id: UUID
58
62
  name: str
59
63
  hash: str
60
64
  size: int
65
+ type_: str
66
+ date: datetime
67
+ created_at: datetime
68
+ updated_at: datetime
61
69
  mission_id: UUID
62
70
  mission_name: str
63
71
  project_id: UUID
64
72
  project_name: str
73
+ categories: List[str] = field(default_factory=list)
74
+ topics: List[str] = field(default_factory=list)
65
75
  state: FileState = FileState.OK
66
76
 
67
77
 
68
- class DataType(str, Enum):
69
- LOCATION = "LOCATION"
70
- STRING = "STRING"
71
- LINK = "LINK"
72
- BOOLEAN = "BOOLEAN"
73
- NUMBER = "NUMBER"
74
- DATE = "DATE"
75
-
76
-
77
- @dataclass(frozen=True, eq=True)
78
- class TagType:
79
- name: str
80
- id: UUID
81
- data_type: DataType
82
- description: Optional[str]
83
-
84
-
85
- def delimiter_row(
86
- *lengths: int, delimiter: str = "-", cols: list[int] | None = None
87
- ) -> List[str]:
88
- ret = []
89
- for i, col_len in enumerate(lengths):
90
- if cols is None or i in cols:
91
- ret.append(delimiter * col_len)
92
- else:
93
- ret.append("")
94
- return ret
95
-
96
-
97
- def projects_to_table(projects: List[Project]) -> Table:
98
- table = Table(title="projects")
99
- table.add_column("id")
100
- table.add_column("name")
101
- table.add_column("description")
102
-
103
- for project in projects:
104
- table.add_row(str(project.id), project.name, project.description)
105
-
106
- return table
107
-
108
-
109
- def missions_to_table(missions: List[Mission]) -> Table:
110
- table = Table(title="missions")
111
- table.add_column("project")
112
- table.add_column("name")
113
- table.add_column("id")
114
-
115
- # order by project, name
116
- missions_tp: List[Tuple[str, str, Mission]] = []
117
- for mission in missions:
118
- missions_tp.append((mission.project_name, mission.name, mission))
119
- missions_tp.sort()
120
-
121
- if not missions_tp:
122
- return table
123
- last_project: Optional[str] = None
124
- for project, _, mission in missions_tp:
125
- # add delimiter row if project changes
126
- if last_project is not None and last_project != project:
127
- table.add_row()
128
- last_project = project
129
-
130
- table.add_row(mission.project_name, mission.name, str(mission.id))
131
-
132
- return table
133
-
134
-
135
- def files_to_table(
136
- files: List[File], *, title: str = "files", delimiters: bool = True
137
- ) -> Table:
138
- table = Table(title=title)
139
- table.add_column("project")
140
- table.add_column("mission")
141
- table.add_column("name")
142
- table.add_column("id")
143
- table.add_column("state")
144
-
145
- # order by project, mission, name
146
- files_tp: List[Tuple[str, str, str, File]] = []
147
- for file in files:
148
- files_tp.append((file.project_name, file.mission_name, file.name, file))
149
- files_tp.sort()
150
-
151
- if not files_tp:
152
- return table
153
-
154
- last_mission: Optional[str] = None
155
- for _, mission, _, file in files_tp:
156
- if last_mission is not None and last_mission != mission and delimiters:
157
- table.add_row()
158
- last_mission = mission
159
-
160
- table.add_row(
161
- file.project_name,
162
- file.mission_name,
163
- file.name,
164
- Text(str(file.id), style="green"),
165
- Text(file.state.value, style=FILE_STATE_COLOR[file.state]),
166
- )
167
-
168
- return table
169
-
170
-
171
- class FilesById(NamedTuple):
172
- ids: List[UUID]
173
-
174
-
175
- class FilesByMission(NamedTuple):
176
- mission: MissionById | MissionByName
177
- files: List[Union[str, UUID]]
178
-
179
-
180
- class MissionById(NamedTuple):
181
- id: UUID
182
-
183
-
184
- class MissionByName(NamedTuple):
185
- name: str
186
- project: Union[str, UUID]
78
+ # this is the file state for the verify command
79
+ class FileVerificationStatus(str, Enum):
80
+ UPLAODED = "uploaded"
81
+ UPLOADING = "uploading"
82
+ COMPUTING_HASH = "computing hash"
83
+ MISSING = "missing"
84
+ MISMATCHED_HASH = "hash mismatch"
85
+ UNKNOWN = "unknown"
kleinkram/printing.py ADDED
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from dataclasses import asdict
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import List
9
+ from typing import Mapping
10
+ from typing import Optional
11
+ from typing import Sequence
12
+ from typing import Tuple
13
+ from typing import Union
14
+
15
+ import dateutil.parser
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+ from kleinkram.core import FileVerificationStatus
21
+ from kleinkram.models import File
22
+ from kleinkram.models import FileState
23
+ from kleinkram.models import MetadataValue
24
+ from kleinkram.models import MetadataValueType
25
+ from kleinkram.models import Mission
26
+ from kleinkram.models import Project
27
+
28
+ FILE_STATE_COLOR = {
29
+ FileState.OK: "green",
30
+ FileState.CORRUPTED: "red",
31
+ FileState.UPLOADING: "yellow",
32
+ FileState.ERROR: "red",
33
+ FileState.CONVERSION_ERROR: "red",
34
+ FileState.LOST: "bold red",
35
+ FileState.FOUND: "yellow",
36
+ }
37
+
38
+
39
+ FILE_VERIFICATION_STATUS_STYLES = {
40
+ FileVerificationStatus.UPLAODED: "green",
41
+ FileVerificationStatus.UPLOADING: "yellow",
42
+ FileVerificationStatus.MISSING: "yellow",
43
+ FileVerificationStatus.MISMATCHED_HASH: "red",
44
+ FileVerificationStatus.UNKNOWN: "gray",
45
+ FileVerificationStatus.COMPUTING_HASH: "purple",
46
+ }
47
+
48
+
49
+ def file_state_to_text(file_state: FileState) -> Text:
50
+ return Text(file_state.value, style=FILE_STATE_COLOR[file_state])
51
+
52
+
53
+ def file_verification_status_to_text(
54
+ file_verification_status: FileVerificationStatus,
55
+ ) -> Text:
56
+ return Text(
57
+ file_verification_status.value,
58
+ style=FILE_VERIFICATION_STATUS_STYLES[file_verification_status],
59
+ )
60
+
61
+
62
+ def format_bytes(size: int) -> str:
63
+ """
64
+ Converts a size in bytes to a human-readable string (e.g., KB, MB, GB, TB).
65
+
66
+ Args:
67
+ size (int): The size in bytes.
68
+
69
+ Returns:
70
+ str: A formatted string representing the size in appropriate units.
71
+ """
72
+
73
+ if size < 0:
74
+ raise ValueError("Size must be a non-negative integer")
75
+
76
+ # Define units and their thresholds
77
+ units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]
78
+ index = 0
79
+
80
+ fsize: float = size
81
+ while fsize >= 1024 and index < len(units) - 1:
82
+ fsize /= 1024.0
83
+ index += 1
84
+
85
+ # Format to 2 decimal places if needed
86
+ if index == 0: # Bytes don't need decimals
87
+ return f"{int(fsize)} {units[index]}"
88
+ else:
89
+ return f"{fsize:.2f} {units[index]}"
90
+
91
+
92
+ MetadataValueInternalType = Union[str, float, bool, datetime]
93
+
94
+
95
+ def parse_metadata_value(value: MetadataValue) -> Union[str, float, bool, datetime]:
96
+ if value.type_ in [
97
+ MetadataValueType.STRING,
98
+ MetadataValueType.LINK,
99
+ MetadataValueType.LOCATION,
100
+ ]:
101
+ return value.value
102
+ if value.type_ == MetadataValueType.NUMBER:
103
+ return float(value.value)
104
+ if value.type_ == MetadataValueType.BOOLEAN:
105
+ return value.value == "true"
106
+ if value.type_ == MetadataValueType.DATE:
107
+ return dateutil.parser.isoparse(value.value)
108
+ assert False, "unreachable"
109
+
110
+
111
+ def projects_to_table(projects: Sequence[Project]) -> Table:
112
+ table = Table(title="projects")
113
+ table.add_column("id")
114
+ table.add_column("name")
115
+ table.add_column("description")
116
+
117
+ for project in projects:
118
+ table.add_row(str(project.id), project.name, project.description)
119
+ return table
120
+
121
+
122
+ def missions_to_table(missions: Sequence[Mission]) -> Table:
123
+ table = Table(title="missions")
124
+ table.add_column("project")
125
+ table.add_column("name")
126
+ table.add_column("id")
127
+ table.add_column("files")
128
+ table.add_column("size")
129
+
130
+ # order by project, name
131
+ missions_tp: List[Tuple[str, str, Mission]] = []
132
+ for mission in missions:
133
+ missions_tp.append((mission.project_name, mission.name, mission))
134
+ missions_tp.sort()
135
+
136
+ if not missions_tp:
137
+ return table
138
+ last_project: Optional[str] = None
139
+ for project, _, mission in missions_tp:
140
+ # add delimiter row if project changes
141
+ if last_project is not None and last_project != project:
142
+ table.add_row()
143
+ last_project = project
144
+
145
+ table.add_row(
146
+ mission.project_name,
147
+ mission.name,
148
+ str(mission.id),
149
+ str(mission.number_of_files),
150
+ format_bytes(mission.size),
151
+ )
152
+ return table
153
+
154
+
155
+ def files_to_table(
156
+ files: Sequence[File], *, title: str = "files", delimiters: bool = True
157
+ ) -> Table:
158
+ table = Table(title=title)
159
+ table.add_column("project")
160
+ table.add_column("mission")
161
+ table.add_column("name")
162
+ table.add_column("id")
163
+ table.add_column("state")
164
+ table.add_column("size")
165
+ table.add_column("categories")
166
+
167
+ # order by project, mission, name
168
+ files_tp: List[Tuple[str, str, str, File]] = []
169
+ for file in files:
170
+ files_tp.append((file.project_name, file.mission_name, file.name, file))
171
+ files_tp.sort()
172
+
173
+ if not files_tp:
174
+ return table
175
+
176
+ last_mission: Optional[str] = None
177
+ for _, mission, _, file in files_tp:
178
+ if last_mission is not None and last_mission != mission and delimiters:
179
+ table.add_row()
180
+ last_mission = mission
181
+
182
+ table.add_row(
183
+ file.project_name,
184
+ file.mission_name,
185
+ file.name,
186
+ Text(str(file.id), style="green"),
187
+ file_state_to_text(file.state),
188
+ format_bytes(file.size),
189
+ ", ".join(file.categories),
190
+ )
191
+ return table
192
+
193
+
194
+ def mission_info_table(
195
+ mission: Mission, print_metadata: bool = False
196
+ ) -> Tuple[Table, ...]:
197
+ table = Table("k", "v", title=f"mission info: {mission.name}", show_header=False)
198
+
199
+ # TODO: add more fields as we store more information in the Mission object
200
+ table.add_row("name", mission.name)
201
+ table.add_row("id", Text(str(mission.id), style="green"))
202
+ table.add_row("project", mission.project_name)
203
+ table.add_row("project id", Text(str(mission.project_id), style="green"))
204
+ table.add_row("created", str(mission.created_at))
205
+ table.add_row("updated", str(mission.updated_at))
206
+ table.add_row("size", format_bytes(mission.size))
207
+ table.add_row("files", str(mission.number_of_files))
208
+
209
+ if not print_metadata:
210
+ return (table,)
211
+
212
+ metadata_table = Table("k", "v", title="mission metadata", show_header=False)
213
+ kv_pairs_sorted = sorted(
214
+ [(k, v) for k, v in mission.metadata.items()], key=lambda x: x[0]
215
+ )
216
+ for k, v in kv_pairs_sorted:
217
+ metadata_table.add_row(k, str(parse_metadata_value(v)))
218
+
219
+ return table, metadata_table
220
+
221
+
222
+ def project_info_table(project: Project) -> Table:
223
+ table = Table("k", "v", title=f"project info: {project.name}", show_header=False)
224
+
225
+ # TODO: add more fields as we store more information in the Project object
226
+ table.add_row("id", Text(str(project.id), style="green"))
227
+ table.add_row("name", project.name)
228
+ table.add_row("description", project.description)
229
+ table.add_row("created", str(project.created_at))
230
+ table.add_row("updated", str(project.updated_at))
231
+
232
+ return table
233
+
234
+
235
+ def file_verification_status_table(
236
+ file_status: Mapping[Path, FileVerificationStatus]
237
+ ) -> Table:
238
+ table = Table(title="file status")
239
+ table.add_column("filename", style="cyan")
240
+ table.add_column("status", style="green")
241
+ for path, status in file_status.items():
242
+ table.add_row(str(path), file_verification_status_to_text(status))
243
+ return table
244
+
245
+
246
+ def print_file_verification_status(
247
+ file_status: Mapping[Path, FileVerificationStatus], *, pprint: bool
248
+ ) -> None:
249
+ """\
250
+ prints the file verification status to stdout / stderr
251
+ either using pprint or as a list for piping
252
+ """
253
+ if pprint:
254
+ Console().print(file_verification_status_table(file_status))
255
+ else:
256
+ for path, status in file_status.items():
257
+ stream = (
258
+ sys.stdout if status == FileVerificationStatus.UPLAODED else sys.stderr
259
+ )
260
+ print(path, file=stream, flush=True)
261
+
262
+
263
+ def print_files(files: Sequence[File], *, pprint: bool) -> None:
264
+ """\
265
+ prints the files to stdout / stderr
266
+ either using pprint or as a list for piping
267
+ """
268
+ if pprint:
269
+ Console().print(files_to_table(files))
270
+ else:
271
+ for file in files:
272
+ stream = sys.stdout if file.state == FileState.OK else sys.stderr
273
+ print(file.id, file=stream, flush=True)
274
+
275
+
276
+ def print_missions(missions: Sequence[Mission], *, pprint: bool) -> None:
277
+ """\
278
+ prints the missions to stdout
279
+ either using pprint or as a list for piping
280
+ """
281
+ if pprint:
282
+ Console().print(missions_to_table(missions))
283
+ else:
284
+ for mission in missions:
285
+ print(mission.id)
286
+
287
+
288
+ def print_projects(projects: Sequence[Project], *, pprint: bool) -> None:
289
+ """\
290
+ prints the projects to stdout
291
+ either using pprint or as a list for piping
292
+ """
293
+ if pprint:
294
+ Console().print(projects_to_table(projects))
295
+ else:
296
+ for project in projects:
297
+ print(project.id)
298
+
299
+
300
+ def print_mission_info(mission: Mission, *, pprint: bool) -> None:
301
+ """\
302
+ prints the mission info to stdout
303
+ either using pprint or as a list for piping
304
+ """
305
+ if pprint:
306
+ Console().print(*mission_info_table(mission, print_metadata=True))
307
+ else:
308
+ mission_dct = asdict(mission)
309
+ for key in mission_dct:
310
+ mission_dct[key] = str(mission_dct[key]) # TODO: improve this
311
+ print(json.dumps(mission_dct))
312
+
313
+
314
+ def print_project_info(project: Project, *, pprint: bool) -> None:
315
+ """\
316
+ prints the project info to stdout
317
+ either using pprint or as a list for piping
318
+ """
319
+ if pprint:
320
+ Console().print(project_info_table(project))
321
+ else:
322
+ project_dct = asdict(project)
323
+ for key in project_dct:
324
+ project_dct[key] = str(project_dct[key]) # TODO: improve this
325
+ print(json.dumps(project_dct))
kleinkram/py.typed ADDED
File without changes
kleinkram/types.py ADDED
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Union
6
+ from uuid import UUID
7
+
8
+ IdLike = Union[UUID, str]
9
+ PathLike = Union[Path, str]