remotivelabs-cli 0.1.1__tar.gz → 0.2.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/PKG-INFO +1 -1
  2. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/auth/cmd.py +2 -2
  3. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/auth/login.py +2 -1
  4. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/auth_tokens.py +20 -4
  5. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/organisations.py +1 -1
  6. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/errors.py +1 -1
  7. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/remotive.py +16 -7
  8. remotivelabs_cli-0.2.0a2/cli/settings/__init__.py +5 -0
  9. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/settings/config_file.py +55 -6
  10. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/settings/core.py +19 -29
  11. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/settings/token_file.py +18 -0
  12. remotivelabs_cli-0.2.0a2/cli/topology/cmd.py +98 -0
  13. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/pyproject.toml +1 -1
  14. remotivelabs_cli-0.1.1/cli/settings/__init__.py +0 -4
  15. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/LICENSE +0 -0
  16. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/README.md +0 -0
  17. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/.DS_Store +0 -0
  18. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/__init__.py +0 -0
  19. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/api/cloud/tokens.py +0 -0
  20. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/brokers.py +0 -0
  21. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/export.py +0 -0
  22. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/files.py +0 -0
  23. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/lib/__about__.py +0 -0
  24. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/lib/broker.py +0 -0
  25. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/license_flows.py +0 -0
  26. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/licenses.py +0 -0
  27. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/playback.py +0 -0
  28. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/record.py +0 -0
  29. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/scripting.py +0 -0
  30. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/broker/signals.py +0 -0
  31. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/__init__.py +0 -0
  32. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/auth/__init__.py +0 -0
  33. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/brokers.py +0 -0
  34. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/cloud_cli.py +0 -0
  35. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/configs.py +0 -0
  36. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/projects.py +0 -0
  37. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/recordings.py +0 -0
  38. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/recordings_playback.py +0 -0
  39. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/resumable_upload.py +0 -0
  40. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/sample_recordings.py +0 -0
  41. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/service_account_tokens.py +0 -0
  42. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/service_accounts.py +0 -0
  43. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/storage/__init__.py +0 -0
  44. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/storage/cmd.py +0 -0
  45. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/storage/copy.py +0 -0
  46. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/storage/uri_or_path.py +0 -0
  47. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/cloud/uri.py +0 -0
  48. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/connect/__init__.py +0 -0
  49. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/connect/connect.py +0 -0
  50. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/connect/protopie/protopie.py +0 -0
  51. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/settings/migrate_all_token_files.py +0 -0
  52. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/settings/migrate_token_file.py +0 -0
  53. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/tools/__init__.py +0 -0
  54. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/tools/can/__init__.py +0 -0
  55. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/tools/can/can.py +0 -0
  56. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/tools/tools.py +0 -0
  57. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/typer/__init__.py +0 -0
  58. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/typer/typer_utils.py +0 -0
  59. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/utils/__init__.py +0 -0
  60. {remotivelabs_cli-0.1.1 → remotivelabs_cli-0.2.0a2}/cli/utils/rest_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.1.1
3
+ Version: 0.2.0a2
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -61,9 +61,9 @@ def print_access_token(
61
61
  else:
62
62
  config = settings.get_cli_config()
63
63
  if account in config.accounts:
64
- token_name = config.accounts[account].credentials_name
64
+ token_file_name = config.accounts[account].credentials_file
65
65
  try:
66
- print(settings.get_token_file(token_name).token)
66
+ print(settings.get_token_file(token_file_name).token)
67
67
  except TokenNotFoundError:
68
68
  ErrorPrinter.print_generic_error(f"Token file for {account} could not be found", exit_code=1)
69
69
  else:
@@ -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
@@ -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
 
@@ -39,8 +41,13 @@ def _prompt_choice( # noqa: C901, PLR0912
39
41
 
40
42
  for token in choices:
41
43
  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)
44
+ if account and account.credentials_file:
45
+ try:
46
+ token_file = settings.get_token_file(account.credentials_file)
47
+ if token_file.name in (token.name or ""):
48
+ included_tokens.append(token)
49
+ except TokenNotFoundError:
50
+ excluded_tokens.append(token)
44
51
  else:
45
52
  excluded_tokens.append(token)
46
53
 
@@ -49,9 +56,18 @@ def _prompt_choice( # noqa: C901, PLR0912
49
56
 
50
57
  included_tokens.sort(key=lambda token: token.created, reverse=True)
51
58
 
59
+ def get_token_or_none(account: Optional[Account]) -> Optional[TokenFile]:
60
+ if account is None:
61
+ return None
62
+ try:
63
+ return settings.get_token_file(account.credentials_file)
64
+ except TokenNotFoundError:
65
+ return None
66
+
52
67
  active_token_index = None
53
68
  for idx, choice in enumerate(included_tokens, start=1):
54
- is_active = active_account is not None and active_account.credentials_name == choice.name
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
55
71
  active_token_index = idx if is_active else active_token_index
56
72
  table.add_row(
57
73
  f"[yellow]{idx}",
@@ -85,7 +85,7 @@ def do_select_default_org(organisation_uid: Optional[str] = None, get: bool = Fa
85
85
  else:
86
86
  account = settings.get_cli_config().get_active()
87
87
  if account is not None:
88
- token = settings.get_token_file(account.credentials_name)
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 "
@@ -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]")
@@ -9,14 +9,14 @@ from rich.console import Console
9
9
  from trogon import Trogon # type: ignore
10
10
  from typer.main import get_group
11
11
 
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
12
16
  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
17
+ from cli.tools.tools import app as tools_app
18
+ from cli.topology.cmd import app as topology_app
19
+ from cli.typer import typer_utils
20
20
 
21
21
  err_console = Console(stderr=True)
22
22
 
@@ -90,5 +90,14 @@ app.add_typer(
90
90
  name="cloud",
91
91
  help="Manage resources in RemotiveCloud",
92
92
  )
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
+ )
93
102
  app.add_typer(connect_app, name="connect", help="Integrations with other systems")
94
103
  app.add_typer(tools_app, name="tools")
@@ -0,0 +1,5 @@
1
+ from cli.settings.core import InvalidSettingsFilePathError, Settings, TokenNotFoundError, settings
2
+
3
+ # from cli.settings.token_file import TokenFile
4
+
5
+ __all__ = ["settings", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]
@@ -4,15 +4,64 @@ import dataclasses
4
4
  import json
5
5
  from dataclasses import dataclass
6
6
  from json import JSONDecodeError
7
- from typing import Dict, Optional
7
+ from typing import Any, Dict, Optional
8
8
 
9
9
  from dacite import from_dict
10
10
 
11
+ from cli.settings.token_file import TokenFile
12
+
13
+
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
+
11
60
 
12
61
  def loads(data: str) -> ConfigFile:
13
62
  try:
14
63
  d = json.loads(data)
15
- return from_dict(ConfigFile, d)
64
+ return _from_dict(d)
16
65
  except JSONDecodeError as e:
17
66
  # ErrorPrinter.print_generic_error("Invalid json format, config.json")
18
67
  raise JSONDecodeError(
@@ -26,7 +75,7 @@ def dumps(config: ConfigFile) -> str:
26
75
 
27
76
  @dataclass
28
77
  class Account:
29
- credentials_name: str
78
+ credentials_file: str
30
79
  default_organization: Optional[str] = None
31
80
  # Add project as well
32
81
 
@@ -66,15 +115,15 @@ class ConfigFile:
66
115
  if self.accounts:
67
116
  self.accounts.pop(email, None)
68
117
 
69
- def init_account(self, email: str, token_name: str) -> None:
118
+ def init_account(self, email: str, token_file: TokenFile) -> None:
70
119
  if self.accounts is None:
71
120
  self.accounts = {}
72
121
 
73
122
  account = self.accounts.get(email)
74
123
  if not account:
75
- account = Account(credentials_name=token_name)
124
+ account = Account(credentials_file=token_file.get_token_file_name())
76
125
  else:
77
- account.credentials_name = token_name
126
+ account.credentials_file = token_file.get_token_file_name()
78
127
  self.accounts[email] = account
79
128
 
80
129
  def set_account_field(self, email: str, default_organization: Optional[str] = None) -> ConfigFile:
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
- import re
5
4
  import shutil
6
5
  import stat
7
6
  import sys
@@ -13,7 +12,7 @@ from typing import Optional, Tuple, Union
13
12
  from rich.console import Console
14
13
 
15
14
  from cli.errors import ErrorPrinter
16
- from cli.settings import config_file
15
+ from cli.settings import config_file, token_file
17
16
  from cli.settings import token_file as tf
18
17
  from cli.settings.config_file import ConfigFile
19
18
  from cli.settings.token_file import TokenFile
@@ -27,9 +26,6 @@ DEPRECATED_CONFIG_DIR_PATH = Path.home() / ".remotive"
27
26
 
28
27
  CLI_CONFIG_FILE_NAME = "config.json"
29
28
  ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
30
- PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
31
- SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
32
-
33
29
 
34
30
  TokenFileMetadata = Tuple[TokenFile, Path]
35
31
 
@@ -128,8 +124,9 @@ class Settings:
128
124
 
129
125
  active_account = self.get_cli_config().get_active()
130
126
  if active_account is not None:
131
- token_name = active_account.credentials_name
132
- return self.get_token_file(token_name)
127
+ token_file_name = active_account.credentials_file
128
+ return self._read_token_file(self.config_dir / token_file_name)
129
+
133
130
  raise TokenNotFoundError
134
131
  # if not self._active_secret_token_path.exists():
135
132
  # raise TokenNotFoundError("no active token file found")
@@ -213,21 +210,14 @@ class Settings:
213
210
  Add a personal token
214
211
  """
215
212
  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)
220
-
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"
213
+ file = token_file.get_token_file_name()
224
214
  path = self.config_dir / file
225
215
  if path.exists() and not overwrite_if_exists:
226
216
  raise FileExistsError(f"Token file already exists: {path}")
227
217
 
228
218
  self._write_token_file(path, token_file)
229
219
  cli_config = self.get_cli_config()
230
- cli_config.init_account(email=token_file.account.email, token_name=token_file.name)
220
+ cli_config.init_account(email=token_file.account.email, token_file=token_file)
231
221
  self._write_config_file(cli_config)
232
222
 
233
223
  if activate:
@@ -249,21 +239,15 @@ class Settings:
249
239
 
250
240
  def add_service_account_token(self, token: str) -> TokenFile:
251
241
  token_file = tf.loads(token)
252
-
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"
242
+ if token_file.type != "service_account":
243
+ raise ValueError("Token type MUST be service_account")
244
+ file = token_file.get_token_file_name()
261
245
  path = self.config_dir / file
262
246
 
263
247
  self._write_token_file(path, token_file)
264
248
  print(f"Service account token stored at {path}")
265
249
  cli_config = self.get_cli_config()
266
- cli_config.init_account(email=token_file.account.email, token_name=token_file.name)
250
+ cli_config.init_account(email=token_file.account.email, token_file=token_file)
267
251
  self._write_config_file(cli_config)
268
252
 
269
253
  # if activate:
@@ -293,10 +277,10 @@ class Settings:
293
277
  return [f[1] for f in self._list_service_account_tokens()]
294
278
 
295
279
  def _list_personal_tokens(self) -> list[TokenFileMetadata]:
296
- return self._list_token_files(prefix=PERSONAL_TOKEN_FILE_PREFIX)
280
+ return self._list_token_files(prefix=token_file.PERSONAL_TOKEN_FILE_PREFIX)
297
281
 
298
282
  def _list_service_account_tokens(self) -> list[TokenFileMetadata]:
299
- return self._list_token_files(prefix=SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
283
+ return self._list_token_files(prefix=token_file.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
300
284
 
301
285
  def _get_token_by_name(self, name: str) -> TokenFileMetadata:
302
286
  token_files = self._list_token_files()
@@ -318,7 +302,9 @@ class Settings:
318
302
  return False
319
303
 
320
304
  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)
305
+ is_token_file = path.name.startswith(tf.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX) or path.name.startswith(
306
+ tf.PERSONAL_TOKEN_FILE_PREFIX
307
+ )
322
308
  has_correct_prefix = path.is_file() and path.name.startswith(prefix)
323
309
  is_active_secret = path == self._active_secret_token_path
324
310
  is_cli_config = path == self._cli_config
@@ -347,6 +333,10 @@ class Settings:
347
333
  os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
348
334
  return path
349
335
 
336
+ # Temporary function while considering how to solve this
337
+ def write_config_file(self, config: ConfigFile) -> Path:
338
+ return self._write_config_file(config)
339
+
350
340
  def _write_config_file(self, config: ConfigFile) -> Path:
351
341
  data = config_file.dumps(config)
352
342
  path = self._write_file(self._cli_config, data)
@@ -2,10 +2,13 @@ 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
 
10
+ # from cli.settings.core import PERSONAL_TOKEN_FILE_PREFIX, SERVICE_ACCOUNT_TOKEN_FILE_PREFIX
11
+
9
12
  DEFAULT_EMAIL = "unknown@remotivecloud.com"
10
13
 
11
14
  TokenType = Literal["authorized_user", "service_account"]
@@ -62,6 +65,10 @@ class TokenFileAccount:
62
65
  email: str
63
66
 
64
67
 
68
+ PERSONAL_TOKEN_FILE_PREFIX = "personal-token-"
69
+ SERVICE_ACCOUNT_TOKEN_FILE_PREFIX = "service-account-token-"
70
+
71
+
65
72
  @dataclass
66
73
  class TokenFile:
67
74
  version: str
@@ -72,6 +79,17 @@ class TokenFile:
72
79
  expires: date
73
80
  account: TokenFileAccount
74
81
 
82
+ def get_token_file_name(self) -> str:
83
+ def email_to_safe_filename(email: str) -> str:
84
+ # Replace any invalid character with an underscore
85
+ return re.sub(r'[<>:"/\\|?*]', "_", email)
86
+
87
+ # From now, user will never be None when adding a token so in this case token_file.user is never None
88
+ email = email_to_safe_filename(self.account.email) if self.account is not None else "unknown"
89
+ if self.type == "authorized_user":
90
+ return f"{PERSONAL_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
91
+ return f"{SERVICE_ACCOUNT_TOKEN_FILE_PREFIX}{self.name}-{email}.json"
92
+
75
93
  def is_expired(self) -> bool:
76
94
  return datetime.today().date() > self.expires
77
95
 
@@ -0,0 +1,98 @@
1
+ import dataclasses
2
+ import datetime
3
+ from typing import Any
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from cli.errors import ErrorPrinter
9
+ from cli.settings import TokenNotFoundError, settings
10
+ from cli.typer import typer_utils
11
+ from cli.utils.rest_helper import RestHelper
12
+
13
+ HELP = """
14
+ RemotiveTopology commands
15
+ """
16
+ console = Console()
17
+ app = typer_utils.create_typer(help=HELP)
18
+
19
+
20
+ @dataclasses.dataclass
21
+ class Subscription:
22
+ type: str
23
+ display_name: str
24
+ feature: str
25
+ start_date: str
26
+ end_date: str
27
+
28
+
29
+ def _print_current_subscription(subscription_info: dict[str, Any]) -> None:
30
+ subscription_type = subscription_info["subscriptionType"]
31
+
32
+ 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")
36
+ 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
44
+
45
+ if expires is not None and expires < datetime.datetime.now():
46
+ console.print(f"Topology subscription has ended, expired {subscription_info['endDate']}")
47
+ else:
48
+ console.print(f"You already have an active topology subscription, it expires {expires if expires is not None else 'Never'}")
49
+
50
+ else:
51
+ ErrorPrinter.print_generic_error("Unexpected exception, please contact support@remotivelabs.com")
52
+ raise typer.Exit(1)
53
+
54
+
55
+ @app.command("start-trial")
56
+ def start_trial(
57
+ organization: str = typer.Option(None, help="Organization to start trial for", envvar="REMOTIVE_CLOUD_ORGANIZATION"),
58
+ ) -> None:
59
+ """
60
+ Allows you ta start a 30 day trial subscription for running RemotiveTopology, you can read more at https://docs.remotivelabs.com/docs/remotive-topology.
61
+
62
+ """
63
+ RestHelper.use_progress("Checking access tokens...", transient=True)
64
+ try:
65
+ _ = settings.get_active_token_file()
66
+ except TokenNotFoundError:
67
+ if len(settings.list_personal_token_files()) == 0:
68
+ console.print(
69
+ "You must first sign in to RemotiveCloud, please use [bold]remotive cloud auth login[/bold] to sign-in"
70
+ "This requires a RemotiveCloud account, if you do not have an account you can sign-up at https://cloud.remotivelabs.com"
71
+ )
72
+ else:
73
+ console.print(
74
+ "You have not active account, please run [bold]remotive cloud auth activate[/bold] to choose an account"
75
+ "or [bold]remotive cloud auth login[/bold] to sign-in"
76
+ )
77
+ return
78
+
79
+ has_access = RestHelper.has_access("/api/whoami")
80
+ if not has_access:
81
+ ErrorPrinter.print_generic_message("Your current active credentials are not valid")
82
+ raise typer.Exit(1)
83
+
84
+ if organization is None and settings.get_cli_config().get_active_default_organisation() is None:
85
+ ErrorPrinter.print_hint("You have not specified any organization and no default organization is set")
86
+ raise typer.Exit(1)
87
+
88
+ sub = RestHelper.handle_get(f"/api/bu/{organization}/features/topology", return_response=True, allow_status_codes=[404, 403])
89
+ if sub.status_code == 404:
90
+ created = RestHelper.handle_post(f"/api/bu/{organization}/features/topology", return_response=True)
91
+ console.print(f"Topology trial started, it expires {created.json()['endDate']}")
92
+ elif sub.status_code == 403:
93
+ ErrorPrinter.print_generic_error(f"You are not allowed to start-trial topology in organization {organization}")
94
+ raise typer.Exit(1)
95
+ else:
96
+ subscription_info = sub.json()
97
+ _print_current_subscription(subscription_info)
98
+ return
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "remotivelabs-cli"
3
- version = "0.1.1"
3
+ version = "0.2.0a2"
4
4
  description = "CLI for operating RemotiveCloud and RemotiveBroker"
5
5
  authors = ["Johan Rask <johan.rask@remotivelabs.com>"]
6
6
  readme = "README.md"
@@ -1,4 +0,0 @@
1
- from cli.settings.core import InvalidSettingsFilePathError, Settings, TokenNotFoundError, settings
2
- from cli.settings.token_file import TokenFile
3
-
4
- __all__ = ["settings", "TokenFile", "TokenNotFoundError", "InvalidSettingsFilePathError", "Settings"]