remotivelabs-cli 0.0.23__tar.gz → 0.0.25__tar.gz
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.
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/PKG-INFO +2 -2
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/cloud_cli.py +2 -1
- remotivelabs_cli-0.0.25/cli/cloud/filestorage.py +143 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/rest_helper.py +9 -2
- remotivelabs_cli-0.0.25/cli/cloud/resumable_upload.py +61 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/connect/connect.py +0 -1
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/connect/protopie/protopie.py +0 -3
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/pyproject.toml +2 -3
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/LICENSE +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/README.md +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/__about__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/__init__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/brokers.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/export.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/files.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/lib/__about__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/lib/broker.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/license_flows.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/licenses.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/playback.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/record.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/scripting.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/broker/signals.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/__init__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/auth.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/auth_tokens.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/brokers.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/configs.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/projects.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/recordings.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/recordings_playback.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/sample_recordings.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/service_account_tokens.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/cloud/service_accounts.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/connect/__init__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/errors.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/remotive.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/requirements.txt +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/settings.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/tools/__init__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/tools/can/__init__.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/tools/can/can.py +0 -0
- {remotivelabs_cli-0.0.23 → remotivelabs_cli-0.0.25}/cli/tools/tools.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: remotivelabs-cli
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.25
|
4
4
|
Summary: CLI for operating RemotiveCloud and RemotiveBroker
|
5
5
|
Author: Johan Rask
|
6
6
|
Author-email: johan.rask@remotivelabs.com
|
@@ -15,7 +15,7 @@ Requires-Dist: plotext (>=5.2,<6.0)
|
|
15
15
|
Requires-Dist: pyjwt (>=2.6,<3.0)
|
16
16
|
Requires-Dist: python-can (>=4.3.1)
|
17
17
|
Requires-Dist: python-socketio (>=4.6.1)
|
18
|
-
Requires-Dist: remotivelabs-broker (>=0.1.17)
|
18
|
+
Requires-Dist: remotivelabs-broker (>=0.1.17,<0.2.0)
|
19
19
|
Requires-Dist: rich (>=13.7.0,<13.8.0)
|
20
20
|
Requires-Dist: trogon (>=0.5.0)
|
21
21
|
Requires-Dist: typer (>=0.9.0,<0.10.0)
|
@@ -2,7 +2,7 @@ import json
|
|
2
2
|
|
3
3
|
import typer
|
4
4
|
|
5
|
-
from . import auth, brokers, configs, projects, recordings, sample_recordings, service_accounts
|
5
|
+
from . import auth, brokers, configs, filestorage, projects, recordings, sample_recordings, service_accounts
|
6
6
|
from . import rest_helper as rest
|
7
7
|
|
8
8
|
app = typer.Typer()
|
@@ -32,6 +32,7 @@ app.add_typer(auth.app, name="auth")
|
|
32
32
|
app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
|
33
33
|
app.add_typer(recordings.app, name="recordings", help="Manage recordings")
|
34
34
|
app.add_typer(configs.app, name="signal-databases", help="Manage signal databases")
|
35
|
+
app.add_typer(filestorage.app, name="storage")
|
35
36
|
app.add_typer(service_accounts.app, name="service-accounts", help="Manage project service account keys")
|
36
37
|
app.add_typer(sample_recordings.app, name="samples", help="Manage sample recordings")
|
37
38
|
|
@@ -0,0 +1,143 @@
|
|
1
|
+
import os.path
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import typer
|
5
|
+
|
6
|
+
from ..errors import ErrorPrinter
|
7
|
+
from . import rest_helper as rest
|
8
|
+
from . import resumable_upload as upload
|
9
|
+
|
10
|
+
app = typer.Typer(
|
11
|
+
rich_markup_mode="rich",
|
12
|
+
help="""
|
13
|
+
Manage files ([yellow]Beta feature not available for all customers[/yellow])
|
14
|
+
|
15
|
+
Copy file from local to remote storage and vice versa, list and delete files.
|
16
|
+
|
17
|
+
""",
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
@app.command(name="ls")
|
22
|
+
def list_files(
|
23
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
24
|
+
prefix: str = typer.Argument(default="rcs://", help="Remote storage path"),
|
25
|
+
):
|
26
|
+
"""
|
27
|
+
Listing remote files
|
28
|
+
|
29
|
+
This will list files and directories in project top level directory
|
30
|
+
remotive cloud storage ls rcs://
|
31
|
+
|
32
|
+
This will list all files and directories matching the path
|
33
|
+
remotive cloud storage ls rcs://fileOrDirectoryPrefix
|
34
|
+
|
35
|
+
This will list all files and directories in the specified directory
|
36
|
+
remotive cloud storage ls rcs://fileOrDirectory/
|
37
|
+
"""
|
38
|
+
|
39
|
+
if prefix.startswith("rcs://"):
|
40
|
+
prefix = __check_rcs_path(prefix)
|
41
|
+
else:
|
42
|
+
ErrorPrinter.print_hint("Path must start with rcs://")
|
43
|
+
exit(1)
|
44
|
+
|
45
|
+
rest.handle_get(
|
46
|
+
f"/api/project/{project}/files/storage{prefix}",
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
@app.command(name="rm")
|
51
|
+
def delete_file(
|
52
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
53
|
+
path: str = typer.Argument(default=..., help="Remote storage path to file to delete"),
|
54
|
+
):
|
55
|
+
"""
|
56
|
+
[red]Deletes[/red] a file from remote storage, this cannot be undone :fire:
|
57
|
+
|
58
|
+
[white]remotive cloud storage rm rcs://directory/filename[/white]
|
59
|
+
"""
|
60
|
+
if path.startswith("rcs://"):
|
61
|
+
prefix = __check_rcs_path(path)
|
62
|
+
else:
|
63
|
+
ErrorPrinter.print_hint("Path must start with rcs://")
|
64
|
+
exit(1)
|
65
|
+
|
66
|
+
rest.handle_delete(
|
67
|
+
f"/api/project/{project}/files/storage{prefix}",
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
@app.command(name="cp")
|
72
|
+
def copy_file(
|
73
|
+
source: str = typer.Argument(default=..., help="Remote or local path to source file"),
|
74
|
+
dest: str = typer.Argument(default=..., help="Remote or local path to destination file"),
|
75
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
76
|
+
):
|
77
|
+
"""
|
78
|
+
Copies a file to or from remote storage
|
79
|
+
|
80
|
+
remotive cloud storage cp rcs://dir/filename .
|
81
|
+
remotive cloud storage cp rcs://dir/filename filename
|
82
|
+
|
83
|
+
remotive cloud storage cp filename rcs://dir/
|
84
|
+
remotive cloud storage cp filename rcs://dir/filename
|
85
|
+
"""
|
86
|
+
|
87
|
+
if not source.startswith("rcs://") and not dest.startswith("rcs://"):
|
88
|
+
ErrorPrinter.print_hint("Source or destination path must be an rcs:// path")
|
89
|
+
exit(2)
|
90
|
+
|
91
|
+
if source.startswith("rcs://") and dest.startswith("rcs://"):
|
92
|
+
ErrorPrinter.print_hint("Currently one of source and destination path must be a local path")
|
93
|
+
exit(2)
|
94
|
+
|
95
|
+
if source.startswith("rcs://"):
|
96
|
+
rcs_path = __check_rcs_path(source)
|
97
|
+
filename = source.rsplit("/", 1)[-1]
|
98
|
+
path = Path(dest)
|
99
|
+
if path.is_dir():
|
100
|
+
if not path.exists():
|
101
|
+
ErrorPrinter.print_generic_error("Destination directory does not exist")
|
102
|
+
exit(1)
|
103
|
+
else:
|
104
|
+
dest = os.path.join(path.absolute(), filename)
|
105
|
+
|
106
|
+
else:
|
107
|
+
if not path.parent.is_dir() or not path.parent.exists():
|
108
|
+
ErrorPrinter.print_generic_error("Destination directory does not exist")
|
109
|
+
exit(1)
|
110
|
+
dest = Path(dest).absolute()
|
111
|
+
|
112
|
+
res = rest.handle_get(
|
113
|
+
f"/api/project/{project}/files/storage{rcs_path}?download=true",
|
114
|
+
return_response=True,
|
115
|
+
)
|
116
|
+
|
117
|
+
rest.download_file(save_file_name=dest, url=res.text)
|
118
|
+
|
119
|
+
else:
|
120
|
+
path = Path(source)
|
121
|
+
if not path.exists():
|
122
|
+
ErrorPrinter.print_hint("Source file does not exist")
|
123
|
+
exit(1)
|
124
|
+
filename = source.rsplit("/", 1)[-1]
|
125
|
+
rcs_path = __check_rcs_path(dest)
|
126
|
+
if rcs_path.endswith("/"):
|
127
|
+
rcs_path = rcs_path + filename
|
128
|
+
res = rest.handle_post(f"/api/project/{project}/files/storage{rcs_path}", return_response=True)
|
129
|
+
json = res.json()
|
130
|
+
url = json["url"]
|
131
|
+
content_type = json["contentType"]
|
132
|
+
try:
|
133
|
+
upload.upload_signed_url(url, source, content_type)
|
134
|
+
except IsADirectoryError:
|
135
|
+
ErrorPrinter.print_hint(f"Supplied source file '{source}' is a directory but must be a file")
|
136
|
+
|
137
|
+
|
138
|
+
def __check_rcs_path(path: str):
|
139
|
+
rcs_path = path.replace("rcs://", "/")
|
140
|
+
if rcs_path.startswith("/."):
|
141
|
+
ErrorPrinter.print_hint("Invalid path")
|
142
|
+
exit(1)
|
143
|
+
return rcs_path
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
10
10
|
from typing import Dict, List, Union
|
11
11
|
|
12
12
|
import requests
|
13
|
+
from requests.exceptions import JSONDecodeError
|
13
14
|
from rich.console import Console
|
14
15
|
from rich.progress import Progress, SpinnerColumn, TextColumn, wrap_file
|
15
16
|
|
@@ -118,7 +119,11 @@ def check_api_result(response, allow_status_codes: List[int] = None):
|
|
118
119
|
def print_api_result(response):
|
119
120
|
if response.status_code >= 200 and response.status_code < 300:
|
120
121
|
if len(response.content) > 4:
|
121
|
-
|
122
|
+
try:
|
123
|
+
print(json.dumps(response.json()))
|
124
|
+
except JSONDecodeError:
|
125
|
+
err_console.print(":boom: [bold red]Json parse error[/bold red]: Please try again and report if the error persists")
|
126
|
+
|
122
127
|
exit(0)
|
123
128
|
else:
|
124
129
|
err_console.print(f":boom: [bold red]Got status code[/bold red]: {response.status_code}")
|
@@ -205,7 +210,9 @@ def download_file(save_file_name: str, url: str):
|
|
205
210
|
if download_resp.status_code == 200:
|
206
211
|
content_length = int(download_resp.headers["Content-Length"])
|
207
212
|
with open(save_file_name, "wb") as out_file:
|
208
|
-
with wrap_file(
|
213
|
+
with wrap_file(
|
214
|
+
download_resp.raw, content_length, refresh_per_second=100, description=f"Downloading to {save_file_name}"
|
215
|
+
) as stream_with_progress:
|
209
216
|
shutil.copyfileobj(stream_with_progress, out_file)
|
210
217
|
else:
|
211
218
|
check_api_result(download_resp)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
import requests
|
4
|
+
from rich.progress import wrap_file
|
5
|
+
|
6
|
+
from ..errors import ErrorPrinter
|
7
|
+
|
8
|
+
|
9
|
+
def __get_uploaded_bytes(upload_url):
|
10
|
+
headers = {"Content-Range": "bytes */*"}
|
11
|
+
response = requests.put(upload_url, headers=headers)
|
12
|
+
if response.status_code != 308:
|
13
|
+
raise Exception(f"Failed to retrieve upload status: {response.status_code} {response.text}")
|
14
|
+
|
15
|
+
# Parse the Range header to get the last byte uploaded
|
16
|
+
range_header = response.headers.get("Range")
|
17
|
+
if range_header:
|
18
|
+
last_byte = int(range_header.split("-")[1])
|
19
|
+
return last_byte + 1
|
20
|
+
return 0
|
21
|
+
|
22
|
+
|
23
|
+
def upload_signed_url(signed_url, source_file_name, content_type):
|
24
|
+
"""
|
25
|
+
Upload file to file storage with signed url and resumable upload.
|
26
|
+
Resumable upload will only work with the same URL and not if a new signed URL is requested with the
|
27
|
+
same object id.
|
28
|
+
:param signed_url:
|
29
|
+
:param source_file_name:
|
30
|
+
:return:
|
31
|
+
"""
|
32
|
+
|
33
|
+
file_size = os.path.getsize(source_file_name)
|
34
|
+
headers = {"x-goog-resumable": "start", "content-type": content_type}
|
35
|
+
response = requests.post(signed_url, headers=headers)
|
36
|
+
if response.status_code not in (200, 201, 308):
|
37
|
+
ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
|
38
|
+
exit(1)
|
39
|
+
|
40
|
+
upload_url = response.headers["Location"]
|
41
|
+
|
42
|
+
# Check how many bytes have already been uploaded
|
43
|
+
uploaded_bytes = __get_uploaded_bytes(upload_url)
|
44
|
+
|
45
|
+
# Upload the remaining file in chunks
|
46
|
+
# Not sure what a good chunk size is or if we even should have resumable uploads here, probably not..
|
47
|
+
chunk_size = 256 * 1024 * 10
|
48
|
+
# Upload the file in chunks
|
49
|
+
with open(source_file_name, "rb") as f:
|
50
|
+
with wrap_file(f, os.stat(source_file_name).st_size, description=f"Uploading {source_file_name}...") as file:
|
51
|
+
file.seek(uploaded_bytes) # Seek to the position of the last uploaded byte
|
52
|
+
for chunk_start in range(uploaded_bytes, file_size, chunk_size):
|
53
|
+
chunk_end = min(chunk_start + chunk_size, file_size) - 1
|
54
|
+
chunk = file.read(chunk_end - chunk_start + 1)
|
55
|
+
headers = {"Content-Range": f"bytes {chunk_start}-{chunk_end}/{file_size}"}
|
56
|
+
response = requests.put(upload_url, headers=headers, data=chunk)
|
57
|
+
if response.status_code not in (200, 201, 308):
|
58
|
+
ErrorPrinter.print_generic_error(f"Failed to upload file: {response.status_code} - {response.text}")
|
59
|
+
exit(1)
|
60
|
+
|
61
|
+
print(f"File {source_file_name} uploaded successfully.")
|
@@ -97,8 +97,6 @@ def _connect_to_broker(
|
|
97
97
|
signal_name = get_signal_name(expression, s.name())
|
98
98
|
io.emit("ppMessage", {"messageId": signal_name, "value": str(s.value())})
|
99
99
|
|
100
|
-
print(signals_to_subscribe_to)
|
101
|
-
|
102
100
|
grpc_connect(on_signals, signals_to_subscribe_to)
|
103
101
|
|
104
102
|
|
@@ -110,7 +108,6 @@ def grpc_connect(on_signals, signals_to_subscribe_to: Union[List[SignalIdentifie
|
|
110
108
|
client.connect(url=broker, api_key=x_api_key)
|
111
109
|
client.on_signals = on_signals
|
112
110
|
|
113
|
-
print(signals_to_subscribe_to)
|
114
111
|
subscription = client.subscribe(signals_to_subscribe_to=signals_to_subscribe_to, changed_values_only=False)
|
115
112
|
pretty_print("Subscription to broker completed")
|
116
113
|
pretty_print("Waiting for signals...")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "remotivelabs-cli"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.25"
|
4
4
|
description = "CLI for operating RemotiveCloud and RemotiveBroker"
|
5
5
|
authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
|
6
6
|
readme = "README.md"
|
@@ -10,7 +10,7 @@ packages = [{include = "cli"}]
|
|
10
10
|
trogon = ">=0.5.0"
|
11
11
|
python = "~=3.8"
|
12
12
|
typer = "~=0.9.0"
|
13
|
-
remotivelabs-broker = "
|
13
|
+
remotivelabs-broker = "~=0.1.17"
|
14
14
|
rich = "~=13.7.0"
|
15
15
|
pyjwt = "~=2.6"
|
16
16
|
zeroconf = "~=0.127.0"
|
@@ -18,7 +18,6 @@ websocket-client ="~=1.6"
|
|
18
18
|
plotext = "~=5.2"
|
19
19
|
python-socketio = ">=4.6.1"
|
20
20
|
python-can = ">=4.3.1"
|
21
|
-
|
22
21
|
[tool.poetry.scripts]
|
23
22
|
remotive = "cli.remotive:app"
|
24
23
|
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|