remotivelabs-cli 0.0.16__py3-none-any.whl → 0.0.18__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/broker/brokers.py CHANGED
@@ -11,7 +11,7 @@ from zeroconf import (
11
11
  Zeroconf,
12
12
  )
13
13
 
14
- from . import export, files, playback, record, scripting, signals
14
+ from . import export, files, licenses, playback, record, scripting, signals
15
15
 
16
16
  app = typer.Typer(rich_markup_mode="rich")
17
17
 
@@ -85,7 +85,8 @@ app.add_typer(record.app, name="record", help="Record data on buses")
85
85
  app.add_typer(files.app, name="files", help="Upload/Download configurations and recordings")
86
86
  app.add_typer(signals.app, name="signals", help="Find and subscribe to signals")
87
87
  app.add_typer(export.app, name="export", help="Export to external formats")
88
- app.add_typer(scripting.app, name="scripting")
88
+ app.add_typer(scripting.app, name="scripting", help="LUA scripting utilities")
89
+ app.add_typer(licenses.app, name="license", help="View and request license to broker")
89
90
 
90
91
  if __name__ == "__main__":
91
92
  app()
cli/broker/export.py CHANGED
@@ -36,7 +36,7 @@ def influxdb(
36
36
  This is a sample for exporting and importing to InfluxDB using remotive-cli and influx-cli
37
37
 
38
38
  Export:
39
- remotive broker export influxdb --url [URL] --output signals.influx --namespace VehicleBus \\
39
+ remotive broker export influxdb --url [URL] --output signals.influx \\
40
40
  --signal vehiclebus:Control.SteeringWheel_Position --signal Control.Accelerator_PedalPosition \\
41
41
  --signal vehiclebus:GpsPosition.GPS_Longitude --signal vehiclebus:GpsPosition.GPS_Latitude
42
42
 
cli/broker/lib/broker.py CHANGED
@@ -12,7 +12,7 @@ import typing
12
12
  import zipfile
13
13
  from dataclasses import dataclass
14
14
  from threading import Thread
15
- from typing import Callable, List, Sequence
15
+ from typing import Callable, List, Sequence, Union
16
16
 
17
17
  import grpc
18
18
  import remotivelabs.broker.generated.sync.traffic_api_pb2 as traffic_api
@@ -33,8 +33,16 @@ class SubscribableSignal:
33
33
  namespace: str
34
34
 
35
35
 
36
+ @dataclass
37
+ class LicenseInfo:
38
+ valid: bool
39
+ expires: str
40
+ email: str
41
+ machine_id: str
42
+
43
+
36
44
  class Broker:
37
- def __init__(self, url: str, api_key):
45
+ def __init__(self, url: str, api_key: Union[str, None] = None):
38
46
  self.url = url
39
47
  self.api_key = api_key
40
48
  self.q = queue.Queue()
@@ -531,3 +539,19 @@ class Broker:
531
539
  playbackConfig=playback_config,
532
540
  playbackMode=br.traffic_api_pb2.PlaybackMode(mode=item["mode"], offsetTime=get_offset_time()),
533
541
  )
542
+
543
+ def get_license(self) -> LicenseInfo:
544
+ license_info = self.system_stub.GetLicenseInfo(br.common_pb2.Empty())
545
+ return LicenseInfo(
546
+ valid=license_info.status == br.system_api_pb2.LicenseStatus.VALID,
547
+ expires=license_info.expires,
548
+ email=license_info.requestId,
549
+ machine_id=license_info.requestMachineId.decode("utf-8"),
550
+ )
551
+
552
+ def apply_license(self, license_data_b64: bytes):
553
+ license = br.system_api_pb2.License()
554
+ license.data = license_data_b64
555
+ license.termsAgreement = True
556
+ self.system_stub.SetLicense(license)
557
+ return self.get_license()
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from typing import Union
6
+
7
+ import grpc
8
+ import requests
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.progress import Progress, SpinnerColumn, TextColumn
12
+
13
+ import cli.cloud.rest_helper as rest
14
+ from cli.broker.lib.broker import Broker, LicenseInfo
15
+
16
+ console = Console(stderr=True)
17
+
18
+
19
+ class LicenseFlow:
20
+ def describe_with_url(self, url: str) -> LicenseInfo:
21
+ b = LicenseFlow.__try_connect_to_broker(
22
+ url=url,
23
+ progress_label=f"Fetching license from broker ({url})...",
24
+ on_error_progress_label=f"Fetching license from broker ({url})... make sure broker is running.",
25
+ )
26
+ return b.get_license()
27
+
28
+ def describe_with_hotspot(self, url: Union[str, None] = "http://192.168.4.1:50051"):
29
+ if url is None:
30
+ url = "http://192.168.4.1:50051"
31
+
32
+ b = LicenseFlow.__try_connect_to_broker(
33
+ url=url, progress_label=f"Fetching license from broker using hotspot ({url})... Make sure to switch to remotivelabs-xxx Wi-Fi"
34
+ )
35
+ return b.get_license()
36
+
37
+ def request_with_url_with_internet(self, url: str):
38
+ console.print("This requires internet connection from your computer during the entire licensing process")
39
+
40
+ email = LicenseFlow.__try_authenticate_and_get_email_from_cloud()
41
+
42
+ broker_to_license = LicenseFlow.__try_connect_to_broker(
43
+ url=url,
44
+ progress_label=f"Fetching existing broker license...({url})...",
45
+ )
46
+
47
+ existing_license = broker_to_license.get_license()
48
+ if existing_license.valid:
49
+ apply = typer.confirm(
50
+ f"This broker already has a valid license, and is licensed to {existing_license.email}. Do you still wish to proceed?"
51
+ )
52
+ if not apply:
53
+ return
54
+
55
+ console.print(
56
+ ":point_right: [bold yellow]This will request a new license or use an existing license if "
57
+ "this hardware has already been licensed[/bold yellow]"
58
+ )
59
+ apply_license = typer.confirm("Do you want to continue and agree to terms and conditions at https://remotivelabs.com/license?")
60
+ if not apply_license:
61
+ return
62
+
63
+ existing_license = broker_to_license.get_license()
64
+ license_data_b64 = LicenseFlow.__try_request_license(email, existing_license, "Requesting license...")
65
+
66
+ with use_progress("Applying license to broker..."):
67
+ new_license = broker_to_license.apply_license(license_data_b64)
68
+ if new_license.valid:
69
+ console.print(f":thumbsup: Successfully applied license, it remains valid until {new_license.expires}")
70
+
71
+ def request_with_hotspot(self, url: Union[str, None] = "http://192.168.4.1:50051"):
72
+ """
73
+ This flow expects changes between networks and tries to guide the user accordingly
74
+ :param url: If None it will use the default hotspot IP
75
+ """
76
+ if url is None:
77
+ url = "http://192.168.4.1:50051"
78
+
79
+ console.print(
80
+ "Licensing a broker over its wifi hotspot will require you to switch between internet connectivity "
81
+ "and remotivelabs-xxx wifi hotspot"
82
+ )
83
+
84
+ email = LicenseFlow.__try_authenticate_and_get_email_from_cloud()
85
+
86
+ broker_to_license = LicenseFlow.__try_connect_to_broker(
87
+ url=url,
88
+ progress_label=f"Fetching license from broker using hotspot ({url})... Make sure to switch to remotivelabs-xxx wifi",
89
+ )
90
+ existing_license = broker_to_license.get_license()
91
+ if existing_license.valid:
92
+ apply = typer.confirm(
93
+ f"This broker already has a valid license and is licensed to {existing_license.email}. Do you still wish to proceed?"
94
+ )
95
+ if not apply:
96
+ return
97
+
98
+ console.print(
99
+ ":point_right: [bold yellow]This will request a new license or use an existing license if "
100
+ "this hardware has already been licensed[/bold yellow]"
101
+ )
102
+ apply_license = typer.confirm("Do you want to continue and agree to terms and conditions at https://remotivelabs.com/license?")
103
+ if not apply_license:
104
+ return
105
+
106
+ license_data_b64 = LicenseFlow.__try_request_license(email, existing_license)
107
+
108
+ broker_to_license = LicenseFlow.__try_connect_to_broker(
109
+ url=url,
110
+ progress_label="Applying license to broker... Make sure to switch back to remotivelabs-xxx wifi hotspot",
111
+ )
112
+ new_license = broker_to_license.apply_license(license_data_b64)
113
+ console.print(f":thumbsup: Successfully applied license, expires {new_license.expires}")
114
+
115
+ @staticmethod
116
+ def __try_connect_to_broker(url: str, progress_label: str, on_error_progress_label: Union[str, None] = None) -> Broker:
117
+ with use_progress(progress_label) as p:
118
+ while True:
119
+ try:
120
+ broker_to_license = Broker(url=url)
121
+ break
122
+ except grpc.RpcError:
123
+ if on_error_progress_label is not None:
124
+ p.update(
125
+ p.task_ids[0],
126
+ description=on_error_progress_label,
127
+ )
128
+ time.sleep(1)
129
+ if on_error_progress_label is None:
130
+ console.print(f":white_check_mark: {progress_label}")
131
+ else:
132
+ console.print(f":white_check_mark: {on_error_progress_label}")
133
+ return broker_to_license
134
+
135
+ @staticmethod
136
+ def __try_authenticate_and_get_email_from_cloud() -> str:
137
+ with use_progress("Fetching user info from cloud... Make sure you are connected to internet"):
138
+ while True:
139
+ try:
140
+ r = rest.handle_get("/api/whoami", return_response=True, use_progress_indicator=False, timeout=5)
141
+ break
142
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
143
+ time.sleep(1)
144
+ console.print(":white_check_mark: Fetching user info from cloud... Make sure you are connected to internet")
145
+ return r.json()["email"]
146
+
147
+ @staticmethod
148
+ def __try_request_license(
149
+ email: str,
150
+ existing_license: LicenseInfo,
151
+ progress_label: str = "Requesting license... make sure to switch back to network with internet access",
152
+ ) -> bytes:
153
+ with use_progress(progress_label):
154
+ while True:
155
+ try:
156
+ license_data_b64 = rest.request_license(email, json.loads(existing_license.machine_id)).encode()
157
+ break
158
+ except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
159
+ time.sleep(1)
160
+ console.print(f":white_check_mark: {progress_label}")
161
+ return license_data_b64
162
+
163
+
164
+ def use_progress(label: str):
165
+ p = Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True, expand=False)
166
+ p.add_task(label, total=1)
167
+ return p
cli/broker/licenses.py ADDED
@@ -0,0 +1,97 @@
1
+ import dataclasses
2
+ import json
3
+ from enum import Enum
4
+
5
+ import typer
6
+ from rich import print_json
7
+
8
+ from .license_flows import LicenseFlow
9
+
10
+ app = typer.Typer()
11
+
12
+ help_text = """
13
+ More info on our docs page
14
+ https://docs.remotivelabs.com/docs/remotive-broker/getting-started
15
+
16
+ --connect describes how the connection is made to the broker and helps us properly connect to broker
17
+
18
+ url = Connects to the broker with the specified url, this is
19
+ hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
20
+ broker over its wifi hotspot. Getting the license from a RemotiveBox over its wifi hotspot
21
+ requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
22
+ 'xxx' is a random generated id.
23
+
24
+ --url is the broker url, this is mandatory when connect type is "url"
25
+ """
26
+
27
+
28
+ class Connection(str, Enum):
29
+ hotspot = "hotspot"
30
+ url = "url"
31
+ # TODO - Support discover using mdns
32
+
33
+
34
+ @app.command()
35
+ def describe(
36
+ connect: Connection = typer.Option("url", case_sensitive=False, help="How to connect to broker"),
37
+ url: str = typer.Option("http://localhost:50051", is_eager=False, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
38
+ ):
39
+ """
40
+ Show licence information
41
+
42
+ More info on our docs page
43
+ https://docs.remotivelabs.com/docs/remotive-broker/getting-started
44
+
45
+ --connect describes how the connection is made to the broker and helps us properly connect to broker
46
+
47
+ url = Connects to the broker with the specified url, this is
48
+ hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
49
+ broker over its wifi hotspot. Getting the license from a RemotiveBox over its wifi hotspot
50
+ requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
51
+ 'xxx' is a random generated id.
52
+
53
+ --url is the broker url, this is mandatory when connect type is "url"
54
+ """
55
+ license_flow = LicenseFlow()
56
+ if connect == Connection.url:
57
+ existing_license = license_flow.describe_with_url(url)
58
+ print_json(json.dumps(dataclasses.asdict(existing_license)))
59
+ if connect == Connection.hotspot:
60
+ existing_license = license_flow.describe_with_hotspot(url if url != "http://localhost:50051" else None)
61
+ print_json(json.dumps(dataclasses.asdict(existing_license)))
62
+
63
+
64
+ @app.command()
65
+ def request(
66
+ connect: Connection = typer.Option("url", case_sensitive=False, help="How to connect to broker"),
67
+ url: str = typer.Option(
68
+ "http://localhost:50051",
69
+ is_eager=False,
70
+ help="Broker url, this is mandatory when connect type is 'url'",
71
+ envvar="REMOTIVE_BROKER_URL",
72
+ ),
73
+ ):
74
+ """
75
+ Requests and applies a new or existing License to a broker, Note that internet access is required on your
76
+ computer
77
+
78
+ More info on our docs page
79
+ https://docs.remotivelabs.com/docs/remotive-broker/getting-started
80
+
81
+ --connect describes how the connection is made to the broker and helps us properly connect to broker
82
+
83
+ url = Use url to connect to broker (use --url)
84
+ hotspot = If you are using RemotiveBox (our reference hardware) you can connect to the
85
+ broker over its wifi hotspot. Licensing a broker on a RemotiveBox over its wifi hotspot
86
+ requires you switch wi-fi network to the RemotiveBox hotspot called 'remotivelabs-xxx' where
87
+ 'xxx' is a random generated id.
88
+ https://docs.remotivelabs.com/docs/remotive-broker/getting-started/remotive-box
89
+
90
+ --url is the broker url, this is mandatory when connect type is "url"
91
+ """
92
+
93
+ license_flow = LicenseFlow()
94
+ if connect == Connection.url:
95
+ license_flow.request_with_url_with_internet(url)
96
+ if connect == Connection.hotspot:
97
+ license_flow.request_with_hotspot(url if url != "http://localhost:50051" else None)
cli/broker/scripting.py CHANGED
@@ -51,6 +51,18 @@ def new_script(
51
51
 
52
52
  local_signals = ",".join(list(map(to_local_signal, signals_to_subscribe_to)))
53
53
 
54
+ def to_subscribe_pattern(sig_name: tuple[str, str]):
55
+ t = Template(
56
+ """
57
+ if (signals["$sig_name"] ~= nil) then
58
+ return return_value_or_bytes(signals["$sig_name"])
59
+ end
60
+ """
61
+ )
62
+ return t.substitute(sig_name=sig_name[1])
63
+
64
+ subscribe_pattern = "".join(list(map(to_subscribe_pattern, signals_to_subscribe_to)))
65
+
54
66
  template = Template(
55
67
  """
56
68
  --
@@ -70,33 +82,26 @@ function output_signal()
70
82
  return "$output_signal"
71
83
  end
72
84
 
73
- local local_frequency_hz = 0
74
-
75
85
  -- Required, declare what frequency you like to get "timer" invoked. 0 means no calls to "timer".
76
86
  function timer_frequency_hz()
77
- return local_frequency_hz
87
+ return 0
78
88
  end
79
89
 
80
- -- Invoked with the frequecy returned by "timer_frequency_hz".
81
- -- @param system_timestamp_us: system time stamp.
90
+ -- Invoked with the frequency returned by "timer_frequency_hz".
91
+ -- @param system_timestamp_us: system time stamp
82
92
  function timer(system_timestamp_us)
83
93
  return return_value_or_bytes("your value")
84
94
  end
85
95
 
86
-
87
96
  -- Invoked when ANY signal declared in "local_signals" arrive
88
97
  -- @param signals_timestamp_us: signal time stamp
89
98
  -- @param system_timestamp_us
90
99
  -- @param signals: array of signals containing all or a subset of signals declared in "local_signals". Make sure to nil check before use.
91
100
  function signals(signals, namespace, signals_timestamp_us, system_timestamp_us)
92
-
93
101
  -- TODO - replace this code with what you want todo
94
102
 
95
- if signals["$signle_input_signal"] == 0 then
96
- return return_value_or_bytes(0)
97
- else
98
- return return_value_or_bytes(signals["$signle_input_signal"] * 2)
99
- end
103
+ $subscribe_pattern
104
+ return return_nothing()
100
105
  end
101
106
 
102
107
  -- helper return function, make sure to use return_value_or_bytes or return_nothing.
@@ -104,12 +109,18 @@ function return_value_or_bytes(value_or_bytes)
104
109
  return value_or_bytes
105
110
  end
106
111
 
112
+ -- helper return function, make sure to use return_value_or_bytes or return_nothing.
113
+ function return_nothing()
114
+ return
115
+ end
116
+
107
117
  """
108
118
  )
109
119
 
110
120
  script = template.substitute(
111
121
  local_signals=local_signals,
112
- signle_input_signal=signals_to_subscribe_to[0][1], # Take one signal and use signal name in tuple
122
+ # signle_input_signal=signals_to_subscribe_to[0][1], # Take one signal and use signal name in tuple
123
+ subscribe_pattern=subscribe_pattern,
113
124
  output_signal=output_signal,
114
125
  )
115
126
 
cli/broker/signals.py CHANGED
@@ -95,7 +95,8 @@ def subscribe( # noqa: C901
95
95
 
96
96
  if script is not None:
97
97
  if len(signal) > 0:
98
- ErrorPrinter.print_generic_error("You must must not specify namespace or signal when using --script")
98
+ ErrorPrinter.print_generic_error("You must must not specify --signal when using --script")
99
+ exit(1)
99
100
 
100
101
  plt.title("Signals")
101
102
 
cli/cloud/recordings.py CHANGED
@@ -459,8 +459,7 @@ def _do_change_playback_mode(mode: str, recording_session: str, broker: str, pro
459
459
  if json_context["recordingSessionId"] != recording_session:
460
460
  ErrorPrinter.print_generic_error(
461
461
  f"The recording id mounted is '{json_context['recordingSessionId']}' "
462
- f"which not the same as you are trying to {mode}, "
463
- "use cmd below to mount this recording"
462
+ f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
464
463
  )
465
464
  ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
466
465
  exit(1)
@@ -3,16 +3,18 @@ from __future__ import annotations
3
3
  import datetime
4
4
  import json
5
5
  import tempfile
6
- from typing import List
6
+ from pathlib import Path
7
+ from typing import List, Union
7
8
 
8
9
  import grpc
9
10
  import rich
10
11
  import typer
12
+ from rich import print as rich_rprint
11
13
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
14
 
13
15
  from cli.errors import ErrorPrinter
14
16
 
15
- from ..broker.lib.broker import Broker
17
+ from ..broker.lib.broker import Broker, SubscribableSignal
16
18
  from . import rest_helper as rest
17
19
 
18
20
  app = typer.Typer(
@@ -89,6 +91,72 @@ def stop(
89
91
  _do_change_playback_mode("stop", recording_session, broker, project)
90
92
 
91
93
 
94
+ # Copied from signals.py
95
+ def read_scripted_code_file(file_path: Path) -> bytes:
96
+ # typer checks that the Path exists
97
+ with open(file_path, "rb") as file:
98
+ return file.read()
99
+
100
+
101
+ @app.command()
102
+ def subscribe(
103
+ recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
104
+ broker: str = typer.Option(None, help="Broker to use"),
105
+ signal: List[str] = typer.Option(None, help="Signal names to subscribe to, mandatory when not using script"),
106
+ script: Path = typer.Option(
107
+ None,
108
+ exists=True,
109
+ file_okay=True,
110
+ dir_okay=False,
111
+ writable=False,
112
+ readable=True,
113
+ resolve_path=True,
114
+ help="Supply a path to Lua script that to use for signal transformation",
115
+ ),
116
+ on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
117
+ project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
118
+ ):
119
+ """
120
+ Allows you to subscribe to signals based on a mounted recording without knowing the broker URL.
121
+ This simplifies when playing recordings from the cloud.
122
+
123
+ Terminal plotting is not yet supported here so we refer to remotive broker signals subscribe --x-plot for this.
124
+ """
125
+ if script is None:
126
+ if len(signal) == 0:
127
+ ErrorPrinter.print_generic_error("You must use include at least one signal and one namespace or use script when subscribing")
128
+ exit(1)
129
+ if script is not None:
130
+ if len(signal) > 0:
131
+ ErrorPrinter.print_generic_error("You must must not specify --signal when using --script")
132
+ exit(1)
133
+
134
+ broker_client = _get_broker_info(project, recording_session, broker, "subscribe")
135
+
136
+ try:
137
+ if script is not None:
138
+ script_src = read_scripted_code_file(script)
139
+ broker_client.subscribe_on_script(script_src, lambda sig: rich_rprint(json.dumps(list(sig))), on_change_only)
140
+ else:
141
+
142
+ def to_subscribable_signal(sig: str):
143
+ arr = sig.split(":")
144
+ if len(arr) != 2:
145
+ ErrorPrinter.print_hint(f"--signal must have format namespace:signal ({sig})")
146
+ exit(1)
147
+ return SubscribableSignal(namespace=arr[0], name=arr[1])
148
+
149
+ signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
150
+ broker_client.long_name_subscribe(signals_to_subscribe_to, lambda sig: rich_rprint(json.dumps(list(sig))), on_change_only)
151
+ print("Subscribing to signals, press Ctrl+C to exit")
152
+ except grpc.RpcError as rpc_error:
153
+ ErrorPrinter.print_grpc_error(rpc_error)
154
+
155
+ except Exception as e:
156
+ ErrorPrinter.print_generic_error(e)
157
+ exit(1)
158
+
159
+
92
160
  def _do_change_playback_mode(
93
161
  mode: str,
94
162
  recording_session: str,
@@ -103,10 +171,8 @@ def _do_change_playback_mode(
103
171
  recordings: list = r["recordings"]
104
172
  files = list(map(lambda rec: {"recording": rec["fileName"], "namespace": rec["metadata"]["namespace"]}, recordings))
105
173
 
106
- if broker is not None:
107
- response = rest.handle_get(f"/api/project/{project}/brokers/{broker}", return_response=True, allow_status_codes=[404])
108
- else:
109
- response = rest.handle_get(f"/api/project/{project}/brokers/personal", return_response=True, allow_status_codes=[404])
174
+ broker_name = broker if broker is not None else "personal"
175
+ response = rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
110
176
  if response.status_code == 404:
111
177
  broker_arg = ""
112
178
  if broker is not None:
@@ -164,8 +230,7 @@ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: st
164
230
  if json_context["recordingSessionId"] != recording_session:
165
231
  ErrorPrinter.print_generic_error(
166
232
  f"The recording id mounted is '{json_context['recordingSessionId']}' "
167
- f"which not the same as you are trying to {mode}, "
168
- "use cmd below to mount this recording"
233
+ f"which not the same as you are trying to {mode}, use cmd below to mount this recording"
169
234
  )
170
235
  ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
171
236
  exit(1)
@@ -176,3 +241,22 @@ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: st
176
241
  else:
177
242
  ErrorPrinter.print_grpc_error(rpc_error)
178
243
  exit(1)
244
+
245
+
246
+ def _get_broker_info(project: str, recording_session: str, broker: Union[str, None], mode: str) -> Broker:
247
+ # Verify it exists
248
+ rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
249
+
250
+ broker_name = broker if broker is not None else "personal"
251
+ response = rest.handle_get(f"/api/project/{project}/brokers/{broker_name}", return_response=True, allow_status_codes=[404])
252
+ if response.status_code == 404:
253
+ broker_arg = ""
254
+ if broker is not None:
255
+ broker_arg = f"--broker {broker} --ensure-broker-started"
256
+ ErrorPrinter.print_generic_error("You need to mount the recording before you play")
257
+ ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} {broker_arg} --project {project}")
258
+ exit(1)
259
+ broker_info = json.loads(response.text)
260
+ broker_client = Broker(broker_info["url"], None)
261
+ _verify_recording_on_broker(broker_client, recording_session, mode, project)
262
+ return broker_client
cli/cloud/rest_helper.py CHANGED
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
3
4
  import json
4
5
  import logging
5
6
  import os
6
7
  import shutil
7
8
  from importlib.metadata import version
8
9
  from pathlib import Path
9
- from typing import List, Union
10
+ from typing import Dict, List, Union
10
11
 
11
12
  import requests
12
13
  from rich.console import Console
@@ -23,11 +24,16 @@ if "REMOTIVE_CLOUD_HTTP_LOGGING" in os.environ:
23
24
  requests_log.setLevel(logging.DEBUG)
24
25
  requests_log.propagate = True
25
26
  global baseurl
27
+ global license_server_base_url
26
28
  base_url = "https://cloud.remotivelabs.com"
29
+ license_server_base_url = "https://license.cloud.remotivelabs.com"
27
30
 
28
31
  if "REMOTIVE_CLOUD_BASE_URL" in os.environ:
29
32
  base_url = os.environ["REMOTIVE_CLOUD_BASE_URL"]
30
33
 
34
+ if "cloud-dev" in base_url:
35
+ license_server_base_url = "https://license.cloud-dev.remotivelabs.com"
36
+
31
37
  # if 'REMOTIVE_CLOUD_AUTH_TOKEN' not in os.environ:
32
38
  # print('export REMOTIVE_CLOUD_AUTH_TOKEN=auth must be set')
33
39
  # exit(0)
@@ -65,12 +71,23 @@ def ensure_auth_token():
65
71
  headers["User-Agent"] = f"remotivelabs-cli {cli_version}"
66
72
 
67
73
 
68
- def handle_get(url, params=None, return_response: bool = False, allow_status_codes=None, progress_label="Fetching..."):
74
+ def handle_get(
75
+ url,
76
+ params=None,
77
+ return_response: bool = False,
78
+ allow_status_codes=None,
79
+ progress_label="Fetching...",
80
+ use_progress_indicator: bool = True,
81
+ timeout: int = 20,
82
+ ):
69
83
  if params is None:
70
84
  params = {}
71
85
  ensure_auth_token()
72
- with use_progress(progress_label):
73
- r = requests.get(f"{base_url}{url}", headers=headers, params=params)
86
+ if use_progress_indicator:
87
+ with use_progress(progress_label):
88
+ r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
89
+ else:
90
+ r = requests.get(f"{base_url}{url}", headers=headers, params=params, timeout=timeout)
74
91
 
75
92
  if return_response:
76
93
  check_api_result(r, allow_status_codes)
@@ -192,3 +209,18 @@ def download_file(save_file_name: str, url: str):
192
209
  shutil.copyfileobj(stream_with_progress, out_file)
193
210
  else:
194
211
  check_api_result(download_resp)
212
+
213
+
214
+ def request_license(email: str, machine_id: Dict[str, any]) -> str:
215
+ # Lets keep the email here so we have the same interface for both authenticated
216
+ # and not authenticated license requests.
217
+ # email will be validated in the license server to make sure it matches with the user of the
218
+ # access token so not any email is sent here
219
+ ensure_auth_token()
220
+ payload = {"id": email, "machine_id": machine_id}
221
+ b64_encoded_bytes = base64.encodebytes(json.dumps(payload).encode())
222
+ license_jsonb64 = {"licensejsonb64": b64_encoded_bytes.decode("utf-8")}
223
+ headers["content-type"] = "application/json"
224
+ r = requests.post(url=f"{license_server_base_url}/api/license/request", headers=headers, data=json.dumps(license_jsonb64))
225
+ check_api_result(r)
226
+ return r.json()["license_data"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: remotivelabs-cli
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -1,14 +1,16 @@
1
1
  cli/__about__.py,sha256=qXVkxWb3aPCF-4MjQhB0wqL2GEblEH4Qwk70o29UkJk,122
2
2
  cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- cli/broker/brokers.py,sha256=sSX--mm5ln5RUFR60VFHQR6NWu4Qz7Jqi2Ws-4TJsDI,3052
4
- cli/broker/export.py,sha256=kPB8xRe-I-fte5d95E0Uy2wLvQp943e4yFARGPKySoY,4386
3
+ cli/broker/brokers.py,sha256=jVbaiCnFoCxQBGhidePg6GxDhurzXtbn8mV59HkzAc4,3181
4
+ cli/broker/export.py,sha256=Xhpz2P32VMow7DujImCzhiP-WqJvdR1-THErRV_4LPU,4363
5
5
  cli/broker/files.py,sha256=_8elBjbmJ5MEEeFGJ7QYkXzyuLDCDpO6UvEVXHbRy7U,4133
6
6
  cli/broker/lib/__about__.py,sha256=xnZ5V6ZcHW9dhWLWdMzVjYJbEnMKpeXm0_S_mbNzypE,141
7
- cli/broker/lib/broker.py,sha256=kpcSaHcjZlcsgNgxwDBEa13RYRtx0Uz-fbBg2pKAHI0,21174
7
+ cli/broker/lib/broker.py,sha256=uolp_AaC3Z_pGeF4rP28FvHvtSRUJFuT5Dxe8aLhdgI,21957
8
+ cli/broker/license_flows.py,sha256=AaKvZgy_hkP5Mv-1dXtQxQxXGidpvuVDVY3jP2AX0t0,7101
9
+ cli/broker/licenses.py,sha256=iJeF6aWKUPhXb24t0pyufFRmMGNCFo-G_ZUz1rstqqs,3957
8
10
  cli/broker/playback.py,sha256=oOfC8Jn4Ib-nc9T6ob_uNXZSeCWfft7MrMQPafH4U2I,4846
9
11
  cli/broker/record.py,sha256=gEvo3myHbIl6UyXzhJE741NiwRrFf7doBg6HXzzp5z0,1382
10
- cli/broker/scripting.py,sha256=sLDtuktWsVk0fJ3RW4kYyh-_YAVJP3VM0xFIQR499Oo,3392
11
- cli/broker/signals.py,sha256=3z4vVivPdo8dfz8XY-s7Ga5I75uQaptWarixrfSsm-0,6547
12
+ cli/broker/scripting.py,sha256=Nb6C8JjfuQXuvd_L8CtlTHEc5iTBRwI4RXMQdf_gWYg,3752
13
+ cli/broker/signals.py,sha256=py_qOwTP5ongpOVLKznMfVPw68iB--1eIvnRdOLND7k,6556
12
14
  cli/cloud/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
15
  cli/cloud/auth.py,sha256=xi47cQWjs6q7Kje4XUbTqEk_BgG3NJ1gMOYPU-_4tgw,3470
14
16
  cli/cloud/auth_tokens.py,sha256=5ox0Pxfij8aTgEcDwRBasaSQvDuLIGCJc-INjmIQN2M,3408
@@ -16,9 +18,9 @@ cli/cloud/brokers.py,sha256=wa3uMg91IZdrP0tMpTdO9cBIkZHtMHxQ-zEXwFiye_I,4127
16
18
  cli/cloud/cloud_cli.py,sha256=nRRXFF_IgUMayerxS-h7oqsNe6tt34Q5VeThq8gatEg,1443
17
19
  cli/cloud/configs.py,sha256=2p1mCHf5BwYNtwbY0Cbed5t6-79WHGKWU4Fv6LuJ21o,4069
18
20
  cli/cloud/projects.py,sha256=-uqltAOficwprOKaPd2R0Itm4sqTz3VJNs9Sc8jtO5k,1369
19
- cli/cloud/recordings.py,sha256=0xQw8iv-NjUUqxQjq5O4WqNYsew2lNlQ9Z0Qevl2f9E,20844
20
- cli/cloud/recordings_playback.py,sha256=ygJRBbJ_BYmrEZ7k0j2b11N3xrrgmOdL3jZckJ-f-uU,7120
21
- cli/cloud/rest_helper.py,sha256=g7lmGosAS0IDlo9Aso0bH0tdlLTCz0IYx3R71DXKtkc,6491
21
+ cli/cloud/recordings.py,sha256=xbdqUYVmKyChV24cASK3zNPkmCPwIUmX9H5LsG98zfI,20820
22
+ cli/cloud/recordings_playback.py,sha256=OXRvoE7Zc_jQrofpqjbWYKZ6pfhJVHBe0bQ9GEfv64Q,10879
23
+ cli/cloud/rest_helper.py,sha256=WK5We-vWg0GwnTq37OyKVuQTc58RFkPuA0kq5wjDtCk,7769
22
24
  cli/cloud/sample_recordings.py,sha256=g1X6JTxvzWInSP9R1BJsDmL4WqvpEKqjdJR_xT4bo1U,639
23
25
  cli/cloud/service_account_tokens.py,sha256=7vjoMd6Xq7orWCUP7TVUVa86JA0OiX8O10NZcHUE6rM,2294
24
26
  cli/cloud/service_accounts.py,sha256=GCYdYPnP5uWVsg1bTIS67CmoPWDng5dupJHmlThrJ80,1606
@@ -33,8 +35,8 @@ cli/tools/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
33
35
  cli/tools/can/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
34
36
  cli/tools/can/can.py,sha256=kSd1c-nxxXyeKkm19oDILiDBZsKOcpjsUT0T3xox5Qs,2172
35
37
  cli/tools/tools.py,sha256=LwQdWMcJ19pCyKUsVfSB2B3R6ui61NxxFWP0Nrnd5Jk,198
36
- remotivelabs_cli-0.0.16.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
37
- remotivelabs_cli-0.0.16.dist-info/METADATA,sha256=A5ZA5VGXfw287Q187R5Pt4cPH5xOVmDWzeRq8offtPk,1224
38
- remotivelabs_cli-0.0.16.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
39
- remotivelabs_cli-0.0.16.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
40
- remotivelabs_cli-0.0.16.dist-info/RECORD,,
38
+ remotivelabs_cli-0.0.18.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
39
+ remotivelabs_cli-0.0.18.dist-info/METADATA,sha256=lqsiXEeEjC4k1kbynvlJ5r5tQYZfArLUCLRXnA6v1xM,1224
40
+ remotivelabs_cli-0.0.18.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
41
+ remotivelabs_cli-0.0.18.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
42
+ remotivelabs_cli-0.0.18.dist-info/RECORD,,