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
tests/test_query.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from uuid import uuid4
4
+
5
+ import pytest
6
+
7
+ from kleinkram.api.query import InvalidMissionQuery
8
+ from kleinkram.api.query import InvalidProjectQuery
9
+ from kleinkram.api.query import MissionQuery
10
+ from kleinkram.api.query import ProjectQuery
11
+ from kleinkram.api.query import check_mission_query_is_creatable
12
+ from kleinkram.api.query import check_project_query_is_creatable
13
+ from kleinkram.api.query import mission_query_is_unique
14
+ from kleinkram.api.query import project_query_is_unique
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ "query, expected",
19
+ [
20
+ pytest.param(MissionQuery(), False, id="match all"),
21
+ pytest.param(MissionQuery(patterns=["*"]), False, id="mission name match all"),
22
+ pytest.param(
23
+ MissionQuery(patterns=["test"]),
24
+ False,
25
+ id="mission name without project",
26
+ ),
27
+ pytest.param(
28
+ MissionQuery(patterns=["test"], project_query=ProjectQuery()),
29
+ False,
30
+ id="mission name with non-unique project",
31
+ ),
32
+ pytest.param(
33
+ MissionQuery(
34
+ patterns=["test"],
35
+ project_query=ProjectQuery(ids=[uuid4()]),
36
+ ),
37
+ True,
38
+ id="mission name with unique project",
39
+ ),
40
+ pytest.param(
41
+ MissionQuery(ids=[uuid4()]),
42
+ True,
43
+ id="mission by id",
44
+ ),
45
+ pytest.param(
46
+ MissionQuery(ids=[uuid4(), uuid4()]),
47
+ False,
48
+ id="multiple mission ids",
49
+ ),
50
+ ],
51
+ )
52
+ def test_mission_query_is_unique(query, expected):
53
+ assert mission_query_is_unique(query) == expected
54
+
55
+
56
+ @pytest.mark.parametrize(
57
+ "query, expected",
58
+ [
59
+ pytest.param(ProjectQuery(), False, id="match all"),
60
+ pytest.param(ProjectQuery(patterns=["*"]), False, id="project name match all"),
61
+ pytest.param(
62
+ ProjectQuery(patterns=["test"]),
63
+ True,
64
+ id="project name",
65
+ ),
66
+ pytest.param(
67
+ ProjectQuery(ids=[uuid4()]),
68
+ True,
69
+ id="project by id",
70
+ ),
71
+ pytest.param(
72
+ ProjectQuery(ids=[uuid4(), uuid4()]),
73
+ False,
74
+ id="multiple project ids",
75
+ ),
76
+ ],
77
+ )
78
+ def test_project_query_is_unique(query, expected):
79
+ assert project_query_is_unique(query) == expected
80
+
81
+
82
+ @pytest.mark.parametrize(
83
+ "query, valid",
84
+ [
85
+ pytest.param(
86
+ MissionQuery(patterns=["test"], project_query=ProjectQuery()),
87
+ False,
88
+ id="non-unique project",
89
+ ),
90
+ pytest.param(
91
+ MissionQuery(
92
+ patterns=["test"],
93
+ project_query=ProjectQuery(ids=[uuid4()]),
94
+ ),
95
+ True,
96
+ id="valid query",
97
+ ),
98
+ pytest.param(
99
+ MissionQuery(ids=[uuid4()]),
100
+ False,
101
+ id="mission by id",
102
+ ),
103
+ ],
104
+ )
105
+ def test_check_mission_query_is_createable(query, valid):
106
+ if not valid:
107
+ with pytest.raises(InvalidMissionQuery):
108
+ check_mission_query_is_creatable(query)
109
+ else:
110
+ check_mission_query_is_creatable(query)
111
+
112
+
113
+ @pytest.mark.parametrize(
114
+ "query, valid",
115
+ [
116
+ pytest.param(
117
+ ProjectQuery(patterns=["test"]),
118
+ True,
119
+ id="project name",
120
+ ),
121
+ pytest.param(
122
+ ProjectQuery(ids=[uuid4()]),
123
+ False,
124
+ id="project by id",
125
+ ),
126
+ pytest.param(
127
+ ProjectQuery(ids=[uuid4(), uuid4()]),
128
+ False,
129
+ id="multiple project ids",
130
+ ),
131
+ ],
132
+ )
133
+ def test_check_project_query_is_creatable(query, valid):
134
+ if not valid:
135
+ with pytest.raises(InvalidProjectQuery):
136
+ check_project_query_is_creatable(query)
137
+ else:
138
+ check_project_query_is_creatable(query)
tests/test_utils.py CHANGED
@@ -5,15 +5,19 @@ from tempfile import TemporaryDirectory
5
5
  from uuid import uuid4
6
6
 
7
7
  import pytest
8
+
8
9
  from kleinkram.errors import FileTypeNotSupported
9
10
  from kleinkram.utils import b64_md5
10
11
  from kleinkram.utils import check_file_paths
11
- from kleinkram.utils import filtered_by_patterns
12
+ from kleinkram.utils import check_filename_is_sanatized
12
13
  from kleinkram.utils import get_filename
13
14
  from kleinkram.utils import get_filename_map
14
15
  from kleinkram.utils import is_valid_uuid4
16
+ from kleinkram.utils import parse_path_like
17
+ from kleinkram.utils import parse_uuid_like
18
+ from kleinkram.utils import singleton_list
15
19
  from kleinkram.utils import split_args
16
- from kleinkram.utils import to_name_or_uuid
20
+ from kleinkram.utils import upper_camel_case_to_words
17
21
 
18
22
 
19
23
  def test_split_args():
@@ -53,23 +57,14 @@ def test_check_file_paths():
53
57
  assert check_file_paths([exists_bag, exits_mcap]) is None
54
58
 
55
59
 
56
- @pytest.mark.parametrize(
57
- "names, patterns, expected",
58
- [
59
- pytest.param(["a.bag", "b.mcap"], ["*.bag"], ["a.bag"], id="one pattern"),
60
- pytest.param(["a", "b", "c"], ["*"], ["a", "b", "c"], id="match all"),
61
- pytest.param(["a", "b", "c"], ["*.bag"], [], id="no match"),
62
- pytest.param(
63
- ["a.bag", "b.mcap"],
64
- ["*.bag", "*.mcap"],
65
- ["a.bag", "b.mcap"],
66
- id="all match",
67
- ),
68
- pytest.param(["a", "b", "c"], ["a", "b"], ["a", "b"], id="full name match"),
69
- ],
70
- )
71
- def test_filtered_by_patterns(names, patterns, expected):
72
- assert filtered_by_patterns(names, patterns) == expected
60
+ def test_check_filename_is_sanatized():
61
+ valid = "t_-est"
62
+ invalid = "test%"
63
+ too_long = "a" * 100
64
+
65
+ assert check_filename_is_sanatized(valid)
66
+ assert not check_filename_is_sanatized(invalid)
67
+ assert not check_filename_is_sanatized(too_long)
73
68
 
74
69
 
75
70
  def test_is_valid_uuid4():
@@ -119,9 +114,36 @@ def test_b64_md5():
119
114
  assert b64_md5(file) == "XrY7u+Ae7tCTyyK7j1rNww=="
120
115
 
121
116
 
122
- def test_to_name_or_uuid():
123
- id_ = uuid4()
124
- not_id = "not an id"
117
+ def test_singleton_list() -> None:
118
+ assert [] == singleton_list(None)
119
+ assert [1] == singleton_list(1)
120
+ assert [[1]] == singleton_list([1])
121
+ assert [True] == singleton_list(True)
122
+
123
+ ob = object()
124
+ assert [ob] == singleton_list(ob)
125
+
126
+
127
+ def test_parse_uuid_like() -> None:
128
+ _id = uuid4()
129
+ assert parse_uuid_like(str(_id)) == _id
130
+ assert parse_uuid_like(_id) == _id
131
+
132
+ with pytest.raises(ValueError):
133
+ parse_uuid_like("invalid")
134
+
135
+
136
+ def test_parse_path_like() -> None:
137
+ assert parse_path_like("test") == Path("test")
138
+ assert parse_path_like(Path("test")) == Path("test")
139
+
125
140
 
126
- assert to_name_or_uuid(str(id_)) == id_
127
- assert to_name_or_uuid(not_id) == not_id
141
+ def test_upper_camel_case_to_words() -> None:
142
+ assert upper_camel_case_to_words("HelloWorld") == ["Hello", "World"]
143
+ assert upper_camel_case_to_words("HelloWorldAgain") == ["Hello", "World", "Again"]
144
+ assert upper_camel_case_to_words("Hello") == ["Hello"]
145
+ assert upper_camel_case_to_words("hello") == ["hello"]
146
+ assert upper_camel_case_to_words("") == []
147
+ assert upper_camel_case_to_words("not_camel_case") == ["not_camel_case"]
148
+ assert upper_camel_case_to_words("*#?-_") == ["*#?-_"]
149
+ assert upper_camel_case_to_words("helloWorld") == ["hello", "World"]
tests/test_wrappers.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ from uuid import uuid4
4
+
5
+ from kleinkram.api.query import FileQuery
6
+ from kleinkram.api.query import MissionQuery
7
+ from kleinkram.api.query import ProjectQuery
8
+ from kleinkram.wrappers import _args_to_file_query
9
+ from kleinkram.wrappers import _args_to_mission_query
10
+ from kleinkram.wrappers import _args_to_project_query
11
+
12
+
13
+ def test_args_to_project_query() -> None:
14
+ assert _args_to_project_query() == ProjectQuery()
15
+ assert _args_to_project_query(project_names=["test"]) == ProjectQuery(
16
+ patterns=["test"]
17
+ )
18
+
19
+ _id = uuid4()
20
+ assert _args_to_project_query(project_ids=[_id]) == ProjectQuery(ids=[_id])
21
+ assert _args_to_project_query(
22
+ project_names=["test"], project_ids=[_id]
23
+ ) == ProjectQuery(patterns=["test"], ids=[_id])
24
+ assert _args_to_project_query(project_ids=[str(_id)]) == ProjectQuery(ids=[_id])
25
+
26
+
27
+ def test_args_to_mission_query() -> None:
28
+ assert _args_to_mission_query() == MissionQuery()
29
+ assert _args_to_mission_query(mission_names=["test"]) == MissionQuery(
30
+ patterns=["test"]
31
+ )
32
+
33
+ _id = uuid4()
34
+ assert _args_to_mission_query(mission_ids=[_id]) == MissionQuery(ids=[_id])
35
+ assert _args_to_mission_query(
36
+ mission_names=["test"], mission_ids=[_id]
37
+ ) == MissionQuery(patterns=["test"], ids=[_id])
38
+ assert _args_to_mission_query(mission_ids=[str(_id)]) == MissionQuery(ids=[_id])
39
+
40
+ assert _args_to_mission_query(project_names=["test"]) == MissionQuery(
41
+ project_query=ProjectQuery(patterns=["test"])
42
+ )
43
+ assert _args_to_mission_query(project_ids=[_id]) == MissionQuery(
44
+ project_query=ProjectQuery(ids=[_id])
45
+ )
46
+
47
+
48
+ def test_args_to_file_query() -> None:
49
+ assert _args_to_file_query() == FileQuery()
50
+ assert _args_to_file_query(file_names=["test"]) == FileQuery(patterns=["test"])
51
+
52
+ _id = uuid4()
53
+ assert _args_to_file_query(file_ids=[_id]) == FileQuery(ids=[_id])
54
+ assert _args_to_file_query(file_names=["test"], file_ids=[_id]) == FileQuery(
55
+ patterns=["test"], ids=[_id]
56
+ )
57
+ assert _args_to_file_query(file_ids=[str(_id)]) == FileQuery(ids=[_id])
58
+
59
+ assert _args_to_file_query(mission_names=["test"]) == FileQuery(
60
+ mission_query=MissionQuery(patterns=["test"])
61
+ )
62
+ assert _args_to_file_query(mission_ids=[_id]) == FileQuery(
63
+ mission_query=MissionQuery(ids=[_id])
64
+ )
65
+
66
+ assert _args_to_file_query(project_names=["test"]) == FileQuery(
67
+ mission_query=MissionQuery(project_query=ProjectQuery(patterns=["test"]))
68
+ )
69
+ assert _args_to_file_query(project_ids=[_id]) == FileQuery(
70
+ mission_query=MissionQuery(project_query=ProjectQuery(ids=[_id]))
71
+ )
kleinkram/api/parsing.py DELETED
@@ -1,86 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
- from typing import Dict
5
- from typing import Optional
6
- from uuid import UUID
7
-
8
- from kleinkram.errors import ParsingError
9
- from kleinkram.models import File
10
- from kleinkram.models import FileState
11
- from kleinkram.models import Mission
12
- from kleinkram.models import Project
13
-
14
- __all__ = [
15
- "_parse_project",
16
- "_parse_mission",
17
- "_parse_file",
18
- ]
19
-
20
-
21
- def _parse_project(project: Dict[str, Any]) -> Project:
22
- try:
23
- project_id = UUID(project["uuid"], version=4)
24
- project_name = project["name"]
25
- project_description = project["description"]
26
-
27
- parsed = Project(
28
- id=project_id, name=project_name, description=project_description
29
- )
30
- except Exception:
31
- raise ParsingError(f"error parsing project: {project}")
32
- return parsed
33
-
34
-
35
- def _parse_mission(
36
- mission: Dict[str, Any], project: Optional[Project] = None
37
- ) -> Mission:
38
- try:
39
- mission_id = UUID(mission["uuid"], version=4)
40
- mission_name = mission["name"]
41
-
42
- project_id = (
43
- project.id if project else UUID(mission["project"]["uuid"], version=4)
44
- )
45
- project_name = project.name if project else mission["project"]["name"]
46
-
47
- parsed = Mission(
48
- id=mission_id,
49
- name=mission_name,
50
- project_id=project_id,
51
- project_name=project_name,
52
- )
53
- except Exception:
54
- raise ParsingError(f"error parsing mission: {mission}")
55
- return parsed
56
-
57
-
58
- def _parse_file(file: Dict[str, Any], mission: Optional[Mission] = None) -> File:
59
- try:
60
- filename = file["filename"]
61
- file_id = UUID(file["uuid"], version=4)
62
- file_size = file["size"]
63
- file_hash = file["hash"]
64
-
65
- project_id = (
66
- mission.project_id if mission else UUID(file["project"]["uuid"], version=4)
67
- )
68
- project_name = mission.project_name if mission else file["project"]["name"]
69
-
70
- mission_id = mission.id if mission else UUID(file["mission"]["uuid"], version=4)
71
- mission_name = mission.name if mission else file["mission"]["name"]
72
-
73
- parsed = File(
74
- id=file_id,
75
- name=filename,
76
- size=file_size,
77
- hash=file_hash,
78
- project_id=project_id,
79
- project_name=project_name,
80
- mission_id=mission_id,
81
- mission_name=mission_name,
82
- state=FileState(file["state"]),
83
- )
84
- except Exception:
85
- raise ParsingError(f"error parsing file: {file}")
86
- return parsed
@@ -1 +0,0 @@
1
- from __future__ import annotations
@@ -1,62 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
-
5
- import typer
6
- from kleinkram.auth import Config
7
-
8
- HELP = """\
9
- Get or set the current endpoint.
10
-
11
- The endpoint is used to determine the API server to connect to\
12
- (default is the API server of https://datasets.leggedrobotics.com).
13
- """
14
-
15
- endpoint_typer = typer.Typer(
16
- name="endpoint",
17
- help=HELP,
18
- no_args_is_help=True,
19
- context_settings={"help_option_names": ["-h", "--help"]},
20
- )
21
-
22
-
23
- @endpoint_typer.command("set")
24
- def set_endpoint(endpoint: str = typer.Argument(None, help="API endpoint to use")):
25
- """
26
- Use this command to switch between different API endpoints.\n
27
- Standard endpoints are:\n
28
- - http://localhost:3000\n
29
- - https://api.datasets.leggedrobotics.com\n
30
- - https://api.datasets.dev.leggedrobotics.com
31
- """
32
-
33
- if not endpoint:
34
- raise ValueError("No endpoint provided.")
35
-
36
- tokenfile = Config()
37
- tokenfile.endpoint = endpoint
38
- tokenfile.save()
39
-
40
- print(f"Endpoint set to: {endpoint}")
41
- if tokenfile.endpoint not in tokenfile.credentials:
42
- print("\nLogin with `klein login`.")
43
-
44
-
45
- @endpoint_typer.command("list")
46
- def list_endpoints():
47
- """
48
- Get the current endpoint
49
-
50
- Also displays all endpoints with saved tokens.
51
- """
52
- config = Config()
53
- print(f"Current endpoint: {config.endpoint}\n", file=sys.stderr)
54
-
55
- if not config.credentials:
56
- print("No saved credentials found.", file=sys.stderr)
57
- return
58
-
59
- print("Found Credentials for:", file=sys.stderr)
60
- for ep in config.credentials.keys():
61
- print(" - ", file=sys.stderr, end="", flush=True)
62
- print(ep, file=sys.stdout, flush=True)
@@ -1,69 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Optional
5
-
6
- import typer
7
- from kleinkram.api.client import AuthenticatedClient
8
- from kleinkram.api.routes import _update_mission_metadata
9
- from kleinkram.errors import MissionNotFound
10
- from kleinkram.resources import get_missions_by_spec
11
- from kleinkram.resources import mission_spec_is_unique
12
- from kleinkram.resources import MissionSpec
13
- from kleinkram.resources import ProjectSpec
14
- from kleinkram.utils import load_metadata
15
- from kleinkram.utils import split_args
16
-
17
- mission_typer = typer.Typer(
18
- no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
19
- )
20
-
21
-
22
- UPDATE_HELP = """\
23
- Update a mission.
24
- """
25
-
26
- NOT_IMPLEMENTED_YET = "Not implemented yet"
27
-
28
-
29
- @mission_typer.command(help=UPDATE_HELP)
30
- def update(
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
- metadata: str = typer.Option(help="path to metadata file (json or yaml)"),
36
- ) -> None:
37
- mission_ids, mission_patterns = split_args([mission])
38
- project_ids, project_patterns = split_args([project] if project else [])
39
-
40
- project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
41
- mission_spec = MissionSpec(
42
- ids=mission_ids,
43
- patterns=mission_patterns,
44
- project_spec=project_spec,
45
- )
46
-
47
- if not mission_spec_is_unique(mission_spec):
48
- raise ValueError(f"mission spec is not unique: {mission_spec}")
49
-
50
- client = AuthenticatedClient()
51
- missions = get_missions_by_spec(client, mission_spec)
52
-
53
- if not missions:
54
- raise MissionNotFound(f"Mission {mission} does not exist")
55
- elif len(missions) > 1:
56
- raise RuntimeError(f"Multiple missions found: {missions}") # unreachable
57
-
58
- metadata_dct = load_metadata(Path(metadata))
59
- _update_mission_metadata(client, missions[0].id, metadata=metadata_dct)
60
-
61
-
62
- @mission_typer.command(help=NOT_IMPLEMENTED_YET)
63
- def create() -> None:
64
- raise NotImplementedError("Not implemented yet")
65
-
66
-
67
- @mission_typer.command(help=NOT_IMPLEMENTED_YET)
68
- def delete() -> None:
69
- raise NotImplementedError("Not implemented yet")
@@ -1,24 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import typer
4
-
5
- project_typer = typer.Typer(
6
- no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
7
- )
8
-
9
- NOT_IMPLEMENTED_YET = "Not implemented yet"
10
-
11
-
12
- @project_typer.command(help=NOT_IMPLEMENTED_YET)
13
- def update() -> None:
14
- raise NotImplementedError("Not implemented yet")
15
-
16
-
17
- @project_typer.command(help=NOT_IMPLEMENTED_YET)
18
- def create() -> None:
19
- raise NotImplementedError("Not implemented yet")
20
-
21
-
22
- @project_typer.command(help=NOT_IMPLEMENTED_YET)
23
- def delete() -> None:
24
- raise NotImplementedError("Not implemented yet")