remotivelabs-cli 0.2.0a2__py3-none-any.whl → 0.2.1__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.

Potentially problematic release.


This version of remotivelabs-cli might be problematic. Click here for more details.

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
 
cli/cloud/auth_tokens.py CHANGED
@@ -29,10 +29,6 @@ def _prompt_choice( # noqa: C901, PLR0912
29
29
  info_message: Optional[str] = None,
30
30
  ) -> Optional[TokenFile]:
31
31
  accounts = settings.get_cli_config().accounts
32
- try:
33
- active_account = settings.get_cli_config().get_active()
34
- except TokenNotFoundError:
35
- active_account = None
36
32
 
37
33
  table = Table("#", "Active", "Type", "Token", "Account", "Created", "Expires")
38
34
 
@@ -56,19 +52,27 @@ def _prompt_choice( # noqa: C901, PLR0912
56
52
 
57
53
  included_tokens.sort(key=lambda token: token.created, reverse=True)
58
54
 
59
- def get_token_or_none(account: Optional[Account]) -> Optional[TokenFile]:
60
- if account is None:
61
- return None
55
+ def get_active_account_or_none() -> Optional[Account]:
62
56
  try:
63
- return settings.get_token_file(account.credentials_file)
57
+ return settings.get_cli_config().get_active()
64
58
  except TokenNotFoundError:
65
59
  return None
66
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()
67
71
  active_token_index = None
68
72
  for idx, choice in enumerate(included_tokens, start=1):
69
- active_token = get_token_or_none(active_account)
70
- is_active = active_account is not None and active_token is not None and active_token.name == choice.name
73
+ is_active = active_token is not None and active_token.name == choice.name
71
74
  active_token_index = idx if is_active else active_token_index
75
+
72
76
  table.add_row(
73
77
  f"[yellow]{idx}",
74
78
  ":white_check_mark:" if is_active else "",
@@ -78,7 +82,6 @@ def _prompt_choice( # noqa: C901, PLR0912
78
82
  str(choice.created),
79
83
  str(choice.expires),
80
84
  )
81
- # console.print("It seems like you have access tokens from previous login, you can select one of these instead of logging in")
82
85
  console.print(table)
83
86
 
84
87
  if skip_prompt:
@@ -181,9 +184,7 @@ def select_personal_token(
181
184
  do_activate(token_name)
182
185
 
183
186
 
184
- def do_activate(
185
- token_name: Optional[str],
186
- ) -> Optional[TokenFile]:
187
+ def do_activate(token_name: Optional[str]) -> Optional[TokenFile]:
187
188
  if token_name is not None:
188
189
  try:
189
190
  token_file = settings.get_token_file(token_name)
@@ -224,8 +225,6 @@ def list_and_select_personal_token(
224
225
  sa_tokens = settings.list_service_account_tokens()
225
226
  personal_tokens.extend(sa_tokens)
226
227
 
227
- # merged = _merge_local_tokens_with_cloud(personal_tokens)
228
-
229
228
  selected_token = _prompt_choice(personal_tokens, skip_prompt=skip_prompt, info_message=info_message)
230
229
  if selected_token is not None:
231
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,7 +84,7 @@ 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:
87
+ if account:
88
88
  token = settings.get_token_file(account.credentials_file)
89
89
  if token.type != "authorized_user":
90
90
  ErrorPrinter.print_hint(
cli/cloud/recordings.py CHANGED
@@ -137,14 +137,13 @@ def mount( # noqa: C901
137
137
  r = Rest.handle_get(url=f"/api/project/{project}/brokers/personal", return_response=True, allow_status_codes=[404])
138
138
 
139
139
  if r.status_code == 200:
140
- broker = r.json()["shortName"]
141
-
140
+ broker_info = r.json()
141
+ broker = broker_info["shortName"]
142
142
  elif r.status_code == 404:
143
143
  r = do_start("personal", project, "", return_response=True)
144
144
  if r.status_code != 200:
145
145
  print(r.text)
146
146
  sys.exit(0)
147
- broker = r.json()["shortName"]
148
147
  else:
149
148
  sys.stderr.write(f"Got http status code {r.status_code}")
150
149
  raise typer.Exit(0)
@@ -154,7 +153,6 @@ def mount( # noqa: C901
154
153
  if r.status_code == 404:
155
154
  if ensure_broker_started:
156
155
  r = do_start(broker, project, "", return_response=True)
157
-
158
156
  if r.status_code != 200:
159
157
  print(r.text)
160
158
  sys.exit(1)
@@ -164,6 +162,9 @@ def mount( # noqa: C901
164
162
  elif r.status_code != 200:
165
163
  sys.stderr.write(f"Got http status code {r.status_code}")
166
164
  raise typer.Exit(1)
165
+
166
+ broker_info = r.json()
167
+ broker = broker_info["shortName"]
167
168
  broker_config_query = ""
168
169
  if transformation_name != "default":
169
170
  broker_config_query = f"?brokerConfigName={transformation_name}"
@@ -174,7 +175,8 @@ def mount( # noqa: C901
174
175
  return_response=True,
175
176
  progress_label="Preparing recording on broker...",
176
177
  )
177
- print("Successfully mounted recording on broker")
178
+ err_console.print("Successfully mounted recording on broker")
179
+ print(json.dumps(broker_info))
178
180
 
179
181
 
180
182
  @app.command(help="Downloads the specified recording file to disk")
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
12
  from cli.broker.brokers import app as broker_app
13
13
  from cli.cloud.cloud_cli import app as cloud_app
14
14
  from cli.connect.connect import app as connect_app
15
15
  from cli.settings import settings
16
- from cli.settings.migrate_all_token_files import migrate_any_legacy_tokens
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
17
20
  from cli.tools.tools import app as tools_app
18
21
  from cli.topology.cmd import app as topology_app
19
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
 
@@ -90,14 +115,16 @@ app.add_typer(
90
115
  name="cloud",
91
116
  help="Manage resources in RemotiveCloud",
92
117
  )
93
- app.add_typer(
94
- topology_app,
95
- name="topology",
96
- help="""
97
- RemotiveTopology actions
98
-
99
- Read more at https://docs.remotivelabs.com/docs/remotive-topology
100
- """,
101
- )
102
118
  app.add_typer(connect_app, name="connect", help="Integrations with other systems")
103
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,5 +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
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
2
8
 
3
- # from cli.settings.token_file import TokenFile
4
-
5
- __all__ = ["settings", "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,70 +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 Any, Dict, Optional
6
+ from typing import Any, Optional
8
7
 
9
8
  from dacite import from_dict
10
9
 
11
10
  from cli.settings.token_file import TokenFile
12
11
 
13
12
 
14
- def upgrade_config(config: dict[str, Any]) -> Optional[dict[str, Any]]:
15
- """
16
- Reads a JSON config from in_path, replaces each account's 'credentials_name'
17
- with 'credentials_file' (by calling get_filename_for_name), and writes the result
18
- back to out_path (or overwrites in_path if out_path is None).
19
- """
20
- from cli.settings import TokenNotFoundError, settings
21
-
22
- accounts = config.get("accounts", {})
23
- to_delete = []
24
- found_old = False
25
- for account, info in list(accounts.items()):
26
- cred_name = info.pop("credentials_name", None)
27
- if not cred_name:
28
- continue
29
- found_old = True
30
- try:
31
- cred_file = settings.get_token_file(cred_name).get_token_file_name()
32
- except TokenNotFoundError:
33
- # schedule this account for removal
34
- to_delete.append(account)
35
- print(f"Dropping account {account!r}: token file for {cred_name} not found")
36
- continue
37
-
38
- info["credentials_file"] = cred_file
39
-
40
- # actually remove them
41
- for account in to_delete:
42
- del accounts[account]
43
-
44
- if found_old:
45
- return config
46
- return None
47
-
48
-
49
- def _from_dict(data: dict[str, Any]) -> ConfigFile:
50
- from cli.settings import settings
51
-
52
- config = upgrade_config(data)
53
- if config is not None:
54
- print("Migrating old configuration format")
55
- updated_config: ConfigFile = from_dict(ConfigFile, config)
56
- settings.write_config_file(updated_config)
57
- return updated_config
58
- return from_dict(ConfigFile, data)
59
-
60
-
61
13
  def loads(data: str) -> ConfigFile:
62
- try:
63
- d = json.loads(data)
64
- return _from_dict(d)
65
- except JSONDecodeError as e:
66
- # ErrorPrinter.print_generic_error("Invalid json format, config.json")
67
- raise JSONDecodeError(
68
- 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
69
- )
14
+ d = json.loads(data)
15
+ return from_dict(ConfigFile, d)
70
16
 
71
17
 
72
18
  def dumps(config: ConfigFile) -> str:
@@ -84,32 +30,30 @@ class Account:
84
30
  class ConfigFile:
85
31
  version: str = "1.0"
86
32
  active: Optional[str] = None
87
- accounts: Dict[str, Account] = dataclasses.field(default_factory=dict)
33
+ accounts: dict[str, Account] = dataclasses.field(default_factory=dict)
88
34
 
89
35
  def get_active_default_organisation(self) -> Optional[str]:
90
36
  active_account = self.get_active()
91
- return active_account.default_organization if active_account is not None else None
37
+ return active_account.default_organization if active_account else None
92
38
 
93
39
  def get_active(self) -> Optional[Account]:
94
- if self.active is not None:
95
- account = self.accounts.get(self.active)
96
- if account is not None:
97
- return account
40
+ if not self.active:
41
+ return None
42
+ account = self.get_account(self.active)
43
+ if not account:
98
44
  raise KeyError(f"Activated account {self.active} is not a valid account")
99
- return None
45
+ return account
100
46
 
101
47
  def activate(self, email: str) -> None:
102
- account = self.accounts.get(email)
103
-
104
- if account is not None:
105
- self.active = email
106
- else:
48
+ account = self.get_account(email)
49
+ if not account:
107
50
  raise KeyError(f"Account {email} does not exists")
51
+ self.active = email
108
52
 
109
53
  def get_account(self, email: str) -> Optional[Account]:
110
- if self.accounts:
111
- return self.accounts[email]
112
- return None
54
+ if not self.accounts:
55
+ return None
56
+ return self.accounts.get(email, None)
113
57
 
114
58
  def remove_account(self, email: str) -> None:
115
59
  if self.accounts:
@@ -119,7 +63,7 @@ class ConfigFile:
119
63
  if self.accounts is None:
120
64
  self.accounts = {}
121
65
 
122
- account = self.accounts.get(email)
66
+ account = self.get_account(email)
123
67
  if not account:
124
68
  account = Account(credentials_file=token_file.get_token_file_name())
125
69
  else:
@@ -130,7 +74,7 @@ class ConfigFile:
130
74
  if self.accounts is None:
131
75
  self.accounts = {}
132
76
 
133
- account = self.accounts.get(email)
77
+ account = self.get_account(email)
134
78
  if not account:
135
79
  raise KeyError(f"Account with email {email} has not been initialized with token")
136
80
 
@@ -139,3 +83,11 @@ class ConfigFile:
139
83
  account.default_organization = default_organization
140
84
 
141
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,13 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import shutil
5
4
  import stat
6
5
  import sys
7
- from dataclasses import dataclass
8
6
  from json import JSONDecodeError
9
7
  from pathlib import Path
10
- from typing import Optional, Tuple, Union
8
+ from typing import Optional, Tuple
11
9
 
12
10
  from rich.console import Console
13
11
 
@@ -21,11 +19,8 @@ err_console = Console(stderr=True)
21
19
 
22
20
 
23
21
  CONFIG_DIR_PATH = Path.home() / ".config" / "remotive"
24
- INCORRECT_CONFIG_DIR_PATH = Path.home() / ".config" / ".remotive"
25
- DEPRECATED_CONFIG_DIR_PATH = Path.home() / ".remotive"
26
-
27
22
  CLI_CONFIG_FILE_NAME = "config.json"
28
- ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
23
+
29
24
 
30
25
  TokenFileMetadata = Tuple[TokenFile, Path]
31
26
 
@@ -34,67 +29,26 @@ class InvalidSettingsFilePathError(Exception):
34
29
  """Raised when trying to access an invalid settings file or file path"""
35
30
 
36
31
 
37
- class NotFoundError(Exception):
38
- """Raised when a token cannot be found in settings"""
39
-
40
-
41
32
  class TokenNotFoundError(Exception):
42
33
  """Raised when a token cannot be found in settings"""
43
34
 
44
35
 
45
- @dataclass()
46
- class CliConfigFile:
47
- default_organisation: Union[str, None]
48
-
49
-
50
36
  class Settings:
51
37
  """
52
- Settings for the remotive CLI
38
+ Settings handles tokens and other config for the remotive CLI
53
39
  """
54
40
 
55
41
  config_dir: Path
56
42
 
57
- def __init__(self, config_dir: Path, deprecated_config_dirs: list[Path] | None = None) -> None:
43
+ def __init__(self, config_dir: Path) -> None:
58
44
  self.config_dir = config_dir
59
- self._active_secret_token_path = self.config_dir / ACTIVE_TOKEN_FILE_NAME
60
- self._cli_config = self.config_dir / CLI_CONFIG_FILE_NAME
61
-
62
- if self.config_dir.exists():
63
- return
64
-
65
- # create the config dir and try to migrate legacy config dirs if they exist
66
45
  self.config_dir.mkdir(parents=True, exist_ok=True)
67
- if deprecated_config_dirs:
68
- for deprecated_config_dir in deprecated_config_dirs:
69
- self._migrate_legacy_config_dir(deprecated_config_dir)
70
-
71
- # def _write_properties(self, filepath: Path, props: CliConfigFile) -> None:
72
- # with open(filepath, "w", encoding="utf-8") as file:
73
- # # keys = sorted(props.keys()) if sort_keys else props.keys()
74
- # # for key in keys:
75
- # file.write(f"default_organisation={props.default_organisation}\n")
76
-
77
- def _read_properties(self, filepath: Path) -> CliConfigFile:
78
- props = {}
79
- with open(filepath, "r", encoding="utf-8") as file:
80
- for line_num, line in enumerate(file, start=1):
81
- line_stripped = line.strip()
82
- if not line_stripped or line_stripped.startswith("#"):
83
- continue
84
- if "=" not in line_stripped:
85
- raise ValueError(f"Invalid line format at line {line_num}: {line}")
86
- key, value = line_stripped.split("=", 1)
87
- key, value = key.strip(), value.strip()
88
- if key in props:
89
- raise ValueError(f"Duplicate key '{key}' found at line {line_num}")
90
- props[key] = value
91
- if "default_organisation" in props:
92
- return CliConfigFile(default_organisation=props["default_organisation"])
93
- 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())
94
49
 
95
50
  def set_default_organisation(self, organisation: str) -> None:
96
51
  cli_config = self.get_cli_config()
97
-
98
52
  try:
99
53
  token = settings.get_active_token_file()
100
54
  cli_config.set_account_field(token.account.email, organisation)
@@ -108,7 +62,6 @@ class Settings:
108
62
  return self._read_config_file()
109
63
  except TokenNotFoundError:
110
64
  return ConfigFile()
111
- # self._write_properties(CONFIG_DIR_PATH / CLI_CONFIG_FILE_NAME, [])
112
65
 
113
66
  def get_active_token(self) -> str:
114
67
  """
@@ -121,30 +74,22 @@ class Settings:
121
74
  """
122
75
  Get the current active token file
123
76
  """
124
-
125
77
  active_account = self.get_cli_config().get_active()
126
78
  if active_account is not None:
127
79
  token_file_name = active_account.credentials_file
128
80
  return self._read_token_file(self.config_dir / token_file_name)
129
81
 
130
82
  raise TokenNotFoundError
131
- # if not self._active_secret_token_path.exists():
132
- # raise TokenNotFoundError("no active token file found")
133
- # return self._read_token_file(self._active_secret_token_path)
134
83
 
135
- def activate_token(self, token: TokenFile) -> None:
84
+ def activate_token(self, token_file: TokenFile) -> None:
136
85
  """
137
86
  Activate a token by name or path
138
87
 
139
88
  The token secret will be set as the current active secret.
140
89
  """
141
- # token_file = self.get_token_file(name)
142
90
  cli_config = self.get_cli_config()
143
- cli_config.activate(token.account.email)
144
- # if token_file.account.email not in cli_config.accounts:
145
- # cli_config.set_account_field(token_file.account.email)
91
+ cli_config.activate(token_file.account.email)
146
92
  self._write_config_file(cli_config)
147
- # self._write_token_file(self._active_secret_token_path, token_file)
148
93
 
149
94
  def clear_active_token(self) -> None:
150
95
  """
@@ -154,9 +99,12 @@ class Settings:
154
99
  config.active = None
155
100
  self._write_config_file(config)
156
101
 
157
- # self._active_secret_token_path.unlink(missing_ok=True)
158
-
159
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
+ """
160
108
  tokens = [t for t in self.list_personal_tokens() if t.account is not None and t.account.email == email]
161
109
  if len(tokens) > 0:
162
110
  return tokens[0]
@@ -169,11 +117,15 @@ class Settings:
169
117
  """
170
118
  Get a token file by name or path
171
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
172
125
  if Path(name).exists():
173
126
  return self._read_token_file(Path(name))
174
- if Path(CONFIG_DIR_PATH / name).exists():
175
- return self._read_token_file(Path(CONFIG_DIR_PATH / name))
176
127
 
128
+ # 3. Try name
177
129
  return self._get_token_by_name(name)[0]
178
130
 
179
131
  def remove_token_file(self, name: str) -> None:
@@ -189,41 +141,30 @@ class Settings:
189
141
 
190
142
  # TODO: what about the active token?
191
143
  path = self._get_token_by_name(name)[1]
192
- # print("Deleting", path)
193
144
  return path.unlink()
194
145
 
195
- # def add_and_activate_short_lived_cli_token(self, token: str) -> TokenFile:
196
- # """
197
- # Activates a short lived token
198
- # """
199
- # token_file = tf.loads(token)
200
- # self._write_token_file(self._active_secret_token_path, token_file)
201
- # return token_file
202
-
203
- def add_personal_token(
204
- self,
205
- token: str,
206
- activate: bool = False,
207
- overwrite_if_exists: bool = False,
208
- ) -> TokenFile:
146
+ def add_personal_token(self, token: str, activate: bool = False, overwrite_if_exists: bool = False) -> TokenFile:
209
147
  """
210
148
  Add a personal token
211
149
  """
212
- token_file = tf.loads(token)
213
- file = token_file.get_token_file_name()
214
- path = self.config_dir / file
150
+ file = tf.loads(token)
151
+ if file.type != "authorized_user":
152
+ raise ValueError("Token type MUST be authorized_user")
153
+
154
+ file_name = file.get_token_file_name()
155
+ path = self.config_dir / file_name
215
156
  if path.exists() and not overwrite_if_exists:
216
157
  raise FileExistsError(f"Token file already exists: {path}")
217
158
 
218
- self._write_token_file(path, token_file)
159
+ self._write_token_file(path, file)
219
160
  cli_config = self.get_cli_config()
220
- cli_config.init_account(email=token_file.account.email, token_file=token_file)
161
+ cli_config.init_account(email=file.account.email, token_file=file)
221
162
  self._write_config_file(cli_config)
222
163
 
223
164
  if activate:
224
- self.activate_token(token_file)
165
+ self.activate_token(file)
225
166
 
226
- return token_file
167
+ return file
227
168
 
228
169
  def list_personal_tokens(self) -> list[TokenFile]:
229
170
  """
@@ -237,33 +178,26 @@ class Settings:
237
178
  """
238
179
  return [f[1] for f in self._list_personal_tokens()]
239
180
 
240
- 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
+ """
241
185
  token_file = tf.loads(token)
242
186
  if token_file.type != "service_account":
243
187
  raise ValueError("Token type MUST be service_account")
188
+
244
189
  file = token_file.get_token_file_name()
245
190
  path = self.config_dir / file
191
+ if path.exists() and not overwrite_if_exists:
192
+ raise FileExistsError(f"Token file already exists: {path}")
246
193
 
247
194
  self._write_token_file(path, token_file)
248
- print(f"Service account token stored at {path}")
249
195
  cli_config = self.get_cli_config()
250
196
  cli_config.init_account(email=token_file.account.email, token_file=token_file)
251
197
  self._write_config_file(cli_config)
252
198
 
253
- # if activate:
254
- # self.activate_token(token_file.account.email)
255
-
256
199
  return token_file
257
200
 
258
- # token_file = tf.loads(token)
259
- # file = f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{service_account}-{token_file.name}.json"
260
- # path = self.config_dir / file
261
- # if path.exists():
262
- # raise FileExistsError(f"Token file already exists: {path}")
263
-
264
- # self._write_token_file(path, token_file)
265
- # return token_file
266
-
267
201
  def list_service_account_tokens(self) -> list[TokenFile]:
268
202
  """
269
203
  List all service account tokens
@@ -290,7 +224,11 @@ class Settings:
290
224
  return matches[0]
291
225
 
292
226
  def _list_token_files(self, prefix: str = "") -> list[TokenFileMetadata]:
293
- """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
+ """
294
232
 
295
233
  def is_valid_json(path: Path) -> bool:
296
234
  try:
@@ -306,12 +244,13 @@ class Settings:
306
244
  tf.PERSONAL_TOKEN_FILE_PREFIX
307
245
  )
308
246
  has_correct_prefix = path.is_file() and path.name.startswith(prefix)
309
- is_active_secret = path == self._active_secret_token_path
310
- is_cli_config = path == self._cli_config
311
- 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
312
252
 
313
253
  paths = [path for path in self.config_dir.iterdir() if is_valid_token_file(path)]
314
-
315
254
  return [(self._read_token_file(token_file), token_file) for token_file in paths]
316
255
 
317
256
  def _read_token_file(self, path: Path) -> TokenFile:
@@ -338,8 +277,11 @@ class Settings:
338
277
  return self._write_config_file(config)
339
278
 
340
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
+ """
341
283
  data = config_file.dumps(config)
342
- path = self._write_file(self._cli_config, data)
284
+ path = self._write_file(self.config_file_path, data)
343
285
  os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
344
286
  return path
345
287
 
@@ -350,43 +292,8 @@ class Settings:
350
292
  path.write_text(data, encoding="utf8")
351
293
  return path
352
294
 
353
- def _migrate_legacy_config_dir(self, path: Path) -> None:
354
- if not path.exists():
355
- return
356
-
357
- sys.stderr.write(f"migrating deprecated config directory {path} to {self.config_dir}\n")
358
- shutil.copytree(str(path), str(self.config_dir), dirs_exist_ok=True)
359
- secret = path / ACTIVE_TOKEN_FILE_NAME
360
- if secret.exists():
361
- sys.stderr.write(f"Removing old activated token {secret}")
362
- secret.unlink(missing_ok=True)
363
- # value = secret.read_text(encoding="utf-8").strip()
364
- # The existing token file might either be a token file, or simply a string. We handle both cases...
365
- # try:
366
- # token = tf.loads(value)
367
- # except JSONDecodeError:
368
- # token = tf.TokenFile(
369
- # version="1.0",
370
- # type="service-account" if value.startswith("sa") else "authorized_user",
371
- # name="MigratedActiveToken",
372
- # token=value,
373
- # created=str(datetime.datetime.now().isoformat()),
374
- # expires="unknown",
375
- # account=TokenFileAccount(email="unknown@remotivecloud.com"),
376
- # )
377
- # self.add_and_activate_short_lived_cli_token(tf.dumps(token))
378
- shutil.rmtree(str(path))
379
-
380
-
381
- def create_settings() -> Settings:
382
- """Create remotive CLI config directory and return its settings instance"""
383
- return Settings(
384
- CONFIG_DIR_PATH,
385
- deprecated_config_dirs=[DEPRECATED_CONFIG_DIR_PATH, INCORRECT_CONFIG_DIR_PATH],
386
- )
387
-
388
-
389
- settings = create_settings()
295
+
296
+ settings = Settings(CONFIG_DIR_PATH)
390
297
  """
391
298
  Global/module-level settings instance. Module-level variables are only loaded once, at import time.
392
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]
@@ -7,9 +7,9 @@ from dataclasses import dataclass
7
7
  from datetime import date, datetime
8
8
  from typing import Any, Literal
9
9
 
10
- # from cli.settings.core import PERSONAL_TOKEN_FILE_PREFIX, SERVICE_ACCOUNT_TOKEN_FILE_PREFIX
11
-
12
10
  DEFAULT_EMAIL = "unknown@remotivecloud.com"
11
+ PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
12
+ SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
13
13
 
14
14
  TokenType = Literal["authorized_user", "service_account"]
15
15
 
@@ -65,10 +65,6 @@ class TokenFileAccount:
65
65
  email: str
66
66
 
67
67
 
68
- PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
69
- SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
70
-
71
-
72
68
  @dataclass
73
69
  class TokenFile:
74
70
  version: str
cli/topology/cmd.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
2
4
  import datetime
3
5
  from typing import Any
@@ -22,30 +24,31 @@ class Subscription:
22
24
  type: str
23
25
  display_name: str
24
26
  feature: str
25
- start_date: str
26
- end_date: str
27
+ start_date: str # TODO: add datetime
28
+ end_date: str # TODO: add datetime
27
29
 
28
30
 
29
31
  def _print_current_subscription(subscription_info: dict[str, Any]) -> None:
30
- subscription_type = subscription_info["subscriptionType"]
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)
31
40
 
32
41
  if subscription_type == "trial":
33
- expires = datetime.datetime.fromisoformat(subscription_info["endDate"])
34
- if expires < datetime.datetime.now():
35
- console.print(f"Your Topology trial expired {subscription_info['endDate']}, please contact support@remotivelabs.com")
42
+ if expires and expires < now:
43
+ console.print(f"Your Topology trial expired {end_date_str}, please contact support@remotivelabs.com")
36
44
  else:
37
- console.print(f"You already have an active topology trial, it expires {subscription_info['endDate']}")
38
- # A paid subscription might not have an endDate
39
- elif subscription_type == "paid":
40
- if "endDate" in subscription_type:
41
- expires = datetime.datetime.fromisoformat(subscription_info["endDate"])
42
- else:
43
- expires = None
45
+ console.print(f"You already have an active topology trial, it expires {end_date_str}")
44
46
 
45
- if expires is not None and expires < datetime.datetime.now():
46
- console.print(f"Topology subscription has ended, expired {subscription_info['endDate']}")
47
+ elif subscription_type == "paid":
48
+ if expires and expires < now:
49
+ console.print(f"Topology subscription has ended, expired {end_date_str}")
47
50
  else:
48
- console.print(f"You already have an active topology subscription, it expires {expires if expires is not None else 'Never'}")
51
+ console.print(f"You already have an active topology subscription, it expires {end_date_str or 'Never'}")
49
52
 
50
53
  else:
51
54
  ErrorPrinter.print_generic_error("Unexpected exception, please contact support@remotivelabs.com")
cli/utils/rest_helper.py CHANGED
@@ -52,7 +52,7 @@ class RestHelper:
52
52
  # token = os.environ["REMOTIVE_CLOUD_AUTH_TOKEN"]
53
53
  # headers = {"Authorization": "Bearer " + token}
54
54
 
55
- __headers: Dict[str, str] = {"User-Agent": f"remotivelabs-cli {version('remotivelabs-cli')}"}
55
+ __headers: Dict[str, str] = {"User-Agent": f"remotivelabs-cli/{version('remotivelabs-cli')}"}
56
56
  __org: str = ""
57
57
 
58
58
  __token: str = ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.2.0a2
3
+ Version: 0.2.1
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -14,15 +14,15 @@ 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=1QEYkaRO4Q1wbOemEwnLCl6YJivC8OdtOguH-bc6N8Y,2930
17
+ cli/cloud/auth/cmd.py,sha256=gLmfjIN9Vrytk9BwGH1cq4WeeryOfwTTnQ4yuZrbRcs,2757
18
18
  cli/cloud/auth/login.py,sha256=dX6M5ysE0n9Zg3gVT7hJbChxTsmuba-Z-1Or6DCFYis,11511
19
- cli/cloud/auth_tokens.py,sha256=RNvvHN9bJ_gfUUa4JL0YXpvTKFYKf922BWjvltElwMg,12964
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=dX8h1SMLxKBvOaxuiRP6nFnkYdek91fjTRtuNRLRHeQ,4116
23
+ cli/cloud/organisations.py,sha256=iEmGMEzOIvuWomoJZ0WBa3Rmrkrup5UH7wjPMoElSn4,4092
24
24
  cli/cloud/projects.py,sha256=ecn5Y8UKhgYnHSJQACUk1GNZt9EF8ug4B-6MCr8rZqM,1487
25
- cli/cloud/recordings.py,sha256=B0XOj8LIm3hBqBzVKPLPvPUCXCKZBTEISssrijK481w,24855
25
+ cli/cloud/recordings.py,sha256=In2fKX668CPsEVBAy7zkU92lEnmu3UcnqiVrqsvLNDQ,24961
26
26
  cli/cloud/recordings_playback.py,sha256=XZoVyujufMQFN2v_Nwsf8tOqn61yLEpAf2z_u5uhXik,11532
27
27
  cli/cloud/resumable_upload.py,sha256=8lEIdncJZoTZzNsQVHH3gm_GunxEmN5JbmWX7awy3p4,3713
28
28
  cli/cloud/sample_recordings.py,sha256=RmuT-a2iMwGj3LXVcPkV5l66uFcf7nyWyJciUjnYkk4,721
@@ -37,24 +37,28 @@ 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
39
  cli/errors.py,sha256=djODw6sdMJXzOsuAUOP3N13nfmm1sIP3Pe6tllGdozM,1657
40
- cli/remotive.py,sha256=5Qt9jayyj1wGSyj2eyLzeJaZ1F8UrtznFzrPPcDqZnY,2988
41
- cli/settings/__init__.py,sha256=ocADDtwE8qsVN_0RM71f2VftFFCHdJrcG37hP7QmC3A,238
42
- cli/settings/config_file.py,sha256=y5wUg2OILhpkxcamyiTkcCQOgy6ZkiEsLkTZnGdpmfA,4510
43
- cli/settings/core.py,sha256=3VQ78muCAcM7buRIhanr6QgF73m3MP0Kdupc6cz3sww,15059
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=RKtdZYOIb9CJjs2dHFlv6TZHgWuPVonAQphRuosXP0I,3050
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
51
- cli/topology/cmd.py,sha256=Fs6tLomKhaie275Tc3FoKGY5cyWx2EEYdroUcfJBnmk,4033
55
+ cli/topology/cmd.py,sha256=SQ5wi7KDoh4iR2Ed7gyfGLNj6UE0K6UkksmBMSD2XAk,3981
52
56
  cli/typer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
57
  cli/typer/typer_utils.py,sha256=8SkvG9aKkfK9fTRsLD9pOBtWn9XSwtOXWg2RAk9FhOI,708
54
58
  cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
- cli/utils/rest_helper.py,sha256=b_FJY6MxnFSqo11qaHxkBFHfVlKf7Zj28Uxv9Oj7XY4,14141
56
- remotivelabs_cli-0.2.0a2.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
57
- remotivelabs_cli-0.2.0a2.dist-info/METADATA,sha256=rxP2gpSWY3w0Lw47fAveqxqkLySPvo9qdMR67QHNoq4,1430
58
- remotivelabs_cli-0.2.0a2.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
59
- remotivelabs_cli-0.2.0a2.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
60
- remotivelabs_cli-0.2.0a2.dist-info/RECORD,,
59
+ cli/utils/rest_helper.py,sha256=De-1Z75p-zjA4hZrylVxWn2wqdPB2gvyvA-ixm_dRFo,14141
60
+ remotivelabs_cli-0.2.1.dist-info/LICENSE,sha256=qDPP_yfuv1fF-u7EfexN-cN3M8aFgGVndGhGLovLKz0,608
61
+ remotivelabs_cli-0.2.1.dist-info/METADATA,sha256=gJSJmVNokErSbGtxifX0Z2uMMFdXy77NC4LLD6Owx8A,1428
62
+ remotivelabs_cli-0.2.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
63
+ remotivelabs_cli-0.2.1.dist-info/entry_points.txt,sha256=lvDhPgagLqW_KTnLPCwKSqfYlEp-1uYVosRiPjsVj10,45
64
+ remotivelabs_cli-0.2.1.dist-info/RECORD,,