kleinkram 0.38.1.dev20241212075157__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.dev20241212075157.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.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
- {kleinkram-0.38.1.dev20241212075157.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.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.dev20250113080249.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
at the moment the endpoint command lets you specify the api and s3 endpoints
|
|
3
|
+
eventually it will be sufficient to just specify the api endpoint and the s3 endpoint will
|
|
4
|
+
be provided by the api
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from kleinkram.config import Endpoint
|
|
15
|
+
from kleinkram.config import add_endpoint
|
|
16
|
+
from kleinkram.config import endpoint_table
|
|
17
|
+
from kleinkram.config import get_config
|
|
18
|
+
from kleinkram.config import select_endpoint
|
|
19
|
+
|
|
20
|
+
HELP = """\
|
|
21
|
+
Get or set the current endpoint.
|
|
22
|
+
|
|
23
|
+
The endpoint is used to determine the API server to connect to\
|
|
24
|
+
(default is the API server of https://datasets.leggedrobotics.com).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
endpoint_typer = typer.Typer(
|
|
28
|
+
name="endpoint",
|
|
29
|
+
help=HELP,
|
|
30
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
31
|
+
invoke_without_command=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@endpoint_typer.callback()
|
|
36
|
+
def endpoint(
|
|
37
|
+
name: Optional[str] = typer.Argument(None, help="Name of the endpoint to use"),
|
|
38
|
+
api: Optional[str] = typer.Argument(None, help="API endpoint to use"),
|
|
39
|
+
s3: Optional[str] = typer.Argument(None, help="S3 endpoint to use"),
|
|
40
|
+
) -> None:
|
|
41
|
+
config = get_config()
|
|
42
|
+
console = Console()
|
|
43
|
+
|
|
44
|
+
if not any([name, api, s3]):
|
|
45
|
+
console.print(endpoint_table(config))
|
|
46
|
+
elif name is not None and not any([api, s3]):
|
|
47
|
+
try:
|
|
48
|
+
select_endpoint(config, name)
|
|
49
|
+
except ValueError:
|
|
50
|
+
console.print(f"Endpoint {name} not found.\n", style="red")
|
|
51
|
+
console.print(endpoint_table(config))
|
|
52
|
+
elif not (name and api and s3):
|
|
53
|
+
raise typer.BadParameter(
|
|
54
|
+
"to add a new endpoint you must specify the api and s3 endpoints"
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
new_endpoint = Endpoint(name, api, s3)
|
|
58
|
+
add_endpoint(config, new_endpoint)
|
|
@@ -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,39 @@ 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
|
|
25
30
|
from kleinkram.config import Config
|
|
31
|
+
from kleinkram.config import check_config_compatibility
|
|
32
|
+
from kleinkram.config import get_config
|
|
26
33
|
from kleinkram.config import get_shared_state
|
|
27
|
-
from kleinkram.
|
|
34
|
+
from kleinkram.config import save_config
|
|
28
35
|
from kleinkram.errors import InvalidCLIVersion
|
|
29
36
|
from kleinkram.utils import format_traceback
|
|
30
37
|
from kleinkram.utils import get_supported_api_version
|
|
31
|
-
from rich.console import Console
|
|
32
|
-
from typer.core import TyperGroup
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
# slightly cursed lambdas so that linters don't complain about unreachable code
|
|
40
|
+
if (lambda: os.name)() == "posix":
|
|
41
|
+
LOG_DIR = Path().home() / ".local" / "state" / "kleinkram"
|
|
42
|
+
elif (lambda: os.name)() == "nt":
|
|
43
|
+
LOG_DIR = Path().home() / "AppData" / "Local" / "kleinkram"
|
|
44
|
+
else:
|
|
45
|
+
raise OSError(f"Unsupported OS {os.name}")
|
|
46
|
+
|
|
35
47
|
LOG_FILE = LOG_DIR / f"{time.time_ns()}.log"
|
|
36
48
|
LOG_FORMAT = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
|
|
37
49
|
|
|
@@ -57,6 +69,15 @@ class LogLevel(str, Enum):
|
|
|
57
69
|
CRITICAL = "CRITICAL"
|
|
58
70
|
|
|
59
71
|
|
|
72
|
+
LOG_LEVEL_MAP = {
|
|
73
|
+
LogLevel.DEBUG: logging.DEBUG,
|
|
74
|
+
LogLevel.INFO: logging.INFO,
|
|
75
|
+
LogLevel.WARNING: logging.WARNING,
|
|
76
|
+
LogLevel.ERROR: logging.ERROR,
|
|
77
|
+
LogLevel.CRITICAL: logging.CRITICAL,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
60
81
|
class CommandTypes(str, Enum):
|
|
61
82
|
AUTH = "Authentication Commands"
|
|
62
83
|
CORE = "Core Commands"
|
|
@@ -105,8 +126,12 @@ def login(
|
|
|
105
126
|
|
|
106
127
|
@app.command(rich_help_panel=CommandTypes.AUTH)
|
|
107
128
|
def logout(all: bool = typer.Option(False, help="logout on all enpoints")) -> None:
|
|
108
|
-
config =
|
|
109
|
-
|
|
129
|
+
config = get_config()
|
|
130
|
+
if all:
|
|
131
|
+
config.endpoint_credentials.clear()
|
|
132
|
+
else:
|
|
133
|
+
config.endpoint_credentials.pop(config.selected_endpoint, None)
|
|
134
|
+
save_config(config)
|
|
110
135
|
|
|
111
136
|
|
|
112
137
|
@app.command(hidden=True)
|
|
@@ -147,21 +172,24 @@ def cli(
|
|
|
147
172
|
),
|
|
148
173
|
log_level: Optional[LogLevel] = typer.Option(None, help="Set log level."),
|
|
149
174
|
):
|
|
175
|
+
if not check_config_compatibility():
|
|
176
|
+
typer.confirm("found incompatible config file, overwrite?", abort=True)
|
|
177
|
+
save_config(Config())
|
|
178
|
+
|
|
150
179
|
_ = version # suppress unused variable warning
|
|
151
180
|
shared_state = get_shared_state()
|
|
152
181
|
shared_state.verbose = verbose
|
|
153
182
|
shared_state.debug = debug
|
|
154
183
|
|
|
155
|
-
if shared_state.debug:
|
|
184
|
+
if shared_state.debug and log_level is None:
|
|
156
185
|
log_level = LogLevel.DEBUG
|
|
186
|
+
if log_level is None:
|
|
187
|
+
log_level = LogLevel.WARNING
|
|
157
188
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
level =
|
|
161
|
-
|
|
162
|
-
else:
|
|
163
|
-
logging.disable(logging.CRITICAL)
|
|
164
|
-
|
|
189
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
logging.basicConfig(
|
|
191
|
+
level=LOG_LEVEL_MAP[log_level], filename=LOG_FILE, format=LOG_FORMAT
|
|
192
|
+
)
|
|
165
193
|
logger.info(f"CLI version: {__version__}")
|
|
166
194
|
|
|
167
195
|
try:
|
|
@@ -170,7 +198,7 @@ def cli(
|
|
|
170
198
|
logger.error(format_traceback(e))
|
|
171
199
|
raise
|
|
172
200
|
except Exception:
|
|
173
|
-
err =
|
|
201
|
+
err = "failed to check version compatibility"
|
|
174
202
|
Console(file=sys.stderr).print(
|
|
175
203
|
err, style="yellow" if shared_state.verbose else None
|
|
176
204
|
)
|