kleinkram 0.36.2__py3-none-any.whl → 0.36.2.dev20241118065826__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.
- kleinkram/__init__.py +6 -0
- kleinkram/__main__.py +6 -0
- kleinkram/_version.py +6 -0
- kleinkram/api/__init__.py +0 -0
- kleinkram/api/client.py +65 -0
- kleinkram/api/file_transfer.py +328 -0
- kleinkram/api/routes.py +460 -0
- kleinkram/app.py +180 -0
- kleinkram/auth.py +96 -0
- kleinkram/commands/__init__.py +1 -0
- kleinkram/commands/download.py +103 -0
- kleinkram/commands/endpoint.py +62 -0
- kleinkram/commands/list.py +93 -0
- kleinkram/commands/mission.py +57 -0
- kleinkram/commands/project.py +24 -0
- kleinkram/commands/upload.py +138 -0
- kleinkram/commands/verify.py +117 -0
- kleinkram/config.py +171 -0
- kleinkram/consts.py +8 -1
- kleinkram/core.py +14 -0
- kleinkram/enums.py +10 -0
- kleinkram/errors.py +59 -0
- kleinkram/main.py +6 -484
- kleinkram/models.py +186 -0
- kleinkram/utils.py +179 -0
- {kleinkram-0.36.2.dist-info/licenses → kleinkram-0.36.2.dev20241118065826.dist-info}/LICENSE +1 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/METADATA +113 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/RECORD +33 -0
- {kleinkram-0.36.2.dist-info → kleinkram-0.36.2.dev20241118065826.dist-info}/WHEEL +2 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/entry_points.txt +2 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_utils.py +153 -0
- kleinkram/api_client.py +0 -63
- kleinkram/auth/auth.py +0 -160
- kleinkram/endpoint/endpoint.py +0 -58
- kleinkram/error_handling.py +0 -177
- kleinkram/file/file.py +0 -144
- kleinkram/helper.py +0 -272
- kleinkram/mission/mission.py +0 -310
- kleinkram/project/project.py +0 -138
- kleinkram/queue/queue.py +0 -8
- kleinkram/tag/tag.py +0 -71
- kleinkram/topic/topic.py +0 -55
- kleinkram/user/user.py +0 -75
- kleinkram-0.36.2.dist-info/METADATA +0 -25
- kleinkram-0.36.2.dist-info/RECORD +0 -20
- kleinkram-0.36.2.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
|
{kleinkram-0.36.2.dist-info/licenses → kleinkram-0.36.2.dev20241118065826.dist-info}/LICENSE
RENAMED
|
@@ -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.36.2.dev20241118065826
|
|
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.36.2.dev20241118065826.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
29
|
+
kleinkram-0.36.2.dev20241118065826.dist-info/METADATA,sha256=xd4wVG3UEjsBcGeOnqTM1jA8bkSyVLXkXINVoe0eusA,2335
|
|
30
|
+
kleinkram-0.36.2.dev20241118065826.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
|
|
31
|
+
kleinkram-0.36.2.dev20241118065826.dist-info/entry_points.txt,sha256=SaB2l5aqhSr8gmaMw2kvQU90a8Bnl7PedU8cWYxkfYo,46
|
|
32
|
+
kleinkram-0.36.2.dev20241118065826.dist-info/top_level.txt,sha256=G1Lj9vHAtZn402Ukkrfll-6BCmnDNy_HVtWeNvXzdDA,16
|
|
33
|
+
kleinkram-0.36.2.dev20241118065826.dist-info/RECORD,,
|
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
|