kleinkram 0.36.3.dev20241113174857__py3-none-any.whl → 0.37.0.dev20241118070559__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 (48) hide show
  1. kleinkram/__init__.py +6 -0
  2. kleinkram/__main__.py +6 -0
  3. kleinkram/_version.py +6 -0
  4. kleinkram/api/__init__.py +0 -0
  5. kleinkram/api/client.py +65 -0
  6. kleinkram/api/file_transfer.py +328 -0
  7. kleinkram/api/routes.py +460 -0
  8. kleinkram/app.py +180 -0
  9. kleinkram/auth.py +96 -0
  10. kleinkram/commands/__init__.py +1 -0
  11. kleinkram/commands/download.py +103 -0
  12. kleinkram/commands/endpoint.py +62 -0
  13. kleinkram/commands/list.py +93 -0
  14. kleinkram/commands/mission.py +57 -0
  15. kleinkram/commands/project.py +24 -0
  16. kleinkram/commands/upload.py +138 -0
  17. kleinkram/commands/verify.py +117 -0
  18. kleinkram/config.py +171 -0
  19. kleinkram/consts.py +8 -1
  20. kleinkram/core.py +14 -0
  21. kleinkram/enums.py +10 -0
  22. kleinkram/errors.py +59 -0
  23. kleinkram/main.py +6 -489
  24. kleinkram/models.py +186 -0
  25. kleinkram/utils.py +179 -0
  26. {kleinkram-0.36.3.dev20241113174857.dist-info/licenses → kleinkram-0.37.0.dev20241118070559.dist-info}/LICENSE +1 -1
  27. kleinkram-0.37.0.dev20241118070559.dist-info/METADATA +113 -0
  28. kleinkram-0.37.0.dev20241118070559.dist-info/RECORD +33 -0
  29. {kleinkram-0.36.3.dev20241113174857.dist-info → kleinkram-0.37.0.dev20241118070559.dist-info}/WHEEL +2 -1
  30. kleinkram-0.37.0.dev20241118070559.dist-info/entry_points.txt +2 -0
  31. kleinkram-0.37.0.dev20241118070559.dist-info/top_level.txt +2 -0
  32. tests/__init__.py +0 -0
  33. tests/test_utils.py +153 -0
  34. kleinkram/api_client.py +0 -63
  35. kleinkram/auth/auth.py +0 -160
  36. kleinkram/endpoint/endpoint.py +0 -58
  37. kleinkram/error_handling.py +0 -177
  38. kleinkram/file/file.py +0 -144
  39. kleinkram/helper.py +0 -272
  40. kleinkram/mission/mission.py +0 -310
  41. kleinkram/project/project.py +0 -138
  42. kleinkram/queue/queue.py +0 -8
  43. kleinkram/tag/tag.py +0 -71
  44. kleinkram/topic/topic.py +0 -55
  45. kleinkram/user/user.py +0 -75
  46. kleinkram-0.36.3.dev20241113174857.dist-info/METADATA +0 -24
  47. kleinkram-0.36.3.dev20241113174857.dist-info/RECORD +0 -20
  48. kleinkram-0.36.3.dev20241113174857.dist-info/entry_points.txt +0 -2
kleinkram/utils.py ADDED
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import glob
5
+ import hashlib
6
+ import os
7
+ import string
8
+ from hashlib import md5
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from typing import Dict
12
+ from typing import List
13
+ from typing import NamedTuple
14
+ from typing import Optional
15
+ from typing import Sequence
16
+ from typing import Tuple
17
+ from typing import Union
18
+ from uuid import UUID
19
+
20
+ import yaml
21
+ from kleinkram._version import __version__
22
+ from kleinkram.errors import FileTypeNotSupported
23
+ from kleinkram.errors import InvalidFileSpec
24
+ from kleinkram.errors import InvalidMissionSpec
25
+ from kleinkram.models import FilesById
26
+ from kleinkram.models import FilesByMission
27
+ from kleinkram.models import MissionById
28
+ from kleinkram.models import MissionByName
29
+ from rich.console import Console
30
+
31
+
32
+ INTERNAL_ALLOWED_CHARS = string.ascii_letters + string.digits + "_" + "-"
33
+
34
+
35
+ def check_file_paths(files: Sequence[Path]) -> None:
36
+ for file in files:
37
+ if file.is_dir():
38
+ raise FileNotFoundError(f"{file} is a directory and not a file")
39
+ if not file.exists():
40
+ raise FileNotFoundError(f"{file} does not exist")
41
+ if file.suffix not in (".bag", ".mcap"):
42
+ raise FileTypeNotSupported(
43
+ f"only `.bag` or `.mcap` files are supported: {file}"
44
+ )
45
+
46
+
47
+ def raw_rich(*objects: Any, **kwargs: Any) -> str:
48
+ """\
49
+ accepts any object that Console.print can print
50
+ returns the raw string output
51
+ """
52
+
53
+ console = Console()
54
+
55
+ with console.capture() as capture:
56
+ console.print(*objects, **kwargs, end="")
57
+
58
+ return capture.get()
59
+
60
+
61
+ def is_valid_uuid4(uuid: str) -> bool:
62
+ try:
63
+ UUID(uuid, version=4)
64
+ return True
65
+ except ValueError:
66
+ return False
67
+
68
+
69
+ def get_filename(path: Path) -> str:
70
+ """\
71
+ takes a path and returns a sanitized filename
72
+
73
+ the format for this internal filename is:
74
+ - replace all disallowed characters with "_"
75
+ - trim to 40 chars + 10 hashed chars
76
+ - the 10 hashed chars are deterministic given the original filename
77
+ """
78
+
79
+ stem = "".join(
80
+ char if char in INTERNAL_ALLOWED_CHARS else "_" for char in path.stem
81
+ )
82
+
83
+ if len(stem) > 50:
84
+ hash = md5(path.name.encode()).hexdigest()
85
+ stem = f"{stem[:40]}{hash[:10]}"
86
+
87
+ return f"{stem}{path.suffix}"
88
+
89
+
90
+ def get_filename_map(file_paths: Sequence[Path]) -> Dict[str, Path]:
91
+ """\
92
+ takes a list of unique filepaths and returns a mapping
93
+ from the original filename to a sanitized internal filename
94
+ see `get_filename` for the internal filename format
95
+ """
96
+
97
+ if len(file_paths) != len(set(file_paths)):
98
+ raise ValueError("files paths must be unique")
99
+
100
+ internal_file_map = {}
101
+ for file in file_paths:
102
+ if file.is_dir():
103
+ raise ValueError(f"got dir {file} expected file")
104
+
105
+ internal_file_map[get_filename(file)] = file
106
+
107
+ if len(internal_file_map) != len(set(internal_file_map.values())):
108
+ raise RuntimeError("hash collision")
109
+
110
+ return internal_file_map
111
+
112
+
113
+ def b64_md5(file: Path) -> str:
114
+ hash_md5 = hashlib.md5()
115
+ with open(file, "rb") as f:
116
+ for chunk in iter(lambda: f.read(4096), b""):
117
+ hash_md5.update(chunk)
118
+ binary_digest = hash_md5.digest()
119
+ return base64.b64encode(binary_digest).decode("utf-8")
120
+
121
+
122
+ def get_valid_mission_spec(
123
+ mission: Union[str, UUID],
124
+ project: Optional[Union[str, UUID]] = None,
125
+ ) -> Union[MissionById, MissionByName]:
126
+ """\
127
+ checks if:
128
+ - atleast one is speicifed
129
+ - if project is not specified then mission must be a valid uuid4
130
+ """
131
+
132
+ if isinstance(mission, UUID):
133
+ return MissionById(id=mission)
134
+ if isinstance(mission, str) and project is not None:
135
+ return MissionByName(name=mission, project=project)
136
+ raise InvalidMissionSpec("must specify mission id or project name / id")
137
+
138
+
139
+ def get_valid_file_spec(
140
+ files: Sequence[Union[str, UUID]],
141
+ mission: Optional[Union[str, UUID]] = None,
142
+ project: Optional[Union[str, UUID]] = None,
143
+ ) -> Union[FilesById, FilesByMission]:
144
+ """\
145
+ """
146
+ if not any([project, mission, files]):
147
+ raise InvalidFileSpec("must specify `project`, `mission` or `files`")
148
+
149
+ # if only files are specified they must be valid uuid4
150
+ if project is None and mission is None:
151
+ if all(map(lambda file: isinstance(file, UUID), files)):
152
+ return FilesById(ids=files) # type: ignore
153
+ raise InvalidFileSpec("if no mission is specified files must be valid uuid4")
154
+
155
+ if mission is None:
156
+ raise InvalidMissionSpec("mission must be specified")
157
+ mission_spec = get_valid_mission_spec(mission, project)
158
+ return FilesByMission(mission=mission_spec, files=list(files))
159
+
160
+
161
+ def to_name_or_uuid(s: str) -> Union[UUID, str]:
162
+ if is_valid_uuid4(s):
163
+ return UUID(s)
164
+ return s
165
+
166
+
167
+ def load_metadata(path: Path) -> Dict[str, str]:
168
+ if not path.exists():
169
+ raise FileNotFoundError(f"metadata file not found: {path}")
170
+ try:
171
+ with path.open() as f:
172
+ return {str(k): str(v) for k, v in yaml.safe_load(f).items()}
173
+ except Exception as e:
174
+ raise ValueError(f"could not parse metadata file: {e}")
175
+
176
+
177
+ def get_supported_api_version() -> Tuple[int, int, int]:
178
+ vers = __version__.split(".")
179
+ return tuple(map(int, vers[:3])) # type: ignore
@@ -671,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you
671
671
  may consider it more useful to permit linking proprietary applications with
672
672
  the library. If this is what you want to do, use the GNU Lesser General
673
673
  Public License instead of this License. But first, please read
674
- <https://www.gnu.org/licenses/why-not-lgpl.html>.
674
+ <https://www.gnu.org/licenses/why-not-lgpl.html>.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.1
2
+ Name: kleinkram
3
+ Version: 0.37.0.dev20241118070559
4
+ Summary: give me your bags
5
+ Author: Cyrill Püntener, Dominique Garmier, Johann Schwabe
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Programming Language :: Python :: 3.8
9
+ Classifier: Programming Language :: Python :: 3.9
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: boto3
18
+ Requires-Dist: botocore
19
+ Requires-Dist: httpx
20
+ Requires-Dist: pyyaml
21
+ Requires-Dist: rich
22
+ Requires-Dist: tqdm
23
+ Requires-Dist: typer
24
+
25
+ # Kleinkram: CLI
26
+
27
+ Install the package
28
+
29
+ ```bash
30
+ pip install kleinkram
31
+ ```
32
+
33
+ Run the CLI
34
+
35
+ ```bash
36
+ klein
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ Here are some basic examples of how to use the CLI.
42
+
43
+ ### Listing Files
44
+
45
+ To list all files in a mission:
46
+
47
+ ```bash
48
+ klein list -p project-name -m mission-name
49
+ ```
50
+
51
+ ### Uploading Files
52
+
53
+ To upload all `*.bag` files in the current directory to a mission:
54
+
55
+ ```bash
56
+ klein upload -p project-name -m mission-name *.bag
57
+ ```
58
+
59
+ If you would like to create a new mission on upload use the `--create` flag.
60
+
61
+ ### Downloading Files
62
+
63
+ To download all files from a mission and save them `out`:
64
+
65
+ ```bash
66
+ klein download -p project-name -m mission-name --dest out
67
+ ```
68
+
69
+ You can additionally specify filenames or ids if you only want to download specific files.
70
+
71
+ Instead of downloading files from a specified mission you can download arbitrary files by specifying their ids:
72
+
73
+ ```bash
74
+ klein download --dest out *id1* *id2* *id3*
75
+ ```
76
+
77
+ For more information consult the [documentation](https://docs.datasets.leggedrobotics.com/usage/cli/cli-getting-started.html).
78
+
79
+ ## Development
80
+
81
+ Clone the repo
82
+
83
+ ```bash
84
+ git clone git@github.com:leggedrobotics/kleinkram.git
85
+ cd kleinkram/cli
86
+ ```
87
+
88
+ Setup the environment
89
+
90
+ ```bash
91
+ virtualenv -ppython3.8 .venv
92
+ source .venv/bin/activate
93
+ pip install -e . -r requirements.txt
94
+ ```
95
+
96
+ Install `pre-commit` hooks
97
+
98
+ ```bash
99
+ pre-commit install
100
+ ```
101
+
102
+ Run the CLI
103
+
104
+ ```bash
105
+ klein --help
106
+ ```
107
+
108
+ ### Run Tests
109
+ ```bash
110
+ pytest .
111
+ ```
112
+
113
+ You can also look in `scripts` for some scripts that might be useful for testing.
@@ -0,0 +1,33 @@
1
+ kleinkram/__init__.py,sha256=adI-FqfuuH56n8U-lJa3uZkH_saxi7dr4Hox_HBuf9A,107
2
+ kleinkram/__main__.py,sha256=B9RiZxfO4jpCmWPUHyKJ7_EoZlEG4sPpH-nz7T_YhhQ,125
3
+ kleinkram/_version.py,sha256=QYJyRTcqFcJj4qWYpqs7WcoOP6jxDMqyvxLY-cD6KcE,129
4
+ kleinkram/app.py,sha256=C5NwoFJ4FsUZwx6h5PInjq6j-NveGBgfbpHQ9Vb8Igo,5553
5
+ kleinkram/auth.py,sha256=QAwT3VWdHEv5cxNnRbZoVykYwdeaoZjtBQBBXfejFdQ,2953
6
+ kleinkram/config.py,sha256=QZWBXOCRjXa-wcSynJkCaYSE_0kU85j2zsdp-miR-eU,4447
7
+ kleinkram/consts.py,sha256=70GELDssDm-YeIbChBRTscnNRcyWRUDWWlYBjVWNLDk,205
8
+ kleinkram/core.py,sha256=XmNhg1i520w-9et8r0LSQRsVN09gJgSXfyHRV1tu2rM,227
9
+ kleinkram/enums.py,sha256=bX0H8_xkh_7DRycJYII8MTjHmjbcFW9ZJ3tzlzRhT2k,149
10
+ kleinkram/errors.py,sha256=bZ92LLweQaorsKZWWsL7BAPxFxODQ01nEeenHOHKrv0,1200
11
+ kleinkram/main.py,sha256=hmFuSnE2p-E4nZ2zDVToOsoELZhosLN0r4wsXnbmsas,168
12
+ kleinkram/models.py,sha256=dOrvK-H6cW8tPbZUbBeE6MROp17c5hp5FeULKZRpZ3s,4329
13
+ kleinkram/utils.py,sha256=72PuaARDOlrxTjh5sHS6llvZng2q78j4IpiUP1RG1qA,5335
14
+ kleinkram/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ kleinkram/api/client.py,sha256=b1mUwpu7zrXVTNjTDfW8aA0hgmsisRIlMOqQTSbd7bg,2142
16
+ kleinkram/api/file_transfer.py,sha256=qUnJMv7fNgyCvP3czHrytMdW33kvW2FGbAXf7eDtDZc,8934
17
+ kleinkram/api/routes.py,sha256=ojAB5Kfnofw6oyeAe_9VS__-YahpWGsiBWl4jjbQO10,12537
18
+ kleinkram/commands/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
19
+ kleinkram/commands/download.py,sha256=393n9kaJJJmsAJGdPTrkd-H2giNsJuihr1JD1mdF6rk,3415
20
+ kleinkram/commands/endpoint.py,sha256=VgaleKTev8Muu4OmS4tptbhrbl58u1UTAxEmoLBCQew,1642
21
+ kleinkram/commands/list.py,sha256=QEhD6mSDn_dVc9XI_5HpBrfjG93IowzFxH4rOyVHjCI,2666
22
+ kleinkram/commands/mission.py,sha256=BXnGUnOBTSHZipy50P2NtornXUAxDCdyqmGpVUMzfxc,1751
23
+ kleinkram/commands/project.py,sha256=q-kJ1YQr-Axo-Zl0qe2J7eGGgKlSbcdDjpAJIynRLaM,587
24
+ kleinkram/commands/upload.py,sha256=Wc3OkmdtbrloTA4FaE2i1BD2gBcsjkR4T8G9qlIqe-M,4450
25
+ kleinkram/commands/verify.py,sha256=CGndDooBIdnYGPpaaBRNvs-3SWoBUquzAJc-3YB6_Yo,3692
26
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ tests/test_utils.py,sha256=1OOimNDLfLU5IWSwCtJRrXLw4SCbEe5rV4Z2XA7wCiU,4611
28
+ kleinkram-0.37.0.dev20241118070559.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
29
+ kleinkram-0.37.0.dev20241118070559.dist-info/METADATA,sha256=dfS6nk0AIra-fJQMVOgCc1kW93rLjRuCeobXbqtdYAk,2335
30
+ kleinkram-0.37.0.dev20241118070559.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
31
+ kleinkram-0.37.0.dev20241118070559.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
32
+ kleinkram-0.37.0.dev20241118070559.dist-info/top_level.txt,sha256=G1Lj9vHAtZn402Ukkrfll-6BCmnDNy_HVtWeNvXzdDA,16
33
+ kleinkram-0.37.0.dev20241118070559.dist-info/RECORD,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ klein = kleinkram.main:main
@@ -0,0 +1,2 @@
1
+ kleinkram
2
+ tests
tests/__init__.py ADDED
File without changes
tests/test_utils.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from tempfile import TemporaryDirectory
5
+ from uuid import uuid4
6
+
7
+ import pytest
8
+ from kleinkram.errors import FileTypeNotSupported
9
+ from kleinkram.errors import InvalidFileSpec
10
+ from kleinkram.errors import InvalidMissionSpec
11
+ from kleinkram.models import FilesById
12
+ from kleinkram.models import FilesByMission
13
+ from kleinkram.models import MissionById
14
+ from kleinkram.models import MissionByName
15
+ from kleinkram.utils import b64_md5
16
+ from kleinkram.utils import check_file_paths
17
+ from kleinkram.utils import get_filename
18
+ from kleinkram.utils import get_filename_map
19
+ from kleinkram.utils import get_valid_file_spec
20
+ from kleinkram.utils import get_valid_mission_spec
21
+ from kleinkram.utils import is_valid_uuid4
22
+ from kleinkram.utils import to_name_or_uuid
23
+
24
+
25
+ def test_check_file_paths():
26
+ with TemporaryDirectory() as temp_dir:
27
+ exits_txt = Path(temp_dir) / "exists.txt"
28
+ exists_bag = Path(temp_dir) / "exists.bag"
29
+ exits_mcap = Path(temp_dir) / "exists.mcap"
30
+ not_exists = Path(temp_dir) / "not_exists.txt"
31
+ is_dir = Path(temp_dir) / "is_dir"
32
+
33
+ exits_txt.touch()
34
+ exists_bag.touch()
35
+ exits_mcap.touch()
36
+ is_dir.mkdir()
37
+
38
+ with pytest.raises(FileTypeNotSupported):
39
+ check_file_paths([exits_txt])
40
+
41
+ with pytest.raises(FileNotFoundError):
42
+ check_file_paths([not_exists])
43
+
44
+ with pytest.raises(FileNotFoundError):
45
+ check_file_paths([is_dir])
46
+
47
+ assert check_file_paths([exists_bag, exits_mcap]) is None
48
+
49
+
50
+ def test_is_valid_uuid4():
51
+ valid = "e896313b-2ab0-466b-b458-8911575fdee9"
52
+ invalid = "hello world"
53
+
54
+ assert is_valid_uuid4(valid)
55
+ assert not is_valid_uuid4(invalid)
56
+
57
+
58
+ @pytest.mark.parametrize(
59
+ "old, new",
60
+ [
61
+ pytest.param(Path("test.bar"), "test.bar", id="short name"),
62
+ pytest.param(Path("symbols_-123.txt"), "symbols_-123.txt", id="symbols"),
63
+ pytest.param(
64
+ Path("invalid sybmols $%^&.txt"),
65
+ "invalid_sybmols_____.txt",
66
+ id="invalid symbols",
67
+ ),
68
+ pytest.param(
69
+ Path(f'{"a" * 100}.txt'), f'{"a" * 40}38bf3e475f.txt', id="too long"
70
+ ),
71
+ pytest.param(Path(f'{"a" * 50}.txt'), f'{"a" * 50}.txt', id="max length"),
72
+ pytest.param(Path("in/a/folder.txt"), "folder.txt", id="in folder"),
73
+ ],
74
+ )
75
+ def test_get_filename(old, new):
76
+ assert get_filename(old) == new
77
+
78
+
79
+ def test_get_filename_map():
80
+ non_unique = [Path("a.txt"), Path("a.txt")]
81
+
82
+ with pytest.raises(ValueError):
83
+ get_filename_map(non_unique)
84
+
85
+ unique = [Path("a.txt"), Path("b.txt")]
86
+ assert get_filename_map(unique) == {get_filename(Path(p)): Path(p) for p in unique}
87
+
88
+
89
+ def test_b64_md5():
90
+ with TemporaryDirectory() as temp_dir:
91
+ file = Path(temp_dir) / "file.txt"
92
+ file.write_text("hello world")
93
+
94
+ assert b64_md5(file) == "XrY7u+Ae7tCTyyK7j1rNww=="
95
+
96
+
97
+ def test_get_valid_mission_spec():
98
+ # only mission name
99
+ with pytest.raises(InvalidMissionSpec):
100
+ get_valid_mission_spec("mission")
101
+
102
+ # only mission id
103
+ id_ = uuid4()
104
+ assert get_valid_mission_spec(id_) == MissionById(id_)
105
+
106
+ # mission name and project name
107
+ assert get_valid_mission_spec("mission", "project") == MissionByName(
108
+ "mission", "project"
109
+ )
110
+
111
+ # mission name and project id
112
+ project_id = uuid4()
113
+ assert get_valid_mission_spec("mission", project_id) == MissionByName(
114
+ "mission", project_id
115
+ )
116
+
117
+ # mission id and project name
118
+ assert get_valid_mission_spec(id_, "project") == MissionById(id_)
119
+
120
+
121
+ def test_get_valid_file_spec():
122
+ # no information
123
+ with pytest.raises(InvalidFileSpec):
124
+ get_valid_file_spec([], None, None)
125
+
126
+ # only file ids
127
+ file_ids = [uuid4(), uuid4()]
128
+ assert get_valid_file_spec(file_ids, None, None) == FilesById(file_ids)
129
+
130
+ # only file names
131
+ with pytest.raises(InvalidFileSpec):
132
+ get_valid_file_spec(["foo"], None, None)
133
+
134
+ # missing mission
135
+ with pytest.raises(InvalidMissionSpec):
136
+ get_valid_file_spec([], None, "project")
137
+
138
+ # mission name and file names
139
+ assert get_valid_file_spec([], "mission", "project") == FilesByMission(
140
+ MissionByName("mission", "project"), []
141
+ )
142
+
143
+ assert get_valid_file_spec(
144
+ file_ids + ["name"], "mission", "project"
145
+ ) == FilesByMission(MissionByName("mission", "project"), file_ids + ["name"])
146
+
147
+
148
+ def test_to_name_or_uuid():
149
+ id_ = uuid4()
150
+ not_id = "not an id"
151
+
152
+ assert to_name_or_uuid(str(id_)) == id_
153
+ assert to_name_or_uuid(not_id) == not_id
kleinkram/api_client.py DELETED
@@ -1,63 +0,0 @@
1
- import httpx
2
-
3
- from kleinkram.auth.auth import TokenFile, CLI_KEY, AUTH_TOKEN, REFRESH_TOKEN
4
-
5
-
6
- class NotAuthenticatedException(Exception):
7
-
8
- def __init__(self, endpoint: str):
9
- self.message = f"You are not authenticated on endpoint '{endpoint}'. Please run 'klein login' to authenticate."
10
-
11
-
12
- class AuthenticatedClient(httpx.Client):
13
- def __init__(self, *args, **kwargs):
14
- super().__init__(*args, **kwargs)
15
-
16
- try:
17
- self.tokenfile = TokenFile()
18
- self._load_cookies()
19
- except Exception:
20
- raise NotAuthenticatedException(self.tokenfile.endpoint)
21
-
22
- def _load_cookies(self):
23
- if self.tokenfile.isCliToken():
24
- self.cookies.set(CLI_KEY, self.tokenfile.getCLIToken())
25
- else:
26
- self.cookies.set(AUTH_TOKEN, self.tokenfile.getAuthToken())
27
-
28
- def refresh_token(self):
29
- if self.tokenfile.isCliToken():
30
- print("CLI key cannot be refreshed.")
31
- return
32
- refresh_token = self.tokenfile.getRefreshToken()
33
- if not refresh_token:
34
- print("No refresh token found. Please login again.")
35
- raise Exception("No refresh token found.")
36
- self.cookies.set(
37
- REFRESH_TOKEN,
38
- refresh_token,
39
- )
40
- response = self.post(
41
- "/auth/refresh-token",
42
- )
43
- response.raise_for_status()
44
- new_access_token = response.cookies.get(AUTH_TOKEN)
45
- new_tokens = {AUTH_TOKEN: new_access_token, REFRESH_TOKEN: refresh_token}
46
- self.tokenfile.saveTokens(new_tokens)
47
- self.cookies.set(AUTH_TOKEN, new_access_token)
48
-
49
- def request(self, method, url, *args, **kwargs):
50
- response = super().request(
51
- method, self.tokenfile.endpoint + url, *args, **kwargs
52
- )
53
- if (url == "/auth/refresh-token") and response.status_code == 401:
54
- print("Refresh token expired. Please login again.")
55
- response.status_code = 403
56
- exit(1)
57
- if response.status_code == 401:
58
- print("Token expired, refreshing token...")
59
- self.refresh_token()
60
- response = super().request(
61
- method, self.tokenfile.endpoint + url, *args, **kwargs
62
- )
63
- return response