remotivelabs-cli 0.0.41__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.
Files changed (43) hide show
  1. cli/.DS_Store +0 -0
  2. cli/api/cloud/tokens.py +62 -0
  3. cli/broker/brokers.py +0 -1
  4. cli/broker/export.py +4 -4
  5. cli/broker/lib/broker.py +9 -13
  6. cli/broker/license_flows.py +1 -1
  7. cli/broker/scripting.py +2 -1
  8. cli/broker/signals.py +9 -10
  9. cli/cloud/auth/cmd.py +25 -6
  10. cli/cloud/auth/login.py +279 -24
  11. cli/cloud/auth_tokens.py +295 -12
  12. cli/cloud/brokers.py +3 -4
  13. cli/cloud/cloud_cli.py +2 -2
  14. cli/cloud/configs.py +1 -2
  15. cli/cloud/organisations.py +92 -20
  16. cli/cloud/projects.py +1 -2
  17. cli/cloud/recordings.py +9 -16
  18. cli/cloud/recordings_playback.py +6 -8
  19. cli/cloud/sample_recordings.py +2 -3
  20. cli/cloud/service_account_tokens.py +21 -5
  21. cli/cloud/service_accounts.py +32 -4
  22. cli/cloud/storage/cmd.py +1 -1
  23. cli/cloud/storage/copy.py +3 -4
  24. cli/connect/connect.py +1 -1
  25. cli/connect/protopie/protopie.py +12 -14
  26. cli/remotive.py +30 -6
  27. cli/settings/__init__.py +1 -2
  28. cli/settings/config_file.py +85 -0
  29. cli/settings/core.py +195 -46
  30. cli/settings/migrate_all_token_files.py +74 -0
  31. cli/settings/migrate_token_file.py +52 -0
  32. cli/settings/token_file.py +69 -4
  33. cli/tools/can/can.py +2 -2
  34. cli/typer/typer_utils.py +18 -1
  35. cli/utils/__init__.py +0 -0
  36. cli/{cloud → utils}/rest_helper.py +109 -38
  37. {remotivelabs_cli-0.0.41.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/METADATA +6 -4
  38. remotivelabs_cli-0.1.0a1.dist-info/RECORD +59 -0
  39. {remotivelabs_cli-0.0.41.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/WHEEL +1 -1
  40. cli/settings/cmd.py +0 -72
  41. remotivelabs_cli-0.0.41.dist-info/RECORD +0 -54
  42. {remotivelabs_cli-0.0.41.dist-info → remotivelabs_cli-0.1.0a1.dist-info}/LICENSE +0 -0
  43. {remotivelabs_cli-0.0.41.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 # pylint: disable=fixme
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(service_account, response.text)
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.handle_delete(f"/api/project/{project}/admin/accounts/{service_account}/keys/{name}")
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)
@@ -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
- @app.command(name="create", help="Create service account")
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(name="update", help="Update service account")
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.Option(..., help="Service account name"),
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
- else:
77
- if not dest.parent.is_dir() or not dest.parent.exists():
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
@@ -17,7 +17,7 @@ app = typer_utils.create_typer()
17
17
 
18
18
 
19
19
  @app.command()
20
- def protopie( # pylint: disable=R0913
20
+ def protopie( # noqa: PLR0913
21
21
  config: Path = typer.Option(
22
22
  None,
23
23
  exists=True,
@@ -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 # type: ignore
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 # type: ignore
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") # type: ignore
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.cmd import app as settings_app
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
- # if value:
41
- # typer.echo(f"Awesome CLI Version: 0.0.22a")
42
- # raise typer.Exit()
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__ = ["app", "settings", "TokenFile", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]
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