remotivelabs-cli 0.2.0a2__tar.gz → 0.2.2__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.

Potentially problematic release.


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

Files changed (69) hide show
  1. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/PKG-INFO +3 -1
  2. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/auth/cmd.py +0 -3
  3. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/auth_tokens.py +15 -16
  4. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/organisations.py +2 -2
  5. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/recordings.py +7 -5
  6. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/remotive.py +54 -22
  7. remotivelabs_cli-0.2.2/cli/settings/__init__.py +21 -0
  8. remotivelabs_cli-0.2.2/cli/settings/config_file.py +93 -0
  9. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/settings/core.py +70 -149
  10. {remotivelabs_cli-0.2.0a2/cli/settings → remotivelabs_cli-0.2.2/cli/settings/migration}/migrate_all_token_files.py +18 -12
  11. remotivelabs_cli-0.2.2/cli/settings/migration/migrate_config_file.py +59 -0
  12. remotivelabs_cli-0.2.2/cli/settings/migration/migrate_legacy_dirs.py +50 -0
  13. remotivelabs_cli-0.2.2/cli/settings/migration/migration_tools.py +36 -0
  14. remotivelabs_cli-0.2.2/cli/settings/state_file.py +32 -0
  15. remotivelabs_cli-0.2.2/cli/settings/token_file.py +136 -0
  16. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/topology/cmd.py +19 -16
  17. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/typer/typer_utils.py +1 -1
  18. remotivelabs_cli-0.2.2/cli/utils/__init__.py +0 -0
  19. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/utils/rest_helper.py +1 -1
  20. remotivelabs_cli-0.2.2/cli/utils/time.py +11 -0
  21. remotivelabs_cli-0.2.2/cli/utils/version_check.py +112 -0
  22. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/pyproject.toml +7 -1
  23. remotivelabs_cli-0.2.0a2/cli/settings/__init__.py +0 -5
  24. remotivelabs_cli-0.2.0a2/cli/settings/config_file.py +0 -141
  25. remotivelabs_cli-0.2.0a2/cli/settings/token_file.py +0 -105
  26. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/LICENSE +0 -0
  27. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/README.md +0 -0
  28. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/.DS_Store +0 -0
  29. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/__init__.py +0 -0
  30. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/api/cloud/tokens.py +0 -0
  31. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/brokers.py +0 -0
  32. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/export.py +0 -0
  33. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/files.py +0 -0
  34. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/lib/__about__.py +0 -0
  35. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/lib/broker.py +0 -0
  36. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/license_flows.py +0 -0
  37. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/licenses.py +0 -0
  38. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/playback.py +0 -0
  39. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/record.py +0 -0
  40. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/scripting.py +0 -0
  41. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/broker/signals.py +0 -0
  42. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/__init__.py +0 -0
  43. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/auth/__init__.py +0 -0
  44. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/auth/login.py +0 -0
  45. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/brokers.py +0 -0
  46. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/cloud_cli.py +0 -0
  47. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/configs.py +0 -0
  48. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/projects.py +0 -0
  49. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/recordings_playback.py +0 -0
  50. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/resumable_upload.py +0 -0
  51. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/sample_recordings.py +0 -0
  52. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/service_account_tokens.py +0 -0
  53. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/service_accounts.py +0 -0
  54. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/storage/__init__.py +0 -0
  55. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/storage/cmd.py +0 -0
  56. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/storage/copy.py +0 -0
  57. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/storage/uri_or_path.py +0 -0
  58. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/cloud/uri.py +0 -0
  59. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/connect/__init__.py +0 -0
  60. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/connect/connect.py +0 -0
  61. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/connect/protopie/protopie.py +0 -0
  62. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/errors.py +0 -0
  63. {remotivelabs_cli-0.2.0a2/cli/tools → remotivelabs_cli-0.2.2/cli/settings/migration}/__init__.py +0 -0
  64. {remotivelabs_cli-0.2.0a2/cli/settings → remotivelabs_cli-0.2.2/cli/settings/migration}/migrate_token_file.py +0 -0
  65. {remotivelabs_cli-0.2.0a2/cli/tools/can → remotivelabs_cli-0.2.2/cli/tools}/__init__.py +0 -0
  66. {remotivelabs_cli-0.2.0a2/cli/typer → remotivelabs_cli-0.2.2/cli/tools/can}/__init__.py +0 -0
  67. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/tools/can/can.py +0 -0
  68. {remotivelabs_cli-0.2.0a2 → remotivelabs_cli-0.2.2}/cli/tools/tools.py +0 -0
  69. {remotivelabs_cli-0.2.0a2/cli/utils → remotivelabs_cli-0.2.2/cli/typer}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.2.0a2
3
+ Version: 0.2.2
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
@@ -13,9 +13,11 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: click (<8.2.0)
15
15
  Requires-Dist: dacite (>=1.9.2,<2.0.0)
16
+ Requires-Dist: email-validator (>=2.2.0,<3.0.0)
16
17
  Requires-Dist: grpc-stubs (>=1.53.0.5)
17
18
  Requires-Dist: mypy-protobuf (>=3.0.0)
18
19
  Requires-Dist: plotext (>=5.2,<6.0)
20
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
19
21
  Requires-Dist: pyjwt (>=2.6,<3.0)
20
22
  Requires-Dist: python-can (>=4.3.1)
21
23
  Requires-Dist: python-socketio (>=4.6.1)
@@ -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
 
@@ -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(
@@ -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")
@@ -6,20 +6,30 @@ 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
23
+ from cli.utils.version_check import check_for_update
20
24
 
21
25
  err_console = Console(stderr=True)
22
26
 
27
+
28
+ def is_featue_flag_enabled(env_var: str) -> bool:
29
+ """Check if an environment variable indicates a feature is enabled."""
30
+ return os.getenv(env_var, "").lower() in ("true", "1", "yes", "on")
31
+
32
+
23
33
  if os.getenv("GRPC_VERBOSITY") is None:
24
34
  os.environ["GRPC_VERBOSITY"] = "NONE"
25
35
 
@@ -32,14 +42,11 @@ For documentation - https://docs.remotivelabs.com
32
42
  """,
33
43
  )
34
44
 
35
- # settings.set_default_config_as_env()
36
-
37
45
 
38
46
  def version_callback(value: bool) -> None:
39
47
  if value:
40
48
  my_version = version("remotivelabs-cli")
41
49
  typer.echo(my_version)
42
- raise typer.Exit()
43
50
 
44
51
 
45
52
  def test_callback(value: int) -> None:
@@ -48,14 +55,30 @@ 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 check_for_newer_version(settings: Settings) -> None:
59
+ check_for_update("remotivelabs-cli", version("remotivelabs-cli"), settings)
60
+
61
+
62
+ def run_migrations(settings: Settings) -> None:
63
+ """
64
+ Run all migration scripts.
65
+
66
+ Each migration script is responsible for a particular migration, and order matters.
67
+ """
68
+ # 1. Migrate legacy settings dirs
69
+ migrate_legacy_settings_dirs(settings.config_dir)
70
+
71
+ # 2. Migrate any legacy tokens
72
+ has_migrated_tokens = migrate_any_legacy_tokens(settings)
73
+
74
+ # 3. Migrate legacy config file format
75
+ migrate_config_file(settings.config_file_path, settings)
76
+
77
+ if has_migrated_tokens:
55
78
  err_console.print("Migrated old credentials and configuration files, you may need to login again or activate correct credentials")
56
79
 
57
80
 
58
- def _set_default_org_as_env() -> None:
81
+ def set_default_org_as_env(settings: Settings) -> None:
59
82
  """
60
83
  If not already set, take the default organisation from file and set as env
61
84
  This has to be done early before it is read
@@ -68,10 +91,17 @@ def _set_default_org_as_env() -> None:
68
91
 
69
92
  @app.callback()
70
93
  def main(
71
- _the_version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=False, help="Print current version"),
94
+ _the_version: bool = typer.Option(
95
+ None,
96
+ "--version",
97
+ callback=version_callback,
98
+ is_eager=True,
99
+ help="Print current version",
100
+ ),
72
101
  ) -> None:
73
- _set_default_org_as_env()
74
- _migrate_old_tokens()
102
+ run_migrations(settings)
103
+ check_for_newer_version(settings)
104
+ set_default_org_as_env(settings)
75
105
  # Do other global stuff, handle other global options here
76
106
 
77
107
 
@@ -90,14 +120,16 @@ app.add_typer(
90
120
  name="cloud",
91
121
  help="Manage resources in RemotiveCloud",
92
122
  )
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
123
  app.add_typer(connect_app, name="connect", help="Integrations with other systems")
103
124
  app.add_typer(tools_app, name="tools")
125
+
126
+ if is_featue_flag_enabled("REMOTIVE_TOPOLOGY_ENABLED"):
127
+ app.add_typer(
128
+ topology_app,
129
+ name="topology",
130
+ help="""
131
+ RemotiveTopology actions
132
+
133
+ Read more at https://docs.remotivelabs.com/docs/remotive-topology
134
+ """,
135
+ )
@@ -0,0 +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
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
8
+
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
+ ]
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ from dataclasses import dataclass
6
+ from typing import Any, Optional
7
+
8
+ from dacite import from_dict
9
+
10
+ from cli.settings.token_file import TokenFile
11
+
12
+
13
+ def loads(data: str) -> ConfigFile:
14
+ d = json.loads(data)
15
+ return from_dict(ConfigFile, d)
16
+
17
+
18
+ def dumps(config: ConfigFile) -> str:
19
+ return json.dumps(dataclasses.asdict(config), default=str)
20
+
21
+
22
+ @dataclass
23
+ class Account:
24
+ credentials_file: str
25
+ default_organization: Optional[str] = None
26
+ # Add project as well
27
+
28
+
29
+ @dataclass
30
+ class ConfigFile:
31
+ version: str = "1.0"
32
+ active: Optional[str] = None
33
+ accounts: dict[str, Account] = dataclasses.field(default_factory=dict)
34
+
35
+ def get_active_default_organisation(self) -> Optional[str]:
36
+ active_account = self.get_active()
37
+ return active_account.default_organization if active_account else None
38
+
39
+ def get_active(self) -> Optional[Account]:
40
+ if not self.active:
41
+ return None
42
+ account = self.get_account(self.active)
43
+ if not account:
44
+ raise KeyError(f"Activated account {self.active} is not a valid account")
45
+ return account
46
+
47
+ def activate(self, email: str) -> None:
48
+ account = self.get_account(email)
49
+ if not account:
50
+ raise KeyError(f"Account {email} does not exists")
51
+ self.active = email
52
+
53
+ def get_account(self, email: str) -> Optional[Account]:
54
+ if not self.accounts:
55
+ return None
56
+ return self.accounts.get(email, None)
57
+
58
+ def remove_account(self, email: str) -> None:
59
+ if self.accounts:
60
+ self.accounts.pop(email, None)
61
+
62
+ def init_account(self, email: str, token_file: TokenFile) -> None:
63
+ if self.accounts is None:
64
+ self.accounts = {}
65
+
66
+ account = self.get_account(email)
67
+ if not account:
68
+ account = Account(credentials_file=token_file.get_token_file_name())
69
+ else:
70
+ account.credentials_file = token_file.get_token_file_name()
71
+ self.accounts[email] = account
72
+
73
+ def set_account_field(self, email: str, default_organization: Optional[str] = None) -> ConfigFile:
74
+ if self.accounts is None:
75
+ self.accounts = {}
76
+
77
+ account = self.get_account(email)
78
+ if not account:
79
+ raise KeyError(f"Account with email {email} has not been initialized with token")
80
+
81
+ # Update only fields explicitly passed
82
+ if default_organization is not None:
83
+ account.default_organization = default_organization
84
+
85
+ return self
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)