remotivelabs-cli 0.0.42__py3-none-any.whl → 0.1.0a2__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 (44) 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 +37 -13
  10. cli/cloud/auth/login.py +279 -24
  11. cli/cloud/auth_tokens.py +319 -12
  12. cli/cloud/brokers.py +3 -4
  13. cli/cloud/cloud_cli.py +5 -5
  14. cli/cloud/configs.py +1 -2
  15. cli/cloud/organisations.py +101 -2
  16. cli/cloud/projects.py +5 -6
  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/errors.py +6 -1
  27. cli/remotive.py +30 -6
  28. cli/settings/__init__.py +1 -2
  29. cli/settings/config_file.py +92 -0
  30. cli/settings/core.py +188 -45
  31. cli/settings/migrate_all_token_files.py +74 -0
  32. cli/settings/migrate_token_file.py +52 -0
  33. cli/settings/token_file.py +69 -4
  34. cli/tools/can/can.py +2 -2
  35. cli/typer/typer_utils.py +18 -1
  36. cli/utils/__init__.py +0 -0
  37. cli/{cloud → utils}/rest_helper.py +114 -39
  38. {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/METADATA +6 -4
  39. remotivelabs_cli-0.1.0a2.dist-info/RECORD +59 -0
  40. {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/WHEEL +1 -1
  41. cli/settings/cmd.py +0 -72
  42. remotivelabs_cli-0.0.42.dist-info/RECORD +0 -54
  43. {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/LICENSE +0 -0
  44. {remotivelabs_cli-0.0.42.dist-info → remotivelabs_cli-0.1.0a2.dist-info}/entry_points.txt +0 -0
cli/cloud/brokers.py CHANGED
@@ -10,14 +10,13 @@ import typer
10
10
  import websocket
11
11
 
12
12
  from cli.typer import typer_utils
13
-
14
- from .rest_helper import RestHelper as Rest
13
+ from cli.utils.rest_helper import RestHelper as Rest
15
14
 
16
15
  app = typer_utils.create_typer()
17
16
 
18
17
 
19
18
  @app.command("list", help="Lists brokers in project")
20
- def list(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None: # pylint: disable=W0622
19
+ def list(project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT")) -> None:
21
20
  Rest.handle_get(f"/api/project/{project}/brokers")
22
21
 
23
22
 
@@ -94,7 +93,7 @@ def logs(
94
93
  def on_message(_wsapp: Any, message: str) -> None:
95
94
  print(message)
96
95
 
97
- def on_error(_wsapp: Any, err: str) -> None: # pylint: disable=W0613
96
+ def on_error(_wsapp: Any, err: str) -> None:
98
97
  print("EXAMPLE error encountered: ", err)
99
98
 
100
99
  Rest.ensure_auth_token()
cli/cloud/cloud_cli.py CHANGED
@@ -1,21 +1,21 @@
1
1
  import typer
2
2
 
3
3
  from cli.cloud import auth, brokers, configs, organisations, projects, recordings, sample_recordings, service_accounts, storage
4
- from cli.cloud.rest_helper import RestHelper
5
4
  from cli.typer import typer_utils
5
+ from cli.utils.rest_helper import RestHelper
6
6
 
7
7
  app = typer_utils.create_typer()
8
8
 
9
9
 
10
- @app.command(help="List licenses for an organisation")
10
+ @app.command(help="List licenses for an organization")
11
11
  def licenses(
12
- organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
12
+ organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
13
13
  filter_option: str = typer.Option("all", help="all, valid, expired"),
14
14
  ) -> None:
15
- RestHelper.handle_get(f"/api/bu/{organisation}/licenses", {"filter": filter_option})
15
+ RestHelper.handle_get(f"/api/bu/{organization}/licenses", {"filter": filter_option})
16
16
 
17
17
 
18
- app.add_typer(organisations.app, name="organisations", help="Manage organisations")
18
+ app.add_typer(organisations.app, name="organizations", help="Manage organizations")
19
19
  app.add_typer(projects.app, name="projects", help="Manage projects")
20
20
  app.add_typer(auth.app, name="auth")
21
21
  app.add_typer(brokers.app, name="brokers", help="Manage cloud broker lifecycle")
cli/cloud/configs.py CHANGED
@@ -8,8 +8,7 @@ import typer
8
8
  from rich.progress import Progress, SpinnerColumn, TextColumn
9
9
 
10
10
  from cli.typer import typer_utils
11
-
12
- from .rest_helper import RestHelper as Rest
11
+ from cli.utils.rest_helper import RestHelper as Rest
13
12
 
14
13
  app = typer_utils.create_typer()
15
14
 
@@ -1,12 +1,111 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
4
+ from dataclasses import dataclass
5
+ from typing import List, Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
2
10
 
3
- from cli.cloud.rest_helper import RestHelper
11
+ from cli.errors import ErrorPrinter
12
+ from cli.settings import settings
4
13
  from cli.typer import typer_utils
14
+ from cli.utils.rest_helper import RestHelper
5
15
 
16
+ console = Console(stderr=False)
6
17
  app = typer_utils.create_typer()
7
18
 
8
19
 
9
- @app.command(name="list", help="List your available organisations")
20
+ @dataclass
21
+ class Organisation:
22
+ display_name: str
23
+ uid: str
24
+
25
+
26
+ def _prompt_choice(choices: List[Organisation]) -> Optional[Organisation]:
27
+ table = Table("#", "Name", "Uid", "Default")
28
+
29
+ token = settings.get_active_token_file()
30
+ config = settings.get_cli_config()
31
+
32
+ current_default_org = config.accounts[token.account.email].default_organization if token is not None else None
33
+
34
+ for idx, choice in enumerate(choices, start=1):
35
+ table.add_row(
36
+ f"[yellow]{idx}",
37
+ f"[bold]{choice.display_name}[/bold]",
38
+ choice.uid,
39
+ ":thumbsup:" if current_default_org is not None and current_default_org == choice.uid else "",
40
+ )
41
+ console.print(table)
42
+
43
+ typer.echo("")
44
+ selection = typer.prompt(f"Enter the number(# 1-{len(choices)}) of the organization to select (or q to quit)")
45
+
46
+ if selection == "q":
47
+ return None
48
+ try:
49
+ index = int(selection) - 1
50
+ if 0 <= index < len(choices):
51
+ return choices[index]
52
+ raise ValueError
53
+ except ValueError:
54
+ typer.echo("Invalid choice, please try again")
55
+ return _prompt_choice(choices)
56
+
57
+
58
+ @app.command("default")
59
+ def select_default_org(
60
+ organization_uid: str = typer.Argument(None, help="Organization uid or empty to select one"),
61
+ get: bool = typer.Option(False, help="Print current default organization"),
62
+ ) -> None:
63
+ do_select_default_org(organization_uid, get)
64
+
65
+
66
+ def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = False) -> None:
67
+ r"""
68
+ Set default organization for the currently activated user, empty to choose from available organizations or organization uid as argument
69
+
70
+ remotive cloud organizations default my_org \[set specific org uid]
71
+ remotive cloud organizations default \[select one from prompt]
72
+ remotive cloud organizations default --get \[print current default]
73
+
74
+ Note that service-accounts does Not have permission to list organizations and will get a 403 Forbidden response so you must
75
+ select the organization uid as argument
76
+ """
77
+ if get:
78
+ default_organisation = settings.get_cli_config().get_active_default_organisation()
79
+ if default_organisation is not None:
80
+ console.print(default_organisation)
81
+ else:
82
+ console.print("No default organization set")
83
+ elif organisation_uid is not None:
84
+ settings.set_default_organisation(organisation_uid)
85
+ else:
86
+ account = settings.get_cli_config().get_active()
87
+ if account is not None:
88
+ token = settings.get_token_file(account.credentials_name)
89
+ if token.type != "authorized_user":
90
+ ErrorPrinter.print_hint(
91
+ "You must supply the organization name as argument when using a service-account since the "
92
+ "service-account is not allowed to list"
93
+ )
94
+ return
95
+
96
+ r = RestHelper.handle_get("/api/bu", return_response=True)
97
+ orgs = r.json()
98
+ orgs = [Organisation(display_name=o["organisation"]["displayName"], uid=o["organisation"]["uid"]) for o in orgs]
99
+
100
+ selected = _prompt_choice(orgs)
101
+
102
+ if selected is not None:
103
+ settings.get_cli_config()
104
+ typer.echo(f"Default organisation: {selected.display_name} (uid: {selected.uid})")
105
+ settings.set_default_organisation(selected.uid)
106
+
107
+
108
+ @app.command(name="list", help="List your available organizations")
10
109
  def list_orgs() -> None:
11
110
  r = RestHelper.handle_get("/api/bu", return_response=True)
12
111
  orgs = [{"uid": org["organisation"]["uid"], "displayName": org["organisation"]["displayName"]} for org in r.json()]
cli/cloud/projects.py CHANGED
@@ -3,15 +3,14 @@ import json
3
3
  import typer
4
4
 
5
5
  from cli.typer import typer_utils
6
-
7
- from .rest_helper import RestHelper as Rest
6
+ from cli.utils.rest_helper import RestHelper as Rest
8
7
 
9
8
  app = typer_utils.create_typer()
10
9
 
11
10
 
12
11
  @app.command(name="list", help="List your projects")
13
- def list_projects(organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION")) -> None:
14
- r = Rest.handle_get(url=f"/api/bu/{organisation}/me", return_response=True)
12
+ def list_projects(organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION")) -> None:
13
+ r = Rest.handle_get(url=f"/api/bu/{organization}/me", return_response=True)
15
14
  if r is None:
16
15
  return
17
16
 
@@ -28,7 +27,7 @@ def list_projects(organisation: str = typer.Option(..., help="Organisation ID",
28
27
  @app.command(name="create")
29
28
  def create_project(
30
29
  project_uid: str = typer.Argument(help="Project UID"),
31
- organisation: str = typer.Option(..., help="Organisation ID", envvar="REMOTIVE_CLOUD_ORGANISATION"),
30
+ organization: str = typer.Option(..., help="Organization ID", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
32
31
  project_display_name: str = typer.Option(default="", help="Project display name"),
33
32
  ) -> None:
34
33
  create_project_req = {
@@ -37,7 +36,7 @@ def create_project(
37
36
  "description": "",
38
37
  }
39
38
 
40
- Rest.handle_post(url=f"/api/bu/{organisation}/project", body=json.dumps(create_project_req))
39
+ Rest.handle_post(url=f"/api/bu/{organization}/project", body=json.dumps(create_project_req))
41
40
 
42
41
 
43
42
  @app.command(name="delete")
cli/cloud/recordings.py CHANGED
@@ -19,11 +19,11 @@ from typing_extensions import Annotated
19
19
  from cli.cloud.uri import URI
20
20
  from cli.errors import ErrorPrinter
21
21
  from cli.typer import typer_utils
22
+ from cli.utils.rest_helper import RestHelper as Rest
23
+ from cli.utils.rest_helper import err_console
22
24
 
23
25
  from ..broker.lib.broker import Broker
24
26
  from .recordings_playback import app as playback_app
25
- from .rest_helper import RestHelper as Rest
26
- from .rest_helper import err_console
27
27
 
28
28
  app = typer_utils.create_typer()
29
29
  app.add_typer(playback_app, name="playback")
@@ -129,7 +129,6 @@ def mount( # noqa: C901
129
129
  transformation_name: str = typer.Option("default", help="Specify a custom signal transformation to use"),
130
130
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
131
131
  ) -> None:
132
- # pylint: disable=R0912
133
132
  Rest.ensure_auth_token()
134
133
 
135
134
  Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
@@ -230,7 +229,7 @@ def delete_recording_file(
230
229
 
231
230
 
232
231
  @app.command()
233
- def upload( # noqa: C901
232
+ def upload( # noqa: C901, PLR0912, PLR0915
234
233
  path: Path = typer.Argument(
235
234
  ...,
236
235
  exists=True,
@@ -244,7 +243,6 @@ def upload( # noqa: C901
244
243
  project: str = typer.Option(..., help="Project ID", envvar="REMOTIVE_CLOUD_PROJECT"),
245
244
  recording_session: Optional[str] = typer.Option(default=None, help="Optional existing recording to upload file to"),
246
245
  ) -> None:
247
- # pylint: disable=R0912,R0914,R0915
248
246
  """
249
247
  Uploads a recording to RemotiveCloud.
250
248
  Except for recordings from RemotiveBroker you can also upload Can ASC (.asc), Can BLF(.blf) and Can LOG (.log, .txt)
@@ -281,8 +279,7 @@ def upload( # noqa: C901
281
279
  return
282
280
 
283
281
  # Exact same as in cloud console
284
- def get_processing_message(step: str) -> str:
285
- # pylint: disable=R0911
282
+ def get_processing_message(step: str) -> str: # noqa: PLR0911
286
283
  if step == "REQUESTED":
287
284
  return "Preparing file..."
288
285
  if step == "VALIDATING":
@@ -333,9 +330,9 @@ def upload( # noqa: C901
333
330
  err_console.print(f":boom: [bold red]Got status code[/bold red]: {upload_response.status_code} {upload_response.text}")
334
331
 
335
332
 
336
- # TODO - Change to use Path for directory # pylint: disable=W0511
333
+ # TODO - Change to use Path for directory
337
334
  @app.command()
338
- def upload_broker_configuration( # noqa: C901
335
+ def upload_broker_configuration(
339
336
  directory: Path = typer.Argument(
340
337
  ...,
341
338
  exists=True,
@@ -353,8 +350,6 @@ def upload_broker_configuration( # noqa: C901
353
350
  """
354
351
  Uploads a broker configuration directory
355
352
  """
356
- # pylint: disable=R0914,R0915
357
-
358
353
  # Must end with /
359
354
 
360
355
  #
@@ -431,7 +426,7 @@ def upload_broker_configuration( # noqa: C901
431
426
  path = file["local_path"]
432
427
  url = upload_urls[key]
433
428
  headers = {"Content-Type": "application/x-www-form-urlencoded"}
434
- r = requests.put(url, open(path, "rb"), headers=headers, timeout=60) # pylint: disable=R1732
429
+ r = requests.put(url, open(path, "rb"), headers=headers, timeout=60)
435
430
  if r.status_code != 200:
436
431
  print("Failed to upload broker configuration")
437
432
  print(r.status_code)
@@ -529,11 +524,9 @@ def stop(
529
524
  _do_change_playback_mode("stop", recording_session, broker, project)
530
525
 
531
526
 
532
- # pylint: disable-next=C0301
533
- def _do_change_playback_mode(
527
+ def _do_change_playback_mode( # noqa: PLR0912
534
528
  mode: str, recording_session: str, broker_name: Optional[str], project: str, seconds: Optional[int] = None
535
- ) -> None: # noqa: C901
536
- # pylint: disable=R0912,R0914,R0915
529
+ ) -> None:
537
530
  response = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
538
531
  if response is None:
539
532
  return
@@ -15,10 +15,10 @@ from rich import print as rich_print
15
15
  from rich.progress import Progress, SpinnerColumn, TextColumn
16
16
 
17
17
  from cli.errors import ErrorPrinter
18
+ from cli.utils.rest_helper import RestHelper as Rest
18
19
 
19
20
  from ..broker.lib.broker import Broker, SubscribableSignal
20
21
  from ..typer import typer_utils
21
- from .rest_helper import RestHelper as Rest
22
22
 
23
23
  app = typer_utils.create_typer(
24
24
  help="""
@@ -101,9 +101,8 @@ def read_scripted_code_file(file_path: Path) -> bytes:
101
101
  return file.read()
102
102
 
103
103
 
104
- # pylint: disable=R0913
105
104
  @app.command()
106
- def subscribe(
105
+ def subscribe( # noqa: PLR0913
107
106
  recording_session: str = typer.Argument(..., help="Recording session id", envvar="REMOTIVE_CLOUD_RECORDING_SESSION"),
108
107
  broker: str = typer.Option(None, help="Broker to use"),
109
108
  signal: List[str] = typer.Option(None, help="Signal names to subscribe to, mandatory when not using script"),
@@ -156,12 +155,12 @@ def subscribe(
156
155
  except grpc.RpcError as rpc_error:
157
156
  ErrorPrinter.print_grpc_error(rpc_error)
158
157
 
159
- except Exception as e: # pylint: disable=W0718
158
+ except Exception as e:
160
159
  ErrorPrinter.print_generic_error(str(e))
161
160
  sys.exit(1)
162
161
 
163
162
 
164
- def _do_change_playback_mode( # noqa: C901
163
+ def _do_change_playback_mode( # noqa: C901, PLR0913, PLR0912
165
164
  mode: str,
166
165
  recording_session: str,
167
166
  brokerstr: str,
@@ -169,8 +168,7 @@ def _do_change_playback_mode( # noqa: C901
169
168
  seconds: int | None = None,
170
169
  progress_on_play: bool = False,
171
170
  repeat: bool = False,
172
- ) -> None: # noqa: C901
173
- # pylint: disable=R0913,R0912
171
+ ) -> None:
174
172
  response = Rest.handle_get(f"/api/project/{project}/files/recording/{recording_session}", return_response=True)
175
173
  if response is None:
176
174
  return
@@ -247,7 +245,7 @@ def _verify_recording_on_broker(broker: Broker, recording_session: str, mode: st
247
245
  ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
248
246
  sys.exit(1)
249
247
  except grpc.RpcError as rpc_error:
250
- if rpc_error.code() == grpc.StatusCode.NOT_FOUND: # pylint: disable=no-member
248
+ if rpc_error.code() == grpc.StatusCode.NOT_FOUND:
251
249
  ErrorPrinter.print_generic_error(f"You must use mount to prepare a recording before you can use {mode}")
252
250
  ErrorPrinter.print_hint(f"remotive cloud recordings mount {recording_session} --project {project}")
253
251
  else:
@@ -3,8 +3,7 @@ import json
3
3
  import typer
4
4
 
5
5
  from cli.typer import typer_utils
6
-
7
- from .rest_helper import RestHelper as Rest
6
+ from cli.utils.rest_helper import RestHelper as Rest
8
7
 
9
8
  app = typer_utils.create_typer()
10
9
 
@@ -18,7 +17,7 @@ def do_import(
18
17
 
19
18
 
20
19
  @app.command("list")
21
- def list() -> None: # pylint: disable=W0622
20
+ def list() -> None:
22
21
  """
23
22
  List available sample recordings
24
23
  """
@@ -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/errors.py CHANGED
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import sys
5
+ from typing import Optional
3
6
 
4
7
  import grpc
5
8
  from rich.console import Console
@@ -31,8 +34,10 @@ class ErrorPrinter:
31
34
  err_console.print(f":point_right: [bold]{message}[/bold]")
32
35
 
33
36
  @staticmethod
34
- def print_generic_error(message: str) -> None:
37
+ def print_generic_error(message: str, exit_code: Optional[int] = None) -> None:
35
38
  err_console.print(f":boom: [bold red]Failed[/bold red]: {message}")
39
+ if exit_code is not None:
40
+ sys.exit(exit_code)
36
41
 
37
42
  @staticmethod
38
43
  def print_generic_message(message: str) -> None: