remotivelabs-cli 0.0.31__py3-none-any.whl → 0.0.32__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.
- cli/cloud/cloud_cli.py +2 -2
- cli/cloud/configs.py +7 -3
- cli/cloud/recordings.py +7 -9
- cli/cloud/rest_helper.py +1 -1
- cli/cloud/resumable_upload.py +2 -2
- cli/cloud/storage/__init__.py +5 -0
- cli/cloud/storage/cmd.py +77 -0
- cli/cloud/storage/copy.py +86 -0
- cli/cloud/storage/uri.py +59 -0
- cli/cloud/storage/uri_or_path.py +45 -0
- {remotivelabs_cli-0.0.31.dist-info → remotivelabs_cli-0.0.32.dist-info}/METADATA +2 -2
- {remotivelabs_cli-0.0.31.dist-info → remotivelabs_cli-0.0.32.dist-info}/RECORD +15 -11
- cli/cloud/filestorage.py +0 -170
- {remotivelabs_cli-0.0.31.dist-info → remotivelabs_cli-0.0.32.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.0.31.dist-info → remotivelabs_cli-0.0.32.dist-info}/WHEEL +0 -0
- {remotivelabs_cli-0.0.31.dist-info → remotivelabs_cli-0.0.32.dist-info}/entry_points.txt +0 -0
cli/cloud/cloud_cli.py
CHANGED
@@ -2,7 +2,7 @@ import typer
|
|
2
2
|
|
3
3
|
from cli.cloud.rest_helper import RestHelper
|
4
4
|
|
5
|
-
from . import auth, brokers, configs,
|
5
|
+
from . import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
|
6
6
|
|
7
7
|
app = typer.Typer()
|
8
8
|
|
@@ -21,7 +21,7 @@ app.add_typer(auth.app, name="auth")
|
|
21
21
|
app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
|
22
22
|
app.add_typer(recordings.app, name="recordings", help="Manage recordings")
|
23
23
|
app.add_typer(configs.app, name="signal-databases", help="Manage signal databases")
|
24
|
-
app.add_typer(
|
24
|
+
app.add_typer(storage.app, name="storage")
|
25
25
|
app.add_typer(service_accounts.app, name="service-accounts", help="Manage project service account keys")
|
26
26
|
app.add_typer(sample_recordings.app, name="samples", help="Manage sample recordings")
|
27
27
|
|
cli/cloud/configs.py
CHANGED
@@ -48,10 +48,14 @@ def upload(
|
|
48
48
|
"""
|
49
49
|
Uploads signal database to project
|
50
50
|
"""
|
51
|
-
|
52
|
-
if
|
51
|
+
res_text = Rest.handle_put(url=f"/api/project/{project}/files/config/{os.path.basename(path)}/uploadfile", return_response=True)
|
52
|
+
if res_text is not None:
|
53
|
+
res_json = res_text.json()
|
53
54
|
Rest.upload_file_with_signed_url(
|
54
|
-
path=path,
|
55
|
+
path=path,
|
56
|
+
url=res_json["url"],
|
57
|
+
upload_headers={"Content-Type": "application/octet-stream"},
|
58
|
+
progress_label=f"Uploading {path}...",
|
55
59
|
)
|
56
60
|
|
57
61
|
|
cli/cloud/recordings.py
CHANGED
@@ -9,7 +9,7 @@ import tempfile
|
|
9
9
|
import time
|
10
10
|
import urllib.parse
|
11
11
|
from pathlib import Path
|
12
|
-
from typing import Any, Dict, List, Tuple, Union
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
13
13
|
from urllib.parse import quote
|
14
14
|
|
15
15
|
import grpc
|
@@ -119,12 +119,10 @@ def __check_rcs_path(path: str) -> str:
|
|
119
119
|
|
120
120
|
|
121
121
|
def do_start(name: str, project: str, api_key: str, return_response: bool = False) -> requests.Response:
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
body = {"size": "S", "apiKey": api_key}
|
122
|
+
body = {"size": "S"}
|
123
|
+
if not api_key:
|
124
|
+
body["apiKey"] = api_key
|
126
125
|
|
127
|
-
name = name if name is not None else "personal"
|
128
126
|
return Rest.handle_post(
|
129
127
|
f"/api/project/{project}/brokers/{name}",
|
130
128
|
body=json.dumps(body),
|
@@ -136,7 +134,7 @@ def do_start(name: str, project: str, api_key: str, return_response: bool = Fals
|
|
136
134
|
@app.command(help="Prepares all recording files and transformations to be available for playback")
|
137
135
|
def mount( # noqa: C901
|
138
136
|
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
139
|
-
broker: str = typer.Option(None, help="Broker to use"),
|
137
|
+
broker: Optional[str] = typer.Option(None, help="Broker to use"),
|
140
138
|
ensure_broker_started: bool = typer.Option(default=False, help="Ensure broker exists, start otherwise"),
|
141
139
|
transformation_name: str = typer.Option("default", help="Specify a custom signal transformation to use"),
|
142
140
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
@@ -153,7 +151,7 @@ def mount( # noqa: C901
|
|
153
151
|
broker = r.json()["shortName"]
|
154
152
|
|
155
153
|
elif r.status_code == 404:
|
156
|
-
r = do_start(
|
154
|
+
r = do_start("personal", project, "", return_response=True)
|
157
155
|
if r.status_code != 200:
|
158
156
|
print(r.text)
|
159
157
|
sys.exit(0)
|
@@ -208,7 +206,7 @@ def download_recording_file(
|
|
208
206
|
)
|
209
207
|
if get_signed_url_resp.status_code == 200:
|
210
208
|
# Next download the actual file
|
211
|
-
Rest.download_file(recording_file_name, get_signed_url_resp.json()["downloadUrl"])
|
209
|
+
Rest.download_file(Path(recording_file_name), get_signed_url_resp.json()["downloadUrl"])
|
212
210
|
print(f"Downloaded {recording_file_name}")
|
213
211
|
else:
|
214
212
|
print(get_signed_url_resp)
|
cli/cloud/rest_helper.py
CHANGED
@@ -250,7 +250,7 @@ class RestHelper:
|
|
250
250
|
return p
|
251
251
|
|
252
252
|
@staticmethod
|
253
|
-
def download_file(save_file_name:
|
253
|
+
def download_file(save_file_name: Path, url: str) -> None:
|
254
254
|
# Next download the actual file
|
255
255
|
download_resp = requests.get(url=url, stream=True, timeout=60)
|
256
256
|
if download_resp.status_code == 200:
|
cli/cloud/resumable_upload.py
CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import os
|
4
4
|
import sys
|
5
|
+
from pathlib import Path
|
5
6
|
from typing import Dict
|
6
7
|
|
7
8
|
import requests
|
@@ -66,7 +67,7 @@ def with_resumable_upload_signed_url(signed_url: str, source_file_name: str, con
|
|
66
67
|
print(f"File {source_file_name} uploaded successfully.")
|
67
68
|
|
68
69
|
|
69
|
-
def upload_signed_url(signed_url: str, source_file_name:
|
70
|
+
def upload_signed_url(signed_url: str, source_file_name: Path, headers: Dict[str, str]) -> None:
|
70
71
|
"""
|
71
72
|
Upload file to file storage with signed url and resumable upload.
|
72
73
|
Resumable upload will only work with the same URL and not if a new signed URL is requested with the
|
@@ -76,7 +77,6 @@ def upload_signed_url(signed_url: str, source_file_name: str, headers: Dict[str,
|
|
76
77
|
:param source_file_name:
|
77
78
|
:return:
|
78
79
|
"""
|
79
|
-
|
80
80
|
with open(source_file_name, "rb") as file:
|
81
81
|
with wrap_file(file, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as f:
|
82
82
|
response = requests.put(signed_url, headers=headers, timeout=60, data=f)
|
cli/cloud/storage/cmd.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing_extensions import Annotated
|
5
|
+
|
6
|
+
from cli.cloud.rest_helper import RestHelper as Rest
|
7
|
+
from cli.cloud.storage.copy import copy
|
8
|
+
from cli.cloud.storage.uri import URI, InvalidURIError, JoinURIError
|
9
|
+
from cli.cloud.storage.uri_or_path import UriOrPath
|
10
|
+
from cli.cloud.storage.uri_or_path import uri as uri_parser
|
11
|
+
from cli.errors import ErrorPrinter
|
12
|
+
|
13
|
+
app = typer.Typer(
|
14
|
+
rich_markup_mode="rich",
|
15
|
+
help="""
|
16
|
+
Manage files ([yellow]Beta feature not available for all customers[/yellow])
|
17
|
+
|
18
|
+
Copy file from local to remote storage and vice versa, list and delete files.
|
19
|
+
|
20
|
+
""",
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
@app.command(name="ls")
|
25
|
+
def list_files(
|
26
|
+
project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
|
27
|
+
uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI)] = "rcs://", # type: ignore
|
28
|
+
) -> None:
|
29
|
+
"""
|
30
|
+
Listing remote files
|
31
|
+
|
32
|
+
This will list files and directories in project top level directory
|
33
|
+
remotive cloud storage ls rcs://
|
34
|
+
|
35
|
+
This will list all files and directories matching the path
|
36
|
+
remotive cloud storage ls rcs://fileOrDirectoryPrefix
|
37
|
+
|
38
|
+
This will list all files and directories in the specified directory
|
39
|
+
remotive cloud storage ls rcs://fileOrDirectory/
|
40
|
+
"""
|
41
|
+
Rest.handle_get(f"/api/project/{project}/files/storage{uri.path}")
|
42
|
+
|
43
|
+
|
44
|
+
@app.command(name="rm")
|
45
|
+
def delete_file(
|
46
|
+
project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
|
47
|
+
uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI, show_default=False)],
|
48
|
+
) -> None:
|
49
|
+
"""
|
50
|
+
[red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
|
51
|
+
|
52
|
+
[white]remotive cloud storage rm rcs://directory/filename[/white]
|
53
|
+
"""
|
54
|
+
Rest.handle_delete(f"/api/project/{project}/files/storage{uri.path}")
|
55
|
+
|
56
|
+
|
57
|
+
@app.command(name="cp")
|
58
|
+
def copy_file(
|
59
|
+
project: Annotated[str, typer.Option(help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT", show_default=False)],
|
60
|
+
source: Annotated[UriOrPath, typer.Argument(help="Remote or local path to source file", parser=uri_parser, show_default=False)],
|
61
|
+
dest: Annotated[UriOrPath, typer.Argument(help="Remote or local path to destination file", parser=uri_parser, show_default=False)],
|
62
|
+
overwrite: Annotated[bool, typer.Option(help="Overwrite existing file on RCS")] = False,
|
63
|
+
) -> None:
|
64
|
+
"""
|
65
|
+
Copies a file to or from remote storage
|
66
|
+
|
67
|
+
remotive cloud storage cp rcs://dir/filename .
|
68
|
+
remotive cloud storage cp rcs://dir/filename filename
|
69
|
+
|
70
|
+
remotive cloud storage cp filename rcs://dir/
|
71
|
+
remotive cloud storage cp filename rcs://dir/filename
|
72
|
+
"""
|
73
|
+
try:
|
74
|
+
return copy(project=project, source=source.value, dest=dest.value, overwrite=overwrite)
|
75
|
+
except (InvalidURIError, JoinURIError, ValueError, FileNotFoundError, FileExistsError) as e:
|
76
|
+
ErrorPrinter.print_hint(str(e))
|
77
|
+
sys.exit(1)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
from cli.cloud.rest_helper import RestHelper as Rest
|
7
|
+
from cli.cloud.resumable_upload import upload_signed_url
|
8
|
+
from cli.cloud.storage.uri import URI
|
9
|
+
|
10
|
+
_RCS_STORAGE_PATH = "/api/project/{project}/files/storage{path}"
|
11
|
+
|
12
|
+
|
13
|
+
def copy(project: str, source: URI | Path, dest: URI | Path, overwrite: bool = False) -> None:
|
14
|
+
if isinstance(source, Path) and isinstance(dest, Path):
|
15
|
+
raise ValueError("Either source or destination must be an rcs:// uri")
|
16
|
+
|
17
|
+
if isinstance(source, URI) and isinstance(dest, URI):
|
18
|
+
raise ValueError("Either source or destination must be a local path")
|
19
|
+
|
20
|
+
if isinstance(source, URI) and isinstance(dest, Path):
|
21
|
+
_download(source=source, dest=dest, project=project, overwrite=overwrite)
|
22
|
+
|
23
|
+
elif isinstance(source, Path) and isinstance(dest, URI):
|
24
|
+
_upload(source=source, dest=dest, project=project, overwrite=overwrite)
|
25
|
+
|
26
|
+
else:
|
27
|
+
raise ValueError("invalid copy operation")
|
28
|
+
|
29
|
+
|
30
|
+
def _upload(source: Path, dest: URI, project: str, overwrite: bool = False) -> None:
|
31
|
+
if not source.exists():
|
32
|
+
raise FileNotFoundError(source)
|
33
|
+
|
34
|
+
files_to_upload = _list_files_for_upload(source, dest)
|
35
|
+
|
36
|
+
for file_path, target_uri in files_to_upload:
|
37
|
+
_upload_single_file(file_path, target_uri, project, overwrite)
|
38
|
+
|
39
|
+
|
40
|
+
def _list_files_for_upload(source: Path, dest: URI) -> list[tuple[Path, URI]]:
|
41
|
+
upload_pairs = []
|
42
|
+
|
43
|
+
if source.is_dir():
|
44
|
+
for file_path in source.rglob("*"):
|
45
|
+
if file_path.is_file():
|
46
|
+
relative_path = file_path.relative_to(source)
|
47
|
+
target_uri = dest / relative_path
|
48
|
+
upload_pairs.append((file_path, target_uri))
|
49
|
+
else:
|
50
|
+
target_uri = dest / source.name if dest.is_dir() else dest
|
51
|
+
upload_pairs.append((source, target_uri))
|
52
|
+
|
53
|
+
return upload_pairs
|
54
|
+
|
55
|
+
|
56
|
+
def _upload_single_file(source: Path, target_uri: URI, project: str, overwrite: bool = False) -> None:
|
57
|
+
target = _RCS_STORAGE_PATH.format(project=project, path=target_uri.path)
|
58
|
+
upload_options = {"overwrite": "always" if overwrite else "never"}
|
59
|
+
res = Rest.handle_post(target, return_response=True, body=json.dumps(upload_options))
|
60
|
+
|
61
|
+
json_res = res.json()
|
62
|
+
url = json_res["url"]
|
63
|
+
headers = json_res["headers"]
|
64
|
+
upload_signed_url(url, source, headers)
|
65
|
+
|
66
|
+
print(f"Uploaded {source} to {target_uri.path}")
|
67
|
+
|
68
|
+
|
69
|
+
def _download(source: URI, dest: Path, project: str, overwrite: bool = False) -> None:
|
70
|
+
if dest.is_dir():
|
71
|
+
if not dest.exists():
|
72
|
+
raise FileNotFoundError(f"Destination directory {dest} does not exist")
|
73
|
+
# create a target file name if destination is a dir
|
74
|
+
dest = dest / source.filename
|
75
|
+
|
76
|
+
else:
|
77
|
+
if not dest.parent.is_dir() or not dest.parent.exists():
|
78
|
+
raise FileNotFoundError(f"Destination directory {dest.parent} does not exist")
|
79
|
+
|
80
|
+
if dest.exists() and not overwrite:
|
81
|
+
raise FileExistsError(f"Destination file {dest} already exists")
|
82
|
+
|
83
|
+
target = _RCS_STORAGE_PATH.format(project=project, path=source.path) + "?download=true"
|
84
|
+
res = Rest.handle_get(target, return_response=True)
|
85
|
+
|
86
|
+
Rest.download_file(save_file_name=dest.absolute(), url=res.text)
|
cli/cloud/storage/uri.py
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from os import PathLike
|
4
|
+
from pathlib import PurePosixPath
|
5
|
+
from urllib.parse import urlparse
|
6
|
+
|
7
|
+
|
8
|
+
class InvalidURIError(Exception):
|
9
|
+
"""Raised when an invalid URI is encountered"""
|
10
|
+
|
11
|
+
|
12
|
+
class JoinURIError(Exception):
|
13
|
+
"""Raised when an error occurs while joining URIs"""
|
14
|
+
|
15
|
+
|
16
|
+
class URI:
|
17
|
+
"""
|
18
|
+
Custom type for rcs (Remotive Cloud Storage) URIs.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, value: str, scheme: str = "rcs"):
|
22
|
+
self.original_uri = value
|
23
|
+
self.scheme = scheme
|
24
|
+
|
25
|
+
parsed = urlparse(value)
|
26
|
+
if parsed.scheme != self.scheme:
|
27
|
+
raise InvalidURIError(f"Invalid URI scheme. Expected '{self.scheme}://', got '{parsed.scheme}://'")
|
28
|
+
if parsed.netloc.startswith((".", "-", "#", " ", "/", "\\")):
|
29
|
+
raise InvalidURIError(f"Invalid URI. Path cannot start with invalid characters: '{value}'")
|
30
|
+
if not parsed.netloc and parsed.path == "/":
|
31
|
+
raise InvalidURIError(f"Invalid URI: '{value}'")
|
32
|
+
|
33
|
+
self.path = f"/{parsed.netloc}{parsed.path}" if parsed.netloc else f"/{parsed.path}"
|
34
|
+
|
35
|
+
self._posix_path = PurePosixPath(self.path)
|
36
|
+
self.filename = self._posix_path.name
|
37
|
+
|
38
|
+
def is_dir(self) -> bool:
|
39
|
+
return self.path.endswith("/")
|
40
|
+
|
41
|
+
def __truediv__(self, other: PathLike[str] | str) -> URI:
|
42
|
+
"""Returns a new URI object with the joined path"""
|
43
|
+
if str(other).startswith("/"):
|
44
|
+
raise JoinURIError(f"Cannot join absolute path '{other}' to URI")
|
45
|
+
|
46
|
+
new_path = self._posix_path / other
|
47
|
+
|
48
|
+
# handle relative paths
|
49
|
+
for part in new_path.parts:
|
50
|
+
if part == "..":
|
51
|
+
new_path = new_path.parent
|
52
|
+
elif part != ".":
|
53
|
+
new_path = new_path / part
|
54
|
+
|
55
|
+
new_uri = f"{self.scheme}://{new_path.relative_to('/')}" # we need to strip the starting '/'
|
56
|
+
return URI(new_uri, scheme=self.scheme)
|
57
|
+
|
58
|
+
def __str__(self) -> str:
|
59
|
+
return self.original_uri
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from cli.cloud.storage.uri import URI, InvalidURIError
|
6
|
+
|
7
|
+
|
8
|
+
def uri(path: str) -> UriOrPath:
|
9
|
+
"""
|
10
|
+
Parses a path and returns a UriOrPath object.
|
11
|
+
|
12
|
+
NOTE: The name of this function is important as it is used by Typer to determine the name/type of the argument
|
13
|
+
"""
|
14
|
+
try:
|
15
|
+
p: Path | URI = URI(path)
|
16
|
+
except InvalidURIError:
|
17
|
+
p = Path(path)
|
18
|
+
return UriOrPath(p)
|
19
|
+
|
20
|
+
|
21
|
+
class UriOrPath:
|
22
|
+
"""
|
23
|
+
Union type for handling local and remote paths for Remotive Cloud Storage
|
24
|
+
|
25
|
+
Note: This custom type only exists because Typer currently does not support union types
|
26
|
+
TODO: Move to commands package when refactored
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, value: Path | URI) -> None:
|
30
|
+
self._value = value
|
31
|
+
|
32
|
+
@property
|
33
|
+
def uri(self) -> URI | None:
|
34
|
+
return self._value if isinstance(self._value, URI) else None
|
35
|
+
|
36
|
+
@property
|
37
|
+
def path(self) -> Path | None:
|
38
|
+
return self._value if isinstance(self._value, Path) else None
|
39
|
+
|
40
|
+
@property
|
41
|
+
def value(self) -> Path | URI:
|
42
|
+
return self._value
|
43
|
+
|
44
|
+
def __str__(self) -> str:
|
45
|
+
return str(self._value)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: remotivelabs-cli
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.32
|
4
4
|
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
5
5
|
Author: Johan Rask
|
6
6
|
Author-email: johan.rask@remotivelabs.com
|
@@ -19,7 +19,7 @@ Requires-Dist: python-socketio (>=4.6.1)
|
|
19
19
|
Requires-Dist: remotivelabs-broker (>=0.1.17,<0.2.0)
|
20
20
|
Requires-Dist: rich (>=13.7.0,<13.8.0)
|
21
21
|
Requires-Dist: trogon (>=0.5.0)
|
22
|
-
Requires-Dist: typer (
|
22
|
+
Requires-Dist: typer (==0.12.5)
|
23
23
|
Requires-Dist: types-requests (>=2.32.0.20240622,<3.0.0.0)
|
24
24
|
Requires-Dist: websocket-client (>=1.6,<2.0)
|
25
25
|
Requires-Dist: zeroconf (>=0.127.0,<0.128.0)
|
@@ -14,18 +14,22 @@ cli/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
cli/cloud/auth.py,sha256=RBTDUGRBVsK28u-aeqzRIzHnW7FqH4KADlVlQEgBCww,2534
|
15
15
|
cli/cloud/auth_tokens.py,sha256=0U60Gk2-TnAUff5anZmTB1rOEninNvYy1o5ihCqgj8A,4525
|
16
16
|
cli/cloud/brokers.py,sha256=DNj79MTkPylKUQbr-iPUhQgfNJLAW8UehnvgpEmNH_k,3890
|
17
|
-
cli/cloud/cloud_cli.py,sha256=
|
18
|
-
cli/cloud/configs.py,sha256=
|
19
|
-
cli/cloud/filestorage.py,sha256=BZbSk9abuLYw9TjGsQpfYMobtE4VLgXFrWD1sVvGolY,5621
|
17
|
+
cli/cloud/cloud_cli.py,sha256=0VvFexnVnYvb1vueYxraHiH_WjbQp0Y42ttL_koDFh8,1255
|
18
|
+
cli/cloud/configs.py,sha256=bZzZF-yNkZkfJK5baU0Xh_vQRrJkmuzEv8xx4_IkXZ8,4714
|
20
19
|
cli/cloud/organisations.py,sha256=txKQmSQEpTmeqlqngai8pwgQQEvRgeDd0dT_VzZ7RNc,752
|
21
20
|
cli/cloud/projects.py,sha256=YrwPJClC2Sq_y1HjPd_tzaiv4GEnnsXSXHBhtQCPdK0,1431
|
22
|
-
cli/cloud/recordings.py,sha256=
|
21
|
+
cli/cloud/recordings.py,sha256=RK-CfYEZ_K4o_nOuRBQ6JCsNpTc8yGWQUMV_BN4aWQA,25315
|
23
22
|
cli/cloud/recordings_playback.py,sha256=PRzftmvG2iePrL9f6qTEXVOnyJ-etcyzn5w9CCxcSto,11539
|
24
|
-
cli/cloud/rest_helper.py,sha256=
|
25
|
-
cli/cloud/resumable_upload.py,sha256=
|
23
|
+
cli/cloud/rest_helper.py,sha256=_3RLk8C101PH5pCq5GhCCYLSiS2TZtXh69Yt5TfiJsY,11471
|
24
|
+
cli/cloud/resumable_upload.py,sha256=8lEIdncJZoTZzNsQVHH3gm_GunxEmN5JbmWX7awy3p4,3713
|
26
25
|
cli/cloud/sample_recordings.py,sha256=OVX32U1dkkkJZysbgr5Dy515oOQKnwBAbZYzV_QUu1g,690
|
27
26
|
cli/cloud/service_account_tokens.py,sha256=263u1bRmBKfYsxL6TV6YjReUBUaVHWc3ETCd7AS3DTU,2297
|
28
27
|
cli/cloud/service_accounts.py,sha256=XOIPobUamCLIaufjyvb33XJDwy6uRqW5ZljZx3GYEfo,1659
|
28
|
+
cli/cloud/storage/__init__.py,sha256=y6V0QswTVY7lHcy5kVUbNKIPc5tyWDqvhakcRFKV6HA,167
|
29
|
+
cli/cloud/storage/cmd.py,sha256=kukFatuL-YOy6VC7FC4PvjjS1teQya7R__fNX0jdubY,2929
|
30
|
+
cli/cloud/storage/copy.py,sha256=thjdhqnFHSstsA5Iidz74c2o4zHfEaDXfRUGOpjWh1o,3228
|
31
|
+
cli/cloud/storage/uri.py,sha256=uGEGggJp7Nnu3oUadBs9bAMUDf2e5YjhZ6gAdeYLBFI,1963
|
32
|
+
cli/cloud/storage/uri_or_path.py,sha256=rTjg9G0h8-jb4QUlza1YMqqs99ifrydnEL7o7rmidKQ,1166
|
29
33
|
cli/connect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
34
|
cli/connect/connect.py,sha256=U--6dtHxUlvE81J37rABFez4TbF7AXWOpZYZnL7sPMY,3994
|
31
35
|
cli/connect/protopie/protopie.py,sha256=KBMbBwdkUVgV2X7AXTHweVqYVHv4akG875FVc36gsyg,6349
|
@@ -36,8 +40,8 @@ cli/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
40
|
cli/tools/can/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
41
|
cli/tools/can/can.py,sha256=8uATViSFlpkdSiIm4fzbuQi1_m7V9Pym-K17TaJQRHU,2262
|
38
42
|
cli/tools/tools.py,sha256=0KU-hXR1f9xHP4BOG9A9eXfmICLmNuQCOU8ueF6iGg0,198
|
39
|
-
remotivelabs_cli-0.0.
|
40
|
-
remotivelabs_cli-0.0.
|
41
|
-
remotivelabs_cli-0.0.
|
42
|
-
remotivelabs_cli-0.0.
|
43
|
-
remotivelabs_cli-0.0.
|
43
|
+
remotivelabs_cli-0.0.32.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
|
44
|
+
remotivelabs_cli-0.0.32.dist-info/METADATA,sha256=pvFD_mvS0qOntQP-7EAivZAPl1vKcNJDfzGSh-jwKEw,1311
|
45
|
+
remotivelabs_cli-0.0.32.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
46
|
+
remotivelabs_cli-0.0.32.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
|
47
|
+
remotivelabs_cli-0.0.32.dist-info/RECORD,,
|
cli/cloud/filestorage.py
DELETED
@@ -1,170 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import json
|
4
|
-
import os.path
|
5
|
-
import sys
|
6
|
-
from pathlib import Path
|
7
|
-
|
8
|
-
import typer
|
9
|
-
|
10
|
-
from ..errors import ErrorPrinter
|
11
|
-
from . import resumable_upload as upload
|
12
|
-
from .rest_helper import RestHelper as Rest
|
13
|
-
|
14
|
-
app = typer.Typer(
|
15
|
-
rich_markup_mode="rich",
|
16
|
-
help="""
|
17
|
-
Manage files ([yellow]Beta feature not available for all customers[/yellow])
|
18
|
-
|
19
|
-
Copy file from local to remote storage and vice versa, list and delete files.
|
20
|
-
|
21
|
-
""",
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
@app.command(name="ls")
|
26
|
-
def list_files(
|
27
|
-
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
28
|
-
prefix: str = typer.Argument(default="rcs://", help="Remote storage path"),
|
29
|
-
) -> None:
|
30
|
-
"""
|
31
|
-
Listing remote files
|
32
|
-
|
33
|
-
This will list files and directories in project top level directory
|
34
|
-
remotive cloud storage ls rcs://
|
35
|
-
|
36
|
-
This will list all files and directories matching the path
|
37
|
-
remotive cloud storage ls rcs://fileOrDirectoryPrefix
|
38
|
-
|
39
|
-
This will list all files and directories in the specified directory
|
40
|
-
remotive cloud storage ls rcs://fileOrDirectory/
|
41
|
-
"""
|
42
|
-
|
43
|
-
if prefix.startswith("rcs://"):
|
44
|
-
prefix = __check_rcs_path(prefix)
|
45
|
-
else:
|
46
|
-
ErrorPrinter.print_hint("Path must start with rcs://")
|
47
|
-
sys.exit(1)
|
48
|
-
|
49
|
-
Rest.handle_get(
|
50
|
-
f"/api/project/{project}/files/storage{prefix}",
|
51
|
-
)
|
52
|
-
|
53
|
-
|
54
|
-
@app.command(name="rm")
|
55
|
-
def delete_file(
|
56
|
-
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
57
|
-
path: str = typer.Argument(default=..., help="Remote storage path to file to delete"),
|
58
|
-
) -> None:
|
59
|
-
"""
|
60
|
-
[red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
|
61
|
-
|
62
|
-
[white]remotive cloud storage rm rcs://directory/filename[/white]
|
63
|
-
"""
|
64
|
-
if path.startswith("rcs://"):
|
65
|
-
prefix = __check_rcs_path(path)
|
66
|
-
else:
|
67
|
-
ErrorPrinter.print_hint("Path must start with rcs://")
|
68
|
-
sys.exit(1)
|
69
|
-
|
70
|
-
Rest.handle_delete(
|
71
|
-
f"/api/project/{project}/files/storage{prefix}",
|
72
|
-
)
|
73
|
-
|
74
|
-
|
75
|
-
@app.command(name="cp")
|
76
|
-
def copy_file( # noqa: C901 # type: ignore[too-many-branches] # pylint: disable=no-member
|
77
|
-
source: str = typer.Argument(default=..., help="Remote or local path to source file"),
|
78
|
-
dest: str = typer.Argument(default=..., help="Remote or local path to destination file"),
|
79
|
-
overwrite: bool = typer.Option(default=False, help="Overwrite existing file on RCS"),
|
80
|
-
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
81
|
-
) -> None:
|
82
|
-
"""
|
83
|
-
Copies a file to or from remote storage
|
84
|
-
|
85
|
-
remotive cloud storage cp rcs://dir/filename .
|
86
|
-
remotive cloud storage cp rcs://dir/filename filename
|
87
|
-
|
88
|
-
remotive cloud storage cp filename rcs://dir/
|
89
|
-
remotive cloud storage cp filename rcs://dir/filename
|
90
|
-
"""
|
91
|
-
|
92
|
-
if not source.startswith("rcs://") and not dest.startswith("rcs://"):
|
93
|
-
ErrorPrinter.print_hint("Source or destination path must be an rcs:// path")
|
94
|
-
sys.exit(2)
|
95
|
-
|
96
|
-
if source.startswith("rcs://") and dest.startswith("rcs://"):
|
97
|
-
ErrorPrinter.print_hint("Currently one of source and destination path must be a local path")
|
98
|
-
sys.exit(2)
|
99
|
-
|
100
|
-
if source.startswith("rcs://"):
|
101
|
-
__copy_to_local(source=source, dest=dest, project=project)
|
102
|
-
else:
|
103
|
-
path = Path(source)
|
104
|
-
if path.is_dir():
|
105
|
-
for file_path in path.rglob("*"):
|
106
|
-
if file_path.is_file():
|
107
|
-
__copy_to_remote(source=source, dest=dest, project=project, overwrite=overwrite)
|
108
|
-
sys.exit(1)
|
109
|
-
else:
|
110
|
-
__copy_to_remote(source=source, dest=dest, project=project, overwrite=overwrite)
|
111
|
-
|
112
|
-
|
113
|
-
def __copy_to_remote(source: str, dest: str, project: str, overwrite: bool) -> None:
|
114
|
-
path = Path(source)
|
115
|
-
if path.is_dir():
|
116
|
-
sys.exit(1)
|
117
|
-
|
118
|
-
if not path.exists():
|
119
|
-
ErrorPrinter.print_hint("Source file does not exist")
|
120
|
-
sys.exit(1)
|
121
|
-
filename = source.rsplit("/", 1)[-1]
|
122
|
-
rcs_path = __check_rcs_path(dest)
|
123
|
-
if rcs_path.endswith("/"):
|
124
|
-
rcs_path = rcs_path + filename
|
125
|
-
upload_options = {"overwrite": "always" if overwrite else "never"}
|
126
|
-
res = Rest.handle_post(f"/api/project/{project}/files/storage{rcs_path}", return_response=True, body=json.dumps(upload_options))
|
127
|
-
if res is None:
|
128
|
-
return
|
129
|
-
json_res = res.json()
|
130
|
-
url = json_res["url"]
|
131
|
-
headers = json_res["headers"]
|
132
|
-
try:
|
133
|
-
upload.upload_signed_url(url, source, headers)
|
134
|
-
except IsADirectoryError:
|
135
|
-
ErrorPrinter.print_hint(f"Supplied source file '{source}' is a directory but must be a file")
|
136
|
-
|
137
|
-
|
138
|
-
def __copy_to_local(source: str, dest: str, project: str) -> None:
|
139
|
-
rcs_path = __check_rcs_path(source)
|
140
|
-
filename = source.rsplit("/", 1)[-1]
|
141
|
-
path = Path(dest)
|
142
|
-
if path.is_dir():
|
143
|
-
if not path.exists():
|
144
|
-
ErrorPrinter.print_generic_error("Destination directory does not exist")
|
145
|
-
sys.exit(1)
|
146
|
-
else:
|
147
|
-
dest = os.path.join(path.absolute(), filename)
|
148
|
-
|
149
|
-
else:
|
150
|
-
if not path.parent.is_dir() or not path.parent.exists():
|
151
|
-
ErrorPrinter.print_generic_error("Destination directory does not exist")
|
152
|
-
sys.exit(1)
|
153
|
-
dest = str(Path(dest).absolute())
|
154
|
-
|
155
|
-
res = Rest.handle_get(
|
156
|
-
f"/api/project/{project}/files/storage{rcs_path}?download=true",
|
157
|
-
return_response=True,
|
158
|
-
)
|
159
|
-
if res is None:
|
160
|
-
return
|
161
|
-
|
162
|
-
Rest.download_file(save_file_name=dest, url=res.text)
|
163
|
-
|
164
|
-
|
165
|
-
def __check_rcs_path(path: str) -> str:
|
166
|
-
rcs_path = path.replace("rcs://", "/")
|
167
|
-
if rcs_path.startswith("/."):
|
168
|
-
ErrorPrinter.print_hint("Invalid path")
|
169
|
-
sys.exit(1)
|
170
|
-
return rcs_path
|
File without changes
|
File without changes
|
File without changes
|