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,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
|
+
)
|