remotivelabs-cli 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remotivelabs-cli might be problematic. Click here for more details.
- cli/broker/lib/broker.py +120 -89
- cli/broker/lib/client.py +224 -0
- cli/broker/lib/helper.py +278 -0
- cli/broker/lib/signalcreator.py +196 -0
- cli/cloud/__init__.py +17 -0
- cli/cloud/auth/cmd.py +71 -33
- cli/cloud/auth/login.py +26 -28
- cli/cloud/auth_tokens.py +35 -247
- cli/cloud/licenses/__init__.py +0 -0
- cli/cloud/licenses/cmd.py +14 -0
- cli/cloud/organisations.py +8 -12
- cli/cloud/service_account_tokens.py +1 -1
- cli/connect/protopie/protopie.py +1 -1
- cli/remotive.py +17 -26
- cli/settings/__init__.py +1 -2
- cli/settings/config_file.py +72 -52
- cli/settings/core.py +173 -168
- cli/settings/migration/migrate_config_file.py +15 -10
- cli/settings/migration/migration_tools.py +4 -3
- cli/settings/state_file.py +56 -21
- cli/settings/token_file.py +13 -21
- cli/topology/cmd.py +6 -6
- cli/utils/rest_helper.py +20 -28
- cli/utils/{version_check.py → versions.py} +30 -20
- {remotivelabs_cli-0.2.2.dist-info → remotivelabs_cli-0.3.0.dist-info}/METADATA +3 -3
- {remotivelabs_cli-0.2.2.dist-info → remotivelabs_cli-0.3.0.dist-info}/RECORD +29 -25
- cli/cloud/cloud_cli.py +0 -29
- {remotivelabs_cli-0.2.2.dist-info → remotivelabs_cli-0.3.0.dist-info}/LICENSE +0 -0
- {remotivelabs_cli-0.2.2.dist-info → remotivelabs_cli-0.3.0.dist-info}/WHEEL +0 -0
- {remotivelabs_cli-0.2.2.dist-info → remotivelabs_cli-0.3.0.dist-info}/entry_points.txt +0 -0
cli/settings/core.py
CHANGED
|
@@ -3,17 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import stat
|
|
5
5
|
import sys
|
|
6
|
-
from json import JSONDecodeError
|
|
7
6
|
from pathlib import Path
|
|
8
|
-
from typing import Optional
|
|
7
|
+
from typing import Optional
|
|
9
8
|
|
|
10
|
-
from pydantic import ValidationError
|
|
11
9
|
from rich.console import Console
|
|
12
10
|
|
|
13
11
|
from cli.errors import ErrorPrinter
|
|
14
|
-
from cli.settings import config_file
|
|
12
|
+
from cli.settings import config_file as cf
|
|
13
|
+
from cli.settings import state_file as sf
|
|
15
14
|
from cli.settings import token_file as tf
|
|
16
|
-
from cli.settings.config_file import ConfigFile
|
|
15
|
+
from cli.settings.config_file import Account, ConfigFile
|
|
17
16
|
from cli.settings.state_file import StateFile
|
|
18
17
|
from cli.settings.token_file import TokenFile
|
|
19
18
|
|
|
@@ -23,20 +22,17 @@ CONFIG_DIR_PATH = Path.home() / ".config" / "remotive"
|
|
|
23
22
|
CLI_CONFIG_FILE_NAME = "config.json"
|
|
24
23
|
CLI_INTERNAL_STATE_FILE_NAME = "app-state.json"
|
|
25
24
|
|
|
26
|
-
TokenFileMetadata = Tuple[TokenFile, Path]
|
|
27
|
-
|
|
28
25
|
|
|
29
26
|
class InvalidSettingsFilePathError(Exception):
|
|
30
27
|
"""Raised when trying to access an invalid settings file or file path"""
|
|
31
28
|
|
|
32
29
|
|
|
33
|
-
class TokenNotFoundError(Exception):
|
|
34
|
-
"""Raised when a token cannot be found in settings"""
|
|
35
|
-
|
|
36
|
-
|
|
37
30
|
class Settings:
|
|
38
31
|
"""
|
|
39
32
|
Settings handles tokens and other config for the remotive CLI
|
|
33
|
+
|
|
34
|
+
TODO: migrate away from singleton instance
|
|
35
|
+
TODO: How do we handle REMOTIVE_CLOUD_ACCESS_TOKEN in combination with active account? What takes precedence?
|
|
40
36
|
"""
|
|
41
37
|
|
|
42
38
|
config_dir: Path
|
|
@@ -50,57 +46,78 @@ class Settings:
|
|
|
50
46
|
self.state_dir = self.config_dir / "state"
|
|
51
47
|
self.state_file_path = self.state_dir / CLI_INTERNAL_STATE_FILE_NAME
|
|
52
48
|
if not self.state_file_path.exists():
|
|
53
|
-
self.
|
|
49
|
+
self._write_state_file(StateFile())
|
|
50
|
+
|
|
51
|
+
def _get_cli_config(self) -> ConfigFile:
|
|
52
|
+
return self._read_config_file()
|
|
53
|
+
|
|
54
|
+
def _get_state_file(self) -> StateFile:
|
|
55
|
+
return self._read_state_file()
|
|
56
|
+
|
|
57
|
+
def should_perform_update_check(self) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Check if we should perform an update check.
|
|
60
|
+
"""
|
|
61
|
+
return self._get_state_file().should_perform_update_check()
|
|
54
62
|
|
|
55
63
|
def set_default_organisation(self, organisation: str) -> None:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
"""
|
|
65
|
+
Set the default organization for the active account
|
|
66
|
+
"""
|
|
67
|
+
config = self._get_cli_config()
|
|
68
|
+
active_account = config.get_active_account()
|
|
69
|
+
if not active_account:
|
|
62
70
|
ErrorPrinter.print_hint("You must have an account activated in order to set default organization")
|
|
63
71
|
sys.exit(1)
|
|
72
|
+
active_account.default_organization = organisation
|
|
73
|
+
self._write_config_file(config)
|
|
64
74
|
|
|
65
|
-
def
|
|
66
|
-
try:
|
|
67
|
-
return self._read_config_file()
|
|
68
|
-
except TokenNotFoundError:
|
|
69
|
-
return ConfigFile()
|
|
70
|
-
|
|
71
|
-
def get_active_token(self) -> str:
|
|
75
|
+
def get_active_account(self) -> Account | None:
|
|
72
76
|
"""
|
|
73
|
-
Get the current active
|
|
77
|
+
Get the current active account
|
|
78
|
+
|
|
79
|
+
TODO: Add email field to Account
|
|
74
80
|
"""
|
|
75
|
-
|
|
76
|
-
return token_file.token
|
|
81
|
+
return self._get_cli_config().get_active_account()
|
|
77
82
|
|
|
78
|
-
def get_active_token_file(self) -> TokenFile:
|
|
83
|
+
def get_active_token_file(self) -> TokenFile | None:
|
|
79
84
|
"""
|
|
80
|
-
Get the current active
|
|
85
|
+
Get the token file for the current active account
|
|
81
86
|
"""
|
|
82
|
-
active_account = self.
|
|
83
|
-
if active_account
|
|
84
|
-
token_file_name = active_account.credentials_file
|
|
85
|
-
return self._read_token_file(self.config_dir / token_file_name)
|
|
87
|
+
active_account = self.get_active_account()
|
|
88
|
+
return self._read_token_file(active_account.credentials_file) if active_account else None
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
def get_active_token(self) -> str | None:
|
|
91
|
+
"""
|
|
92
|
+
Get the token secret for the current active account
|
|
93
|
+
"""
|
|
94
|
+
token_file = self.get_active_token_file()
|
|
95
|
+
return token_file.token if token_file else None
|
|
88
96
|
|
|
89
|
-
def activate_token(self, token_file: TokenFile) ->
|
|
97
|
+
def activate_token(self, token_file: TokenFile) -> TokenFile:
|
|
90
98
|
"""
|
|
91
99
|
Activate a token by name or path
|
|
92
100
|
|
|
93
101
|
The token secret will be set as the current active secret.
|
|
102
|
+
|
|
103
|
+
Returns the activated token file
|
|
94
104
|
"""
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
self._write_config_file(
|
|
105
|
+
config = self._get_cli_config()
|
|
106
|
+
config.activate_account(token_file.account.email)
|
|
107
|
+
self._write_config_file(config)
|
|
108
|
+
return token_file
|
|
98
109
|
|
|
99
|
-
def
|
|
110
|
+
def is_active_account(self, email: str) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Returns True if the given email is the active account
|
|
113
|
+
"""
|
|
114
|
+
return self._get_cli_config().active == email
|
|
115
|
+
|
|
116
|
+
def clear_active_account(self) -> None:
|
|
100
117
|
"""
|
|
101
118
|
Clear the current active token
|
|
102
119
|
"""
|
|
103
|
-
config = self.
|
|
120
|
+
config = self._get_cli_config()
|
|
104
121
|
config.active = None
|
|
105
122
|
self._write_config_file(config)
|
|
106
123
|
|
|
@@ -110,87 +127,56 @@ class Settings:
|
|
|
110
127
|
|
|
111
128
|
If multiple tokens are found, the first one is returned.
|
|
112
129
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return tokens[0]
|
|
116
|
-
tokens = [t for t in self.list_service_account_tokens() if t.account is not None and t.account.email == email]
|
|
117
|
-
if len(tokens) > 0:
|
|
118
|
-
return tokens[0]
|
|
119
|
-
return None
|
|
130
|
+
accounts = self._get_cli_config().accounts.get(email)
|
|
131
|
+
return self._read_token_file(accounts.credentials_file) if accounts else None
|
|
120
132
|
|
|
121
|
-
def get_token_file(self, name: str) -> TokenFile:
|
|
133
|
+
def get_token_file(self, name: str) -> TokenFile | None:
|
|
122
134
|
"""
|
|
123
135
|
Get a token file by name or path
|
|
124
136
|
"""
|
|
125
137
|
# 1. Try relative path
|
|
126
138
|
if (self.config_dir / name).exists():
|
|
127
|
-
return self._read_token_file(
|
|
139
|
+
return self._read_token_file(name)
|
|
128
140
|
|
|
129
|
-
# 2. Try
|
|
130
|
-
|
|
131
|
-
return self._read_token_file(Path(name))
|
|
132
|
-
|
|
133
|
-
# 3. Try name
|
|
134
|
-
return self._get_token_by_name(name)[0]
|
|
141
|
+
# 2. Try name
|
|
142
|
+
return self._get_token_by_name(name)
|
|
135
143
|
|
|
136
144
|
def remove_token_file(self, name: str) -> None:
|
|
137
145
|
"""
|
|
138
146
|
Remove a token file by name or path
|
|
139
|
-
|
|
140
|
-
TODO: what about manually downloaded tokens?
|
|
141
147
|
"""
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
token_file = self.get_token_file(name)
|
|
149
|
+
if not token_file:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# If the token is active, clear it first
|
|
153
|
+
email = token_file.account.email
|
|
154
|
+
if self.is_active_account(email):
|
|
155
|
+
self.clear_active_account()
|
|
156
|
+
|
|
157
|
+
# Remove the token file
|
|
158
|
+
path = self.config_dir / self._get_cli_config().accounts[email].credentials_file
|
|
159
|
+
path.unlink()
|
|
160
|
+
|
|
161
|
+
# Remove the account from the config file
|
|
162
|
+
config = self._get_cli_config()
|
|
163
|
+
config.remove_account(email)
|
|
164
|
+
self._write_config_file(config)
|
|
150
165
|
|
|
151
166
|
def add_personal_token(self, token: str, activate: bool = False, overwrite_if_exists: bool = False) -> TokenFile:
|
|
152
167
|
"""
|
|
153
168
|
Add a personal token
|
|
154
169
|
"""
|
|
155
|
-
|
|
156
|
-
if
|
|
170
|
+
token_file = tf.loads(token)
|
|
171
|
+
if token_file.type != "authorized_user":
|
|
157
172
|
raise ValueError("Token type MUST be authorized_user")
|
|
158
173
|
|
|
159
|
-
|
|
160
|
-
path = self.config_dir / file_name
|
|
161
|
-
if path.exists() and not overwrite_if_exists:
|
|
162
|
-
raise FileExistsError(f"Token file already exists: {path}")
|
|
163
|
-
|
|
164
|
-
self._write_token_file(path, file)
|
|
165
|
-
cli_config = self.get_cli_config()
|
|
166
|
-
cli_config.init_account(email=file.account.email, token_file=file)
|
|
167
|
-
self._write_config_file(cli_config)
|
|
174
|
+
token_file = self.add_token_as_account(token_file, overwrite_if_exists)
|
|
168
175
|
|
|
169
176
|
if activate:
|
|
170
|
-
self.activate_token(
|
|
171
|
-
|
|
172
|
-
return file
|
|
177
|
+
self.activate_token(token_file)
|
|
173
178
|
|
|
174
|
-
|
|
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())
|
|
182
|
-
|
|
183
|
-
def list_personal_tokens(self) -> list[TokenFile]:
|
|
184
|
-
"""
|
|
185
|
-
List all personal tokens
|
|
186
|
-
"""
|
|
187
|
-
return [f[0] for f in self._list_personal_tokens()]
|
|
188
|
-
|
|
189
|
-
def list_personal_token_files(self) -> list[Path]:
|
|
190
|
-
"""
|
|
191
|
-
List paths to all personal token files
|
|
192
|
-
"""
|
|
193
|
-
return [f[1] for f in self._list_personal_tokens()]
|
|
179
|
+
return token_file
|
|
194
180
|
|
|
195
181
|
def add_service_account_token(self, token: str, overwrite_if_exists: bool = False) -> TokenFile:
|
|
196
182
|
"""
|
|
@@ -200,110 +186,129 @@ class Settings:
|
|
|
200
186
|
if token_file.type != "service_account":
|
|
201
187
|
raise ValueError("Token type MUST be service_account")
|
|
202
188
|
|
|
203
|
-
|
|
204
|
-
|
|
189
|
+
return self.add_token_as_account(token_file, overwrite_if_exists)
|
|
190
|
+
|
|
191
|
+
def add_token_as_account(self, token_file: TokenFile, overwrite_if_exists: bool = False) -> TokenFile:
|
|
192
|
+
"""
|
|
193
|
+
Add an account to the config file
|
|
194
|
+
"""
|
|
195
|
+
file_name = token_file.get_token_file_name()
|
|
196
|
+
path = self.config_dir / file_name
|
|
205
197
|
if path.exists() and not overwrite_if_exists:
|
|
206
198
|
raise FileExistsError(f"Token file already exists: {path}")
|
|
207
199
|
|
|
208
200
|
self._write_token_file(path, token_file)
|
|
209
|
-
cli_config = self.
|
|
201
|
+
cli_config = self._get_cli_config()
|
|
210
202
|
cli_config.init_account(email=token_file.account.email, token_file=token_file)
|
|
211
203
|
self._write_config_file(cli_config)
|
|
212
204
|
|
|
213
205
|
return token_file
|
|
214
206
|
|
|
215
|
-
def
|
|
207
|
+
def list_accounts(self) -> dict[str, Account]:
|
|
216
208
|
"""
|
|
217
|
-
List all
|
|
209
|
+
List all accounts
|
|
218
210
|
"""
|
|
219
|
-
return
|
|
211
|
+
return self._get_cli_config().accounts
|
|
220
212
|
|
|
221
|
-
def
|
|
222
|
-
"""
|
|
223
|
-
List paths to all service account token files
|
|
213
|
+
def list_personal_accounts(self) -> dict[str, Account]:
|
|
224
214
|
"""
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _list_personal_tokens(self) -> list[TokenFileMetadata]:
|
|
228
|
-
return self._list_token_files(prefix=token_file.PERSONAL_TOKEN_FILE_PREFIX)
|
|
215
|
+
List all personal accounts
|
|
229
216
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
217
|
+
TODO: add account type to Account
|
|
218
|
+
"""
|
|
219
|
+
accounts = self.list_accounts()
|
|
220
|
+
return {
|
|
221
|
+
email: account
|
|
222
|
+
for email, account in accounts.items()
|
|
223
|
+
if self._read_token_file(account.credentials_file).type == "authorized_user"
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
def list_service_accounts(self) -> dict[str, Account]:
|
|
227
|
+
"""
|
|
228
|
+
List all personal accounts
|
|
239
229
|
|
|
240
|
-
|
|
230
|
+
TODO: add account type to Account
|
|
241
231
|
"""
|
|
242
|
-
|
|
232
|
+
accounts = self.list_accounts()
|
|
233
|
+
return {
|
|
234
|
+
email: account
|
|
235
|
+
for email, account in accounts.items()
|
|
236
|
+
if self._read_token_file(account.credentials_file).type == "service_account"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def list_token_files(self) -> list[TokenFile]:
|
|
240
|
+
"""
|
|
241
|
+
List all token files
|
|
242
|
+
"""
|
|
243
|
+
accounts = self._get_cli_config().accounts.values()
|
|
244
|
+
return [self._read_token_file(account.credentials_file) for account in accounts]
|
|
243
245
|
|
|
244
|
-
|
|
246
|
+
def list_personal_token_files(self) -> list[TokenFile]:
|
|
247
|
+
"""
|
|
248
|
+
List all personal token files
|
|
245
249
|
"""
|
|
250
|
+
return [token_file for token_file in self.list_token_files() if token_file.type == "authorized_user"]
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
# TODO - this should be printed but printing it here causes it to be displayed to many times
|
|
253
|
-
# err_console.print(f"File is not valid json, skipping. {path}")
|
|
254
|
-
return False
|
|
252
|
+
def list_service_account_token_files(self) -> list[TokenFile]:
|
|
253
|
+
"""
|
|
254
|
+
List all service account token files
|
|
255
|
+
"""
|
|
256
|
+
return [token_file for token_file in self.list_token_files() if token_file.type == "service_account"]
|
|
255
257
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
258
|
+
def set_last_update_check_time(self, timestamp: str) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Sets the timestamp of the last self update check
|
|
261
|
+
"""
|
|
262
|
+
state = self._read_state_file()
|
|
263
|
+
state.last_update_check_time = timestamp
|
|
264
|
+
self._write_state_file(state)
|
|
266
265
|
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
def _get_token_by_name(self, name: str) -> TokenFile | None:
|
|
267
|
+
"""
|
|
268
|
+
Token name is only available as a property of TokenFile, so we must iterate over all tokens to find the right one
|
|
269
|
+
"""
|
|
270
|
+
token_files = self.list_token_files()
|
|
271
|
+
matches = [token_file for token_file in token_files if token_file.name == name]
|
|
272
|
+
if len(matches) != 1:
|
|
273
|
+
return None
|
|
274
|
+
return matches[0]
|
|
269
275
|
|
|
270
|
-
def _read_token_file(self,
|
|
276
|
+
def _read_token_file(self, file_name: str) -> TokenFile:
|
|
277
|
+
path = self.config_dir / file_name
|
|
271
278
|
data = self._read_file(path)
|
|
272
279
|
return tf.loads(data)
|
|
273
280
|
|
|
274
|
-
def _read_config_file(self) -> ConfigFile:
|
|
275
|
-
data = self._read_file(self.config_dir / CLI_CONFIG_FILE_NAME)
|
|
276
|
-
return config_file.loads(data)
|
|
277
|
-
|
|
278
|
-
def _read_file(self, path: Path) -> str:
|
|
279
|
-
if not path.exists():
|
|
280
|
-
raise TokenNotFoundError(f"File could not be found: {path}")
|
|
281
|
-
return path.read_text(encoding="utf-8")
|
|
282
|
-
|
|
283
281
|
def _write_token_file(self, path: Path, token: TokenFile) -> Path:
|
|
284
282
|
data = tf.dumps(token)
|
|
285
|
-
|
|
286
|
-
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
287
|
-
return path
|
|
283
|
+
return self._write_file(path, data)
|
|
288
284
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return
|
|
285
|
+
def _read_config_file(self) -> ConfigFile:
|
|
286
|
+
data = self._read_file(self.config_file_path)
|
|
287
|
+
return cf.loads(data)
|
|
292
288
|
|
|
293
289
|
def _write_config_file(self, config: ConfigFile) -> Path:
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
290
|
+
data = cf.dumps(config)
|
|
291
|
+
return self._write_file(self.config_file_path, data)
|
|
292
|
+
|
|
293
|
+
def _read_state_file(self) -> StateFile:
|
|
294
|
+
data = self._read_file(self.state_file_path)
|
|
295
|
+
return sf.loads(data)
|
|
296
|
+
|
|
297
|
+
def _write_state_file(self, state: StateFile) -> Path:
|
|
298
|
+
data = sf.dumps(state)
|
|
299
|
+
return self._write_file(self.state_file_path, data)
|
|
300
|
+
|
|
301
|
+
def _read_file(self, path: Path) -> str:
|
|
302
|
+
if not path.exists():
|
|
303
|
+
raise FileNotFoundError(f"File could not be found: {path}")
|
|
304
|
+
return path.read_text(encoding="utf-8")
|
|
301
305
|
|
|
302
306
|
def _write_file(self, path: Path, data: str) -> Path:
|
|
303
307
|
if self.config_dir not in path.parents:
|
|
304
308
|
raise InvalidSettingsFilePathError(f"file {path} not in settings dir {self.config_dir}")
|
|
305
309
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
306
310
|
path.write_text(data, encoding="utf8")
|
|
311
|
+
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
|
|
307
312
|
return path
|
|
308
313
|
|
|
309
314
|
|
|
@@ -5,10 +5,8 @@ import sys
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
|
-
from dacite import from_dict
|
|
9
|
-
|
|
10
8
|
from cli.settings.config_file import ConfigFile, loads
|
|
11
|
-
from cli.settings.core import Settings
|
|
9
|
+
from cli.settings.core import Settings
|
|
12
10
|
from cli.settings.migration.migration_tools import get_token_file
|
|
13
11
|
|
|
14
12
|
|
|
@@ -23,13 +21,20 @@ def migrate_account_data(config: dict[str, Any], settings: Settings) -> Optional
|
|
|
23
21
|
cred_name = account_info.pop("credentials_name", None)
|
|
24
22
|
if not cred_name:
|
|
25
23
|
continue
|
|
24
|
+
|
|
25
|
+
# found legacy account, try to migrate it, or drop it...
|
|
26
26
|
found_old = True
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
|
|
28
|
+
token_file = get_token_file(cred_name, settings.config_dir)
|
|
29
|
+
if not token_file:
|
|
30
|
+
sys.stderr.write(f"Dropping account {account_email!r}: credentials file for {cred_name} not found")
|
|
31
|
+
to_delete.append(account_email)
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
cred_file = token_file.get_token_file_name()
|
|
35
|
+
if not cred_file:
|
|
36
|
+
sys.stderr.write(f"Dropping account {account_email!r}: credentials file for {cred_name} not found")
|
|
31
37
|
to_delete.append(account_email)
|
|
32
|
-
sys.stderr.write(f"Dropping account {account_email!r}: token file for {cred_name} not found")
|
|
33
38
|
continue
|
|
34
39
|
|
|
35
40
|
account_info["credentials_file"] = cred_file
|
|
@@ -54,6 +59,6 @@ def migrate_config_file(path: Path, settings: Settings) -> ConfigFile:
|
|
|
54
59
|
return loads(data)
|
|
55
60
|
|
|
56
61
|
sys.stderr.write("Migrating old configuration format")
|
|
57
|
-
migrated_config: ConfigFile = from_dict(
|
|
58
|
-
settings.
|
|
62
|
+
migrated_config: ConfigFile = ConfigFile.from_dict(migrated_data)
|
|
63
|
+
settings._write_config_file(migrated_config)
|
|
59
64
|
return migrated_config
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from itertools import chain
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
4
|
-
from cli.settings.core import TokenNotFoundError
|
|
5
6
|
from cli.settings.token_file import TokenFile
|
|
6
7
|
|
|
7
8
|
|
|
@@ -23,7 +24,7 @@ def list_token_files(config_dir: Path) -> list[TokenFile]:
|
|
|
23
24
|
return token_files
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
def get_token_file(cred_name: str, config_dir: Path) -> TokenFile:
|
|
27
|
+
def get_token_file(cred_name: str, config_dir: Path) -> TokenFile | None:
|
|
27
28
|
"""
|
|
28
29
|
Get the token file for a given credentials name.
|
|
29
30
|
|
|
@@ -32,5 +33,5 @@ def get_token_file(cred_name: str, config_dir: Path) -> TokenFile:
|
|
|
32
33
|
token_files = list_token_files(config_dir)
|
|
33
34
|
matches = [token_file for token_file in token_files if token_file.name == cred_name]
|
|
34
35
|
if len(matches) != 1:
|
|
35
|
-
|
|
36
|
+
return None
|
|
36
37
|
return matches[0]
|
cli/settings/state_file.py
CHANGED
|
@@ -1,32 +1,67 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
from
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
from typing import Optional
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Any, Optional
|
|
8
6
|
|
|
9
|
-
from
|
|
7
|
+
from pydantic import BaseModel
|
|
10
8
|
|
|
11
9
|
from cli.utils.time import parse_datetime
|
|
12
10
|
|
|
13
11
|
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
class StateFile(BaseModel):
|
|
13
|
+
"""
|
|
14
|
+
Contains CLI state and other application specific data.
|
|
15
|
+
"""
|
|
16
|
+
|
|
16
17
|
version: str = "1.0"
|
|
17
18
|
last_update_check_time: Optional[str] = None
|
|
18
19
|
|
|
19
|
-
def dumps(self) -> str:
|
|
20
|
-
return json.dumps(dataclasses.asdict(self), default=str)
|
|
21
|
-
|
|
22
20
|
def should_perform_update_check(self) -> bool:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
"""
|
|
22
|
+
Check if we should perform an update check.
|
|
23
|
+
|
|
24
|
+
Returns True if the last update check time is older than 2 hours.
|
|
25
|
+
|
|
26
|
+
For Docker environments, returns False and sets a backdated timestamp
|
|
27
|
+
to prevent constant update checks due to ephemeral disks.
|
|
28
|
+
"""
|
|
29
|
+
if not self.last_update_check_time:
|
|
30
|
+
if os.environ.get("RUNS_IN_DOCKER"):
|
|
31
|
+
# To prevent that we always check update in docker due to ephemeral disks we write an "old" check if the state
|
|
32
|
+
# is missing. If no disk is mounted we will never get the update check but if its mounted properly we will get
|
|
33
|
+
# it on the second attempt. This is good enough
|
|
34
|
+
self.last_update_check_time = (datetime.now() - timedelta(hours=10)).isoformat()
|
|
35
|
+
return False
|
|
36
|
+
return True # Returning True will trigger a check, which will properly set last_update_check_time
|
|
37
|
+
|
|
38
|
+
seconds = (datetime.now() - parse_datetime(self.last_update_check_time)).seconds
|
|
39
|
+
return (seconds / 3600) > 2
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_json_str(cls, data: str) -> StateFile:
|
|
43
|
+
return cls.model_validate_json(data)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls, data: dict[str, Any]) -> StateFile:
|
|
47
|
+
return cls.model_validate(data)
|
|
48
|
+
|
|
49
|
+
def to_json_str(self) -> str:
|
|
50
|
+
return self.model_dump_json()
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
return self.model_dump()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def loads(data: str) -> StateFile:
|
|
57
|
+
"""
|
|
58
|
+
Creates a StateFile from a JSON string.
|
|
59
|
+
"""
|
|
60
|
+
return StateFile.from_json_str(data)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def dumps(state: StateFile) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Returns the JSON string representation of the StateFile.
|
|
66
|
+
"""
|
|
67
|
+
return state.to_json_str()
|