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.
- 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 +266 -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} +57 -25
- kleinkram/cli/error_handling.py +67 -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 +88 -29
- kleinkram/wrappers.py +401 -0
- {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/METADATA +3 -3
- kleinkram-0.38.1.dev20250207122632.dist-info/RECORD +49 -0
- {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/WHEEL +1 -1
- {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/top_level.txt +1 -0
- testing/__init__.py +0 -0
- testing/backend_fixtures.py +67 -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_error_handling.py +44 -0
- tests/test_fixtures.py +34 -0
- tests/test_printing.py +62 -0
- tests/test_query.py +138 -0
- tests/test_utils.py +46 -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.dev20241212075157.dist-info/LICENSE +0 -674
- kleinkram-0.38.1.dev20241212075157.dist-info/RECORD +0 -37
- tests/test_resources.py +0 -137
- {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/entry_points.txt +0 -0
|
@@ -4,20 +4,19 @@ from typing import List
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
|
+
|
|
7
8
|
from kleinkram.api.client import AuthenticatedClient
|
|
9
|
+
from kleinkram.api.query import FileQuery
|
|
10
|
+
from kleinkram.api.query import MissionQuery
|
|
11
|
+
from kleinkram.api.query import ProjectQuery
|
|
12
|
+
from kleinkram.api.routes import get_files
|
|
13
|
+
from kleinkram.api.routes import get_missions
|
|
14
|
+
from kleinkram.api.routes import get_projects
|
|
8
15
|
from kleinkram.config import get_shared_state
|
|
9
|
-
from kleinkram.
|
|
10
|
-
from kleinkram.
|
|
11
|
-
from kleinkram.
|
|
12
|
-
from kleinkram.resources import FileSpec
|
|
13
|
-
from kleinkram.resources import get_files_by_spec
|
|
14
|
-
from kleinkram.resources import get_missions_by_spec
|
|
15
|
-
from kleinkram.resources import get_projects_by_spec
|
|
16
|
-
from kleinkram.resources import MissionSpec
|
|
17
|
-
from kleinkram.resources import ProjectSpec
|
|
16
|
+
from kleinkram.printing import print_files
|
|
17
|
+
from kleinkram.printing import print_missions
|
|
18
|
+
from kleinkram.printing import print_projects
|
|
18
19
|
from kleinkram.utils import split_args
|
|
19
|
-
from rich.console import Console
|
|
20
|
-
|
|
21
20
|
|
|
22
21
|
HELP = """\
|
|
23
22
|
List projects, missions, or files.
|
|
@@ -46,24 +45,19 @@ def files(
|
|
|
46
45
|
mission_ids, mission_patterns = split_args(missions or [])
|
|
47
46
|
project_ids, project_patterns = split_args(projects or [])
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
project_query = ProjectQuery(patterns=project_patterns, ids=project_ids)
|
|
49
|
+
mission_query = MissionQuery(
|
|
50
|
+
project_query=project_query,
|
|
52
51
|
ids=mission_ids,
|
|
53
52
|
patterns=mission_patterns,
|
|
54
53
|
)
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
file_query = FileQuery(
|
|
55
|
+
mission_query=mission_query, patterns=file_patterns, ids=file_ids
|
|
57
56
|
)
|
|
58
57
|
|
|
59
58
|
client = AuthenticatedClient()
|
|
60
|
-
parsed_files =
|
|
61
|
-
|
|
62
|
-
if get_shared_state().verbose:
|
|
63
|
-
Console().print(files_to_table(parsed_files))
|
|
64
|
-
else:
|
|
65
|
-
for file in parsed_files:
|
|
66
|
-
print(file.id)
|
|
59
|
+
parsed_files = list(get_files(client, file_query=file_query))
|
|
60
|
+
print_files(parsed_files, pprint=get_shared_state().verbose)
|
|
67
61
|
|
|
68
62
|
|
|
69
63
|
@list_typer.command()
|
|
@@ -76,18 +70,16 @@ def missions(
|
|
|
76
70
|
mission_ids, mission_patterns = split_args(missions or [])
|
|
77
71
|
project_ids, project_patterns = split_args(projects or [])
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
74
|
+
mission_query = MissionQuery(
|
|
81
75
|
ids=mission_ids,
|
|
82
76
|
patterns=mission_patterns,
|
|
83
|
-
|
|
77
|
+
project_query=project_query,
|
|
84
78
|
)
|
|
85
79
|
|
|
86
80
|
client = AuthenticatedClient()
|
|
87
|
-
parsed_missions =
|
|
88
|
-
|
|
89
|
-
if get_shared_state().verbose:
|
|
90
|
-
Console().print(missions_to_table(parsed_missions))
|
|
81
|
+
parsed_missions = list(get_missions(client, mission_query=mission_query))
|
|
82
|
+
print_missions(parsed_missions, pprint=get_shared_state().verbose)
|
|
91
83
|
|
|
92
84
|
|
|
93
85
|
@list_typer.command()
|
|
@@ -95,13 +87,8 @@ def projects(
|
|
|
95
87
|
projects: Optional[List[str]] = typer.Argument(None, help="project names"),
|
|
96
88
|
) -> None:
|
|
97
89
|
project_ids, project_patterns = split_args(projects or [])
|
|
98
|
-
|
|
90
|
+
project_query = ProjectQuery(patterns=project_patterns, ids=project_ids)
|
|
99
91
|
|
|
100
92
|
client = AuthenticatedClient()
|
|
101
|
-
parsed_projects =
|
|
102
|
-
|
|
103
|
-
if get_shared_state().verbose:
|
|
104
|
-
Console().print(projects_to_table(parsed_projects))
|
|
105
|
-
else:
|
|
106
|
-
for project in parsed_projects:
|
|
107
|
-
print(project.id)
|
|
93
|
+
parsed_projects = list(get_projects(client, project_query=project_query))
|
|
94
|
+
print_projects(parsed_projects, pprint=get_shared_state().verbose)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
import kleinkram.api.routes
|
|
9
|
+
import kleinkram.core
|
|
10
|
+
from kleinkram.api.client import AuthenticatedClient
|
|
11
|
+
from kleinkram.api.query import MissionQuery
|
|
12
|
+
from kleinkram.api.query import ProjectQuery
|
|
13
|
+
from kleinkram.api.routes import get_mission
|
|
14
|
+
from kleinkram.api.routes import get_project
|
|
15
|
+
from kleinkram.config import get_shared_state
|
|
16
|
+
from kleinkram.printing import print_mission_info
|
|
17
|
+
from kleinkram.utils import load_metadata
|
|
18
|
+
from kleinkram.utils import split_args
|
|
19
|
+
|
|
20
|
+
CREATE_HELP = "create a mission"
|
|
21
|
+
UPDATE_HELP = "update a mission"
|
|
22
|
+
DELETE_HELP = "delete a mission"
|
|
23
|
+
INFO_HELP = "get information about a mission"
|
|
24
|
+
NOT_IMPLEMENTED_YET = """\
|
|
25
|
+
Not implemented yet, open an issue if you want specific functionality
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
mission_typer = typer.Typer(
|
|
29
|
+
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mission_typer.command(help=CREATE_HELP)
|
|
34
|
+
def create(
|
|
35
|
+
project: str = typer.Option(..., "--project", "-p", help="project id or name"),
|
|
36
|
+
mission_name: str = typer.Option(..., "--mission", "-m", help="mission name"),
|
|
37
|
+
metadata: Optional[str] = typer.Option(
|
|
38
|
+
None, help="path to metadata file (json or yaml)"
|
|
39
|
+
),
|
|
40
|
+
ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
|
|
41
|
+
) -> None:
|
|
42
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
43
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
44
|
+
|
|
45
|
+
metadata_dct = load_metadata(Path(metadata)) if metadata else {} # noqa
|
|
46
|
+
|
|
47
|
+
client = AuthenticatedClient()
|
|
48
|
+
project_id = get_project(client, project_query).id
|
|
49
|
+
mission_id = kleinkram.api.routes._create_mission(
|
|
50
|
+
client,
|
|
51
|
+
project_id,
|
|
52
|
+
mission_name,
|
|
53
|
+
metadata=metadata_dct,
|
|
54
|
+
ignore_missing_tags=ignore_missing_tags,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
mission_parsed = get_mission(client, MissionQuery(ids=[mission_id]))
|
|
58
|
+
print_mission_info(mission_parsed, pprint=get_shared_state().verbose)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@mission_typer.command(help=INFO_HELP)
|
|
62
|
+
def info(
|
|
63
|
+
project: Optional[str] = typer.Option(
|
|
64
|
+
None, "--project", "-p", help="project id or name"
|
|
65
|
+
),
|
|
66
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
67
|
+
) -> None:
|
|
68
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
69
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
70
|
+
|
|
71
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
72
|
+
mission_query = MissionQuery(
|
|
73
|
+
ids=mission_ids,
|
|
74
|
+
patterns=mission_patterns,
|
|
75
|
+
project_query=project_query,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
client = AuthenticatedClient()
|
|
79
|
+
mission_parsed = get_mission(client, mission_query)
|
|
80
|
+
print_mission_info(mission_parsed, pprint=get_shared_state().verbose)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mission_typer.command(help=UPDATE_HELP)
|
|
84
|
+
def update(
|
|
85
|
+
project: Optional[str] = typer.Option(
|
|
86
|
+
None, "--project", "-p", help="project id or name"
|
|
87
|
+
),
|
|
88
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
89
|
+
metadata: str = typer.Option(help="path to metadata file (json or yaml)"),
|
|
90
|
+
) -> None:
|
|
91
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
92
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
93
|
+
|
|
94
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
95
|
+
mission_query = MissionQuery(
|
|
96
|
+
ids=mission_ids,
|
|
97
|
+
patterns=mission_patterns,
|
|
98
|
+
project_query=project_query,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
metadata_dct = load_metadata(Path(metadata))
|
|
102
|
+
|
|
103
|
+
client = AuthenticatedClient()
|
|
104
|
+
mission_id = get_mission(client, mission_query).id
|
|
105
|
+
kleinkram.core.update_mission(
|
|
106
|
+
client=client, mission_id=mission_id, metadata=metadata_dct
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
mission_parsed = get_mission(client, mission_query)
|
|
110
|
+
print_mission_info(mission_parsed, pprint=get_shared_state().verbose)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@mission_typer.command(help=DELETE_HELP)
|
|
114
|
+
def delete(
|
|
115
|
+
project: Optional[str] = typer.Option(
|
|
116
|
+
None, "--project", "-p", help="project id or name"
|
|
117
|
+
),
|
|
118
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
119
|
+
confirm: bool = typer.Option(
|
|
120
|
+
False, "--confirm", "-y", "--yes", help="confirm deletion"
|
|
121
|
+
),
|
|
122
|
+
) -> None:
|
|
123
|
+
if not confirm:
|
|
124
|
+
typer.confirm(f"delete {project} {mission}", abort=True)
|
|
125
|
+
|
|
126
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
127
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
128
|
+
|
|
129
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
130
|
+
mission_query = MissionQuery(
|
|
131
|
+
ids=mission_ids,
|
|
132
|
+
patterns=mission_patterns,
|
|
133
|
+
project_query=project_query,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
client = AuthenticatedClient()
|
|
137
|
+
mission_parsed = get_mission(client, mission_query)
|
|
138
|
+
kleinkram.core.delete_mission(client=client, mission_id=mission_parsed.id)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@mission_typer.command(help=NOT_IMPLEMENTED_YET)
|
|
142
|
+
def prune(
|
|
143
|
+
project: Optional[str] = typer.Option(
|
|
144
|
+
None, "--project", "-p", help="project id or name"
|
|
145
|
+
),
|
|
146
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
147
|
+
) -> None:
|
|
148
|
+
"""\
|
|
149
|
+
delete files with bad file states, e.g. missing not uploaded corrupted etc.
|
|
150
|
+
TODO: open for suggestions what this should do
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
raise NotImplementedError("Not implemented yet")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
import kleinkram.api.routes
|
|
8
|
+
import kleinkram.core
|
|
9
|
+
from kleinkram.api.client import AuthenticatedClient
|
|
10
|
+
from kleinkram.api.query import ProjectQuery
|
|
11
|
+
from kleinkram.api.routes import get_project
|
|
12
|
+
from kleinkram.config import get_shared_state
|
|
13
|
+
from kleinkram.printing import print_project_info
|
|
14
|
+
from kleinkram.utils import split_args
|
|
15
|
+
|
|
16
|
+
project_typer = typer.Typer(
|
|
17
|
+
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
NOT_IMPLEMENTED_YET = """\
|
|
22
|
+
Not implemented yet, open an issue if you want specific functionality
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
CREATE_HELP = "create a project"
|
|
26
|
+
INFO_HELP = "get information about a project"
|
|
27
|
+
UPDATE_HELP = "update a project"
|
|
28
|
+
DELETE_HELP = "delete a project"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@project_typer.command(help=CREATE_HELP)
|
|
32
|
+
def create(
|
|
33
|
+
project: str = typer.Option(..., "--project", "-p", help="project name"),
|
|
34
|
+
description: str = typer.Option(
|
|
35
|
+
..., "--description", "-d", help="project description"
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
client = AuthenticatedClient()
|
|
39
|
+
project_id = kleinkram.api.routes._create_project(client, project, description)
|
|
40
|
+
|
|
41
|
+
project_parsed = get_project(client, ProjectQuery(ids=[project_id]))
|
|
42
|
+
print_project_info(project_parsed, pprint=get_shared_state().verbose)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@project_typer.command(help=INFO_HELP)
|
|
46
|
+
def info(
|
|
47
|
+
project: str = typer.Option(..., "--project", "-p", help="project id or name")
|
|
48
|
+
) -> None:
|
|
49
|
+
project_ids, project_patterns = split_args([project])
|
|
50
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
51
|
+
|
|
52
|
+
client = AuthenticatedClient()
|
|
53
|
+
project_parsed = get_project(client=client, query=project_query)
|
|
54
|
+
print_project_info(project_parsed, pprint=get_shared_state().verbose)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@project_typer.command(help=UPDATE_HELP)
|
|
58
|
+
def update(
|
|
59
|
+
project: str = typer.Option(..., "--project", "-p", help="project id or name"),
|
|
60
|
+
description: Optional[str] = typer.Option(
|
|
61
|
+
None, "--description", "-d", help="project description"
|
|
62
|
+
),
|
|
63
|
+
new_name: Optional[str] = typer.Option(
|
|
64
|
+
None, "--new-name", "-n", "--name", help="new project name"
|
|
65
|
+
),
|
|
66
|
+
) -> None:
|
|
67
|
+
if description is None and new_name is None:
|
|
68
|
+
raise typer.BadParameter(
|
|
69
|
+
"nothing to update, provide --description or --new-name"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
project_ids, project_patterns = split_args([project])
|
|
73
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
74
|
+
|
|
75
|
+
client = AuthenticatedClient()
|
|
76
|
+
project_id = get_project(client=client, query=project_query).id
|
|
77
|
+
kleinkram.core.update_project(
|
|
78
|
+
client=client, project_id=project_id, description=description, new_name=new_name
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
project_parsed = get_project(client, ProjectQuery(ids=[project_id]))
|
|
82
|
+
print_project_info(project_parsed, pprint=get_shared_state().verbose)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@project_typer.command(help=DELETE_HELP)
|
|
86
|
+
def delete(
|
|
87
|
+
project: str = typer.Option(..., "--project", "-p", help="project id or name")
|
|
88
|
+
) -> None:
|
|
89
|
+
project_ids, project_patterns = split_args([project])
|
|
90
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
91
|
+
|
|
92
|
+
client = AuthenticatedClient()
|
|
93
|
+
project_id = get_project(client=client, query=project_query).id
|
|
94
|
+
kleinkram.core.delete_project(client=client, project_id=project_id)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@project_typer.command(help=NOT_IMPLEMENTED_YET)
|
|
98
|
+
def prune() -> None:
|
|
99
|
+
raise NotImplementedError(NOT_IMPLEMENTED_YET)
|
kleinkram/cli/_upload.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
import kleinkram.core
|
|
10
|
+
import kleinkram.utils
|
|
11
|
+
from kleinkram.api.client import AuthenticatedClient
|
|
12
|
+
from kleinkram.api.query import MissionQuery
|
|
13
|
+
from kleinkram.api.query import ProjectQuery
|
|
14
|
+
from kleinkram.config import get_shared_state
|
|
15
|
+
from kleinkram.errors import FileNameNotSupported
|
|
16
|
+
from kleinkram.errors import MissionNotFound
|
|
17
|
+
from kleinkram.utils import load_metadata
|
|
18
|
+
from kleinkram.utils import split_args
|
|
19
|
+
|
|
20
|
+
HELP = """\
|
|
21
|
+
Upload files to kleinkram.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
upload_typer = typer.Typer(
|
|
25
|
+
name="upload",
|
|
26
|
+
no_args_is_help=True,
|
|
27
|
+
invoke_without_command=True,
|
|
28
|
+
help=HELP,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@upload_typer.callback()
|
|
33
|
+
def upload(
|
|
34
|
+
files: List[str] = typer.Argument(help="files to upload"),
|
|
35
|
+
project: Optional[str] = typer.Option(
|
|
36
|
+
None, "--project", "-p", help="project id or name"
|
|
37
|
+
),
|
|
38
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
39
|
+
create: bool = typer.Option(False, help="create mission if it does not exist"),
|
|
40
|
+
metadata: Optional[str] = typer.Option(
|
|
41
|
+
None, help="path to metadata file (json or yaml)"
|
|
42
|
+
),
|
|
43
|
+
fix_filenames: bool = typer.Option(
|
|
44
|
+
False,
|
|
45
|
+
help="fix filenames before upload, this does not change the filenames locally",
|
|
46
|
+
),
|
|
47
|
+
ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
|
|
48
|
+
) -> None:
|
|
49
|
+
# get filepaths
|
|
50
|
+
file_paths = [Path(file) for file in files]
|
|
51
|
+
|
|
52
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
53
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
54
|
+
|
|
55
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
56
|
+
mission_query = MissionQuery(
|
|
57
|
+
ids=mission_ids,
|
|
58
|
+
patterns=mission_patterns,
|
|
59
|
+
project_query=project_query,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not fix_filenames:
|
|
63
|
+
for file in file_paths:
|
|
64
|
+
if not kleinkram.utils.check_filename_is_sanatized(file.stem):
|
|
65
|
+
raise FileNameNotSupported(
|
|
66
|
+
f"Only `{''.join(kleinkram.utils.INTERNAL_ALLOWED_CHARS)}` are "
|
|
67
|
+
f"allowed in filenames and at most 50 chars: {file}. "
|
|
68
|
+
f"Consider using `--fix-filenames`"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
kleinkram.core.upload(
|
|
73
|
+
client=AuthenticatedClient(),
|
|
74
|
+
query=mission_query,
|
|
75
|
+
file_paths=file_paths,
|
|
76
|
+
create=create,
|
|
77
|
+
metadata=load_metadata(Path(metadata)) if metadata else None,
|
|
78
|
+
ignore_missing_metadata=ignore_missing_tags,
|
|
79
|
+
verbose=get_shared_state().verbose,
|
|
80
|
+
)
|
|
81
|
+
except MissionNotFound:
|
|
82
|
+
if create:
|
|
83
|
+
raise # dont change the error message
|
|
84
|
+
raise MissionNotFound("Mission not found. Use `--create` to create it.")
|
kleinkram/cli/_verify.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
import kleinkram.core
|
|
11
|
+
from kleinkram.api.client import AuthenticatedClient
|
|
12
|
+
from kleinkram.api.query import MissionQuery
|
|
13
|
+
from kleinkram.api.query import ProjectQuery
|
|
14
|
+
from kleinkram.config import get_shared_state
|
|
15
|
+
from kleinkram.printing import print_file_verification_status
|
|
16
|
+
from kleinkram.utils import split_args
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HELP = """\
|
|
22
|
+
Verify if files were uploaded correctly.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
verify_typer = typer.Typer(name="verify", invoke_without_command=True, help=HELP)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@verify_typer.callback()
|
|
29
|
+
def verify(
|
|
30
|
+
files: List[str] = typer.Argument(help="files to upload"),
|
|
31
|
+
project: Optional[str] = typer.Option(
|
|
32
|
+
None, "--project", "-p", help="project id or name"
|
|
33
|
+
),
|
|
34
|
+
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
35
|
+
skip_hash: bool = typer.Option(False, help="skip hash check"),
|
|
36
|
+
) -> None:
|
|
37
|
+
# get all filepaths
|
|
38
|
+
file_paths = [Path(file) for file in files]
|
|
39
|
+
|
|
40
|
+
# get mission query
|
|
41
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
42
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
43
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
44
|
+
mission_query = MissionQuery(
|
|
45
|
+
ids=mission_ids, patterns=mission_patterns, project_query=project_query
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
verbose = get_shared_state().verbose
|
|
49
|
+
file_status = kleinkram.core.verify(
|
|
50
|
+
client=AuthenticatedClient(),
|
|
51
|
+
query=mission_query,
|
|
52
|
+
file_paths=file_paths,
|
|
53
|
+
skip_hash=skip_hash,
|
|
54
|
+
verbose=verbose,
|
|
55
|
+
)
|
|
56
|
+
print_file_verification_status(file_status, pprint=verbose)
|
kleinkram/{app.py → cli/app.py}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
import time
|
|
6
7
|
from enum import Enum
|
|
@@ -10,28 +11,40 @@ from typing import Optional
|
|
|
10
11
|
|
|
11
12
|
import typer
|
|
12
13
|
from click import Context
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from typer.core import TyperGroup
|
|
16
|
+
|
|
13
17
|
from kleinkram._version import __version__
|
|
14
18
|
from kleinkram.api.client import AuthenticatedClient
|
|
15
19
|
from kleinkram.api.routes import _claim_admin
|
|
16
20
|
from kleinkram.api.routes import _get_api_version
|
|
17
21
|
from kleinkram.auth import login_flow
|
|
18
|
-
from kleinkram.
|
|
19
|
-
from kleinkram.
|
|
20
|
-
from kleinkram.
|
|
21
|
-
from kleinkram.
|
|
22
|
-
from kleinkram.
|
|
23
|
-
from kleinkram.
|
|
24
|
-
from kleinkram.
|
|
22
|
+
from kleinkram.cli._download import download_typer
|
|
23
|
+
from kleinkram.cli._endpoint import endpoint_typer
|
|
24
|
+
from kleinkram.cli._list import list_typer
|
|
25
|
+
from kleinkram.cli._mission import mission_typer
|
|
26
|
+
from kleinkram.cli._project import project_typer
|
|
27
|
+
from kleinkram.cli._upload import upload_typer
|
|
28
|
+
from kleinkram.cli._verify import verify_typer
|
|
29
|
+
from kleinkram.cli.error_handling import ErrorHandledTyper
|
|
30
|
+
from kleinkram.cli.error_handling import display_error
|
|
25
31
|
from kleinkram.config import Config
|
|
32
|
+
from kleinkram.config import check_config_compatibility
|
|
33
|
+
from kleinkram.config import get_config
|
|
26
34
|
from kleinkram.config import get_shared_state
|
|
27
|
-
from kleinkram.
|
|
35
|
+
from kleinkram.config import save_config
|
|
28
36
|
from kleinkram.errors import InvalidCLIVersion
|
|
29
37
|
from kleinkram.utils import format_traceback
|
|
30
38
|
from kleinkram.utils import get_supported_api_version
|
|
31
|
-
from rich.console import Console
|
|
32
|
-
from typer.core import TyperGroup
|
|
33
39
|
|
|
34
|
-
|
|
40
|
+
# slightly cursed lambdas so that linters don't complain about unreachable code
|
|
41
|
+
if (lambda: os.name)() == "posix":
|
|
42
|
+
LOG_DIR = Path().home() / ".local" / "state" / "kleinkram"
|
|
43
|
+
elif (lambda: os.name)() == "nt":
|
|
44
|
+
LOG_DIR = Path().home() / "AppData" / "Local" / "kleinkram"
|
|
45
|
+
else:
|
|
46
|
+
raise OSError(f"Unsupported OS {os.name}")
|
|
47
|
+
|
|
35
48
|
LOG_FILE = LOG_DIR / f"{time.time_ns()}.log"
|
|
36
49
|
LOG_FORMAT = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
|
|
37
50
|
|
|
@@ -57,6 +70,15 @@ class LogLevel(str, Enum):
|
|
|
57
70
|
CRITICAL = "CRITICAL"
|
|
58
71
|
|
|
59
72
|
|
|
73
|
+
LOG_LEVEL_MAP = {
|
|
74
|
+
LogLevel.DEBUG: logging.DEBUG,
|
|
75
|
+
LogLevel.INFO: logging.INFO,
|
|
76
|
+
LogLevel.WARNING: logging.WARNING,
|
|
77
|
+
LogLevel.ERROR: logging.ERROR,
|
|
78
|
+
LogLevel.CRITICAL: logging.CRITICAL,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
60
82
|
class CommandTypes(str, Enum):
|
|
61
83
|
AUTH = "Authentication Commands"
|
|
62
84
|
CORE = "Core Commands"
|
|
@@ -88,9 +110,12 @@ app.add_typer(project_typer, name="project", rich_help_panel=CommandTypes.CRUD)
|
|
|
88
110
|
# attach error handler to app
|
|
89
111
|
@app.error_handler(Exception)
|
|
90
112
|
def base_handler(exc: Exception) -> int:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
shared_state = get_shared_state()
|
|
114
|
+
|
|
115
|
+
display_error(exc=exc, verbose=shared_state.verbose)
|
|
116
|
+
logger.error(format_traceback(exc))
|
|
117
|
+
|
|
118
|
+
if not shared_state.debug:
|
|
94
119
|
return 1
|
|
95
120
|
raise exc
|
|
96
121
|
|
|
@@ -105,8 +130,12 @@ def login(
|
|
|
105
130
|
|
|
106
131
|
@app.command(rich_help_panel=CommandTypes.AUTH)
|
|
107
132
|
def logout(all: bool = typer.Option(False, help="logout on all enpoints")) -> None:
|
|
108
|
-
config =
|
|
109
|
-
|
|
133
|
+
config = get_config()
|
|
134
|
+
if all:
|
|
135
|
+
config.endpoint_credentials.clear()
|
|
136
|
+
else:
|
|
137
|
+
config.endpoint_credentials.pop(config.selected_endpoint, None)
|
|
138
|
+
save_config(config)
|
|
110
139
|
|
|
111
140
|
|
|
112
141
|
@app.command(hidden=True)
|
|
@@ -147,21 +176,24 @@ def cli(
|
|
|
147
176
|
),
|
|
148
177
|
log_level: Optional[LogLevel] = typer.Option(None, help="Set log level."),
|
|
149
178
|
):
|
|
179
|
+
if not check_config_compatibility():
|
|
180
|
+
typer.confirm("found incompatible config file, overwrite?", abort=True)
|
|
181
|
+
save_config(Config())
|
|
182
|
+
|
|
150
183
|
_ = version # suppress unused variable warning
|
|
151
184
|
shared_state = get_shared_state()
|
|
152
185
|
shared_state.verbose = verbose
|
|
153
186
|
shared_state.debug = debug
|
|
154
187
|
|
|
155
|
-
if shared_state.debug:
|
|
188
|
+
if shared_state.debug and log_level is None:
|
|
156
189
|
log_level = LogLevel.DEBUG
|
|
190
|
+
if log_level is None:
|
|
191
|
+
log_level = LogLevel.WARNING
|
|
157
192
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
level =
|
|
161
|
-
|
|
162
|
-
else:
|
|
163
|
-
logging.disable(logging.CRITICAL)
|
|
164
|
-
|
|
193
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
logging.basicConfig(
|
|
195
|
+
level=LOG_LEVEL_MAP[log_level], filename=LOG_FILE, format=LOG_FORMAT
|
|
196
|
+
)
|
|
165
197
|
logger.info(f"CLI version: {__version__}")
|
|
166
198
|
|
|
167
199
|
try:
|
|
@@ -170,7 +202,7 @@ def cli(
|
|
|
170
202
|
logger.error(format_traceback(e))
|
|
171
203
|
raise
|
|
172
204
|
except Exception:
|
|
173
|
-
err =
|
|
205
|
+
err = "failed to check version compatibility"
|
|
174
206
|
Console(file=sys.stderr).print(
|
|
175
207
|
err, style="yellow" if shared_state.verbose else None
|
|
176
208
|
)
|