remotivelabs-cli 0.1.2__py3-none-any.whl → 0.2.0__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/cloud/auth/cmd.py CHANGED
@@ -27,9 +27,6 @@ def login(browser: bool = typer.Option(default=True, help="Does not automaticall
27
27
  If not able to open a browser it will show fallback to headless login and show a link that
28
28
  users can copy into any browser when this is unsupported where running the cli - such as in docker,
29
29
  virtual machine or ssh sessions.
30
-
31
- This will be used as the current access token in all subsequent requests. This would
32
- be the same as activating a personal access key or service-account access key.
33
30
  """
34
31
  do_login(headless=not browser)
35
32
 
@@ -61,9 +58,9 @@ def print_access_token(
61
58
  else:
62
59
  config = settings.get_cli_config()
63
60
  if account in config.accounts:
64
- token_name = config.accounts[account].credentials_name
61
+ token_file_name = config.accounts[account].credentials_file
65
62
  try:
66
- print(settings.get_token_file(token_name).token)
63
+ print(settings.get_token_file(token_file_name).token)
67
64
  except TokenNotFoundError:
68
65
  ErrorPrinter.print_generic_error(f"Token file for {account} could not be found", exit_code=1)
69
66
  else:
cli/cloud/auth/login.py CHANGED
@@ -18,7 +18,8 @@ from typing_extensions import override
18
18
 
19
19
  from cli.cloud.auth_tokens import do_activate, prompt_to_set_org
20
20
  from cli.errors import ErrorPrinter
21
- from cli.settings import TokenFile, TokenNotFoundError, settings
21
+ from cli.settings import TokenNotFoundError, settings
22
+ from cli.settings.token_file import TokenFile
22
23
  from cli.utils.rest_helper import RestHelper as Rest
23
24
 
24
25
  httpd: HTTPServer
cli/cloud/auth_tokens.py CHANGED
@@ -9,7 +9,9 @@ from rich.table import Table
9
9
  from cli.api.cloud import tokens
10
10
  from cli.cloud.organisations import do_select_default_org
11
11
  from cli.errors import ErrorPrinter
12
- from cli.settings import TokenFile, TokenNotFoundError, settings
12
+ from cli.settings import TokenNotFoundError, settings
13
+ from cli.settings.config_file import Account
14
+ from cli.settings.token_file import TokenFile
13
15
  from cli.typer import typer_utils
14
16
  from cli.utils.rest_helper import RestHelper as Rest
15
17
 
@@ -27,10 +29,6 @@ def _prompt_choice( # noqa: C901, PLR0912
27
29
  info_message: Optional[str] = None,
28
30
  ) -> Optional[TokenFile]:
29
31
  accounts = settings.get_cli_config().accounts
30
- try:
31
- active_account = settings.get_cli_config().get_active()
32
- except TokenNotFoundError:
33
- active_account = None
34
32
 
35
33
  table = Table("#", "Active", "Type", "Token", "Account", "Created", "Expires")
36
34
 
@@ -39,8 +37,13 @@ def _prompt_choice( # noqa: C901, PLR0912
39
37
 
40
38
  for token in choices:
41
39
  account = accounts.get(token.account.email)
42
- if account and account.credentials_name and account.credentials_name in (token.name or ""):
43
- included_tokens.append(token)
40
+ if account and account.credentials_file:
41
+ try:
42
+ token_file = settings.get_token_file(account.credentials_file)
43
+ if token_file.name in (token.name or ""):
44
+ included_tokens.append(token)
45
+ except TokenNotFoundError:
46
+ excluded_tokens.append(token)
44
47
  else:
45
48
  excluded_tokens.append(token)
46
49
 
@@ -49,10 +52,27 @@ def _prompt_choice( # noqa: C901, PLR0912
49
52
 
50
53
  included_tokens.sort(key=lambda token: token.created, reverse=True)
51
54
 
55
+ def get_active_account_or_none() -> Optional[Account]:
56
+ try:
57
+ return settings.get_cli_config().get_active()
58
+ except TokenNotFoundError:
59
+ return None
60
+
61
+ def get_active_token_or_none() -> Optional[TokenFile]:
62
+ try:
63
+ active_account = get_active_account_or_none()
64
+ if active_account is not None:
65
+ return settings.get_token_file(active_account.credentials_file)
66
+ except TokenNotFoundError:
67
+ pass
68
+ return None
69
+
70
+ active_token = get_active_token_or_none()
52
71
  active_token_index = None
53
72
  for idx, choice in enumerate(included_tokens, start=1):
54
- is_active = active_account is not None and active_account.credentials_name == choice.name
73
+ is_active = active_token is not None and active_token.name == choice.name
55
74
  active_token_index = idx if is_active else active_token_index
75
+
56
76
  table.add_row(
57
77
  f"[yellow]{idx}",
58
78
  ":white_check_mark:" if is_active else "",
@@ -62,7 +82,6 @@ def _prompt_choice( # noqa: C901, PLR0912
62
82
  str(choice.created),
63
83
  str(choice.expires),
64
84
  )
65
- # console.print("It seems like you have access tokens from previous login, you can select one of these instead of logging in")
66
85
  console.print(table)
67
86
 
68
87
  if skip_prompt:
@@ -165,9 +184,7 @@ def select_personal_token(
165
184
  do_activate(token_name)
166
185
 
167
186
 
168
- def do_activate(
169
- token_name: Optional[str],
170
- ) -> Optional[TokenFile]:
187
+ def do_activate(token_name: Optional[str]) -> Optional[TokenFile]:
171
188
  if token_name is not None:
172
189
  try:
173
190
  token_file = settings.get_token_file(token_name)
@@ -208,8 +225,6 @@ def list_and_select_personal_token(
208
225
  sa_tokens = settings.list_service_account_tokens()
209
226
  personal_tokens.extend(sa_tokens)
210
227
 
211
- # merged = _merge_local_tokens_with_cloud(personal_tokens)
212
-
213
228
  selected_token = _prompt_choice(personal_tokens, skip_prompt=skip_prompt, info_message=info_message)
214
229
  if selected_token is not None:
215
230
  settings.activate_token(selected_token)
@@ -76,7 +76,7 @@ def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = Fa
76
76
  """
77
77
  if get:
78
78
  default_organisation = settings.get_cli_config().get_active_default_organisation()
79
- if default_organisation is not None:
79
+ if default_organisation:
80
80
  console.print(default_organisation)
81
81
  else:
82
82
  console.print("No default organization set")
@@ -84,8 +84,8 @@ def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = Fa
84
84
  settings.set_default_organisation(organisation_uid)
85
85
  else:
86
86
  account = settings.get_cli_config().get_active()
87
- if account is not None:
88
- token = settings.get_token_file(account.credentials_name)
87
+ if account:
88
+ token = settings.get_token_file(account.credentials_file)
89
89
  if token.type != "authorized_user":
90
90
  ErrorPrinter.print_hint(
91
91
  "You must supply the organization name as argument when using a service-account since the "
cli/errors.py CHANGED
@@ -41,4 +41,4 @@ class ErrorPrinter:
41
41
 
42
42
  @staticmethod
43
43
  def print_generic_message(message: str) -> None:
44
- err_console.print(f"[bold]{message}[/bold]:")
44
+ err_console.print(f"[bold]{message}[/bold]")
cli/remotive.py CHANGED
@@ -6,20 +6,29 @@ from importlib.metadata import version
6
6
  import typer
7
7
  from rich import print as rich_print
8
8
  from rich.console import Console
9
- from trogon import Trogon # type: ignore
9
+ from trogon import Trogon
10
10
  from typer.main import get_group
11
11
 
12
- from cli.settings.migrate_all_token_files import migrate_any_legacy_tokens
13
-
14
- from .broker.brokers import app as broker_app
15
- from .cloud.cloud_cli import app as cloud_app
16
- from .connect.connect import app as connect_app
17
- from .settings import settings
18
- from .tools.tools import app as tools_app
19
- from .typer import typer_utils
12
+ from cli.broker.brokers import app as broker_app
13
+ from cli.cloud.cloud_cli import app as cloud_app
14
+ from cli.connect.connect import app as connect_app
15
+ from cli.settings import settings
16
+ from cli.settings.core import Settings
17
+ from cli.settings.migration.migrate_all_token_files import migrate_any_legacy_tokens
18
+ from cli.settings.migration.migrate_config_file import migrate_config_file
19
+ from cli.settings.migration.migrate_legacy_dirs import migrate_legacy_settings_dirs
20
+ from cli.tools.tools import app as tools_app
21
+ from cli.topology.cmd import app as topology_app
22
+ from cli.typer import typer_utils
20
23
 
21
24
  err_console = Console(stderr=True)
22
25
 
26
+
27
+ def is_featue_flag_enabled(env_var: str) -> bool:
28
+ """Check if an environment variable indicates a feature is enabled."""
29
+ return os.getenv(env_var, "").lower() in ("true", "1", "yes", "on")
30
+
31
+
23
32
  if os.getenv("GRPC_VERBOSITY") is None:
24
33
  os.environ["GRPC_VERBOSITY"] = "NONE"
25
34
 
@@ -32,8 +41,6 @@ For documentation - https://docs.remotivelabs.com
32
41
  """,
33
42
  )
34
43
 
35
- # settings.set_default_config_as_env()
36
-
37
44
 
38
45
  def version_callback(value: bool) -> None:
39
46
  if value:
@@ -48,14 +55,26 @@ def test_callback(value: int) -> None:
48
55
  raise typer.Exit()
49
56
 
50
57
 
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):
58
+ def run_migrations(settings: Settings) -> None:
59
+ """
60
+ Run all migration scripts.
61
+
62
+ Each migration script is responsible for a particular migration, and order matters.
63
+ """
64
+ # 1. Migrate legacy settings dirs
65
+ migrate_legacy_settings_dirs(settings.config_dir)
66
+
67
+ # 2. Migrate any legacy tokens
68
+ has_migrated_tokens = migrate_any_legacy_tokens(settings)
69
+
70
+ # 3. Migrate legacy config file format
71
+ migrate_config_file(settings.config_file_path, settings)
72
+
73
+ if has_migrated_tokens:
55
74
  err_console.print("Migrated old credentials and configuration files, you may need to login again or activate correct credentials")
56
75
 
57
76
 
58
- def _set_default_org_as_env() -> None:
77
+ def set_default_org_as_env(settings: Settings) -> None:
59
78
  """
60
79
  If not already set, take the default organisation from file and set as env
61
80
  This has to be done early before it is read
@@ -68,10 +87,16 @@ def _set_default_org_as_env() -> None:
68
87
 
69
88
  @app.callback()
70
89
  def main(
71
- _the_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=False, help="Print current version"),
90
+ _the_version: bool = typer.Option(
91
+ None,
92
+ "--version",
93
+ callback=version_callback,
94
+ is_eager=False,
95
+ help="Print current version",
96
+ ),
72
97
  ) -> None:
73
- _set_default_org_as_env()
74
- _migrate_old_tokens()
98
+ run_migrations(settings)
99
+ set_default_org_as_env(settings)
75
100
  # Do other global stuff, handle other global options here
76
101
 
77
102
 
@@ -92,3 +117,14 @@ app.add_typer(
92
117
  )
93
118
  app.add_typer(connect_app, name="connect", help="Integrations with other systems")
94
119
  app.add_typer(tools_app, name="tools")
120
+
121
+ if is_featue_flag_enabled("REMOTIVE_TOPOLOGY_ENABLED"):
122
+ app.add_typer(
123
+ topology_app,
124
+ name="topology",
125
+ help="""
126
+ RemotiveTopology actions
127
+
128
+ Read more at https://docs.remotivelabs.com/docs/remotive-topology
129
+ """,
130
+ )
cli/settings/__init__.py CHANGED
@@ -1,4 +1,21 @@
1
+ from cli.settings.config_file import Account, ConfigFile
2
+ from cli.settings.config_file import dumps as dumps_config_file
3
+ from cli.settings.config_file import loads as loads_config_file
1
4
  from cli.settings.core import InvalidSettingsFilePathError, Settings, TokenNotFoundError, settings
2
5
  from cli.settings.token_file import TokenFile
6
+ from cli.settings.token_file import dumps as dumps_token_file
7
+ from cli.settings.token_file import loads as loads_token_file
3
8
 
4
- __all__ = ["settings", "TokenFile", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]
9
+ __all__ = [
10
+ "settings",
11
+ "TokenNotFoundError",
12
+ "InvalidSettingsFilePathError",
13
+ "Settings",
14
+ "TokenFile",
15
+ "ConfigFile",
16
+ "Account",
17
+ "dumps_config_file",
18
+ "loads_config_file",
19
+ "dumps_token_file",
20
+ "loads_token_file",
21
+ ]
@@ -3,21 +3,16 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  import json
5
5
  from dataclasses import dataclass
6
- from json import JSONDecodeError
7
- from typing import Dict, Optional
6
+ from typing import Any, Optional
8
7
 
9
8
  from dacite import from_dict
10
9
 
10
+ from cli.settings.token_file import TokenFile
11
+
11
12
 
12
13
  def loads(data: str) -> ConfigFile:
13
- try:
14
- d = json.loads(data)
15
- return from_dict(ConfigFile, d)
16
- except JSONDecodeError as e:
17
- # ErrorPrinter.print_generic_error("Invalid json format, config.json")
18
- raise JSONDecodeError(
19
- f"File config.json is not valid json, please edit or remove file to have it re-created ({e.msg})", pos=e.pos, doc=e.doc
20
- )
14
+ d = json.loads(data)
15
+ return from_dict(ConfigFile, d)
21
16
 
22
17
 
23
18
  def dumps(config: ConfigFile) -> str:
@@ -26,7 +21,7 @@ def dumps(config: ConfigFile) -> str:
26
21
 
27
22
  @dataclass
28
23
  class Account:
29
- credentials_name: str
24
+ credentials_file: str
30
25
  default_organization: Optional[str] = None
31
26
  # Add project as well
32
27
 
@@ -35,53 +30,51 @@ class Account:
35
30
  class ConfigFile:
36
31
  version: str = "1.0"
37
32
  active: Optional[str] = None
38
- accounts: Dict[str, Account] = dataclasses.field(default_factory=dict)
33
+ accounts: dict[str, Account] = dataclasses.field(default_factory=dict)
39
34
 
40
35
  def get_active_default_organisation(self) -> Optional[str]:
41
36
  active_account = self.get_active()
42
- return active_account.default_organization if active_account is not None else None
37
+ return active_account.default_organization if active_account else None
43
38
 
44
39
  def get_active(self) -> Optional[Account]:
45
- if self.active is not None:
46
- account = self.accounts.get(self.active)
47
- if account is not None:
48
- return account
40
+ if not self.active:
41
+ return None
42
+ account = self.get_account(self.active)
43
+ if not account:
49
44
  raise KeyError(f"Activated account {self.active} is not a valid account")
50
- return None
45
+ return account
51
46
 
52
47
  def activate(self, email: str) -> None:
53
- account = self.accounts.get(email)
54
-
55
- if account is not None:
56
- self.active = email
57
- else:
48
+ account = self.get_account(email)
49
+ if not account:
58
50
  raise KeyError(f"Account {email} does not exists")
51
+ self.active = email
59
52
 
60
53
  def get_account(self, email: str) -> Optional[Account]:
61
- if self.accounts:
62
- return self.accounts[email]
63
- return None
54
+ if not self.accounts:
55
+ return None
56
+ return self.accounts.get(email, None)
64
57
 
65
58
  def remove_account(self, email: str) -> None:
66
59
  if self.accounts:
67
60
  self.accounts.pop(email, None)
68
61
 
69
- def init_account(self, email: str, token_name: str) -> None:
62
+ def init_account(self, email: str, token_file: TokenFile) -> None:
70
63
  if self.accounts is None:
71
64
  self.accounts = {}
72
65
 
73
- account = self.accounts.get(email)
66
+ account = self.get_account(email)
74
67
  if not account:
75
- account = Account(credentials_name=token_name)
68
+ account = Account(credentials_file=token_file.get_token_file_name())
76
69
  else:
77
- account.credentials_name = token_name
70
+ account.credentials_file = token_file.get_token_file_name()
78
71
  self.accounts[email] = account
79
72
 
80
73
  def set_account_field(self, email: str, default_organization: Optional[str] = None) -> ConfigFile:
81
74
  if self.accounts is None:
82
75
  self.accounts = {}
83
76
 
84
- account = self.accounts.get(email)
77
+ account = self.get_account(email)
85
78
  if not account:
86
79
  raise KeyError(f"Account with email {email} has not been initialized with token")
87
80
 
@@ -90,3 +83,11 @@ class ConfigFile:
90
83
  account.default_organization = default_organization
91
84
 
92
85
  return self
86
+
87
+ @staticmethod
88
+ def from_json_str(data: str) -> ConfigFile:
89
+ return loads(data)
90
+
91
+ @staticmethod
92
+ def from_dict(data: dict[str, Any]) -> ConfigFile:
93
+ return from_dict(ConfigFile, data)
cli/settings/core.py CHANGED
@@ -1,19 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import re
5
- import shutil
6
4
  import stat
7
5
  import sys
8
- from dataclasses import dataclass
9
6
  from json import JSONDecodeError
10
7
  from pathlib import Path
11
- from typing import Optional, Tuple, Union
8
+ from typing import Optional, Tuple
12
9
 
13
10
  from rich.console import Console
14
11
 
15
12
  from cli.errors import ErrorPrinter
16
- from cli.settings import config_file
13
+ from cli.settings import config_file, token_file
17
14
  from cli.settings import token_file as tf
18
15
  from cli.settings.config_file import ConfigFile
19
16
  from cli.settings.token_file import TokenFile
@@ -22,13 +19,7 @@ err_console = Console(stderr=True)
22
19
 
23
20
 
24
21
  CONFIG_DIR_PATH = Path.home() / ".config" / "remotive"
25
- INCORRECT_CONFIG_DIR_PATH = Path.home() / ".config" / ".remotive"
26
- DEPRECATED_CONFIG_DIR_PATH = Path.home() / ".remotive"
27
-
28
22
  CLI_CONFIG_FILE_NAME = "config.json"
29
- ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
30
- PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
31
- SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
32
23
 
33
24
 
34
25
  TokenFileMetadata = Tuple[TokenFile, Path]
@@ -38,67 +29,26 @@ class InvalidSettingsFilePathError(Exception):
38
29
  """Raised when trying to access an invalid settings file or file path"""
39
30
 
40
31
 
41
- class NotFoundError(Exception):
42
- """Raised when a token cannot be found in settings"""
43
-
44
-
45
32
  class TokenNotFoundError(Exception):
46
33
  """Raised when a token cannot be found in settings"""
47
34
 
48
35
 
49
- @dataclass()
50
- class CliConfigFile:
51
- default_organisation: Union[str, None]
52
-
53
-
54
36
  class Settings:
55
37
  """
56
- Settings for the remotive CLI
38
+ Settings handles tokens and other config for the remotive CLI
57
39
  """
58
40
 
59
41
  config_dir: Path
60
42
 
61
- def __init__(self, config_dir: Path, deprecated_config_dirs: list[Path] | None = None) -> None:
43
+ def __init__(self, config_dir: Path) -> None:
62
44
  self.config_dir = config_dir
63
- self._active_secret_token_path = self.config_dir / ACTIVE_TOKEN_FILE_NAME
64
- self._cli_config = self.config_dir / CLI_CONFIG_FILE_NAME
65
-
66
- if self.config_dir.exists():
67
- return
68
-
69
- # create the config dir and try to migrate legacy config dirs if they exist
70
45
  self.config_dir.mkdir(parents=True, exist_ok=True)
71
- if deprecated_config_dirs:
72
- for deprecated_config_dir in deprecated_config_dirs:
73
- self._migrate_legacy_config_dir(deprecated_config_dir)
74
-
75
- # def _write_properties(self, filepath: Path, props: CliConfigFile) -> None:
76
- # with open(filepath, "w", encoding="utf-8") as file:
77
- # # keys = sorted(props.keys()) if sort_keys else props.keys()
78
- # # for key in keys:
79
- # file.write(f"default_organisation={props.default_organisation}\n")
80
-
81
- def _read_properties(self, filepath: Path) -> CliConfigFile:
82
- props = {}
83
- with open(filepath, "r", encoding="utf-8") as file:
84
- for line_num, line in enumerate(file, start=1):
85
- line_stripped = line.strip()
86
- if not line_stripped or line_stripped.startswith("#"):
87
- continue
88
- if "=" not in line_stripped:
89
- raise ValueError(f"Invalid line format at line {line_num}: {line}")
90
- key, value = line_stripped.split("=", 1)
91
- key, value = key.strip(), value.strip()
92
- if key in props:
93
- raise ValueError(f"Duplicate key '{key}' found at line {line_num}")
94
- props[key] = value
95
- if "default_organisation" in props:
96
- return CliConfigFile(default_organisation=props["default_organisation"])
97
- return CliConfigFile(default_organisation=None)
46
+ self.config_file_path = self.config_dir / CLI_CONFIG_FILE_NAME
47
+ if not self.config_file_path.exists():
48
+ self._write_config_file(ConfigFile())
98
49
 
99
50
  def set_default_organisation(self, organisation: str) -> None:
100
51
  cli_config = self.get_cli_config()
101
-
102
52
  try:
103
53
  token = settings.get_active_token_file()
104
54
  cli_config.set_account_field(token.account.email, organisation)
@@ -112,7 +62,6 @@ class Settings:
112
62
  return self._read_config_file()
113
63
  except TokenNotFoundError:
114
64
  return ConfigFile()
115
- # self._write_properties(CONFIG_DIR_PATH / CLI_CONFIG_FILE_NAME, [])
116
65
 
117
66
  def get_active_token(self) -> str:
118
67
  """
@@ -125,29 +74,22 @@ class Settings:
125
74
  """
126
75
  Get the current active token file
127
76
  """
128
-
129
77
  active_account = self.get_cli_config().get_active()
130
78
  if active_account is not None:
131
- token_name = active_account.credentials_name
132
- return self.get_token_file(token_name)
79
+ token_file_name = active_account.credentials_file
80
+ return self._read_token_file(self.config_dir / token_file_name)
81
+
133
82
  raise TokenNotFoundError
134
- # if not self._active_secret_token_path.exists():
135
- # raise TokenNotFoundError("no active token file found")
136
- # return self._read_token_file(self._active_secret_token_path)
137
83
 
138
- def activate_token(self, token: TokenFile) -> None:
84
+ def activate_token(self, token_file: TokenFile) -> None:
139
85
  """
140
86
  Activate a token by name or path
141
87
 
142
88
  The token secret will be set as the current active secret.
143
89
  """
144
- # token_file = self.get_token_file(name)
145
90
  cli_config = self.get_cli_config()
146
- cli_config.activate(token.account.email)
147
- # if token_file.account.email not in cli_config.accounts:
148
- # cli_config.set_account_field(token_file.account.email)
91
+ cli_config.activate(token_file.account.email)
149
92
  self._write_config_file(cli_config)
150
- # self._write_token_file(self._active_secret_token_path, token_file)
151
93
 
152
94
  def clear_active_token(self) -> None:
153
95
  """
@@ -157,9 +99,12 @@ class Settings:
157
99
  config.active = None
158
100
  self._write_config_file(config)
159
101
 
160
- # self._active_secret_token_path.unlink(missing_ok=True)
161
-
162
102
  def get_token_file_by_email(self, email: str) -> Optional[TokenFile]:
103
+ """
104
+ Get a token file by email.
105
+
106
+ If multiple tokens are found, the first one is returned.
107
+ """
163
108
  tokens = [t for t in self.list_personal_tokens() if t.account is not None and t.account.email == email]
164
109
  if len(tokens) > 0:
165
110
  return tokens[0]
@@ -172,11 +117,15 @@ class Settings:
172
117
  """
173
118
  Get a token file by name or path
174
119
  """
120
+ # 1. Try relative path
121
+ if (self.config_dir / name).exists():
122
+ return self._read_token_file(self.config_dir / name)
123
+
124
+ # 2. Try absolute path
175
125
  if Path(name).exists():
176
126
  return self._read_token_file(Path(name))
177
- if Path(CONFIG_DIR_PATH / name).exists():
178
- return self._read_token_file(Path(CONFIG_DIR_PATH / name))
179
127
 
128
+ # 3. Try name
180
129
  return self._get_token_by_name(name)[0]
181
130
 
182
131
  def remove_token_file(self, name: str) -> None:
@@ -192,48 +141,30 @@ class Settings:
192
141
 
193
142
  # TODO: what about the active token?
194
143
  path = self._get_token_by_name(name)[1]
195
- # print("Deleting", path)
196
144
  return path.unlink()
197
145
 
198
- # def add_and_activate_short_lived_cli_token(self, token: str) -> TokenFile:
199
- # """
200
- # Activates a short lived token
201
- # """
202
- # token_file = tf.loads(token)
203
- # self._write_token_file(self._active_secret_token_path, token_file)
204
- # return token_file
205
-
206
- def add_personal_token(
207
- self,
208
- token: str,
209
- activate: bool = False,
210
- overwrite_if_exists: bool = False,
211
- ) -> TokenFile:
146
+ def add_personal_token(self, token: str, activate: bool = False, overwrite_if_exists: bool = False) -> TokenFile:
212
147
  """
213
148
  Add a personal token
214
149
  """
215
- token_file = tf.loads(token)
216
-
217
- def email_to_safe_filename(email: str) -> str:
218
- # Replace any invalid character with an underscore
219
- return re.sub(r'[<>:"/\\|?*]', "_", email)
150
+ file = tf.loads(token)
151
+ if file.type != "authorized_user":
152
+ raise ValueError("Token type MUST be authorized_user")
220
153
 
221
- # From now, user will never be None when adding a token so in this case token_file.user is never None
222
- email = email_to_safe_filename(token_file.account.email) if token_file.account is not None else "unknown"
223
- file = f"{PERSONAL_TOKEN_FILE_PREFIX}{token_file.name}-{email}.json"
224
- path = self.config_dir / file
154
+ file_name = file.get_token_file_name()
155
+ path = self.config_dir / file_name
225
156
  if path.exists() and not overwrite_if_exists:
226
157
  raise FileExistsError(f"Token file already exists: {path}")
227
158
 
228
- self._write_token_file(path, token_file)
159
+ self._write_token_file(path, file)
229
160
  cli_config = self.get_cli_config()
230
- cli_config.init_account(email=token_file.account.email, token_name=token_file.name)
161
+ cli_config.init_account(email=file.account.email, token_file=file)
231
162
  self._write_config_file(cli_config)
232
163
 
233
164
  if activate:
234
- self.activate_token(token_file)
165
+ self.activate_token(file)
235
166
 
236
- return token_file
167
+ return file
237
168
 
238
169
  def list_personal_tokens(self) -> list[TokenFile]:
239
170
  """
@@ -247,39 +178,26 @@ class Settings:
247
178
  """
248
179
  return [f[1] for f in self._list_personal_tokens()]
249
180
 
250
- def add_service_account_token(self, token: str) -> TokenFile:
181
+ def add_service_account_token(self, token: str, overwrite_if_exists: bool = False) -> TokenFile:
182
+ """
183
+ Add a service account token
184
+ """
251
185
  token_file = tf.loads(token)
186
+ if token_file.type != "service_account":
187
+ raise ValueError("Token type MUST be service_account")
252
188
 
253
- def email_to_safe_filename(email: str) -> str:
254
- # Replace any invalid character with an underscore
255
- return re.sub(r'[<>:"/\\|?*]', "_", email)
256
-
257
- # From now, user will never be None when adding a token so in this case token_file.user is never None
258
-
259
- email = email_to_safe_filename(token_file.account.email) if token_file.account is not None else "unknown"
260
- file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{token_file.name}-{email}.json"
189
+ file = token_file.get_token_file_name()
261
190
  path = self.config_dir / file
191
+ if path.exists() and not overwrite_if_exists:
192
+ raise FileExistsError(f"Token file already exists: {path}")
262
193
 
263
194
  self._write_token_file(path, token_file)
264
- print(f"Service account token stored at {path}")
265
195
  cli_config = self.get_cli_config()
266
- cli_config.init_account(email=token_file.account.email, token_name=token_file.name)
196
+ cli_config.init_account(email=token_file.account.email, token_file=token_file)
267
197
  self._write_config_file(cli_config)
268
198
 
269
- # if activate:
270
- # self.activate_token(token_file.account.email)
271
-
272
199
  return token_file
273
200
 
274
- # token_file = tf.loads(token)
275
- # file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{service_account}-{token_file.name}.json"
276
- # path = self.config_dir / file
277
- # if path.exists():
278
- # raise FileExistsError(f"Token file already exists: {path}")
279
-
280
- # self._write_token_file(path, token_file)
281
- # return token_file
282
-
283
201
  def list_service_account_tokens(self) -> list[TokenFile]:
284
202
  """
285
203
  List all service account tokens
@@ -293,10 +211,10 @@ class Settings:
293
211
  return [f[1] for f in self._list_service_account_tokens()]
294
212
 
295
213
  def _list_personal_tokens(self) -> list[TokenFileMetadata]:
296
- return self._list_token_files(prefix=PERSONAL_TOKEN_FILE_PREFIX)
214
+ return self._list_token_files(prefix=token_file.PERSONAL_TOKEN_FILE_PREFIX)
297
215
 
298
216
  def _list_service_account_tokens(self) -> list[TokenFileMetadata]:
299
- return self._list_token_files(prefix=SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
217
+ return self._list_token_files(prefix=token_file.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
300
218
 
301
219
  def _get_token_by_name(self, name: str) -> TokenFileMetadata:
302
220
  token_files = self._list_token_files()
@@ -306,7 +224,11 @@ class Settings:
306
224
  return matches[0]
307
225
 
308
226
  def _list_token_files(self, prefix: str = "") -> list[TokenFileMetadata]:
309
- """list all tokens with the correct prefix in the config dir, but omit files that are not token files"""
227
+ """
228
+ list all tokens with the correct prefix in the config dir, but omit files that are not token files
229
+
230
+ TODO: improve is_valid_json and is_valid_token_file using token_file parsing instead
231
+ """
310
232
 
311
233
  def is_valid_json(path: Path) -> bool:
312
234
  try:
@@ -318,14 +240,17 @@ class Settings:
318
240
  return False
319
241
 
320
242
  def is_valid_token_file(path: Path) -> bool:
321
- is_token_file = path.name.startswith(SERVICE_ACCOUNT_TOKEN_FILE_PREFIX) or path.name.startswith(PERSONAL_TOKEN_FILE_PREFIX)
243
+ is_token_file = path.name.startswith(tf.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX) or path.name.startswith(
244
+ tf.PERSONAL_TOKEN_FILE_PREFIX
245
+ )
322
246
  has_correct_prefix = path.is_file() and path.name.startswith(prefix)
323
- is_active_secret = path == self._active_secret_token_path
324
- is_cli_config = path == self._cli_config
325
- return is_token_file and is_valid_json(path) and has_correct_prefix and not is_active_secret and not is_cli_config
247
+ is_cli_config = path == self.config_file_path
248
+ is_present_in_cli_config_accounts = any(
249
+ path.name == account.credentials_file for account in self.get_cli_config().accounts.values()
250
+ )
251
+ return is_token_file and is_valid_json(path) and has_correct_prefix and not is_cli_config and is_present_in_cli_config_accounts
326
252
 
327
253
  paths = [path for path in self.config_dir.iterdir() if is_valid_token_file(path)]
328
-
329
254
  return [(self._read_token_file(token_file), token_file) for token_file in paths]
330
255
 
331
256
  def _read_token_file(self, path: Path) -> TokenFile:
@@ -347,9 +272,16 @@ class Settings:
347
272
  os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
348
273
  return path
349
274
 
275
+ # Temporary function while considering how to solve this
276
+ def write_config_file(self, config: ConfigFile) -> Path:
277
+ return self._write_config_file(config)
278
+
350
279
  def _write_config_file(self, config: ConfigFile) -> Path:
280
+ """
281
+ TODO: add read cache to avoid parsing the config every time we read it
282
+ """
351
283
  data = config_file.dumps(config)
352
- path = self._write_file(self._cli_config, data)
284
+ path = self._write_file(self.config_file_path, data)
353
285
  os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
354
286
  return path
355
287
 
@@ -360,43 +292,8 @@ class Settings:
360
292
  path.write_text(data, encoding="utf8")
361
293
  return path
362
294
 
363
- def _migrate_legacy_config_dir(self, path: Path) -> None:
364
- if not path.exists():
365
- return
366
-
367
- sys.stderr.write(f"migrating deprecated config directory {path} to {self.config_dir}\n")
368
- shutil.copytree(str(path), str(self.config_dir), dirs_exist_ok=True)
369
- secret = path / ACTIVE_TOKEN_FILE_NAME
370
- if secret.exists():
371
- sys.stderr.write(f"Removing old activated token {secret}")
372
- secret.unlink(missing_ok=True)
373
- # value = secret.read_text(encoding="utf-8").strip()
374
- # The existing token file might either be a token file, or simply a string. We handle both cases...
375
- # try:
376
- # token = tf.loads(value)
377
- # except JSONDecodeError:
378
- # token = tf.TokenFile(
379
- # version="1.0",
380
- # type="service-account" if value.startswith("sa") else "authorized_user",
381
- # name="MigratedActiveToken",
382
- # token=value,
383
- # created=str(datetime.datetime.now().isoformat()),
384
- # expires="unknown",
385
- # account=TokenFileAccount(email="unknown@remotivecloud.com"),
386
- # )
387
- # self.add_and_activate_short_lived_cli_token(tf.dumps(token))
388
- shutil.rmtree(str(path))
389
-
390
-
391
- def create_settings() -> Settings:
392
- """Create remotive CLI config directory and return its settings instance"""
393
- return Settings(
394
- CONFIG_DIR_PATH,
395
- deprecated_config_dirs=[DEPRECATED_CONFIG_DIR_PATH, INCORRECT_CONFIG_DIR_PATH],
396
- )
397
-
398
-
399
- settings = create_settings()
295
+
296
+ settings = Settings(CONFIG_DIR_PATH)
400
297
  """
401
298
  Global/module-level settings instance. Module-level variables are only loaded once, at import time.
402
299
 
File without changes
@@ -1,12 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from cli.settings import settings
4
- from cli.settings.core import ACTIVE_TOKEN_FILE_NAME
5
- from cli.settings.migrate_token_file import InvalidTokenError, UnsupportedTokenVersionError, migrate_legacy_token
3
+ from cli.settings.core import Settings
4
+ from cli.settings.migration.migrate_token_file import InvalidTokenError, UnsupportedTokenVersionError, migrate_legacy_token
5
+ from cli.settings.migration.migration_tools import list_token_files
6
6
  from cli.settings.token_file import TokenFile, dumps
7
7
 
8
+ ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
8
9
 
9
- def _migrate_legacy_tokens(tokens: list[TokenFile]) -> tuple[list[TokenFile], set[str]]:
10
+
11
+ def migrate_legacy_tokens(tokens: list[TokenFile]) -> tuple[list[TokenFile], set[str]]:
10
12
  """
11
13
  Determine which tokens can be updated and which should be removed.
12
14
 
@@ -28,7 +30,7 @@ def _migrate_legacy_tokens(tokens: list[TokenFile]) -> tuple[list[TokenFile], se
28
30
  return updated_tokens, invalid_tokens
29
31
 
30
32
 
31
- def _write_updated_tokens(updated_tokens: list[TokenFile]) -> None:
33
+ def _write_updated_tokens(settings: Settings, updated_tokens: list[TokenFile]) -> None:
32
34
  for updated_token in updated_tokens:
33
35
  settings.remove_token_file(name=updated_token.name)
34
36
  if updated_token.type == "authorized_user":
@@ -39,12 +41,12 @@ def _write_updated_tokens(updated_tokens: list[TokenFile]) -> None:
39
41
  raise ValueError(f"Unsupported token type: {updated_token.type}")
40
42
 
41
43
 
42
- def _remove_invalid_tokens(invalid_tokens: set[str]) -> None:
44
+ def _remove_invalid_tokens(settings: Settings, invalid_tokens: set[str]) -> None:
43
45
  for token_name in invalid_tokens:
44
46
  settings.remove_token_file(name=token_name)
45
47
 
46
48
 
47
- def _remove_old_secret_file() -> bool:
49
+ def _remove_old_secret_file(settings: Settings) -> bool:
48
50
  old_activated_secret_file = settings.config_dir / ACTIVE_TOKEN_FILE_NAME
49
51
  old_secret_exists = old_activated_secret_file.exists()
50
52
  if old_secret_exists:
@@ -52,21 +54,25 @@ def _remove_old_secret_file() -> bool:
52
54
  return old_secret_exists
53
55
 
54
56
 
55
- def migrate_any_legacy_tokens(tokens: list[TokenFile]) -> bool:
57
+ def migrate_any_legacy_tokens(settings: Settings) -> bool:
56
58
  """
57
59
  Migrate any legacy tokens to the latest TokenFile format.
58
60
 
61
+ If the legacy secret file exists (cloud.secret.token), it will be removed.
62
+
59
63
  Returns True if any tokens were migrated, False otherwise.
60
64
  """
65
+ tokens = list_token_files(settings.config_dir)
66
+
61
67
  # Get tokens to update/remove
62
- updated_tokens, invalid_tokens = _migrate_legacy_tokens(tokens)
68
+ updated_tokens, invalid_tokens = migrate_legacy_tokens(tokens)
63
69
 
64
70
  # Perform file operations
65
- _write_updated_tokens(updated_tokens)
66
- _remove_invalid_tokens(invalid_tokens)
71
+ _write_updated_tokens(settings, updated_tokens)
72
+ _remove_invalid_tokens(settings, invalid_tokens)
67
73
 
68
74
  # Remove old secret file if exists
69
- old_secret_removed = _remove_old_secret_file()
75
+ old_secret_removed = _remove_old_secret_file(settings)
70
76
  if old_secret_removed:
71
77
  return True # We migrated at least one token
72
78
 
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any, Optional
7
+
8
+ from dacite import from_dict
9
+
10
+ from cli.settings.config_file import ConfigFile, loads
11
+ from cli.settings.core import Settings, TokenNotFoundError
12
+ from cli.settings.migration.migration_tools import get_token_file
13
+
14
+
15
+ def migrate_account_data(config: dict[str, Any], settings: Settings) -> Optional[dict[str, Any]]:
16
+ """
17
+ Migrates Account property credentials_name to credentials_file
18
+ """
19
+ accounts = config.get("accounts", {})
20
+ to_delete = []
21
+ found_old = False
22
+ for account_email, account_info in list(accounts.items()):
23
+ cred_name = account_info.pop("credentials_name", None)
24
+ if not cred_name:
25
+ continue
26
+ found_old = True
27
+ try:
28
+ cred_file = get_token_file(cred_name, settings.config_dir).get_token_file_name()
29
+ except TokenNotFoundError:
30
+ # schedule this account for removal
31
+ to_delete.append(account_email)
32
+ sys.stderr.write(f"Dropping account {account_email!r}: token file for {cred_name} not found")
33
+ continue
34
+
35
+ account_info["credentials_file"] = cred_file
36
+
37
+ # actually remove them (also remove active if it was the one being removed)
38
+ for account_email in to_delete:
39
+ del accounts[account_email]
40
+ if config.get("active", None) == account_email:
41
+ config["active"] = None
42
+
43
+ return config if found_old else None
44
+
45
+
46
+ def migrate_config_file(path: Path, settings: Settings) -> ConfigFile:
47
+ """
48
+ Migrates data in config file to new format
49
+ """
50
+ data = path.read_text()
51
+ loaded_data: dict[str, Any] = json.loads(data)
52
+ migrated_data = migrate_account_data(loaded_data, settings)
53
+ if not migrated_data:
54
+ return loads(data)
55
+
56
+ sys.stderr.write("Migrating old configuration format")
57
+ migrated_config: ConfigFile = from_dict(data_class=ConfigFile, data=migrated_data)
58
+ settings.write_config_file(migrated_config)
59
+ return migrated_config
@@ -0,0 +1,50 @@
1
+ """
2
+ Migrate all files from legacy config directories to the new config directory. Any migration of the content is handled by specific migration
3
+ scripts later in the migration process.
4
+ """
5
+
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ INCORRECT_CONFIG_DIR_PATH = Path.home() / ".config" / ".remotive"
11
+ DEPRECATED_CONFIG_DIR_PATH = Path.home() / ".remotive"
12
+ DEPRECATED_CONFIG_DIRS = [DEPRECATED_CONFIG_DIR_PATH, INCORRECT_CONFIG_DIR_PATH]
13
+
14
+
15
+ def _copy_dir_fail_on_conflict(src: Path, dst: Path) -> None:
16
+ if not dst.is_dir():
17
+ dst.mkdir(parents=True, exist_ok=True)
18
+
19
+ for item in src.iterdir():
20
+ src_file = src / item.name
21
+ dst_file = dst / item.name
22
+
23
+ if src_file.is_file():
24
+ if dst_file.exists():
25
+ raise FileExistsError(f"File '{dst_file}' already exists.")
26
+ shutil.copy2(src_file, dst_file) # preserve metadata
27
+
28
+
29
+ def migrate_legacy_settings_dir(path: Path, target_dir: Path) -> None:
30
+ if not path.exists():
31
+ return
32
+
33
+ sys.stderr.write(f"found legacy config directory {path}, trying to migrate to {target_dir}\n")
34
+ try:
35
+ _copy_dir_fail_on_conflict(path, target_dir)
36
+ shutil.rmtree(str(path))
37
+ except FileExistsError as e:
38
+ sys.stderr.write(
39
+ f"file {e.filename} already exists in {target_dir}, so files in {path} cannot be migrated without risk of data loss. \
40
+ Please remove or move the files to {target_dir} manually and make sure to remove {path}.\n"
41
+ )
42
+ raise e
43
+
44
+
45
+ def migrate_legacy_settings_dirs(target_dir: Path) -> None:
46
+ """
47
+ Migrate any valid configuration from legacy config directories to the new config directory.
48
+ """
49
+ for deprecated_config_dir in DEPRECATED_CONFIG_DIRS:
50
+ migrate_legacy_settings_dir(deprecated_config_dir, target_dir)
@@ -0,0 +1,36 @@
1
+ from itertools import chain
2
+ from pathlib import Path
3
+
4
+ from cli.settings.core import TokenNotFoundError
5
+ from cli.settings.token_file import TokenFile
6
+
7
+
8
+ def list_token_files(config_dir: Path) -> list[TokenFile]:
9
+ """
10
+ List all token files in the config directory
11
+
12
+ Note! Dont use settings, as that will couple settings to the old config and token formats we want to migrate away from.
13
+ """
14
+ token_files = []
15
+ patterns = ["personal-token-*.json", "service-account-token-*.json"]
16
+ files = list(chain.from_iterable(config_dir.glob(pattern) for pattern in patterns))
17
+ for file in files:
18
+ try:
19
+ token_file = TokenFile.from_json_str(file.read_text())
20
+ token_files.append(token_file)
21
+ except Exception:
22
+ print(f"warning: invalid token file {file}. Consider removing it.")
23
+ return token_files
24
+
25
+
26
+ def get_token_file(cred_name: str, config_dir: Path) -> TokenFile:
27
+ """
28
+ Get the token file for a given credentials name.
29
+
30
+ Note! Dont use settings, as that will couple settings to the old config and token formats we want to migrate away from.
31
+ """
32
+ token_files = list_token_files(config_dir)
33
+ matches = [token_file for token_file in token_files if token_file.name == cred_name]
34
+ if len(matches) != 1:
35
+ raise TokenNotFoundError(f"Token file for {cred_name} not found")
36
+ return matches[0]
@@ -2,11 +2,14 @@ from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
4
  import json
5
+ import re
5
6
  from dataclasses import dataclass
6
7
  from datetime import date, datetime
7
8
  from typing import Any, Literal
8
9
 
9
10
  DEFAULT_EMAIL = "unknown@remotivecloud.com"
11
+ PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
12
+ SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
10
13
 
11
14
  TokenType = Literal["authorized_user", "service_account"]
12
15
 
@@ -72,6 +75,17 @@ class TokenFile:
72
75
  expires: date
73
76
  account: TokenFileAccount
74
77
 
78
+ def get_token_file_name(self) -> str:
79
+ def email_to_safe_filename(email: str) -> str:
80
+ # Replace any invalid character with an underscore
81
+ return re.sub(r'[<>:"/\\|?*]', "_", email)
82
+
83
+ # From now, user will never be None when adding a token so in this case token_file.user is never None
84
+ email = email_to_safe_filename(self.account.email) if self.account is not None else "unknown"
85
+ if self.type == "authorized_user":
86
+ return f"{PERSONAL_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
87
+ return f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
88
+
75
89
  def is_expired(self) -> bool:
76
90
  return datetime.today().date() > self.expires
77
91
 
cli/topology/cmd.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import datetime
5
+ from typing import Any
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from cli.errors import ErrorPrinter
11
+ from cli.settings import TokenNotFoundError, settings
12
+ from cli.typer import typer_utils
13
+ from cli.utils.rest_helper import RestHelper
14
+
15
+ HELP = """
16
+ RemotiveTopology commands
17
+ """
18
+ console = Console()
19
+ app = typer_utils.create_typer(help=HELP)
20
+
21
+
22
+ @dataclasses.dataclass
23
+ class Subscription:
24
+ type: str
25
+ display_name: str
26
+ feature: str
27
+ start_date: str # TODO: add datetime
28
+ end_date: str # TODO: add datetime
29
+
30
+
31
+ def _print_current_subscription(subscription_info: dict[str, Any]) -> None:
32
+ subscription_type = subscription_info.get("subscriptionType")
33
+ end_date_str = subscription_info.get("endDate")
34
+ now = datetime.datetime.now()
35
+
36
+ def parse_date(date_str: str | None) -> datetime.datetime | None:
37
+ return datetime.datetime.fromisoformat(date_str) if date_str else None
38
+
39
+ expires = parse_date(end_date_str)
40
+
41
+ if subscription_type == "trial":
42
+ if expires and expires < now:
43
+ console.print(f"Your Topology trial expired {end_date_str}, please contact support@remotivelabs.com")
44
+ else:
45
+ console.print(f"You already have an active topology trial, it expires {end_date_str}")
46
+
47
+ elif subscription_type == "paid":
48
+ if expires and expires < now:
49
+ console.print(f"Topology subscription has ended, expired {end_date_str}")
50
+ else:
51
+ console.print(f"You already have an active topology subscription, it expires {end_date_str or 'Never'}")
52
+
53
+ else:
54
+ ErrorPrinter.print_generic_error("Unexpected exception, please contact support@remotivelabs.com")
55
+ raise typer.Exit(1)
56
+
57
+
58
+ @app.command("start-trial")
59
+ def start_trial(
60
+ organization: str = typer.Option(None, help="Organization to start trial for", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
61
+ ) -> None:
62
+ """
63
+ Allows you ta start a 30 day trial subscription for running RemotiveTopology, you can read more at https://docs.remotivelabs.com/docs/remotive-topology.
64
+
65
+ """
66
+ RestHelper.use_progress("Checking access tokens...", transient=True)
67
+ try:
68
+ _ = settings.get_active_token_file()
69
+ except TokenNotFoundError:
70
+ if len(settings.list_personal_token_files()) == 0:
71
+ console.print(
72
+ "You must first sign in to RemotiveCloud, please use [bold]remotive cloud auth login[/bold] to sign-in"
73
+ "This requires a RemotiveCloud account, if you do not have an account you can sign-up at https://cloud.remotivelabs.com"
74
+ )
75
+ else:
76
+ console.print(
77
+ "You have not active account, please run [bold]remotive cloud auth activate[/bold] to choose an account"
78
+ "or [bold]remotive cloud auth login[/bold] to sign-in"
79
+ )
80
+ return
81
+
82
+ has_access = RestHelper.has_access("/api/whoami")
83
+ if not has_access:
84
+ ErrorPrinter.print_generic_message("Your current active credentials are not valid")
85
+ raise typer.Exit(1)
86
+
87
+ if organization is None and settings.get_cli_config().get_active_default_organisation() is None:
88
+ ErrorPrinter.print_hint("You have not specified any organization and no default organization is set")
89
+ raise typer.Exit(1)
90
+
91
+ sub = RestHelper.handle_get(f"/api/bu/{organization}/features/topology", return_response=True, allow_status_codes=[404, 403])
92
+ if sub.status_code == 404:
93
+ created = RestHelper.handle_post(f"/api/bu/{organization}/features/topology", return_response=True)
94
+ console.print(f"Topology trial started, it expires {created.json()['endDate']}")
95
+ elif sub.status_code == 403:
96
+ ErrorPrinter.print_generic_error(f"You are not allowed to start-trial topology in organization {organization}")
97
+ raise typer.Exit(1)
98
+ else:
99
+ subscription_info = sub.json()
100
+ _print_current_subscription(subscription_info)
101
+ return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -14,13 +14,13 @@ cli/broker/scripting.py,sha256=LFLdaBNxe2sfpcxhDmRlAbEorjL3SJZNK-zEdLQ9ySU,3854
14
14
  cli/broker/signals.py,sha256=MFj_bOLIxHY1v3XPkKk6n8U3JLaY8nrXHahRQaVse6s,8207
15
15
  cli/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  cli/cloud/auth/__init__.py,sha256=MtQ01-n8CgZb9Y_SvxwZUgj44Yo0dFAU3_XwhQiUYtw,54
17
- cli/cloud/auth/cmd.py,sha256=aOvbrzNb7moClgmde25T0coBs8Ge696xUB_w6qfvN8I,2920
18
- cli/cloud/auth/login.py,sha256=sAlUeBVypK4ibBR9gdU0DMUmwvxqOjvFNTLUHPseyCE,11476
19
- cli/cloud/auth_tokens.py,sha256=XHXrCwgK_akrCbFxcxtDr7GDM9cd-T_s40PrRovY4Lk,12344
17
+ cli/cloud/auth/cmd.py,sha256=gLmfjIN9Vrytk9BwGH1cq4WeeryOfwTTnQ4yuZrbRcs,2757
18
+ cli/cloud/auth/login.py,sha256=dX6M5ysE0n9Zg3gVT7hJbChxTsmuba-Z-1Or6DCFYis,11511
19
+ cli/cloud/auth_tokens.py,sha256=K_HSBto2XfbD-Hxhb0SAFhxSDZdSXo961UcvdVWNkZI,12831
20
20
  cli/cloud/brokers.py,sha256=QTA9bmaK06LKEccF6IBgWBonC4VFrKwFQBsACX_IzYw,3896
21
21
  cli/cloud/cloud_cli.py,sha256=q-oiaLcKC-BRamXfIFGn-BskRmJ3utA7-tI39lSs3Cs,1309
22
22
  cli/cloud/configs.py,sha256=uv46nUoGXOr99smQHahv_ageDv6bGYfUnlRlxcS5D9A,5125
23
- cli/cloud/organisations.py,sha256=QBD2RnCbpZ9XQrTIcvD74o321JHNnKxDleKlYmSZEuI,4116
23
+ cli/cloud/organisations.py,sha256=iEmGMEzOIvuWomoJZ0WBa3Rmrkrup5UH7wjPMoElSn4,4092
24
24
  cli/cloud/projects.py,sha256=ecn5Y8UKhgYnHSJQACUk1GNZt9EF8ug4B-6MCr8rZqM,1487
25
25
  cli/cloud/recordings.py,sha256=In2fKX668CPsEVBAy7zkU92lEnmu3UcnqiVrqsvLNDQ,24961
26
26
  cli/cloud/recordings_playback.py,sha256=XZoVyujufMQFN2v_Nwsf8tOqn61yLEpAf2z_u5uhXik,11532
@@ -36,24 +36,29 @@ cli/cloud/uri.py,sha256=QZCus--KJQlVwGCOzZqiglvj8VvSRKxfVvN33Pilgyg,3616
36
36
  cli/connect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  cli/connect/connect.py,sha256=SH2DNTTVLu2dNpk6xIah1-KJZAqrK_7Skt8RKp8Mjh8,4231
38
38
  cli/connect/protopie/protopie.py,sha256=ElmrGaV0ivb85wo0gLzCAXZhmSmIDASaCVlF1iQblLI,6532
39
- cli/errors.py,sha256=_P-qpayY06qgLA7wvoDFsyMaTfo_zjNY--fgHPAuhrs,1658
40
- cli/remotive.py,sha256=PsxR3Hhp3zhj-A06sqvtSkmXwv9moWHT_ify8Vg5xQA,2756
41
- cli/settings/__init__.py,sha256=5ZRq04PHp3WU_5e7mGwWUoFYUPWfnnS16yF45wUv7mY,248
42
- cli/settings/config_file.py,sha256=6WHlJT74aQvqc5elcW1FDafcG0NttYvPawmArN5H2MQ,2869
43
- cli/settings/core.py,sha256=vRS_3JZDKRMGMa65C3LT178BCUcZ2RAlp2knn158CA4,15669
44
- cli/settings/migrate_all_token_files.py,sha256=7kvHbpP4BtILJ8kPtb_bFnTnBYX9isZ4rwB5lfnEkbA,2722
45
- cli/settings/migrate_token_file.py,sha256=Fp7Z_lNqSdoWY05TYwFW2QH8q9QhmB2TYSok6hV1Mic,1530
46
- cli/settings/token_file.py,sha256=KISVaSVV2pzfduCtJYUtCLtJa3htrpP4_qTODp5IhW8,2210
39
+ cli/errors.py,sha256=djODw6sdMJXzOsuAUOP3N13nfmm1sIP3Pe6tllGdozM,1657
40
+ cli/remotive.py,sha256=FkXJCq0mw_bak75u6aukleIld-jjgn4hjuBQ28Lcz1U,3831
41
+ cli/settings/__init__.py,sha256=t1qkaGrJ4xx8WMHlmBTbQ1VdJL4YOcz8VFfRkGa2_jQ,711
42
+ cli/settings/config_file.py,sha256=6sdHUtZSUIgubwpfwEEn7GarTK1M_iQhtRJZzFDdP5o,2784
43
+ cli/settings/core.py,sha256=FqME9ghV6bBNDrZnQCm3jxz-7T8M04dSFumYK1-QOhs,10911
44
+ cli/settings/migration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ cli/settings/migration/migrate_all_token_files.py,sha256=xoVvAqn_tGskEW148uf3xZx1mpJKUnERMTcBo0nkCnI,3010
46
+ cli/settings/migration/migrate_config_file.py,sha256=hw4EpRwJz1zUNxfCOk0PvMuZjAlaGy4m_rDbMsHZO_w,2047
47
+ cli/settings/migration/migrate_legacy_dirs.py,sha256=N0t2io3bT_ub8BcVPw1CeQ4eeexRUiu3jXq3DL018OE,1819
48
+ cli/settings/migration/migrate_token_file.py,sha256=Fp7Z_lNqSdoWY05TYwFW2QH8q9QhmB2TYSok6hV1Mic,1530
49
+ cli/settings/migration/migration_tools.py,sha256=P72tuw6-aS_Kd0qn-0ZecplsYxMTu0LTXM5sMSNTVEM,1378
50
+ cli/settings/token_file.py,sha256=llfP1GohXtLVIDy3UyNB8FneGRuCesnce9uasv-7XKM,2953
47
51
  cli/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
52
  cli/tools/can/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
53
  cli/tools/can/can.py,sha256=TtP5w8vb0QG4ObNhkWIDpRMdNelirFffoc_lFZy8ePM,2260
50
54
  cli/tools/tools.py,sha256=jhLfrFDqkmWV3eBAzNwBf6WgDGrz7sOhgVCia36Twn8,232
55
+ cli/topology/cmd.py,sha256=SQ5wi7KDoh4iR2Ed7gyfGLNj6UE0K6UkksmBMSD2XAk,3981
51
56
  cli/typer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
57
  cli/typer/typer_utils.py,sha256=8SkvG9aKkfK9fTRsLD9pOBtWn9XSwtOXWg2RAk9FhOI,708
53
58
  cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
59
  cli/utils/rest_helper.py,sha256=b_FJY6MxnFSqo11qaHxkBFHfVlKf7Zj28Uxv9Oj7XY4,14141
55
- remotivelabs_cli-0.1.2.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
56
- remotivelabs_cli-0.1.2.dist-info/METADATA,sha256=Ln8gpBfVhzI2AHb9zRgd6AmJ32Q0nUqgW2fz1qpwidY,1428
57
- remotivelabs_cli-0.1.2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
58
- remotivelabs_cli-0.1.2.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
59
- remotivelabs_cli-0.1.2.dist-info/RECORD,,
60
+ remotivelabs_cli-0.2.0.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
61
+ remotivelabs_cli-0.2.0.dist-info/METADATA,sha256=nyRzH6JasTH4wYLC7itZSo9yIYqvcmNUf3kajMN9aQ8,1428
62
+ remotivelabs_cli-0.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
63
+ remotivelabs_cli-0.2.0.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
64
+ remotivelabs_cli-0.2.0.dist-info/RECORD,,