remotivelabs-cli 0.2.0a2__py3-none-any.whl → 0.2.2__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/settings/core.py CHANGED
@@ -1,31 +1,27 @@
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
 
10
+ from pydantic import ValidationError
12
11
  from rich.console import Console
13
12
 
14
13
  from cli.errors import ErrorPrinter
15
14
  from cli.settings import config_file, token_file
16
15
  from cli.settings import token_file as tf
17
16
  from cli.settings.config_file import ConfigFile
17
+ from cli.settings.state_file import StateFile
18
18
  from cli.settings.token_file import TokenFile
19
19
 
20
20
  err_console = Console(stderr=True)
21
21
 
22
-
23
22
  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
23
  CLI_CONFIG_FILE_NAME = "config.json"
28
- ACTIVE_TOKEN_FILE_NAME = "cloud.secret.token"
24
+ CLI_INTERNAL_STATE_FILE_NAME = "app-state.json"
29
25
 
30
26
  TokenFileMetadata = Tuple[TokenFile, Path]
31
27
 
@@ -34,67 +30,30 @@ class InvalidSettingsFilePathError(Exception):
34
30
  """Raised when trying to access an invalid settings file or file path"""
35
31
 
36
32
 
37
- class NotFoundError(Exception):
38
- """Raised when a token cannot be found in settings"""
39
-
40
-
41
33
  class TokenNotFoundError(Exception):
42
34
  """Raised when a token cannot be found in settings"""
43
35
 
44
36
 
45
- @dataclass()
46
- class CliConfigFile:
47
- default_organisation: Union[str, None]
48
-
49
-
50
37
  class Settings:
51
38
  """
52
- Settings for the remotive CLI
39
+ Settings handles tokens and other config for the remotive CLI
53
40
  """
54
41
 
55
42
  config_dir: Path
56
43
 
57
- def __init__(self, config_dir: Path, deprecated_config_dirs: list[Path] | None = None) -> None:
44
+ def __init__(self, config_dir: Path) -> None:
58
45
  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
46
  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)
47
+ self.config_file_path = self.config_dir / CLI_CONFIG_FILE_NAME
48
+ if not self.config_file_path.exists():
49
+ self._write_config_file(ConfigFile())
50
+ self.state_dir = self.config_dir / "state"
51
+ self.state_file_path = self.state_dir / CLI_INTERNAL_STATE_FILE_NAME
52
+ if not self.state_file_path.exists():
53
+ self.write_state_file(StateFile())
94
54
 
95
55
  def set_default_organisation(self, organisation: str) -> None:
96
56
  cli_config = self.get_cli_config()
97
-
98
57
  try:
99
58
  token = settings.get_active_token_file()
100
59
  cli_config.set_account_field(token.account.email, organisation)
@@ -108,7 +67,6 @@ class Settings:
108
67
  return self._read_config_file()
109
68
  except TokenNotFoundError:
110
69
  return ConfigFile()
111
- # self._write_properties(CONFIG_DIR_PATH / CLI_CONFIG_FILE_NAME, [])
112
70
 
113
71
  def get_active_token(self) -> str:
114
72
  """
@@ -121,30 +79,22 @@ class Settings:
121
79
  """
122
80
  Get the current active token file
123
81
  """
124
-
125
82
  active_account = self.get_cli_config().get_active()
126
83
  if active_account is not None:
127
84
  token_file_name = active_account.credentials_file
128
85
  return self._read_token_file(self.config_dir / token_file_name)
129
86
 
130
87
  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
88
 
135
- def activate_token(self, token: TokenFile) -> None:
89
+ def activate_token(self, token_file: TokenFile) -> None:
136
90
  """
137
91
  Activate a token by name or path
138
92
 
139
93
  The token secret will be set as the current active secret.
140
94
  """
141
- # token_file = self.get_token_file(name)
142
95
  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)
96
+ cli_config.activate(token_file.account.email)
146
97
  self._write_config_file(cli_config)
147
- # self._write_token_file(self._active_secret_token_path, token_file)
148
98
 
149
99
  def clear_active_token(self) -> None:
150
100
  """
@@ -154,9 +104,12 @@ class Settings:
154
104
  config.active = None
155
105
  self._write_config_file(config)
156
106
 
157
- # self._active_secret_token_path.unlink(missing_ok=True)
158
-
159
107
  def get_token_file_by_email(self, email: str) -> Optional[TokenFile]:
108
+ """
109
+ Get a token file by email.
110
+
111
+ If multiple tokens are found, the first one is returned.
112
+ """
160
113
  tokens = [t for t in self.list_personal_tokens() if t.account is not None and t.account.email == email]
161
114
  if len(tokens) > 0:
162
115
  return tokens[0]
@@ -169,11 +122,15 @@ class Settings:
169
122
  """
170
123
  Get a token file by name or path
171
124
  """
125
+ # 1. Try relative path
126
+ if (self.config_dir / name).exists():
127
+ return self._read_token_file(self.config_dir / name)
128
+
129
+ # 2. Try absolute path
172
130
  if Path(name).exists():
173
131
  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
132
 
133
+ # 3. Try name
177
134
  return self._get_token_by_name(name)[0]
178
135
 
179
136
  def remove_token_file(self, name: str) -> None:
@@ -189,41 +146,39 @@ class Settings:
189
146
 
190
147
  # TODO: what about the active token?
191
148
  path = self._get_token_by_name(name)[1]
192
- # print("Deleting", path)
193
149
  return path.unlink()
194
150
 
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:
151
+ def add_personal_token(self, token: str, activate: bool = False, overwrite_if_exists: bool = False) -> TokenFile:
209
152
  """
210
153
  Add a personal token
211
154
  """
212
- token_file = tf.loads(token)
213
- file = token_file.get_token_file_name()
214
- path = self.config_dir / file
155
+ file = tf.loads(token)
156
+ if file.type != "authorized_user":
157
+ raise ValueError("Token type MUST be authorized_user")
158
+
159
+ file_name = file.get_token_file_name()
160
+ path = self.config_dir / file_name
215
161
  if path.exists() and not overwrite_if_exists:
216
162
  raise FileExistsError(f"Token file already exists: {path}")
217
163
 
218
- self._write_token_file(path, token_file)
164
+ self._write_token_file(path, file)
219
165
  cli_config = self.get_cli_config()
220
- cli_config.init_account(email=token_file.account.email, token_file=token_file)
166
+ cli_config.init_account(email=file.account.email, token_file=file)
221
167
  self._write_config_file(cli_config)
222
168
 
223
169
  if activate:
224
- self.activate_token(token_file)
170
+ self.activate_token(file)
225
171
 
226
- return token_file
172
+ return file
173
+
174
+ def read_state_file(self) -> StateFile:
175
+ try:
176
+ return StateFile.loads(self.state_file_path.read_text(encoding="utf-8"))
177
+ except Exception:
178
+ return StateFile()
179
+
180
+ def write_state_file(self, state_file: StateFile) -> None:
181
+ self._write_file(self.state_file_path, state_file.dumps())
227
182
 
228
183
  def list_personal_tokens(self) -> list[TokenFile]:
229
184
  """
@@ -237,33 +192,26 @@ class Settings:
237
192
  """
238
193
  return [f[1] for f in self._list_personal_tokens()]
239
194
 
240
- def add_service_account_token(self, token: str) -> TokenFile:
195
+ def add_service_account_token(self, token: str, overwrite_if_exists: bool = False) -> TokenFile:
196
+ """
197
+ Add a service account token
198
+ """
241
199
  token_file = tf.loads(token)
242
200
  if token_file.type != "service_account":
243
201
  raise ValueError("Token type MUST be service_account")
202
+
244
203
  file = token_file.get_token_file_name()
245
204
  path = self.config_dir / file
205
+ if path.exists() and not overwrite_if_exists:
206
+ raise FileExistsError(f"Token file already exists: {path}")
246
207
 
247
208
  self._write_token_file(path, token_file)
248
- print(f"Service account token stored at {path}")
249
209
  cli_config = self.get_cli_config()
250
210
  cli_config.init_account(email=token_file.account.email, token_file=token_file)
251
211
  self._write_config_file(cli_config)
252
212
 
253
- # if activate:
254
- # self.activate_token(token_file.account.email)
255
-
256
213
  return token_file
257
214
 
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
215
  def list_service_account_tokens(self) -> list[TokenFile]:
268
216
  """
269
217
  List all service account tokens
@@ -290,13 +238,17 @@ class Settings:
290
238
  return matches[0]
291
239
 
292
240
  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"""
241
+ """
242
+ list all tokens with the correct prefix in the config dir, but omit files that are not token files
243
+
244
+ TODO: improve is_valid_json and is_valid_token_file using token_file parsing instead
245
+ """
294
246
 
295
247
  def is_valid_json(path: Path) -> bool:
296
248
  try:
297
249
  self._read_token_file(path)
298
250
  return True
299
- except JSONDecodeError:
251
+ except (JSONDecodeError, ValidationError):
300
252
  # TODO - this should be printed but printing it here causes it to be displayed to many times
301
253
  # err_console.print(f"File is not valid json, skipping. {path}")
302
254
  return False
@@ -306,12 +258,13 @@ class Settings:
306
258
  tf.PERSONAL_TOKEN_FILE_PREFIX
307
259
  )
308
260
  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
261
+ is_cli_config = path == self.config_file_path
262
+ is_present_in_cli_config_accounts = any(
263
+ path.name == account.credentials_file for account in self.get_cli_config().accounts.values()
264
+ )
265
+ 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
266
 
313
267
  paths = [path for path in self.config_dir.iterdir() if is_valid_token_file(path)]
314
-
315
268
  return [(self._read_token_file(token_file), token_file) for token_file in paths]
316
269
 
317
270
  def _read_token_file(self, path: Path) -> TokenFile:
@@ -338,8 +291,11 @@ class Settings:
338
291
  return self._write_config_file(config)
339
292
 
340
293
  def _write_config_file(self, config: ConfigFile) -> Path:
294
+ """
295
+ TODO: add read cache to avoid parsing the config every time we read it
296
+ """
341
297
  data = config_file.dumps(config)
342
- path = self._write_file(self._cli_config, data)
298
+ path = self._write_file(self.config_file_path, data)
343
299
  os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
344
300
  return path
345
301
 
@@ -350,43 +306,8 @@ class Settings:
350
306
  path.write_text(data, encoding="utf8")
351
307
  return path
352
308
 
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()
309
+
310
+ settings = Settings(CONFIG_DIR_PATH)
390
311
  """
391
312
  Global/module-level settings instance. Module-level variables are only loaded once, at import time.
392
313
 
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]
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import json
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Optional
8
+
9
+ from dacite import from_dict
10
+
11
+ from cli.utils.time import parse_datetime
12
+
13
+
14
+ @dataclass
15
+ class StateFile:
16
+ version: str = "1.0"
17
+ last_update_check_time: Optional[str] = None
18
+
19
+ def dumps(self) -> str:
20
+ return json.dumps(dataclasses.asdict(self), default=str)
21
+
22
+ def should_perform_update_check(self) -> bool:
23
+ if self.last_update_check_time:
24
+ seconds = (datetime.now() - parse_datetime(self.last_update_check_time)).seconds
25
+ return (seconds / 3600) > 2
26
+ # This will solve the issue
27
+ return True
28
+
29
+ @staticmethod
30
+ def loads(data: str) -> StateFile:
31
+ d = json.loads(data)
32
+ return from_dict(StateFile, d)