kleinkram 0.38.1.dev20241212075157__py3-none-any.whl → 0.38.1.dev20250207122632__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kleinkram might be problematic. Click here for more details.

Files changed (58) hide show
  1. kleinkram/__init__.py +33 -2
  2. kleinkram/api/client.py +21 -16
  3. kleinkram/api/deser.py +165 -0
  4. kleinkram/api/file_transfer.py +13 -24
  5. kleinkram/api/pagination.py +56 -0
  6. kleinkram/api/query.py +111 -0
  7. kleinkram/api/routes.py +266 -97
  8. kleinkram/auth.py +21 -20
  9. kleinkram/cli/__init__.py +0 -0
  10. kleinkram/{commands/download.py → cli/_download.py} +18 -44
  11. kleinkram/cli/_endpoint.py +58 -0
  12. kleinkram/{commands/list.py → cli/_list.py} +25 -38
  13. kleinkram/cli/_mission.py +153 -0
  14. kleinkram/cli/_project.py +99 -0
  15. kleinkram/cli/_upload.py +84 -0
  16. kleinkram/cli/_verify.py +56 -0
  17. kleinkram/{app.py → cli/app.py} +57 -25
  18. kleinkram/cli/error_handling.py +67 -0
  19. kleinkram/config.py +141 -107
  20. kleinkram/core.py +251 -3
  21. kleinkram/errors.py +13 -45
  22. kleinkram/main.py +1 -1
  23. kleinkram/models.py +48 -149
  24. kleinkram/printing.py +325 -0
  25. kleinkram/py.typed +0 -0
  26. kleinkram/types.py +9 -0
  27. kleinkram/utils.py +88 -29
  28. kleinkram/wrappers.py +401 -0
  29. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/METADATA +3 -3
  30. kleinkram-0.38.1.dev20250207122632.dist-info/RECORD +49 -0
  31. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/WHEEL +1 -1
  32. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/top_level.txt +1 -0
  33. testing/__init__.py +0 -0
  34. testing/backend_fixtures.py +67 -0
  35. tests/conftest.py +7 -0
  36. tests/test_config.py +115 -0
  37. tests/test_core.py +165 -0
  38. tests/test_end_to_end.py +29 -39
  39. tests/test_error_handling.py +44 -0
  40. tests/test_fixtures.py +34 -0
  41. tests/test_printing.py +62 -0
  42. tests/test_query.py +138 -0
  43. tests/test_utils.py +46 -24
  44. tests/test_wrappers.py +71 -0
  45. kleinkram/api/parsing.py +0 -86
  46. kleinkram/commands/__init__.py +0 -1
  47. kleinkram/commands/endpoint.py +0 -62
  48. kleinkram/commands/mission.py +0 -69
  49. kleinkram/commands/project.py +0 -24
  50. kleinkram/commands/upload.py +0 -164
  51. kleinkram/commands/verify.py +0 -142
  52. kleinkram/consts.py +0 -8
  53. kleinkram/enums.py +0 -10
  54. kleinkram/resources.py +0 -158
  55. kleinkram-0.38.1.dev20241212075157.dist-info/LICENSE +0 -674
  56. kleinkram-0.38.1.dev20241212075157.dist-info/RECORD +0 -37
  57. tests/test_resources.py +0 -137
  58. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250207122632.dist-info}/entry_points.txt +0 -0
@@ -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,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.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
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.errors import ErrorHandledTyper
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
- LOG_DIR = Path() / "logs"
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
- if not get_shared_state().debug:
92
- Console(file=sys.stderr).print(f"{type(exc).__name__}: {exc}", style="red")
93
- logger.error(format_traceback(exc))
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 = Config()
109
- config.clear_credentials(all=all)
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
- 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
-
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 = ("failed to check version compatibility",)
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
  )