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.
Files changed (84) hide show
  1. remotivelabs/cli/__init__.py +0 -0
  2. remotivelabs/cli/api/cloud/tokens.py +62 -0
  3. remotivelabs/cli/broker/__init__.py +33 -0
  4. remotivelabs/cli/broker/defaults.py +1 -0
  5. remotivelabs/cli/broker/discovery.py +43 -0
  6. remotivelabs/cli/broker/export.py +92 -0
  7. remotivelabs/cli/broker/files.py +119 -0
  8. remotivelabs/cli/broker/lib/__about__.py +4 -0
  9. remotivelabs/cli/broker/lib/broker.py +625 -0
  10. remotivelabs/cli/broker/lib/client.py +224 -0
  11. remotivelabs/cli/broker/lib/helper.py +277 -0
  12. remotivelabs/cli/broker/lib/signalcreator.py +196 -0
  13. remotivelabs/cli/broker/license_flows.py +167 -0
  14. remotivelabs/cli/broker/licenses.py +98 -0
  15. remotivelabs/cli/broker/playback.py +117 -0
  16. remotivelabs/cli/broker/record.py +41 -0
  17. remotivelabs/cli/broker/recording_session/__init__.py +3 -0
  18. remotivelabs/cli/broker/recording_session/client.py +67 -0
  19. remotivelabs/cli/broker/recording_session/cmd.py +254 -0
  20. remotivelabs/cli/broker/recording_session/time.py +49 -0
  21. remotivelabs/cli/broker/scripting.py +129 -0
  22. remotivelabs/cli/broker/signals.py +220 -0
  23. remotivelabs/cli/broker/version.py +31 -0
  24. remotivelabs/cli/cloud/__init__.py +17 -0
  25. remotivelabs/cli/cloud/auth/__init__.py +3 -0
  26. remotivelabs/cli/cloud/auth/cmd.py +128 -0
  27. remotivelabs/cli/cloud/auth/login.py +283 -0
  28. remotivelabs/cli/cloud/auth_tokens.py +149 -0
  29. remotivelabs/cli/cloud/brokers.py +109 -0
  30. remotivelabs/cli/cloud/configs.py +109 -0
  31. remotivelabs/cli/cloud/licenses/__init__.py +0 -0
  32. remotivelabs/cli/cloud/licenses/cmd.py +14 -0
  33. remotivelabs/cli/cloud/organisations.py +112 -0
  34. remotivelabs/cli/cloud/projects.py +44 -0
  35. remotivelabs/cli/cloud/recordings.py +580 -0
  36. remotivelabs/cli/cloud/recordings_playback.py +274 -0
  37. remotivelabs/cli/cloud/resumable_upload.py +87 -0
  38. remotivelabs/cli/cloud/sample_recordings.py +25 -0
  39. remotivelabs/cli/cloud/service_account_tokens.py +62 -0
  40. remotivelabs/cli/cloud/service_accounts.py +72 -0
  41. remotivelabs/cli/cloud/storage/__init__.py +5 -0
  42. remotivelabs/cli/cloud/storage/cmd.py +76 -0
  43. remotivelabs/cli/cloud/storage/copy.py +86 -0
  44. remotivelabs/cli/cloud/storage/uri_or_path.py +45 -0
  45. remotivelabs/cli/cloud/uri.py +113 -0
  46. remotivelabs/cli/connect/__init__.py +0 -0
  47. remotivelabs/cli/connect/connect.py +118 -0
  48. remotivelabs/cli/connect/protopie/protopie.py +185 -0
  49. remotivelabs/cli/py.typed +0 -0
  50. remotivelabs/cli/remotive.py +123 -0
  51. remotivelabs/cli/settings/__init__.py +20 -0
  52. remotivelabs/cli/settings/config_file.py +113 -0
  53. remotivelabs/cli/settings/core.py +333 -0
  54. remotivelabs/cli/settings/migration/__init__.py +0 -0
  55. remotivelabs/cli/settings/migration/migrate_all_token_files.py +80 -0
  56. remotivelabs/cli/settings/migration/migrate_config_file.py +64 -0
  57. remotivelabs/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  58. remotivelabs/cli/settings/migration/migrate_token_file.py +52 -0
  59. remotivelabs/cli/settings/migration/migration_tools.py +38 -0
  60. remotivelabs/cli/settings/state_file.py +67 -0
  61. remotivelabs/cli/settings/token_file.py +128 -0
  62. remotivelabs/cli/tools/__init__.py +0 -0
  63. remotivelabs/cli/tools/can/__init__.py +0 -0
  64. remotivelabs/cli/tools/can/can.py +78 -0
  65. remotivelabs/cli/tools/tools.py +9 -0
  66. remotivelabs/cli/topology/__init__.py +28 -0
  67. remotivelabs/cli/topology/all.py +322 -0
  68. remotivelabs/cli/topology/cli/__init__.py +3 -0
  69. remotivelabs/cli/topology/cli/run_in_docker.py +58 -0
  70. remotivelabs/cli/topology/cli/topology_cli.py +16 -0
  71. remotivelabs/cli/topology/cmd.py +130 -0
  72. remotivelabs/cli/topology/start_trial.py +134 -0
  73. remotivelabs/cli/typer/__init__.py +0 -0
  74. remotivelabs/cli/typer/typer_utils.py +27 -0
  75. remotivelabs/cli/utils/__init__.py +0 -0
  76. remotivelabs/cli/utils/console.py +99 -0
  77. remotivelabs/cli/utils/rest_helper.py +369 -0
  78. remotivelabs/cli/utils/time.py +11 -0
  79. remotivelabs/cli/utils/versions.py +120 -0
  80. remotivelabs_cli-0.5.0a1.dist-info/METADATA +51 -0
  81. remotivelabs_cli-0.5.0a1.dist-info/RECORD +84 -0
  82. remotivelabs_cli-0.5.0a1.dist-info/WHEEL +4 -0
  83. remotivelabs_cli-0.5.0a1.dist-info/entry_points.txt +3 -0
  84. remotivelabs_cli-0.5.0a1.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from remotivelabs.cli.cloud.resumable_upload import upload_signed_url
7
+ from remotivelabs.cli.cloud.uri import URI
8
+ from remotivelabs.cli.utils.console import print_success
9
+ from remotivelabs.cli.utils.rest_helper import RestHelper as Rest
10
+
11
+ _RCS_STORAGE_PATH = "/api/project/{project}/files/storage{path}"
12
+
13
+
14
+ def copy(project: str, source: URI | Path, dest: URI | Path, overwrite: bool = False) -> None:
15
+ if isinstance(source, Path) and isinstance(dest, Path):
16
+ raise ValueError("Either source or destination must be an rcs:// uri")
17
+
18
+ if isinstance(source, URI) and isinstance(dest, URI):
19
+ raise ValueError("Either source or destination must be a local path")
20
+
21
+ if isinstance(source, URI) and isinstance(dest, Path):
22
+ _download(source=source, dest=dest, project=project, overwrite=overwrite)
23
+
24
+ elif isinstance(source, Path) and isinstance(dest, URI):
25
+ _upload(source=source, dest=dest, project=project, overwrite=overwrite)
26
+
27
+ else:
28
+ raise ValueError("invalid copy operation")
29
+
30
+
31
+ def _upload(source: Path, dest: URI, project: str, overwrite: bool = False) -> None:
32
+ if not source.exists():
33
+ raise FileNotFoundError(f"Source file does not exist: {source}")
34
+
35
+ files_to_upload = _list_files_for_upload(source, dest)
36
+
37
+ for file_path, target_uri in files_to_upload:
38
+ _upload_single_file(file_path, target_uri, project, overwrite)
39
+
40
+
41
+ def _list_files_for_upload(source: Path, dest: URI) -> list[tuple[Path, URI]]:
42
+ upload_pairs = []
43
+
44
+ if source.is_dir():
45
+ for file_path in source.rglob("*"):
46
+ if file_path.is_file():
47
+ relative_path = file_path.relative_to(source)
48
+ target_uri = dest / relative_path
49
+ upload_pairs.append((file_path, target_uri))
50
+ else:
51
+ target_uri = dest / source.name if dest.is_dir() else dest
52
+ upload_pairs.append((source, target_uri))
53
+
54
+ return upload_pairs
55
+
56
+
57
+ def _upload_single_file(source: Path, target_uri: URI, project: str, overwrite: bool = False) -> None:
58
+ target = _RCS_STORAGE_PATH.format(project=project, path=target_uri.path)
59
+ upload_options = {"overwrite": "always" if overwrite else "never"}
60
+ res = Rest.handle_post(target, return_response=True, body=json.dumps(upload_options))
61
+
62
+ json_res = res.json()
63
+ url = json_res["url"]
64
+ headers = json_res["headers"]
65
+ upload_signed_url(url, source, headers)
66
+
67
+ print_success(f"Uploaded {source} to {target_uri.path}")
68
+
69
+
70
+ def _download(source: URI, dest: Path, project: str, overwrite: bool = False) -> None:
71
+ if dest.is_dir():
72
+ if not dest.exists():
73
+ raise FileNotFoundError(f"Destination directory {dest} does not exist")
74
+ # create a target file name if destination is a dir
75
+ dest = dest / source.filename
76
+
77
+ elif not dest.parent.is_dir() or not dest.parent.exists():
78
+ raise FileNotFoundError(f"Destination directory {dest.parent} does not exist")
79
+
80
+ if dest.exists() and not overwrite:
81
+ raise FileExistsError(f"Destination file {dest} already exists")
82
+
83
+ target = _RCS_STORAGE_PATH.format(project=project, path=source.path) + "?download=true"
84
+ res = Rest.handle_get(target, return_response=True)
85
+
86
+ Rest.download_file(save_file_name=dest.absolute(), url=res.text)
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from remotivelabs.cli.cloud.uri import URI, InvalidURIError
6
+
7
+
8
+ def uri(path: str) -> UriOrPath:
9
+ """
10
+ Parses a path and returns a UriOrPath object.
11
+
12
+ NOTE: The name of this function is important as it is used by Typer to determine the name/type of the argument
13
+ """
14
+ try:
15
+ p: Path | URI = URI(path)
16
+ except InvalidURIError:
17
+ p = Path(path)
18
+ return UriOrPath(p)
19
+
20
+
21
+ class UriOrPath:
22
+ """
23
+ Union type for handling local and remote paths for Remotive Cloud Storage
24
+
25
+ Note: This custom type only exists because Typer currently does not support union types
26
+ TODO: Move to commands package when refactored
27
+ """
28
+
29
+ def __init__(self, value: Path | URI) -> None:
30
+ self._value = value
31
+
32
+ @property
33
+ def uri(self) -> URI | None:
34
+ return self._value if isinstance(self._value, URI) else None
35
+
36
+ @property
37
+ def path(self) -> Path | None:
38
+ return self._value if isinstance(self._value, Path) else None
39
+
40
+ @property
41
+ def value(self) -> Path | URI:
42
+ return self._value
43
+
44
+ def __str__(self) -> str:
45
+ return str(self._value)
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from os import PathLike
4
+ from pathlib import PurePosixPath
5
+ from urllib.parse import urlparse
6
+
7
+
8
+ class InvalidURIError(Exception):
9
+ """Raised when an invalid URI is encountered"""
10
+
11
+
12
+ class JoinURIError(Exception):
13
+ """Raised when an error occurs while joining URIs"""
14
+
15
+
16
+ class URI:
17
+ """
18
+ Custom type for rcs (Remotive Cloud Storage) URIs.
19
+
20
+ The URI format follows the pattern: rcs://bucket/path/to/resource
21
+ """
22
+
23
+ scheme: str
24
+ """The URI scheme (default: 'rcs')"""
25
+
26
+ path: str
27
+ """The full path component, including leading slash"""
28
+
29
+ filename: str
30
+ """The name of the file or last path segment"""
31
+
32
+ bucket: str
33
+ """The first path segment after the leading slash"""
34
+
35
+ parent: URI
36
+ """The parent URI. If at root, returns a copy of itself."""
37
+
38
+ def __init__(self, value: str, scheme: str = "rcs"):
39
+ """
40
+ Create a new URI.
41
+
42
+ Args:
43
+ value: The URI string in format "scheme://path/to/resource"
44
+ scheme: The URI scheme (default: 'rcs')
45
+
46
+ Raises:
47
+ InvalidURIError: If the URI format is invalid
48
+ """
49
+ self._raw = value
50
+ self.scheme = scheme
51
+
52
+ parsed = urlparse(value)
53
+ if parsed.scheme != self.scheme:
54
+ raise InvalidURIError(f"Invalid URI scheme. Expected '{self.scheme}://', got '{parsed.scheme}://'")
55
+ if parsed.netloc.startswith((".", "-", "#", " ", "/", "\\")):
56
+ raise InvalidURIError(f"Invalid URI. Path cannot start with invalid characters: '{value}'")
57
+ if not parsed.netloc and parsed.path == "/":
58
+ raise InvalidURIError(f"Invalid URI: '{value}'")
59
+
60
+ self.path = f"/{parsed.netloc}{parsed.path}" if parsed.netloc else f"/{parsed.path}"
61
+
62
+ self._posix_path = PurePosixPath(self.path)
63
+ self.filename = self._posix_path.name
64
+ self.bucket = self._posix_path.parts[1] if len(self._posix_path.parts) > 1 else ""
65
+
66
+ if self._posix_path == PurePosixPath("/"):
67
+ self.parent = self
68
+ else:
69
+ parent_path = self._posix_path.parent
70
+ new_uri = f"{self.scheme}://{str(parent_path)[1:]}"
71
+ self.parent = URI(new_uri, scheme=self.scheme)
72
+
73
+ def is_dir(self) -> bool:
74
+ """Check if the URI points to a directory."""
75
+ return self.path.endswith("/")
76
+
77
+ def __truediv__(self, other: PathLike[str] | str) -> URI:
78
+ """
79
+ Join this URI with another path component.
80
+
81
+ Args:
82
+ other: Path component to join
83
+
84
+ Returns:
85
+ A new URI with the joined path
86
+
87
+ Raises:
88
+ JoinURIError: If trying to join an absolute path
89
+ TypeError: If the path component is not a string or PathLike
90
+ """
91
+ if str(other).startswith("/"):
92
+ raise JoinURIError(f"Cannot join absolute path '{other}' to URI")
93
+
94
+ is_dir = str(other).endswith("/")
95
+ new_path = self._posix_path / other
96
+
97
+ for part in new_path.parts:
98
+ if part == "..":
99
+ new_path = new_path.parent
100
+ elif part != ".":
101
+ new_path = new_path / part
102
+
103
+ new_uri = f"{self.scheme}://{new_path.relative_to('/')}" # we need to strip the starting '/'
104
+ new_uri = new_uri if not is_dir else f"{new_uri}/" # and append slash if the added path was a dir
105
+ return URI(new_uri, scheme=self.scheme)
106
+
107
+ def __str__(self) -> str:
108
+ """Return the original URI string."""
109
+ return self._raw
110
+
111
+ def __repr__(self) -> str:
112
+ """Return the original URI string."""
113
+ return f"URI({self._raw})"
File without changes
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from typing_extensions import List
9
+
10
+ from remotivelabs.cli.broker.lib.broker import SubscribableSignal
11
+ from remotivelabs.cli.connect.protopie import protopie as ppie
12
+ from remotivelabs.cli.typer import typer_utils
13
+ from remotivelabs.cli.utils.console import print_hint
14
+
15
+ app = typer_utils.create_typer()
16
+
17
+
18
+ @app.command()
19
+ def protopie( # noqa: PLR0913
20
+ config: Path = typer.Option(
21
+ None,
22
+ exists=True,
23
+ file_okay=True,
24
+ dir_okay=False,
25
+ writable=False,
26
+ readable=True,
27
+ resolve_path=True,
28
+ help="Configuration file with signal subscriptions and mapping if needed",
29
+ ),
30
+ signal: List[str] = typer.Option([], help="Signal names to subscribe to, mandatory when not using script"),
31
+ signal_name_expression: str = typer.Option(
32
+ None, help='[Experimental] Python expression to rename signal names, i.e \'lower().replace(".","_")\''
33
+ ),
34
+ changed_values_only: bool = typer.Option(
35
+ True, help="Only receive signal when its value is changed to minimize amount of data received"
36
+ ),
37
+ broker_url: str = typer.Option(..., help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
38
+ api_key: str = typer.Option(None, help="Cloud Broker API-KEY", envvar="REMOTIVE_BROKER_API_KEY"),
39
+ pp_connect_host: str = typer.Option("http://localhost:9981", help="ProtoPie Connect URL"),
40
+ ) -> None:
41
+ """
42
+ ProtoPie Connect bridge-app to connect signals with RemotiveBroker
43
+
44
+ Subscribe to signals and send signal values to your Pie in a simple way. You can subscribe to signals from command line
45
+ using --signal or use --config to use a json configuration file.
46
+
47
+ ```
48
+ $ remotive connect protopie --signal vss:Vehicle.Chassis.SteeringWheel.Angle --signal vss:Vehicle.Speed
49
+ ```
50
+
51
+ You can use a configuration file for this if you have many signals, want to share the configuration or you want
52
+ custom mapping of the signal name.
53
+ ```
54
+ $ remotive connect protopie --config my-protopie-config.json
55
+ ```
56
+
57
+ Sample my-protopie-config.json
58
+ ```
59
+ {
60
+ "subscription": {
61
+ "Vehicle.CurrentLocation.Heading": {
62
+ "namespace": "vss"
63
+ },
64
+ "Vehicle.Speed": {
65
+ "namespace": "vss",
66
+ "mapTo": ["Speed", "VehicleSpeed"]
67
+ }
68
+ }
69
+ }
70
+ ```
71
+ For simple changes to signal names its possible to use a simple python expression that will be applied to all signal
72
+ names before its published to ProtoPie connect, i.e replacing chars or substring to match variables in Pie. This is
73
+ intended for simple use cases when you do not have a configuration file, otherwise we recommend using the mapTo
74
+ field in the configuration.
75
+
76
+ This will replace all occurrences of . (dot) with _ (underscore)
77
+ ```
78
+ $ remotive connect protopie --signal vss:Vehicle.Chassis.SteeringWheel.Angle --signal-name-expression 'replace(".", "_")'
79
+ ```
80
+ """
81
+
82
+ if len(signal) > 0 and config is not None:
83
+ print_hint("You must choose either --signal or --config, not both")
84
+ sys.exit(1)
85
+
86
+ if len(signal) == 0 and config is None:
87
+ print_hint("You must choose either --signal or --config")
88
+ sys.exit(1)
89
+
90
+ def to_subscribable_signal(sig: str) -> SubscribableSignal:
91
+ arr = sig.split(":")
92
+
93
+ if len(arr) != 2:
94
+ print_hint(f"--signal must have format namespace:signal ({sig})")
95
+ sys.exit(1)
96
+
97
+ return SubscribableSignal(namespace=arr[0], name=arr[1])
98
+
99
+ if len(signal) > 0:
100
+ signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
101
+ else:
102
+ with open(config, "r", encoding="utf8") as f:
103
+ c = json.load(f)
104
+ s = c["subscription"]
105
+ ss = []
106
+ for entry in s.keys():
107
+ ss.append(SubscribableSignal(namespace=s[entry]["namespace"], name=entry))
108
+ signals_to_subscribe_to = ss
109
+
110
+ ppie.do_connect( # type: ignore
111
+ address=pp_connect_host,
112
+ broker_url=broker_url,
113
+ api_key=api_key,
114
+ expression=signal_name_expression,
115
+ config=config,
116
+ signals=signals_to_subscribe_to,
117
+ on_change_only=changed_values_only,
118
+ )
@@ -0,0 +1,185 @@
1
+ # type: ignore
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Tuple, Union
10
+
11
+ import grpc
12
+ import socketio
13
+ from socketio.exceptions import ConnectionError as SocketIoConnectionError
14
+
15
+ from remotivelabs.cli.broker.lib.broker import SubscribableSignal
16
+ from remotivelabs.cli.broker.lib.client import BrokerException, Client, SignalIdentifier, SignalsInFrame
17
+ from remotivelabs.cli.settings import settings
18
+ from remotivelabs.cli.utils.console import print_generic_error, print_generic_message, print_success, print_unformatted_to_stderr
19
+
20
+ PP_CONNECT_APP_NAME = "RemotiveBridge"
21
+
22
+ io = socketio.Client()
23
+
24
+ _has_received_signal = False
25
+ is_connected = False
26
+ config_path: Path
27
+ x_api_key: str
28
+ broker: Any
29
+
30
+
31
+ @io.on("connect")
32
+ def on_connect() -> None:
33
+ print_success("Connected to ProtoPie Connect")
34
+ io.emit("ppBridgeApp", {"name": PP_CONNECT_APP_NAME})
35
+ io.emit("PLUGIN_STARTED", {"name": PP_CONNECT_APP_NAME})
36
+
37
+ global is_connected # noqa: PLW0603
38
+ is_connected = True
39
+
40
+
41
+ # TODO - Receive message from ProtoPie connect
42
+
43
+
44
+ def get_signals_and_namespaces(
45
+ config: Union[Path, None] = None, signals_to_subscribe_to: Union[List[SubscribableSignal], None] = None
46
+ ) -> Tuple[List[str], List[str], Union[Dict[str, str], None]]:
47
+ if config is not None:
48
+ with open(config) as f:
49
+ mapping = json.load(f)
50
+ sub = mapping["subscription"]
51
+ signals = list(sub.keys())
52
+ namespaces = list(map(lambda x: sub[x]["namespace"], signals))
53
+ else:
54
+ if signals_to_subscribe_to is None:
55
+ signals = []
56
+ namespaces = []
57
+ else:
58
+ signals = list(map(lambda s: s.name, signals_to_subscribe_to))
59
+ namespaces = list(map(lambda s: s.namespace, signals_to_subscribe_to))
60
+ sub = None
61
+ return signals, namespaces, sub
62
+
63
+
64
+ def get_signal_name(expression: str, s_name: str) -> str:
65
+ if expression is not None:
66
+ try:
67
+ sig_name = eval(f"s_name.{expression}")
68
+ return str(sig_name)
69
+ except Exception as e:
70
+ print_generic_error(f"Failed to evaluate your python expression {expression}")
71
+ print_unformatted_to_stderr(e)
72
+ # This was the only way I could make this work, exiting on another thread than main
73
+ os._exit(1)
74
+ else:
75
+ return s_name
76
+
77
+
78
+ def _connect_to_broker(
79
+ config: Union[Path, None] = None,
80
+ signals_to_subscribe_to: Union[List[SubscribableSignal], None] = None,
81
+ expression: str = "",
82
+ on_change_only: bool = False,
83
+ ) -> None: # noqa: C901
84
+ signals, namespaces, sub = get_signals_and_namespaces(config, signals_to_subscribe_to)
85
+
86
+ def on_signals(frame: SignalsInFrame) -> None:
87
+ global _has_received_signal # noqa: PLW0603
88
+ if not _has_received_signal:
89
+ print_generic_message("Bridge-app is properly receiving signals, you are good to go :thumbsup:")
90
+ _has_received_signal = True
91
+
92
+ for s in frame:
93
+ if config and sub is not None:
94
+ sig = sub[s.name()]
95
+ sig = s.name() if "mapTo" not in sig.keys() else sig["mapTo"]
96
+ if isinstance(sig, list):
97
+ for ss in sig:
98
+ io.emit("ppMessage", {"messageId": get_signal_name(expression, ss), "value": str(s.value())})
99
+ else:
100
+ io.emit("ppMessage", {"messageId": get_signal_name(expression, sig), "value": str(s.value())})
101
+ else:
102
+ signal_name = get_signal_name(expression, s.name())
103
+ io.emit("ppMessage", {"messageId": signal_name, "value": str(s.value())})
104
+
105
+ grpc_connect(on_signals, signals_to_subscribe_to, on_change_only)
106
+
107
+
108
+ def grpc_connect(
109
+ on_signals: Any, signals_to_subscribe_to: Union[List[SignalIdentifier], None] = None, on_change_only: bool = False
110
+ ) -> None:
111
+ try:
112
+ print_generic_message("Connecting and subscribing to broker...")
113
+ subscription = None
114
+ client = Client(client_id="cli")
115
+ client.connect(url=broker, api_key=x_api_key)
116
+ client.on_signals = on_signals
117
+
118
+ if signals_to_subscribe_to is None:
119
+ # TODO: use logs instead of print?
120
+ print_generic_error("No signals to subscribe to")
121
+ return
122
+ subscription = client.subscribe(signals_to_subscribe_to=signals_to_subscribe_to, changed_values_only=on_change_only)
123
+ print_generic_message("Subscription to broker completed")
124
+ print_generic_message("Waiting for signals...")
125
+
126
+ while True:
127
+ time.sleep(1)
128
+
129
+ except grpc.RpcError as e:
130
+ print_generic_error("Problems connecting or subscribing")
131
+ if isinstance(e, grpc.Call):
132
+ print_generic_error(f"{e.code()} - {e.details()}")
133
+ else:
134
+ print_generic_error(e)
135
+
136
+ except BrokerException as e:
137
+ print_generic_error(e)
138
+ if subscription is not None:
139
+ subscription.cancel()
140
+
141
+ except KeyboardInterrupt:
142
+ print_generic_message("Keyboard interrupt received. Closing subscription.")
143
+ if subscription is not None:
144
+ subscription.cancel()
145
+
146
+ except Exception as e:
147
+ print_generic_error(e)
148
+
149
+
150
+ def do_connect( # noqa: PLR0913
151
+ address: str,
152
+ broker_url: str,
153
+ api_key: Union[str, None],
154
+ config: Union[Path, None],
155
+ signals: List[SubscribableSignal],
156
+ expression: Union[str, None],
157
+ on_change_only: bool = False,
158
+ ) -> None:
159
+ global broker # noqa: PLW0603
160
+ global x_api_key # noqa: PLW0603
161
+ global config_path # noqa: PLW0603
162
+ broker = broker_url
163
+
164
+ if broker_url.startswith("https"):
165
+ if api_key is None:
166
+ print_generic_message("No --api-key, reading token from file")
167
+ x_api_key = settings.get_active_token_secret()
168
+ else:
169
+ x_api_key = api_key
170
+ elif api_key is not None:
171
+ x_api_key = api_key
172
+ try:
173
+ io.connect(address)
174
+ config_path = config
175
+ while is_connected is None:
176
+ time.sleep(1)
177
+ _connect_to_broker(signals_to_subscribe_to=signals, config=config, expression=expression, on_change_only=on_change_only)
178
+ except SocketIoConnectionError as e:
179
+ print_generic_error("Failed to connect to ProtoPie Connect")
180
+ print_unformatted_to_stderr(e)
181
+ sys.exit(1)
182
+ except Exception as e:
183
+ print_generic_error("Unexpected error")
184
+ print_unformatted_to_stderr(e)
185
+ sys.exit(1)
File without changes
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import typer
6
+ from trogon import Trogon
7
+ from typer.main import get_group
8
+
9
+ from remotivelabs.cli.broker import app as broker_app
10
+ from remotivelabs.cli.cloud import app as cloud_app
11
+ from remotivelabs.cli.connect.connect import app as connect_app
12
+ from remotivelabs.cli.settings import Settings, settings
13
+ from remotivelabs.cli.settings.migration.migrate_all_token_files import migrate_any_legacy_tokens
14
+ from remotivelabs.cli.settings.migration.migrate_config_file import migrate_config_file
15
+ from remotivelabs.cli.settings.migration.migrate_legacy_dirs import migrate_legacy_settings_dirs
16
+ from remotivelabs.cli.tools.tools import app as tools_app
17
+ from remotivelabs.cli.topology.cmd import app as topology_app
18
+ from remotivelabs.cli.typer import typer_utils
19
+ from remotivelabs.cli.utils import versions
20
+ from remotivelabs.cli.utils.console import print_generic_error, print_generic_message
21
+
22
+
23
+ def is_featue_flag_enabled(env_var: str) -> bool:
24
+ """Check if an environment variable indicates a feature is enabled."""
25
+ return os.getenv(env_var, "").lower() in ("true", "1", "yes", "on")
26
+
27
+
28
+ if os.getenv("GRPC_VERBOSITY") is None:
29
+ os.environ["GRPC_VERBOSITY"] = "NONE"
30
+
31
+ app = typer_utils.create_typer(
32
+ rich_markup_mode="rich",
33
+ help="""
34
+ Welcome to RemotiveLabs CLI - Simplify and automate tasks for cloud resources and brokers
35
+
36
+ For documentation - https://docs.remotivelabs.com
37
+ """,
38
+ )
39
+
40
+
41
+ def version_callback(value: bool) -> None:
42
+ if value:
43
+ typer.echo(f"remotivelabs-cli {versions.cli_version()} ({versions.platform_info()})")
44
+
45
+
46
+ def test_callback(value: int) -> None:
47
+ if value:
48
+ print_generic_message(str(value))
49
+ raise typer.Exit()
50
+
51
+
52
+ def check_for_newer_version(settings: Settings) -> None:
53
+ versions.check_for_update(settings)
54
+
55
+
56
+ def run_migrations(settings: Settings) -> None:
57
+ """
58
+ Run all migration scripts.
59
+
60
+ Each migration script is responsible for a particular migration, and order matters.
61
+ """
62
+ # 1. Migrate legacy settings dirs
63
+ migrate_legacy_settings_dirs(settings.config_dir)
64
+
65
+ # 2. Migrate any legacy tokens
66
+ has_migrated_tokens = migrate_any_legacy_tokens(settings)
67
+
68
+ # 3. Migrate legacy config file format
69
+ migrate_config_file(settings.config_file_path, settings)
70
+
71
+ if has_migrated_tokens:
72
+ print_generic_error("Migrated old credentials and configuration files, you may need to login again or activate correct credentials")
73
+
74
+
75
+ def set_default_org_as_env(settings: Settings) -> None:
76
+ """
77
+ If not already set, take the default organisation from file and set as env
78
+ This has to be done early before it is read
79
+ """
80
+ if "REMOTIVE_CLOUD_ORGANIZATION" not in os.environ:
81
+ active_account = settings.get_active_account()
82
+ if active_account and active_account.default_organization:
83
+ os.environ["REMOTIVE_CLOUD_ORGANIZATION"] = active_account.default_organization
84
+
85
+
86
+ @app.callback()
87
+ def main(
88
+ _the_version: bool = typer.Option(
89
+ None,
90
+ "--version",
91
+ callback=version_callback,
92
+ is_eager=True,
93
+ help="Print current version",
94
+ ),
95
+ ) -> None:
96
+ run_migrations(settings)
97
+ check_for_newer_version(settings)
98
+ set_default_org_as_env(settings)
99
+ # Do other global stuff, handle other global options here
100
+
101
+
102
+ @app.command()
103
+ def tui(ctx: typer.Context) -> None:
104
+ """
105
+ Explore remotive-cli and generate commands with this textual user interface application
106
+ """
107
+
108
+ Trogon(get_group(app), click_context=ctx).run()
109
+
110
+
111
+ app.add_typer(broker_app, name="broker", help="Manage a single broker - local or cloud")
112
+ app.add_typer(cloud_app, name="cloud", help="Manage resources in RemotiveCloud")
113
+ app.add_typer(connect_app, name="connect", help="Integrations with other systems")
114
+ app.add_typer(tools_app, name="tools")
115
+ app.add_typer(
116
+ topology_app,
117
+ name="topology",
118
+ help="""
119
+ Interact and manage RemotiveTopology resources
120
+
121
+ Read more at https://docs.remotivelabs.com/docs/remotive-topology
122
+ """,
123
+ )