remotivelabs-cli 0.0.42__py3-none-any.whl → 0.1.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.
- cli/.DS_Store +0 -0
- cli/api/cloud/tokens.py +62 -0
- cli/broker/brokers.py +0 -1
- cli/broker/export.py +4 -4
- cli/broker/lib/broker.py +9 -13
- cli/broker/license_flows.py +1 -1
- cli/broker/scripting.py +2 -1
- cli/broker/signals.py +9 -10
- cli/cloud/auth/cmd.py +25 -6
- cli/cloud/auth/login.py +279 -24
- cli/cloud/auth_tokens.py +295 -12
- cli/cloud/brokers.py +3 -4
- cli/cloud/cloud_cli.py +2 -2
- cli/cloud/configs.py +1 -2
- cli/cloud/organisations.py +90 -2
- cli/cloud/projects.py +1 -2
- cli/cloud/recordings.py +9 -16
- cli/cloud/recordings_playback.py +6 -8
- cli/cloud/sample_recordings.py +2 -3
- cli/cloud/service_account_tokens.py +21 -5
- cli/cloud/service_accounts.py +32 -4
- cli/cloud/storage/cmd.py +1 -1
- cli/cloud/storage/copy.py +3 -4
- cli/connect/connect.py +1 -1
- cli/connect/protopie/protopie.py +12 -14
- cli/remotive.py +30 -6
- cli/settings/__init__.py +1 -2
- cli/settings/config_file.py +85 -0
- cli/settings/core.py +195 -46
- cli/settings/migrate_all_token_files.py +74 -0
- cli/settings/migrate_token_file.py +52 -0
- cli/settings/token_file.py +69 -4
- cli/tools/can/can.py +2 -2
- cli/typer/typer_utils.py +18 -1
- cli/utils/__init__.py +0 -0
- cli/{cloud → utils}/rest_helper.py +109 -38
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/METADATA +6 -4
- remotivelabs_cli-0.1.0a1.dist-info/RECORD +59 -0
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/WHEEL +1 -1
- cli/settings/cmd.py +0 -72
- remotivelabs_cli-0.0.42.dist-info/RECORD +0 -54
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/entry_points.txt +0 -0
@@ -4,13 +4,12 @@ import typer
|
|
4
4
|
|
5
5
|
from cli.settings import settings
|
6
6
|
from cli.typer import typer_utils
|
7
|
-
|
8
|
-
from .rest_helper import RestHelper as Rest
|
7
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
9
8
|
|
10
9
|
app = typer_utils.create_typer()
|
11
10
|
|
12
11
|
|
13
|
-
# TODO: add add interactive flag to set target directory
|
12
|
+
# TODO: add add interactive flag to set target directory
|
14
13
|
@app.command(name="create", help="Create and download a new service account access token")
|
15
14
|
def create(
|
16
15
|
expire_in_days: int = typer.Option(default=365, help="Number of this token is valid"),
|
@@ -23,7 +22,7 @@ def create(
|
|
23
22
|
body=json.dumps({"daysUntilExpiry": expire_in_days}),
|
24
23
|
)
|
25
24
|
|
26
|
-
sat = settings.add_service_account_token(
|
25
|
+
sat = settings.add_service_account_token(response.text)
|
27
26
|
print(f"Service account access token added: {sat.name}")
|
28
27
|
print("\033[93m This file contains secrets and must be kept safe")
|
29
28
|
|
@@ -36,10 +35,27 @@ def list_tokens(
|
|
36
35
|
Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys")
|
37
36
|
|
38
37
|
|
38
|
+
@app.command(name="describe", help="Describe service account access token")
|
39
|
+
def describe(
|
40
|
+
name: str = typer.Argument(..., help="Access token name"),
|
41
|
+
service_account: str = typer.Option(..., help="Service account name"),
|
42
|
+
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
43
|
+
) -> None:
|
44
|
+
Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
|
45
|
+
|
46
|
+
|
39
47
|
@app.command(name="revoke", help="Revoke service account access token")
|
40
48
|
def revoke(
|
41
49
|
name: str = typer.Argument(..., help="Access token name"),
|
50
|
+
delete: bool = typer.Option(True, help="Also deletes the token after revocation"),
|
42
51
|
service_account: str = typer.Option(..., help="Service account name"),
|
43
52
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
44
53
|
) -> None:
|
45
|
-
Rest.
|
54
|
+
res = Rest.handle_get(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}", return_response=True)
|
55
|
+
if not res.json()["revoked"]:
|
56
|
+
Rest.handle_patch(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}/revoke", quiet=True)
|
57
|
+
if delete:
|
58
|
+
Rest.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}", quiet=True)
|
59
|
+
token_with_name = [token for token in settings.list_service_account_tokens() if token.name == name]
|
60
|
+
if len(token_with_name) > 0:
|
61
|
+
settings.remove_token_file(name)
|
cli/cloud/service_accounts.py
CHANGED
@@ -6,9 +6,9 @@ from typing import List
|
|
6
6
|
import typer
|
7
7
|
|
8
8
|
from cli.typer import typer_utils
|
9
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
9
10
|
|
10
11
|
from . import service_account_tokens
|
11
|
-
from .rest_helper import RestHelper as Rest
|
12
12
|
|
13
13
|
app = typer_utils.create_typer()
|
14
14
|
|
@@ -18,7 +18,27 @@ def list_service_accounts(project: str = typer.Option(..., help="Project ID", en
|
|
18
18
|
Rest.handle_get(f"/api/project/{project}/admin/accounts")
|
19
19
|
|
20
20
|
|
21
|
-
|
21
|
+
ROLES_DESCRIPTION = """
|
22
|
+
[bold]Supported roles [/bold]
|
23
|
+
project/admin - Full project support, view, edit, delete and manage users
|
24
|
+
project/user - View, edit, upload, delete but no admin
|
25
|
+
project/viewer - View only
|
26
|
+
project/storageCreator - Can upload to storage but not view, overwrite or delete
|
27
|
+
org/topologyRunner - Can start RemotiveTopology
|
28
|
+
"""
|
29
|
+
|
30
|
+
|
31
|
+
@app.command(
|
32
|
+
name="create",
|
33
|
+
help=f"""
|
34
|
+
Create a new service account with one or more roles.
|
35
|
+
|
36
|
+
[bold]Must only use lowercase letters, digits, or dashes, and must not contain -- or end with a dash.[/bold]
|
37
|
+
|
38
|
+
remotive cloud service-accounts create --role project/user --role project/storageCreator
|
39
|
+
{ROLES_DESCRIPTION}
|
40
|
+
""",
|
41
|
+
)
|
22
42
|
def create_service_account(
|
23
43
|
name: str,
|
24
44
|
role: List[str] = typer.Option(..., help="Roles to apply"),
|
@@ -28,9 +48,17 @@ def create_service_account(
|
|
28
48
|
Rest.handle_post(url=f"/api/project/{project}/admin/accounts", body=json.dumps(data))
|
29
49
|
|
30
50
|
|
31
|
-
@app.command(
|
51
|
+
@app.command(
|
52
|
+
name="update",
|
53
|
+
help=f"""
|
54
|
+
Update an existing service account with one or more roles.
|
55
|
+
|
56
|
+
remotive cloud service-accounts update --role project/user --role project/storageCreator
|
57
|
+
{ROLES_DESCRIPTION}
|
58
|
+
""",
|
59
|
+
)
|
32
60
|
def update_service_account(
|
33
|
-
service_account: str = typer.
|
61
|
+
service_account: str = typer.Argument(..., help="Service account name"),
|
34
62
|
role: List[str] = typer.Option(..., help="Roles to apply"),
|
35
63
|
project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
|
36
64
|
) -> None:
|
cli/cloud/storage/cmd.py
CHANGED
@@ -3,13 +3,13 @@ import sys
|
|
3
3
|
import typer
|
4
4
|
from typing_extensions import Annotated
|
5
5
|
|
6
|
-
from cli.cloud.rest_helper import RestHelper as Rest
|
7
6
|
from cli.cloud.storage.copy import copy
|
8
7
|
from cli.cloud.storage.uri_or_path import UriOrPath
|
9
8
|
from cli.cloud.storage.uri_or_path import uri as uri_parser
|
10
9
|
from cli.cloud.uri import URI, InvalidURIError, JoinURIError
|
11
10
|
from cli.errors import ErrorPrinter
|
12
11
|
from cli.typer import typer_utils
|
12
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
13
13
|
|
14
14
|
HELP = """
|
15
15
|
Manage files ([yellow]Beta feature not available for all customers[/yellow])
|
cli/cloud/storage/copy.py
CHANGED
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
3
3
|
import json
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
|
-
from cli.cloud.rest_helper import RestHelper as Rest
|
7
6
|
from cli.cloud.resumable_upload import upload_signed_url
|
8
7
|
from cli.cloud.uri import URI
|
8
|
+
from cli.utils.rest_helper import RestHelper as Rest
|
9
9
|
|
10
10
|
_RCS_STORAGE_PATH = "/api/project/{project}/files/storage{path}"
|
11
11
|
|
@@ -73,9 +73,8 @@ def _download(source: URI, dest: Path, project: str, overwrite: bool = False) ->
|
|
73
73
|
# create a target file name if destination is a dir
|
74
74
|
dest = dest / source.filename
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
raise FileNotFoundError(f"Destination directory {dest.parent} does not exist")
|
76
|
+
elif not dest.parent.is_dir() or not dest.parent.exists():
|
77
|
+
raise FileNotFoundError(f"Destination directory {dest.parent} does not exist")
|
79
78
|
|
80
79
|
if dest.exists() and not overwrite:
|
81
80
|
raise FileExistsError(f"Destination file {dest} already exists")
|
cli/connect/connect.py
CHANGED
cli/connect/protopie/protopie.py
CHANGED
@@ -1,26 +1,25 @@
|
|
1
1
|
# type: ignore
|
2
|
-
# pylint: skip-file
|
3
2
|
from __future__ import annotations
|
4
3
|
|
5
4
|
import json
|
6
5
|
import os
|
6
|
+
import sys
|
7
7
|
import time
|
8
8
|
import traceback
|
9
9
|
from pathlib import Path
|
10
10
|
from typing import Any, Dict, List, Tuple, Union
|
11
11
|
|
12
12
|
import grpc
|
13
|
-
import socketio
|
13
|
+
import socketio
|
14
14
|
from remotivelabs.broker.sync import BrokerException, Client, SignalIdentifier, SignalsInFrame
|
15
15
|
from rich import print as pretty_print
|
16
16
|
from rich.console import Console
|
17
|
-
from socketio.exceptions import ConnectionError as SocketIoConnectionError
|
17
|
+
from socketio.exceptions import ConnectionError as SocketIoConnectionError
|
18
18
|
|
19
19
|
from cli.broker.lib.broker import SubscribableSignal
|
20
20
|
from cli.errors import ErrorPrinter
|
21
21
|
from cli.settings import settings
|
22
22
|
|
23
|
-
global PP_CONNECT_APP_NAME
|
24
23
|
PP_CONNECT_APP_NAME = "RemotiveBridge"
|
25
24
|
|
26
25
|
io = socketio.Client()
|
@@ -34,13 +33,13 @@ x_api_key: str
|
|
34
33
|
broker: Any
|
35
34
|
|
36
35
|
|
37
|
-
@io.on("connect")
|
36
|
+
@io.on("connect")
|
38
37
|
def on_connect() -> None:
|
39
38
|
print("Connected to ProtoPie Connect")
|
40
39
|
io.emit("ppBridgeApp", {"name": PP_CONNECT_APP_NAME})
|
41
40
|
io.emit("PLUGIN_STARTED", {"name": PP_CONNECT_APP_NAME})
|
42
41
|
|
43
|
-
global is_connected
|
42
|
+
global is_connected # noqa: PLW0603
|
44
43
|
is_connected = True
|
45
44
|
|
46
45
|
|
@@ -90,7 +89,7 @@ def _connect_to_broker(
|
|
90
89
|
signals, namespaces, sub = get_signals_and_namespaces(config, signals_to_subscribe_to)
|
91
90
|
|
92
91
|
def on_signals(frame: SignalsInFrame) -> None:
|
93
|
-
global _has_received_signal
|
92
|
+
global _has_received_signal # noqa: PLW0603
|
94
93
|
if not _has_received_signal:
|
95
94
|
pretty_print("Bridge-app is properly receiving signals, you are good to go :thumbsup:")
|
96
95
|
_has_received_signal = True
|
@@ -151,10 +150,9 @@ def grpc_connect(
|
|
151
150
|
except Exception as e:
|
152
151
|
print(traceback.format_exc())
|
153
152
|
err_console.print(f":boom: {e}")
|
154
|
-
# exit(1)
|
155
153
|
|
156
154
|
|
157
|
-
def do_connect(
|
155
|
+
def do_connect( # noqa: PLR0913
|
158
156
|
address: str,
|
159
157
|
broker_url: str,
|
160
158
|
api_key: Union[str, None],
|
@@ -163,9 +161,9 @@ def do_connect(
|
|
163
161
|
expression: Union[str, None],
|
164
162
|
on_change_only: bool = False,
|
165
163
|
) -> None:
|
166
|
-
global broker
|
167
|
-
global x_api_key
|
168
|
-
global config_path
|
164
|
+
global broker # noqa: PLW0603
|
165
|
+
global x_api_key # noqa: PLW0603
|
166
|
+
global config_path # noqa: PLW0603
|
169
167
|
broker = broker_url
|
170
168
|
|
171
169
|
if broker_url.startswith("https"):
|
@@ -188,8 +186,8 @@ def do_connect(
|
|
188
186
|
except SocketIoConnectionError as e:
|
189
187
|
err_console.print(":boom: [bold red]Failed to connect to ProtoPie Connect[/bold red]")
|
190
188
|
err_console.print(e)
|
191
|
-
exit(1)
|
189
|
+
sys.exit(1)
|
192
190
|
except Exception as e:
|
193
191
|
err_console.print(":boom: [bold red]Unexpected error[/bold red]")
|
194
192
|
err_console.print(e)
|
195
|
-
exit(1)
|
193
|
+
sys.exit(1)
|
cli/remotive.py
CHANGED
@@ -1,18 +1,25 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import os
|
2
4
|
from importlib.metadata import version
|
3
5
|
|
4
6
|
import typer
|
5
7
|
from rich import print as rich_print
|
8
|
+
from rich.console import Console
|
6
9
|
from trogon import Trogon # type: ignore
|
7
10
|
from typer.main import get_group
|
8
11
|
|
12
|
+
from cli.settings.migrate_all_token_files import migrate_any_legacy_tokens
|
13
|
+
|
9
14
|
from .broker.brokers import app as broker_app
|
10
15
|
from .cloud.cloud_cli import app as cloud_app
|
11
16
|
from .connect.connect import app as connect_app
|
12
|
-
from .settings
|
17
|
+
from .settings import settings
|
13
18
|
from .tools.tools import app as tools_app
|
14
19
|
from .typer import typer_utils
|
15
20
|
|
21
|
+
err_console = Console(stderr=True)
|
22
|
+
|
16
23
|
if os.getenv("GRPC_VERBOSITY") is None:
|
17
24
|
os.environ["GRPC_VERBOSITY"] = "NONE"
|
18
25
|
|
@@ -25,6 +32,8 @@ For documentation - https://docs.remotivelabs.com
|
|
25
32
|
""",
|
26
33
|
)
|
27
34
|
|
35
|
+
# settings.set_default_config_as_env()
|
36
|
+
|
28
37
|
|
29
38
|
def version_callback(value: bool) -> None:
|
30
39
|
if value:
|
@@ -37,17 +46,33 @@ def test_callback(value: int) -> None:
|
|
37
46
|
if value:
|
38
47
|
rich_print(value)
|
39
48
|
raise typer.Exit()
|
40
|
-
|
41
|
-
|
42
|
-
|
49
|
+
|
50
|
+
|
51
|
+
def _migrate_old_tokens() -> None:
|
52
|
+
tokens = settings.list_personal_tokens()
|
53
|
+
tokens.extend(settings.list_service_account_tokens())
|
54
|
+
if migrate_any_legacy_tokens(tokens):
|
55
|
+
err_console.print("Migrated old credentials and configuration files, you may need to login again or activate correct credentials")
|
56
|
+
|
57
|
+
|
58
|
+
def _set_default_org_as_env() -> None:
|
59
|
+
"""
|
60
|
+
If not already set, take the default organisation from file and set as env
|
61
|
+
This has to be done early before it is read
|
62
|
+
"""
|
63
|
+
if "REMOTIVE_CLOUD_ORGANISATION" not in os.environ:
|
64
|
+
org = settings.get_cli_config().get_active_default_organisation()
|
65
|
+
if org is not None:
|
66
|
+
os.environ["REMOTIVE_CLOUD_ORGANISATION"] = org
|
43
67
|
|
44
68
|
|
45
69
|
@app.callback()
|
46
70
|
def main(
|
47
71
|
_the_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=False, help="Print current version"),
|
48
72
|
) -> None:
|
73
|
+
_set_default_org_as_env()
|
74
|
+
_migrate_old_tokens()
|
49
75
|
# Do other global stuff, handle other global options here
|
50
|
-
return
|
51
76
|
|
52
77
|
|
53
78
|
@app.command()
|
@@ -65,6 +90,5 @@ app.add_typer(
|
|
65
90
|
name="cloud",
|
66
91
|
help="Manage resources in RemotiveCloud",
|
67
92
|
)
|
68
|
-
app.add_typer(settings_app, name="config", help="Manage access tokens")
|
69
93
|
app.add_typer(connect_app, name="connect", help="Integrations with other systems")
|
70
94
|
app.add_typer(tools_app, name="tools")
|
cli/settings/__init__.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
from cli.settings.cmd import app
|
2
1
|
from cli.settings.core import InvalidSettingsFilePathError, Settings, TokenNotFoundError, settings
|
3
2
|
from cli.settings.token_file import TokenFile
|
4
3
|
|
5
|
-
__all__ = ["
|
4
|
+
__all__ = ["settings", "TokenFile", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]
|
@@ -0,0 +1,85 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import dataclasses
|
4
|
+
import json
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Dict, Optional
|
7
|
+
|
8
|
+
from dacite import from_dict
|
9
|
+
|
10
|
+
|
11
|
+
def loads(data: str) -> ConfigFile:
|
12
|
+
d = json.loads(data)
|
13
|
+
return from_dict(ConfigFile, d)
|
14
|
+
|
15
|
+
|
16
|
+
def dumps(config: ConfigFile) -> str:
|
17
|
+
return json.dumps(dataclasses.asdict(config), default=str)
|
18
|
+
|
19
|
+
|
20
|
+
@dataclass
|
21
|
+
class Account:
|
22
|
+
credentials_name: str
|
23
|
+
default_organization: Optional[str] = None
|
24
|
+
# Add project as well
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class ConfigFile:
|
29
|
+
version: str = "1.0"
|
30
|
+
active: Optional[str] = None
|
31
|
+
accounts: Dict[str, Account] = dataclasses.field(default_factory=dict)
|
32
|
+
|
33
|
+
def get_active_default_organisation(self) -> Optional[str]:
|
34
|
+
active_account = self.get_active()
|
35
|
+
return active_account.default_organization if active_account is not None else None
|
36
|
+
|
37
|
+
def get_active(self) -> Optional[Account]:
|
38
|
+
if self.active is not None:
|
39
|
+
account = self.accounts.get(self.active)
|
40
|
+
if account is not None:
|
41
|
+
return account
|
42
|
+
raise KeyError(f"Activated account {self.active} is not a valid account")
|
43
|
+
return None
|
44
|
+
|
45
|
+
def activate(self, email: str) -> None:
|
46
|
+
account = self.accounts.get(email)
|
47
|
+
|
48
|
+
if account is not None:
|
49
|
+
self.active = email
|
50
|
+
else:
|
51
|
+
raise KeyError(f"Account {email} does not exists")
|
52
|
+
|
53
|
+
def get_account(self, email: str) -> Optional[Account]:
|
54
|
+
if self.accounts:
|
55
|
+
return self.accounts[email]
|
56
|
+
return None
|
57
|
+
|
58
|
+
def remove_account(self, email: str) -> None:
|
59
|
+
if self.accounts:
|
60
|
+
self.accounts.pop(email, None)
|
61
|
+
|
62
|
+
def init_account(self, email: str, token_name: str) -> None:
|
63
|
+
if self.accounts is None:
|
64
|
+
self.accounts = {}
|
65
|
+
|
66
|
+
account = self.accounts.get(email)
|
67
|
+
if not account:
|
68
|
+
account = Account(credentials_name=token_name)
|
69
|
+
else:
|
70
|
+
account.credentials_name = token_name
|
71
|
+
self.accounts[email] = account
|
72
|
+
|
73
|
+
def set_account_field(self, email: str, default_organization: Optional[str] = None) -> ConfigFile:
|
74
|
+
if self.accounts is None:
|
75
|
+
self.accounts = {}
|
76
|
+
|
77
|
+
account = self.accounts.get(email)
|
78
|
+
if not account:
|
79
|
+
raise KeyError(f"Account with email {email} has not been initialized with token")
|
80
|
+
|
81
|
+
# Update only fields explicitly passed
|
82
|
+
if default_organization is not None:
|
83
|
+
account.default_organization = default_organization
|
84
|
+
|
85
|
+
return self
|