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.

Files changed (57) 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 +270 -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} +50 -22
  18. kleinkram/cli/error_handling.py +44 -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 +82 -27
  28. kleinkram/wrappers.py +401 -0
  29. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/METADATA +3 -3
  30. kleinkram-0.38.1.dev20250113080249.dist-info/RECORD +48 -0
  31. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
  32. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/top_level.txt +1 -0
  33. testing/__init__.py +0 -0
  34. testing/backend_fixtures.py +69 -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_fixtures.py +29 -0
  40. tests/test_printing.py +62 -0
  41. tests/test_query.py +138 -0
  42. tests/test_utils.py +34 -24
  43. tests/test_wrappers.py +71 -0
  44. kleinkram/api/parsing.py +0 -86
  45. kleinkram/commands/__init__.py +0 -1
  46. kleinkram/commands/endpoint.py +0 -62
  47. kleinkram/commands/mission.py +0 -69
  48. kleinkram/commands/project.py +0 -24
  49. kleinkram/commands/upload.py +0 -164
  50. kleinkram/commands/verify.py +0 -142
  51. kleinkram/consts.py +0 -8
  52. kleinkram/enums.py +0 -10
  53. kleinkram/resources.py +0 -158
  54. kleinkram-0.38.1.dev20241125112529.dist-info/LICENSE +0 -674
  55. kleinkram-0.38.1.dev20241125112529.dist-info/RECORD +0 -37
  56. tests/test_resources.py +0 -137
  57. {kleinkram-0.38.1.dev20241125112529.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.models import files_to_table
10
- from kleinkram.models import missions_to_table
11
- from kleinkram.models import projects_to_table
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
- project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
50
- mission_spec = MissionSpec(
51
- project_spec=project_spec,
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
- file_spec = FileSpec(
56
- mission_spec=mission_spec, patterns=file_patterns, ids=file_ids
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 = get_files_by_spec(client, file_spec)
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
- project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
80
- mission_spec = MissionSpec(
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
- project_spec=project_spec,
77
+ project_query=project_query,
84
78
  )
85
79
 
86
80
  client = AuthenticatedClient()
87
- parsed_missions = get_missions_by_spec(client, mission_spec)
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
- project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
90
+ project_query = ProjectQuery(patterns=project_patterns, ids=project_ids)
99
91
 
100
92
  client = AuthenticatedClient()
101
- parsed_projects = get_projects_by_spec(client, project_spec)
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)
@@ -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.")
@@ -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)
@@ -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.commands.download import download_typer
19
- from kleinkram.commands.endpoint import endpoint_typer
20
- from kleinkram.commands.list import list_typer
21
- from kleinkram.commands.mission import mission_typer
22
- from kleinkram.commands.project import project_typer
23
- from kleinkram.commands.upload import upload_typer
24
- from kleinkram.commands.verify import verify_typer
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.errors import ErrorHandledTyper
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
- LOG_DIR = Path() / "logs"
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 = Config()
109
- config.clear_credentials(all=all)
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
- if log_level is not None:
159
- LOG_DIR.mkdir(parents=True, exist_ok=True)
160
- level = logging.getLevelName(log_level)
161
- logging.basicConfig(level=level, filename=LOG_FILE, format=LOG_FORMAT)
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 = ("failed to check version compatibility",)
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
  )