remotivelabs-cli 0.0.16__tar.gz → 0.0.17__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.
Files changed (41) hide show
  1. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/PKG-INFO +1 -1
  2. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/brokers.py +3 -2
  3. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/export.py +1 -1
  4. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/lib/broker.py +26 -2
  5. remotivelabs_cli-0.0.17/cli/broker/license_flows.py +167 -0
  6. remotivelabs_cli-0.0.17/cli/broker/licenses.py +97 -0
  7. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/signals.py +2 -1
  8. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/recordings.py +1 -2
  9. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/recordings_playback.py +92 -8
  10. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/rest_helper.py +36 -4
  11. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/pyproject.toml +1 -1
  12. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/LICENSE +0 -0
  13. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/README.md +0 -0
  14. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/__about__.py +0 -0
  15. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/__init__.py +0 -0
  16. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/files.py +0 -0
  17. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/lib/__about__.py +0 -0
  18. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/playback.py +0 -0
  19. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/record.py +0 -0
  20. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/broker/scripting.py +0 -0
  21. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/__init__.py +0 -0
  22. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/auth.py +0 -0
  23. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/auth_tokens.py +0 -0
  24. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/brokers.py +0 -0
  25. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/cloud_cli.py +0 -0
  26. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/configs.py +0 -0
  27. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/projects.py +0 -0
  28. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/sample_recordings.py +0 -0
  29. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/service_account_tokens.py +0 -0
  30. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/cloud/service_accounts.py +0 -0
  31. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/connect/__init__.py +0 -0
  32. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/connect/connect.py +0 -0
  33. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/connect/protopie/protopie.py +0 -0
  34. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/errors.py +0 -0
  35. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/remotive.py +0 -0
  36. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/requirements.txt +0 -0
  37. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/settings.py +0 -0
  38. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/tools/__init__.py +0 -0
  39. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/tools/can/__init__.py +0 -0
  40. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/cli/tools/can/can.py +0 -0
  41. {remotivelabs_cli-0.0.16 → remotivelabs_cli-0.0.17}/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.16
3
+ Version: 0.0.17
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -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()
@@ -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
 
@@ -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
@@ -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)
@@ -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
 
@@ -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
@@ -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
  [tool.poetry]
2
2
  name = "remotivelabs-cli"
3
- version = "0.0.16"
3
+ version = "0.0.17"
4
4
  description = "CLI for operating RemotiveCloud and RemotiveBroker"
5
5
  authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
6
6
  readme = "README.md"