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.

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.dev20241212075157.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.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
  32. {kleinkram-0.38.1.dev20241212075157.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.dev20241212075157.dist-info/LICENSE +0 -674
  55. kleinkram-0.38.1.dev20241212075157.dist-info/RECORD +0 -37
  56. tests/test_resources.py +0 -137
  57. {kleinkram-0.38.1.dev20241212075157.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/entry_points.txt +0 -0
@@ -1,164 +0,0 @@
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
- from kleinkram.api.client import AuthenticatedClient
9
- from kleinkram.api.file_transfer import upload_files
10
- from kleinkram.api.routes import _create_mission as _create_mission_api
11
- from kleinkram.config import get_shared_state
12
- from kleinkram.errors import MissionNotFound
13
- from kleinkram.errors import ProjectNotFound
14
- from kleinkram.models import Mission
15
- from kleinkram.resources import check_mission_spec_is_creatable
16
- from kleinkram.resources import get_missions_by_spec
17
- from kleinkram.resources import get_projects_by_spec
18
- from kleinkram.resources import InvalidMissionSpec
19
- from kleinkram.resources import mission_spec_is_unique
20
- from kleinkram.resources import MissionSpec
21
- from kleinkram.resources import ProjectSpec
22
- from kleinkram.utils import check_file_paths
23
- from kleinkram.utils import get_filename_map
24
- from kleinkram.utils import load_metadata
25
- from kleinkram.utils import split_args
26
- from rich.console import Console
27
-
28
-
29
- HELP = """\
30
- Upload files to kleinkram.
31
- """
32
-
33
- upload_typer = typer.Typer(
34
- name="upload",
35
- no_args_is_help=True,
36
- invoke_without_command=True,
37
- help=HELP,
38
- )
39
-
40
-
41
- # TODO: move this to core
42
- def _create_mission(
43
- client: AuthenticatedClient,
44
- mission_spec: MissionSpec,
45
- metadata_path: Optional[Path],
46
- ignore_missing_tags: bool,
47
- ) -> Mission:
48
- check_mission_spec_is_creatable(mission_spec)
49
- mission_name = mission_spec.patterns[0]
50
-
51
- # get the metadata
52
- metadata_dct = {}
53
- if metadata_path is not None:
54
- metadata_dct = load_metadata(metadata_path)
55
-
56
- # get project
57
- projects = get_projects_by_spec(client, mission_spec.project_spec)
58
-
59
- if not projects:
60
- raise ProjectNotFound(f"project {mission_spec.project_spec} does not exist")
61
- elif len(projects) > 1:
62
- raise AssertionError("unreachable")
63
-
64
- parsed_project = projects[0]
65
-
66
- _create_mission_api(
67
- client,
68
- parsed_project.id,
69
- mission_name,
70
- metadata=metadata_dct,
71
- ignore_missing_tags=ignore_missing_tags,
72
- )
73
-
74
- missions = get_missions_by_spec(client, mission_spec)
75
- assert len(missions) is not None, "unreachable, the ghost is back"
76
-
77
- return missions[0]
78
-
79
-
80
- @upload_typer.callback()
81
- def upload(
82
- files: List[str] = typer.Argument(help="files to upload"),
83
- project: Optional[str] = typer.Option(
84
- None, "--project", "-p", help="project id or name"
85
- ),
86
- mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
87
- create: bool = typer.Option(False, help="create mission if it does not exist"),
88
- metadata: Optional[str] = typer.Option(
89
- None, help="path to metadata file (json or yaml)"
90
- ),
91
- fix_filenames: bool = typer.Option(False, help="fix filenames"),
92
- ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
93
- ) -> None:
94
- # check files and `fix` filenames
95
- if files is None:
96
- files = []
97
-
98
- file_paths = [Path(file) for file in files]
99
- check_file_paths(file_paths)
100
- files_map = get_filename_map(file_paths)
101
-
102
- if not fix_filenames:
103
- for name, path in files_map.items():
104
- if name != path.name:
105
- raise ValueError(
106
- f"invalid filename format {path.name}, use `--fix-filenames`"
107
- )
108
-
109
- mission_ids, mission_patterns = split_args([mission])
110
- project_ids, project_patterns = split_args([project] if project else [])
111
-
112
- project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
113
- mission_spec = MissionSpec(
114
- ids=mission_ids,
115
- patterns=mission_patterns,
116
- project_spec=project_spec,
117
- )
118
-
119
- if not mission_spec_is_unique(mission_spec):
120
- raise InvalidMissionSpec(f"mission spec is not unique: {mission_spec}")
121
-
122
- client = AuthenticatedClient()
123
- missions = get_missions_by_spec(client, mission_spec)
124
-
125
- if len(missions) > 1:
126
- raise AssertionError("unreachable")
127
-
128
- if not create and not missions:
129
- raise MissionNotFound(f"mission: {mission_spec} does not exist, use `--create`")
130
-
131
- # create mission if it does not exist
132
- mission_parsed = (
133
- missions[0]
134
- if missions
135
- else _create_mission(
136
- client,
137
- mission_spec,
138
- metadata_path=Path(metadata) if metadata else None,
139
- ignore_missing_tags=ignore_missing_tags,
140
- )
141
- )
142
-
143
- console = Console()
144
- filtered_files_map = {}
145
- remote_file_names = [file.name for file in mission_parsed.files]
146
- for name, path in files_map.items():
147
- if name in remote_file_names:
148
- console.print(
149
- f"file: {name} (path: {path}) already exists in mission", style="yellow"
150
- )
151
- else:
152
- filtered_files_map[name] = path
153
-
154
- if not filtered_files_map:
155
- console.print("\nNO FILES UPLOADED", style="yellow")
156
- return
157
-
158
- upload_files(
159
- client,
160
- filtered_files_map,
161
- mission_parsed.id,
162
- n_workers=2,
163
- verbose=get_shared_state().verbose,
164
- )
@@ -1,142 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import sys
5
- from enum import Enum
6
- from pathlib import Path
7
- from typing import Dict
8
- from typing import List
9
- from typing import Optional
10
-
11
- import typer
12
- from kleinkram.api.client import AuthenticatedClient
13
- from kleinkram.config import get_shared_state
14
- from kleinkram.errors import InvalidMissionSpec
15
- from kleinkram.errors import MissionNotFound
16
- from kleinkram.models import FileState
17
- from kleinkram.resources import FileSpec
18
- from kleinkram.resources import get_files_by_spec
19
- from kleinkram.resources import get_missions_by_spec
20
- from kleinkram.resources import mission_spec_is_unique
21
- from kleinkram.resources import MissionSpec
22
- from kleinkram.resources import ProjectSpec
23
- from kleinkram.utils import b64_md5
24
- from kleinkram.utils import check_file_paths
25
- from kleinkram.utils import get_filename_map
26
- from kleinkram.utils import split_args
27
- from rich.console import Console
28
- from rich.table import Table
29
- from rich.text import Text
30
- from tqdm import tqdm
31
-
32
- logger = logging.getLogger(__name__)
33
-
34
-
35
- class FileVerificationStatus(str, Enum):
36
- UPLAODED = "uploaded"
37
- UPLOADING = "uploading"
38
- COMPUTING_HASH = "computing hash"
39
- MISSING = "missing"
40
- MISMATCHED_HASH = "hash mismatch"
41
- UNKNOWN = "unknown"
42
-
43
-
44
- FILE_STATUS_STYLES = {
45
- FileVerificationStatus.UPLAODED: "green",
46
- FileVerificationStatus.UPLOADING: "yellow",
47
- FileVerificationStatus.MISSING: "yellow",
48
- FileVerificationStatus.MISMATCHED_HASH: "red",
49
- FileVerificationStatus.UNKNOWN: "gray",
50
- FileVerificationStatus.COMPUTING_HASH: "purple",
51
- }
52
-
53
-
54
- HELP = """\
55
- Verify if files were uploaded correctly.
56
- """
57
-
58
- verify_typer = typer.Typer(name="verify", invoke_without_command=True, help=HELP)
59
-
60
-
61
- @verify_typer.callback()
62
- def verify(
63
- files: List[str] = typer.Argument(help="files to upload"),
64
- project: Optional[str] = typer.Option(
65
- None, "--project", "-p", help="project id or name"
66
- ),
67
- mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
68
- skip_hash: bool = typer.Option(False, help="skip hash check"),
69
- ) -> None:
70
- # get all filepaths
71
- if files is None:
72
- files = []
73
-
74
- file_paths = [Path(file) for file in files]
75
- check_file_paths(file_paths)
76
- files_map = get_filename_map(file_paths)
77
-
78
- # get the mission by the provided spec
79
- mission_ids, mission_patterns = split_args([mission])
80
- project_ids, project_patterns = split_args([project] if project else [])
81
-
82
- project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
83
- mission_spec = MissionSpec(
84
- ids=mission_ids, patterns=mission_patterns, project_spec=project_spec
85
- )
86
- file_spec = FileSpec(mission_spec=mission_spec)
87
-
88
- client = AuthenticatedClient()
89
-
90
- # check first that the mission even exists, the mission could be empty
91
- if not mission_spec_is_unique(mission_spec):
92
- raise InvalidMissionSpec(f"mission spec is not unique: {mission_spec}")
93
- missions = get_missions_by_spec(client, mission_spec)
94
- if len(missions) > 1:
95
- raise AssertionError("unreachable")
96
- if not missions:
97
- raise MissionNotFound(f"mission: {mission_spec} does not exist")
98
-
99
- # get all files from that mission
100
- remote_files = {file.name: file for file in get_files_by_spec(client, file_spec)}
101
-
102
- # verify files
103
- file_status: Dict[Path, FileVerificationStatus] = {}
104
- for name, file in tqdm(
105
- files_map.items(),
106
- desc="verifying files",
107
- unit="file",
108
- disable=not get_shared_state().verbose,
109
- ):
110
- if name not in remote_files:
111
- file_status[file] = FileVerificationStatus.MISSING
112
- continue
113
-
114
- remote_file = remote_files[name]
115
-
116
- if remote_file.state == FileState.UPLOADING:
117
- file_status[file] = FileVerificationStatus.UPLOADING
118
- elif remote_file.state == FileState.OK:
119
- if remote_file.hash is None:
120
- file_status[file] = FileVerificationStatus.COMPUTING_HASH
121
- elif skip_hash or remote_file.hash == b64_md5(file):
122
- file_status[file] = FileVerificationStatus.UPLAODED
123
- else:
124
- file_status[file] = FileVerificationStatus.MISMATCHED_HASH
125
- else:
126
- file_status[file] = FileVerificationStatus.UNKNOWN
127
-
128
- if get_shared_state().verbose:
129
- table = Table(title="file status")
130
- table.add_column("filename", style="cyan")
131
- table.add_column("status", style="green")
132
-
133
- for path, status in file_status.items():
134
- table.add_row(str(path), Text(status, style=FILE_STATUS_STYLES[status]))
135
-
136
- Console().print(table)
137
- else:
138
- for path, status in file_status.items():
139
- stream = (
140
- sys.stdout if status == FileVerificationStatus.UPLAODED else sys.stderr
141
- )
142
- print(path, file=stream)
kleinkram/consts.py DELETED
@@ -1,8 +0,0 @@
1
- from __future__ import annotations
2
-
3
- LOCAL_API_URL = "http://localhost:3000"
4
- LOCAL_S3_URL = "http://localhost:9000"
5
-
6
-
7
- API_URL = "http://localhost:3000"
8
- # API_URL = "https://api.datasets.leggedrobotics.com"
kleinkram/enums.py DELETED
@@ -1,10 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from enum import Enum
4
-
5
-
6
- class PermissionLevel(Enum):
7
- READ = 0
8
- CREATE = 10
9
- WRITE = 20
10
- DELETE = 30
kleinkram/resources.py DELETED
@@ -1,158 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from concurrent.futures import ThreadPoolExecutor
4
- from dataclasses import dataclass
5
- from dataclasses import field
6
- from itertools import chain
7
- from typing import List
8
- from uuid import UUID
9
-
10
- from kleinkram.api.client import AuthenticatedClient
11
- from kleinkram.api.routes import _get_files_by_mission
12
- from kleinkram.api.routes import _get_missions_by_project
13
- from kleinkram.api.routes import _get_projects
14
- from kleinkram.errors import InvalidMissionSpec
15
- from kleinkram.errors import InvalidProjectSpec
16
- from kleinkram.models import File
17
- from kleinkram.models import Mission
18
- from kleinkram.models import Project
19
- from kleinkram.utils import filtered_by_patterns
20
-
21
- MAX_PARALLEL_REQUESTS = 32
22
- SPECIAL_PATTERN_CHARS = ["*", "?", "[", "]"]
23
-
24
-
25
- @dataclass
26
- class ProjectSpec:
27
- patterns: List[str] = field(default_factory=list)
28
- ids: List[UUID] = field(default_factory=list)
29
-
30
-
31
- @dataclass
32
- class MissionSpec:
33
- patterns: List[str] = field(default_factory=list)
34
- ids: List[UUID] = field(default_factory=list)
35
- project_spec: ProjectSpec = field(default=ProjectSpec())
36
-
37
-
38
- @dataclass
39
- class FileSpec:
40
- patterns: List[str] = field(default_factory=list)
41
- ids: List[UUID] = field(default_factory=list)
42
- mission_spec: MissionSpec = field(default=MissionSpec())
43
-
44
-
45
- def check_mission_spec_is_creatable(spec: MissionSpec) -> None:
46
- if not mission_spec_is_unique(spec):
47
- raise InvalidMissionSpec(f"Mission spec is not unique: {spec}")
48
- # cant create a missing by id
49
- if spec.ids:
50
- raise InvalidMissionSpec(f"cant create mission by id: {spec}")
51
-
52
-
53
- def check_project_spec_is_creatable(spec: ProjectSpec) -> None:
54
- if not project_spec_is_unique(spec):
55
- raise InvalidProjectSpec(f"Project spec is not unique: {spec}")
56
- # cant create a missing by id
57
- if spec.ids:
58
- raise InvalidProjectSpec(f"cant create project by id: {spec}")
59
-
60
-
61
- def _pattern_is_unique(pattern: str) -> bool:
62
- for char in SPECIAL_PATTERN_CHARS:
63
- if char in pattern:
64
- return False
65
- return True
66
-
67
-
68
- def project_spec_is_unique(spec: ProjectSpec) -> bool:
69
- # a single project id is specified
70
- if len(spec.ids) == 1 and not spec.patterns:
71
- return True
72
-
73
- # a single project name is specified
74
- if len(spec.patterns) == 1 and _pattern_is_unique(spec.patterns[0]):
75
- return True
76
- return False
77
-
78
-
79
- def mission_spec_is_unique(spec: MissionSpec) -> bool:
80
- # a single mission id is specified
81
- if len(spec.ids) == 1 and not spec.patterns:
82
- return True
83
-
84
- # a single mission name a unique project spec are specified
85
- if (
86
- project_spec_is_unique(spec.project_spec)
87
- and len(spec.patterns) == 1
88
- and _pattern_is_unique(spec.patterns[0])
89
- ):
90
- return True
91
- return False
92
-
93
-
94
- def get_projects_by_spec(
95
- client: AuthenticatedClient, spec: ProjectSpec
96
- ) -> List[Project]:
97
- projects = _get_projects(client)
98
-
99
- matched_names = filtered_by_patterns(
100
- [project.name for project in projects], spec.patterns
101
- )
102
-
103
- if not spec.patterns and not spec.ids:
104
- return projects
105
-
106
- return [
107
- project
108
- for project in projects
109
- if project.name in matched_names or project.id in spec.ids
110
- ]
111
-
112
-
113
- def get_missions_by_spec(
114
- client: AuthenticatedClient, spec: MissionSpec
115
- ) -> List[Mission]:
116
- projects = get_projects_by_spec(client, spec.project_spec)
117
-
118
- with ThreadPoolExecutor(max_workers=MAX_PARALLEL_REQUESTS) as executor:
119
- missions = chain.from_iterable(
120
- executor.map(
121
- lambda project: _get_missions_by_project(client, project), projects
122
- )
123
- )
124
-
125
- missions = list(missions)
126
-
127
- if not spec.patterns and not spec.ids:
128
- return list(missions)
129
-
130
- matched_names = filtered_by_patterns(
131
- [mission.name for mission in missions], spec.patterns
132
- )
133
-
134
- filter = [
135
- mission
136
- for mission in missions
137
- if mission.name in matched_names or mission.id in spec.patterns
138
- ]
139
-
140
- return filter
141
-
142
-
143
- def get_files_by_spec(client: AuthenticatedClient, spec: FileSpec) -> List[File]:
144
- missions = get_missions_by_spec(client, spec.mission_spec)
145
-
146
- # collect files
147
- with ThreadPoolExecutor(max_workers=MAX_PARALLEL_REQUESTS) as executor:
148
- files = chain.from_iterable(
149
- executor.map(
150
- lambda mission: _get_files_by_mission(client, mission), missions
151
- )
152
- )
153
-
154
- if not spec.patterns and not spec.ids:
155
- return list(files)
156
- matched_names = filtered_by_patterns([file.name for file in files], spec.patterns)
157
-
158
- return [file for file in files if file.name in matched_names or file.id in spec.ids]