remotivelabs-cli 0.5.0a1__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.
- remotivelabs/cli/__init__.py +0 -0
- remotivelabs/cli/api/cloud/tokens.py +62 -0
- remotivelabs/cli/broker/__init__.py +33 -0
- remotivelabs/cli/broker/defaults.py +1 -0
- remotivelabs/cli/broker/discovery.py +43 -0
- remotivelabs/cli/broker/export.py +92 -0
- remotivelabs/cli/broker/files.py +119 -0
- remotivelabs/cli/broker/lib/__about__.py +4 -0
- remotivelabs/cli/broker/lib/broker.py +625 -0
- remotivelabs/cli/broker/lib/client.py +224 -0
- remotivelabs/cli/broker/lib/helper.py +277 -0
- remotivelabs/cli/broker/lib/signalcreator.py +196 -0
- remotivelabs/cli/broker/license_flows.py +167 -0
- remotivelabs/cli/broker/licenses.py +98 -0
- remotivelabs/cli/broker/playback.py +117 -0
- remotivelabs/cli/broker/record.py +41 -0
- remotivelabs/cli/broker/recording_session/__init__.py +3 -0
- remotivelabs/cli/broker/recording_session/client.py +67 -0
- remotivelabs/cli/broker/recording_session/cmd.py +254 -0
- remotivelabs/cli/broker/recording_session/time.py +49 -0
- remotivelabs/cli/broker/scripting.py +129 -0
- remotivelabs/cli/broker/signals.py +220 -0
- remotivelabs/cli/broker/version.py +31 -0
- remotivelabs/cli/cloud/__init__.py +17 -0
- remotivelabs/cli/cloud/auth/__init__.py +3 -0
- remotivelabs/cli/cloud/auth/cmd.py +128 -0
- remotivelabs/cli/cloud/auth/login.py +283 -0
- remotivelabs/cli/cloud/auth_tokens.py +149 -0
- remotivelabs/cli/cloud/brokers.py +109 -0
- remotivelabs/cli/cloud/configs.py +109 -0
- remotivelabs/cli/cloud/licenses/__init__.py +0 -0
- remotivelabs/cli/cloud/licenses/cmd.py +14 -0
- remotivelabs/cli/cloud/organisations.py +112 -0
- remotivelabs/cli/cloud/projects.py +44 -0
- remotivelabs/cli/cloud/recordings.py +580 -0
- remotivelabs/cli/cloud/recordings_playback.py +274 -0
- remotivelabs/cli/cloud/resumable_upload.py +87 -0
- remotivelabs/cli/cloud/sample_recordings.py +25 -0
- remotivelabs/cli/cloud/service_account_tokens.py +62 -0
- remotivelabs/cli/cloud/service_accounts.py +72 -0
- remotivelabs/cli/cloud/storage/__init__.py +5 -0
- remotivelabs/cli/cloud/storage/cmd.py +76 -0
- remotivelabs/cli/cloud/storage/copy.py +86 -0
- remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
- remotivelabs/cli/cloud/uri.py +113 -0
- remotivelabs/cli/connect/__init__.py +0 -0
- remotivelabs/cli/connect/connect.py +118 -0
- remotivelabs/cli/connect/protopie/protopie.py +185 -0
- remotivelabs/cli/py.typed +0 -0
- remotivelabs/cli/remotive.py +123 -0
- remotivelabs/cli/settings/__init__.py +20 -0
- remotivelabs/cli/settings/config_file.py +113 -0
- remotivelabs/cli/settings/core.py +333 -0
- remotivelabs/cli/settings/migration/__init__.py +0 -0
- remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
- remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
- remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
- remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
- remotivelabs/cli/settings/migration/migration_tools.py +38 -0
- remotivelabs/cli/settings/state_file.py +67 -0
- remotivelabs/cli/settings/token_file.py +128 -0
- remotivelabs/cli/tools/__init__.py +0 -0
- remotivelabs/cli/tools/can/__init__.py +0 -0
- remotivelabs/cli/tools/can/can.py +78 -0
- remotivelabs/cli/tools/tools.py +9 -0
- remotivelabs/cli/topology/__init__.py +28 -0
- remotivelabs/cli/topology/all.py +322 -0
- remotivelabs/cli/topology/cli/__init__.py +3 -0
- remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
- remotivelabs/cli/topology/cli/topology_cli.py +16 -0
- remotivelabs/cli/topology/cmd.py +130 -0
- remotivelabs/cli/topology/start_trial.py +134 -0
- remotivelabs/cli/typer/__init__.py +0 -0
- remotivelabs/cli/typer/typer_utils.py +27 -0
- remotivelabs/cli/utils/__init__.py +0 -0
- remotivelabs/cli/utils/console.py +99 -0
- remotivelabs/cli/utils/rest_helper.py +369 -0
- remotivelabs/cli/utils/time.py +11 -0
- remotivelabs/cli/utils/versions.py +120 -0
- remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
- remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
- remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
- remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
- remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
|
|
13
|
+
import grpc
|
|
14
|
+
import requests
|
|
15
|
+
import typer
|
|
16
|
+
from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, track
|
|
17
|
+
from typing_extensions import Annotated
|
|
18
|
+
|
|
19
|
+
from remotivelabs.cli.broker.lib.broker import Broker
|
|
20
|
+
from remotivelabs.cli.cloud.recordings_playback import app as playback_app
|
|
21
|
+
from remotivelabs.cli.cloud.uri import URI
|
|
22
|
+
from remotivelabs.cli.typer import typer_utils
|
|
23
|
+
from remotivelabs.cli.utils.console import (
|
|
24
|
+
print_generic_error,
|
|
25
|
+
print_generic_message,
|
|
26
|
+
print_grpc_error,
|
|
27
|
+
print_hint,
|
|
28
|
+
print_success,
|
|
29
|
+
print_unformatted,
|
|
30
|
+
print_unformatted_to_stderr,
|
|
31
|
+
)
|
|
32
|
+
from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
|
|
33
|
+
|
|
34
|
+
app = typer_utils.create_typer()
|
|
35
|
+
app.add_typer(playback_app, name="playback")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def uid(p: Any) -> Any:
|
|
39
|
+
# TODO: use log instead of print for debug information?
|
|
40
|
+
print_generic_message(p)
|
|
41
|
+
return p["uid"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ruff: noqa: FA100
|
|
45
|
+
# ruff: noqa: C901
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command("list")
|
|
49
|
+
def list_recordings(
|
|
50
|
+
is_processing: bool = typer.Option(default=False, help="Use this option to see only those that are beeing processed or are invalid"),
|
|
51
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
List all recording sessions in a project. You can choose to see all valid recordings (default) or use
|
|
55
|
+
--is-processing and you will get those that are currently beeing processed or that failed to be validated.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if is_processing:
|
|
59
|
+
res = Rest.handle_get(f"/api/project/{project}/files/recording/processing", return_response=True)
|
|
60
|
+
json_res: List[Dict[str, Any]] = res.json()
|
|
61
|
+
print_generic_message(json.dumps(list(filter(lambda r: r["status"] == "RUNNING" or r["status"] == "FAILED", json_res))))
|
|
62
|
+
else:
|
|
63
|
+
Rest.handle_get(f"/api/project/{project}/files/recording")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.command(help="Shows details about a specific recording in project")
|
|
67
|
+
def describe(
|
|
68
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
69
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
70
|
+
) -> None:
|
|
71
|
+
Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command(name="import")
|
|
75
|
+
def import_as_recording(
|
|
76
|
+
uri: Annotated[URI, typer.Argument(help="Remote storage path", parser=URI, show_default=False)],
|
|
77
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
78
|
+
) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Imports a file from Storage as a recording.
|
|
81
|
+
|
|
82
|
+
NOTE that Storage is not yet available to all customers
|
|
83
|
+
"""
|
|
84
|
+
Rest.handle_post(
|
|
85
|
+
url=f"/api/project/{project}/files/recording",
|
|
86
|
+
return_response=True,
|
|
87
|
+
progress_label=f"Importing {uri.path}...",
|
|
88
|
+
body=json.dumps({"path": uri.path}),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
print_hint(f"Import started, you can track progress with 'remotive cloud recordings list --is-processing --project {project}'")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def do_start(name: str, project: str, api_key: str, return_response: bool = False) -> requests.Response:
|
|
95
|
+
body = {"size": "S"}
|
|
96
|
+
if not api_key:
|
|
97
|
+
body["apiKey"] = api_key
|
|
98
|
+
|
|
99
|
+
return Rest.handle_post(
|
|
100
|
+
f"/api/project/{project}/brokers/{name}",
|
|
101
|
+
body=json.dumps(body),
|
|
102
|
+
return_response=return_response,
|
|
103
|
+
progress_label=f"Starting {name}...",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command(help="Prepares all recording files and transformations to be available for playback")
|
|
108
|
+
def mount( # noqa: C901
|
|
109
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
110
|
+
broker: Optional[str] = typer.Option(None, help="Broker to use"),
|
|
111
|
+
ensure_broker_started: bool = typer.Option(default=False, help="Ensure broker exists, start otherwise"),
|
|
112
|
+
transformation_name: str = typer.Option("default", help="Specify a custom signal transformation to use"),
|
|
113
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
114
|
+
) -> None:
|
|
115
|
+
Rest.ensure_auth_token()
|
|
116
|
+
|
|
117
|
+
Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
|
|
118
|
+
|
|
119
|
+
if broker is None:
|
|
120
|
+
r = Rest.handle_get(url=f"/api/project/{project}/brokers/personal", return_response=True, allow_status_codes=[404])
|
|
121
|
+
|
|
122
|
+
if r.status_code == 200:
|
|
123
|
+
broker_info = r.json()
|
|
124
|
+
broker = broker_info["shortName"]
|
|
125
|
+
elif r.status_code == 404:
|
|
126
|
+
r = do_start("personal", project, "", return_response=True)
|
|
127
|
+
if r.status_code != 200:
|
|
128
|
+
print_generic_error(r.text)
|
|
129
|
+
sys.exit(0)
|
|
130
|
+
else:
|
|
131
|
+
print_generic_error(f"Got http status code {r.status_code}")
|
|
132
|
+
raise typer.Exit(0)
|
|
133
|
+
else:
|
|
134
|
+
r = Rest.handle_get(url=f"/api/project/{project}/brokers/{broker}", return_response=True, allow_status_codes=[404])
|
|
135
|
+
|
|
136
|
+
if r.status_code == 404:
|
|
137
|
+
if ensure_broker_started:
|
|
138
|
+
r = do_start(broker, project, "", return_response=True)
|
|
139
|
+
if r.status_code != 200:
|
|
140
|
+
print_generic_error(r.text)
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
else:
|
|
143
|
+
print_generic_error(f"Broker {broker} not running")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
elif r.status_code != 200:
|
|
146
|
+
sys.stderr.write(f"Got http status code {r.status_code}")
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
|
|
149
|
+
broker_info = r.json()
|
|
150
|
+
broker = broker_info["shortName"]
|
|
151
|
+
broker_config_query = ""
|
|
152
|
+
if transformation_name != "default":
|
|
153
|
+
broker_config_query = f"?brokerConfigName={transformation_name}"
|
|
154
|
+
|
|
155
|
+
Rest.handle_get(
|
|
156
|
+
f"/api/project/{project}/files/recording/{recording_session}/upload{broker_config_query}",
|
|
157
|
+
params={"brokerName": broker},
|
|
158
|
+
return_response=True,
|
|
159
|
+
progress_label="Preparing recording on broker...",
|
|
160
|
+
)
|
|
161
|
+
print_unformatted_to_stderr("Successfully mounted recording on broker")
|
|
162
|
+
print_unformatted(json.dumps(broker_info))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command(help="Downloads the specified recording file to disk")
|
|
166
|
+
def download_recording_file(
|
|
167
|
+
recording_file_name: str = typer.Argument(..., help="Recording file to download"),
|
|
168
|
+
recording_session: str = typer.Option(
|
|
169
|
+
..., help="Recording session id that this file belongs to", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"
|
|
170
|
+
),
|
|
171
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
172
|
+
) -> None:
|
|
173
|
+
Rest.ensure_auth_token()
|
|
174
|
+
recording_file_name_qouted = quote(recording_file_name, safe="")
|
|
175
|
+
get_signed_url_resp = requests.get(
|
|
176
|
+
f"{Rest.get_base_url()}/api/project/{project}/files/recording/{recording_session}/recording-file/{recording_file_name_qouted}",
|
|
177
|
+
headers=Rest.get_headers(),
|
|
178
|
+
allow_redirects=True,
|
|
179
|
+
timeout=60,
|
|
180
|
+
)
|
|
181
|
+
if get_signed_url_resp.status_code == 200:
|
|
182
|
+
# Next download the actual file
|
|
183
|
+
Rest.download_file(Path(recording_file_name), get_signed_url_resp.json()["downloadUrl"])
|
|
184
|
+
print_success(f"Downloaded {recording_file_name}")
|
|
185
|
+
else:
|
|
186
|
+
print_generic_error(get_signed_url_resp.text)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@app.command(name="delete")
|
|
190
|
+
def delete(
|
|
191
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
192
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
193
|
+
) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Deletes the specified recording session including all media files and configurations.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
Rest.handle_delete(f"/api/project/{project}/files/recording/{recording_session}")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.command(name="delete-recording-file")
|
|
202
|
+
def delete_recording_file(
|
|
203
|
+
recording_file_name: str = typer.Argument(..., help="Recording file to download"),
|
|
204
|
+
recording_session: str = typer.Option(
|
|
205
|
+
..., help="Recording session id that this file belongs to", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"
|
|
206
|
+
),
|
|
207
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Deletes the specified recording file
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
Rest.handle_delete(f"/api/project/{project}/files/recording/{recording_session}/recording-file/{recording_file_name}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@app.command()
|
|
217
|
+
def upload( # noqa: C901, PLR0912, PLR0915
|
|
218
|
+
path: Path = typer.Argument(
|
|
219
|
+
...,
|
|
220
|
+
exists=True,
|
|
221
|
+
file_okay=True,
|
|
222
|
+
dir_okay=False,
|
|
223
|
+
writable=False,
|
|
224
|
+
readable=True,
|
|
225
|
+
resolve_path=True,
|
|
226
|
+
help="Path to recording file to upload",
|
|
227
|
+
),
|
|
228
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
229
|
+
recording_session: Optional[str] = typer.Option(default=None, help="Optional existing recording to upload file to"),
|
|
230
|
+
) -> None:
|
|
231
|
+
"""
|
|
232
|
+
Uploads a recording to RemotiveCloud.
|
|
233
|
+
Except for recordings from RemotiveBroker you can also upload Can ASC (.asc), Can BLF(.blf) and Can LOG (.log, .txt)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
filename = os.path.basename(path.name)
|
|
237
|
+
Rest.ensure_auth_token()
|
|
238
|
+
|
|
239
|
+
if recording_session is None:
|
|
240
|
+
r = Rest.handle_post(f"/api/project/{project}/files/recording/{filename}", return_response=True)
|
|
241
|
+
else:
|
|
242
|
+
r = Rest.handle_post(f"/api/project/{project}/files/recording/{recording_session}/recording-file/{filename}", return_response=True)
|
|
243
|
+
if r is None:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
upload_url = r.text
|
|
247
|
+
url_path = urllib.parse.urlparse(upload_url).path
|
|
248
|
+
# Upload_id is the first part of the path
|
|
249
|
+
match = re.match(r"^/([^/]+)/organisation/(.*)$", url_path)
|
|
250
|
+
if match:
|
|
251
|
+
upload_id = match.group(1)
|
|
252
|
+
else:
|
|
253
|
+
print_generic_error("Something went wrong, please try again. Please contact RemotiveLabs support if this problem remains")
|
|
254
|
+
print_hint("Please make sure to use the latest version of RemotiveCLI")
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
|
|
257
|
+
upload_response = Rest.upload_file_with_signed_url(
|
|
258
|
+
path=path, url=upload_url, upload_headers={"Content-Type": "application/x-www-form-urlencoded"}, return_response=True
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if upload_response is None:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
# Exact same as in cloud console
|
|
265
|
+
def get_processing_message(step: str) -> str: # noqa: PLR0911
|
|
266
|
+
if step == "REQUESTED":
|
|
267
|
+
return "Preparing file..."
|
|
268
|
+
if step == "VALIDATING":
|
|
269
|
+
return "Validating file..."
|
|
270
|
+
if step == "CONVERT":
|
|
271
|
+
return "Converting file..."
|
|
272
|
+
if step == "SPLIT":
|
|
273
|
+
return "Splitting file..."
|
|
274
|
+
if step == "ZIP":
|
|
275
|
+
return "Compressing file..."
|
|
276
|
+
if step == "FINALIZE":
|
|
277
|
+
return "Finishing up..."
|
|
278
|
+
return "Processing..."
|
|
279
|
+
|
|
280
|
+
if 200 <= upload_response.status_code < 300:
|
|
281
|
+
# We need to print the error message outside the with Progress so the indicator is closed
|
|
282
|
+
error_message: Union[str, None] = None
|
|
283
|
+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True) as p:
|
|
284
|
+
t = p.add_task("Processing...", total=1)
|
|
285
|
+
while True:
|
|
286
|
+
time.sleep(1)
|
|
287
|
+
r = Rest.handle_get(
|
|
288
|
+
f"/api/project/{project}/files/recording/processing", return_response=True, use_progress_indicator=False
|
|
289
|
+
)
|
|
290
|
+
if r is None:
|
|
291
|
+
return
|
|
292
|
+
status_list: List[Dict[str, Any]] = r.json()
|
|
293
|
+
res = list(filter(lambda s: s["uploadId"] == upload_id, status_list))
|
|
294
|
+
if len(res) == 1:
|
|
295
|
+
tracking_state = res[0]
|
|
296
|
+
if tracking_state["status"] != "FAILED" and tracking_state["status"] != "SUCCESS":
|
|
297
|
+
p.update(task_id=t, description=get_processing_message(tracking_state["step"]))
|
|
298
|
+
else:
|
|
299
|
+
if tracking_state["status"] == "FAILED":
|
|
300
|
+
error_message = f"Processing of uploaded file failed: {tracking_state['errors'][0]['message']}"
|
|
301
|
+
else:
|
|
302
|
+
print_success("File successfully uploaded")
|
|
303
|
+
break
|
|
304
|
+
else:
|
|
305
|
+
error_message = "Something went wrong, please try again. Please contact RemotiveLabs support if this problem remains"
|
|
306
|
+
break
|
|
307
|
+
if error_message is not None:
|
|
308
|
+
print_generic_error(error_message)
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
311
|
+
else:
|
|
312
|
+
print_generic_error(f"Got status code: {upload_response.status_code} {upload_response.text}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# TODO - Change to use Path for directory
|
|
316
|
+
@app.command()
|
|
317
|
+
def upload_broker_configuration(
|
|
318
|
+
directory: Path = typer.Argument(
|
|
319
|
+
...,
|
|
320
|
+
exists=True,
|
|
321
|
+
file_okay=False,
|
|
322
|
+
dir_okay=True,
|
|
323
|
+
writable=False,
|
|
324
|
+
readable=True,
|
|
325
|
+
resolve_path=True,
|
|
326
|
+
help="Directory to upload",
|
|
327
|
+
),
|
|
328
|
+
recording_session: str = typer.Option(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
329
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
330
|
+
overwrite: bool = typer.Option(False, help="Overwrite existing configuration if it exists"),
|
|
331
|
+
) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Uploads a broker configuration directory
|
|
334
|
+
"""
|
|
335
|
+
# Must end with /
|
|
336
|
+
|
|
337
|
+
#
|
|
338
|
+
# List files in specified directory. Look for interfaces.json and use that directory where this is located
|
|
339
|
+
# as configuration home directory
|
|
340
|
+
#
|
|
341
|
+
files = list(filter(lambda item: "interfaces.json" in item, glob.iglob(str(directory) + "/**/**", recursive=True)))
|
|
342
|
+
if len(files) == 0:
|
|
343
|
+
sys.stderr.write("No interfaces.json found in directory, this file is required")
|
|
344
|
+
raise typer.Exit(1)
|
|
345
|
+
if len(files) > 1:
|
|
346
|
+
sys.stderr.write(f"{len(files)} interfaces.json found in directoryw which is not supported")
|
|
347
|
+
raise typer.Exit(1)
|
|
348
|
+
broker_config_dir_name = os.path.dirname(files[0]).rsplit("/", 1)[-1]
|
|
349
|
+
|
|
350
|
+
#
|
|
351
|
+
# Get the current details about broker configurations to see if a config with this
|
|
352
|
+
# name already exists
|
|
353
|
+
#
|
|
354
|
+
# task = progress.add_task(description=f"Preparing upload of {broker_config_dir_name}", total=1)
|
|
355
|
+
details_resp = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
|
|
356
|
+
if details_resp is None:
|
|
357
|
+
return
|
|
358
|
+
details = details_resp.json()
|
|
359
|
+
existing_configs = details["brokerConfigurations"]
|
|
360
|
+
if len(existing_configs) > 0:
|
|
361
|
+
data = list(filter(lambda x: x["name"] == broker_config_dir_name, existing_configs))
|
|
362
|
+
if len(data) > 0:
|
|
363
|
+
if overwrite:
|
|
364
|
+
Rest.handle_delete(
|
|
365
|
+
f"/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_dir_name}", quiet=True
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
sys.stderr.write("Broker configuration already exists, use --overwrite to replace\n")
|
|
369
|
+
raise typer.Exit(1)
|
|
370
|
+
|
|
371
|
+
#
|
|
372
|
+
# From the list of files, create a tuple of local_path to the actual file
|
|
373
|
+
# and a remote path as it should be stored in cloud
|
|
374
|
+
#
|
|
375
|
+
file_infos = list(
|
|
376
|
+
map(
|
|
377
|
+
lambda item: {"local_path": item, "remote_path": f"/{broker_config_dir_name}{item.rsplit(broker_config_dir_name, 1)[-1]}"},
|
|
378
|
+
glob.iglob(str(directory) + "/**/*.*", recursive=True),
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
#
|
|
383
|
+
# convert this remote paths and ask cloud to prepare upload urls for those
|
|
384
|
+
#
|
|
385
|
+
json_request_upload_urls_req = {"name": "not_used", "paths": list(map(lambda x: x["remote_path"], file_infos))}
|
|
386
|
+
|
|
387
|
+
response = Rest.handle_put(
|
|
388
|
+
url=f"/api/project/{project}/files/recording/{recording_session}/configuration",
|
|
389
|
+
return_response=True,
|
|
390
|
+
body=json.dumps(json_request_upload_urls_req),
|
|
391
|
+
)
|
|
392
|
+
if response is None:
|
|
393
|
+
return
|
|
394
|
+
if response.status_code != 200:
|
|
395
|
+
print_generic_error(f"Failed to prepare configuration upload: {response.text} - {response.status_code}")
|
|
396
|
+
raise typer.Exit(1)
|
|
397
|
+
|
|
398
|
+
#
|
|
399
|
+
# Upload urls is a remote_path : upload_url dict
|
|
400
|
+
# '/my_config/interfaces.json' : "<upload_url>"
|
|
401
|
+
#
|
|
402
|
+
upload_urls = json.loads(response.text)
|
|
403
|
+
|
|
404
|
+
# For each file - upload
|
|
405
|
+
for file in track(file_infos, description="Uploading..."):
|
|
406
|
+
key = file["remote_path"]
|
|
407
|
+
path = file["local_path"]
|
|
408
|
+
url = upload_urls[key]
|
|
409
|
+
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
410
|
+
r = requests.put(url, open(path, "rb"), headers=headers, timeout=60)
|
|
411
|
+
if r.status_code != 200:
|
|
412
|
+
print_generic_error(f"Failed to upload broker configuration: {r.text} - {r.status_code}")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
|
|
415
|
+
print_success(f"Uploaded broker configuration {broker_config_dir_name}")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@app.command(help="Downloads the specified broker configuration directory as zip file")
|
|
419
|
+
def download_broker_configuration(
|
|
420
|
+
broker_config_name: str = typer.Argument(..., help="Broker config name"),
|
|
421
|
+
recording_session: str = typer.Option(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
422
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
423
|
+
) -> None:
|
|
424
|
+
Rest.ensure_auth_token()
|
|
425
|
+
r = Rest.handle_get(
|
|
426
|
+
url=f"/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_name}", return_response=True
|
|
427
|
+
)
|
|
428
|
+
if r is None:
|
|
429
|
+
return
|
|
430
|
+
filename = get_filename_from_cd(r.headers.get("content-disposition"))
|
|
431
|
+
if filename is not None:
|
|
432
|
+
with open(filename, "wb") as f:
|
|
433
|
+
f.write(r.content)
|
|
434
|
+
print_success(f"Downloaded file {filename}")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@app.command(help="Delete the specified broker configuration")
|
|
438
|
+
def delete_broker_configuration(
|
|
439
|
+
broker_config_name: str = typer.Argument(..., help="Broker config name"),
|
|
440
|
+
recording_session: str = typer.Option(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
441
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
442
|
+
) -> None:
|
|
443
|
+
Rest.handle_delete(url=f"/api/project/{project}/files/recording/{recording_session}/configuration/{broker_config_name}")
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@app.command(help="Copy recording to another project")
|
|
447
|
+
def copy(
|
|
448
|
+
recording_session: str = typer.Argument(..., help="Recording session id"),
|
|
449
|
+
from_project: str = typer.Option(..., help="Source project"),
|
|
450
|
+
to_project: str = typer.Option(..., help="Destination project"),
|
|
451
|
+
) -> None:
|
|
452
|
+
Rest.handle_post(
|
|
453
|
+
url=f"/api/project/{from_project}/files/recording/{recording_session}/copy", body=json.dumps({"projectUid": to_project})
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@app.command(deprecated=True)
|
|
458
|
+
def play(
|
|
459
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
460
|
+
broker: str = typer.Option(None, help="Broker to use"),
|
|
461
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
462
|
+
) -> None:
|
|
463
|
+
"""
|
|
464
|
+
Plays a recording (Deprecated - Use recordings playback play)"
|
|
465
|
+
"""
|
|
466
|
+
_do_change_playback_mode("play", recording_session, broker, project)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@app.command(deprecated=True)
|
|
470
|
+
def pause(
|
|
471
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
472
|
+
broker: str = typer.Option(None, help="Broker to use"),
|
|
473
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
474
|
+
) -> None:
|
|
475
|
+
"""
|
|
476
|
+
Pause recording (Deprecated - Use recordings playback pause")
|
|
477
|
+
"""
|
|
478
|
+
_do_change_playback_mode("pause", recording_session, broker, project)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@app.command(deprecated=True)
|
|
482
|
+
def seek(
|
|
483
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
484
|
+
seconds: int = typer.Option(..., min=0, help="Target offset in seconds"),
|
|
485
|
+
broker: str = typer.Option(None, help="Broker to use"),
|
|
486
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
487
|
+
) -> None:
|
|
488
|
+
"""
|
|
489
|
+
Seek into recording (Deprecated - Use recordings playback seek)
|
|
490
|
+
"""
|
|
491
|
+
_do_change_playback_mode("seek", recording_session, broker, project, seconds)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
@app.command(deprecated=True)
|
|
495
|
+
def stop(
|
|
496
|
+
recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
|
|
497
|
+
broker: str = typer.Option(None, help="Broker to use"),
|
|
498
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
|
499
|
+
) -> None:
|
|
500
|
+
"""
|
|
501
|
+
Stop recording (Deprecated - Use recordings playback stop)
|
|
502
|
+
"""
|
|
503
|
+
_do_change_playback_mode("stop", recording_session, broker, project)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _do_change_playback_mode( # noqa: PLR0912
|
|
507
|
+
mode: str, recording_session: str, broker_name: Optional[str], project: str, seconds: Optional[int] = None
|
|
508
|
+
) -> None:
|
|
509
|
+
response = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
|
|
510
|
+
if response is None:
|
|
511
|
+
return
|
|
512
|
+
r = json.loads(response.text)
|
|
513
|
+
recordings: List[Any] = r["recordings"]
|
|
514
|
+
files = list(map(lambda rec: {"recording": rec["fileName"], "namespace": rec["metadata"]["namespace"]}, recordings))
|
|
515
|
+
|
|
516
|
+
if broker_name is not None:
|
|
517
|
+
response = Rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
|
|
518
|
+
else:
|
|
519
|
+
response = Rest.handle_get(f"/api/project/{project}/brokers/personal", return_response=True, allow_status_codes=[404])
|
|
520
|
+
if response is None:
|
|
521
|
+
return
|
|
522
|
+
if response.status_code == 404:
|
|
523
|
+
broker_arg = ""
|
|
524
|
+
if broker_name is not None:
|
|
525
|
+
broker_arg = f" --broker {broker_name} --ensure-broker-started"
|
|
526
|
+
print_generic_error("You need to mount the recording before you play")
|
|
527
|
+
print_hint(f"remotive cloud recordings mount {recording_session}{broker_arg} --project {project}")
|
|
528
|
+
sys.exit(1)
|
|
529
|
+
|
|
530
|
+
broker_info = json.loads(response.text)
|
|
531
|
+
broker = Broker(broker_info["url"], None)
|
|
532
|
+
try:
|
|
533
|
+
# Here we try to verify that we are operating on a recording that is mounted on the
|
|
534
|
+
# broker so we can verify this before we try playback and can also present some good
|
|
535
|
+
# error messages
|
|
536
|
+
tmp = os.path.join(tempfile.gettempdir(), os.urandom(24).hex())
|
|
537
|
+
broker.download(".cloud.context", tmp, True)
|
|
538
|
+
with open(tmp, "r", encoding="utf8") as f:
|
|
539
|
+
json_context = json.loads(f.read())
|
|
540
|
+
if json_context["recordingSessionId"] != recording_session:
|
|
541
|
+
print_generic_error(
|
|
542
|
+
f"The recording id mounted is '{json_context['recordingSessionId']}' "
|
|
543
|
+
f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
|
|
544
|
+
)
|
|
545
|
+
print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
except grpc.RpcError as rpc_error:
|
|
548
|
+
print_grpc_error(rpc_error)
|
|
549
|
+
sys.exit(1)
|
|
550
|
+
if mode == "pause":
|
|
551
|
+
broker.pause_play(files, True)
|
|
552
|
+
elif mode == "play":
|
|
553
|
+
r = broker.play(files, True)
|
|
554
|
+
elif mode == "seek":
|
|
555
|
+
if seconds is not None:
|
|
556
|
+
broker.seek(files, int(seconds * 1000000), True)
|
|
557
|
+
else:
|
|
558
|
+
broker.seek(files, 0, True)
|
|
559
|
+
elif mode == "stop":
|
|
560
|
+
broker.seek(files, 0, True)
|
|
561
|
+
else:
|
|
562
|
+
raise ValueError(f"Illegal command {mode}")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def get_filename_from_cd(cd: Union[str, None]) -> Union[str, None]:
|
|
566
|
+
"""
|
|
567
|
+
Get filename from content-disposition
|
|
568
|
+
"""
|
|
569
|
+
if not cd:
|
|
570
|
+
return None
|
|
571
|
+
fname = re.findall("filename=(.+)", cd)
|
|
572
|
+
if len(fname) == 0:
|
|
573
|
+
return None
|
|
574
|
+
return str(fname[0])
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def use_progress(label: str) -> Tuple[Progress, TaskID]:
|
|
578
|
+
p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True)
|
|
579
|
+
t = p.add_task(label, total=1)
|
|
580
|
+
return (p, t)
|