kleinkram 0.38.1.dev20241125112529__py3-none-any.whl → 0.38.1.dev20250113080249__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.
- kleinkram/__init__.py +33 -2
- kleinkram/api/client.py +21 -16
- kleinkram/api/deser.py +165 -0
- kleinkram/api/file_transfer.py +13 -24
- kleinkram/api/pagination.py +56 -0
- kleinkram/api/query.py +111 -0
- kleinkram/api/routes.py +270 -97
- kleinkram/auth.py +21 -20
- kleinkram/cli/__init__.py +0 -0
- kleinkram/{commands/download.py → cli/_download.py} +18 -44
- kleinkram/cli/_endpoint.py +58 -0
- kleinkram/{commands/list.py → cli/_list.py} +25 -38
- kleinkram/cli/_mission.py +153 -0
- kleinkram/cli/_project.py +99 -0
- kleinkram/cli/_upload.py +84 -0
- kleinkram/cli/_verify.py +56 -0
- kleinkram/{app.py → cli/app.py} +50 -22
- kleinkram/cli/error_handling.py +44 -0
- kleinkram/config.py +141 -107
- kleinkram/core.py +251 -3
- kleinkram/errors.py +13 -45
- kleinkram/main.py +1 -1
- kleinkram/models.py +48 -149
- kleinkram/printing.py +325 -0
- kleinkram/py.typed +0 -0
- kleinkram/types.py +9 -0
- kleinkram/utils.py +82 -27
- kleinkram/wrappers.py +401 -0
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/METADATA +3 -3
- kleinkram-0.38.1.dev20250113080249.dist-info/RECORD +48 -0
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/top_level.txt +1 -0
- testing/__init__.py +0 -0
- testing/backend_fixtures.py +69 -0
- tests/conftest.py +7 -0
- tests/test_config.py +115 -0
- tests/test_core.py +165 -0
- tests/test_end_to_end.py +29 -39
- tests/test_fixtures.py +29 -0
- tests/test_printing.py +62 -0
- tests/test_query.py +138 -0
- tests/test_utils.py +34 -24
- tests/test_wrappers.py +71 -0
- kleinkram/api/parsing.py +0 -86
- kleinkram/commands/__init__.py +0 -1
- kleinkram/commands/endpoint.py +0 -62
- kleinkram/commands/mission.py +0 -69
- kleinkram/commands/project.py +0 -24
- kleinkram/commands/upload.py +0 -164
- kleinkram/commands/verify.py +0 -142
- kleinkram/consts.py +0 -8
- kleinkram/enums.py +0 -10
- kleinkram/resources.py +0 -158
- kleinkram-0.38.1.dev20241125112529.dist-info/LICENSE +0 -674
- kleinkram-0.38.1.dev20241125112529.dist-info/RECORD +0 -37
- tests/test_resources.py +0 -137
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.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
|
|
9
|
+
class InvalidFileQuery(Exception): ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidMissionQuery(Exception): ...
|
|
18
13
|
|
|
19
14
|
|
|
20
|
-
class
|
|
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
|
|
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
|
-
|
|
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
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|