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/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, Tuple
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, token_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.write_state_file(StateFile())
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
- cli_config = self.get_cli_config()
57
- try:
58
- token = settings.get_active_token_file()
59
- cli_config.set_account_field(token.account.email, organisation)
60
- self._write_config_file(cli_config)
61
- except TokenNotFoundError:
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 get_cli_config(self) -> ConfigFile:
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 token secret
77
+ Get the current active account
78
+
79
+ TODO: Add email field to Account
74
80
  """
75
- token_file = self.get_active_token_file()
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 token file
85
+ Get the token file for the current active account
81
86
  """
82
- active_account = self.get_cli_config().get_active()
83
- if active_account is not None:
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
- raise TokenNotFoundError
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) -> None:
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
- cli_config = self.get_cli_config()
96
- cli_config.activate(token_file.account.email)
97
- self._write_config_file(cli_config)
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 clear_active_token(self) -> None:
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.get_cli_config()
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
- tokens = [t for t in self.list_personal_tokens() if t.account is not None and t.account.email == email]
114
- if len(tokens) > 0:
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(self.config_dir / name)
139
+ return self._read_token_file(name)
128
140
 
129
- # 2. Try absolute path
130
- if Path(name).exists():
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
- if Path(name).exists():
143
- if self.config_dir not in Path(name).parents:
144
- raise InvalidSettingsFilePathError(f"cannot remove a token file not located in settings dir {self.config_dir}")
145
- return Path(name).unlink()
146
-
147
- # TODO: what about the active token?
148
- path = self._get_token_by_name(name)[1]
149
- return path.unlink()
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
- file = tf.loads(token)
156
- if file.type != "authorized_user":
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
- file_name = file.get_token_file_name()
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(file)
171
-
172
- return file
177
+ self.activate_token(token_file)
173
178
 
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())
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
- file = token_file.get_token_file_name()
204
- path = self.config_dir / file
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.get_cli_config()
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 list_service_account_tokens(self) -> list[TokenFile]:
207
+ def list_accounts(self) -> dict[str, Account]:
216
208
  """
217
- List all service account tokens
209
+ List all accounts
218
210
  """
219
- return [f[0] for f in self._list_service_account_tokens()]
211
+ return self._get_cli_config().accounts
220
212
 
221
- def list_service_account_token_files(self) -> list[Path]:
222
- """
223
- List paths to all service account token files
213
+ def list_personal_accounts(self) -> dict[str, Account]:
224
214
  """
225
- return [f[1] for f in self._list_service_account_tokens()]
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
- def _list_service_account_tokens(self) -> list[TokenFileMetadata]:
231
- return self._list_token_files(prefix=token_file.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX)
232
-
233
- def _get_token_by_name(self, name: str) -> TokenFileMetadata:
234
- token_files = self._list_token_files()
235
- matches = [token_file for token_file in token_files if token_file[0].name == name]
236
- if len(matches) != 1:
237
- raise TokenNotFoundError(f"Ambiguous token file name {name}, found {len(matches)} files")
238
- return matches[0]
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
- def _list_token_files(self, prefix: str = "") -> list[TokenFileMetadata]:
230
+ TODO: add account type to Account
241
231
  """
242
- list all tokens with the correct prefix in the config dir, but omit files that are not token files
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
- TODO: improve is_valid_json and is_valid_token_file using token_file parsing instead
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
- def is_valid_json(path: Path) -> bool:
248
- try:
249
- self._read_token_file(path)
250
- return True
251
- except (JSONDecodeError, ValidationError):
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
- def is_valid_token_file(path: Path) -> bool:
257
- is_token_file = path.name.startswith(tf.SERVICE_ACCOUNT_TOKEN_FILE_PREFIX) or path.name.startswith(
258
- tf.PERSONAL_TOKEN_FILE_PREFIX
259
- )
260
- has_correct_prefix = path.is_file() and path.name.startswith(prefix)
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
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
- paths = [path for path in self.config_dir.iterdir() if is_valid_token_file(path)]
268
- return [(self._read_token_file(token_file), token_file) for token_file in paths]
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, path: Path) -> TokenFile:
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
- path = self._write_file(path, data)
286
- os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
287
- return path
283
+ return self._write_file(path, data)
288
284
 
289
- # Temporary function while considering how to solve this
290
- def write_config_file(self, config: ConfigFile) -> Path:
291
- return self._write_config_file(config)
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
- TODO: add read cache to avoid parsing the config every time we read it
296
- """
297
- data = config_file.dumps(config)
298
- path = self._write_file(self.config_file_path, data)
299
- os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
300
- return path
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, TokenNotFoundError
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
- 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
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(data_class=ConfigFile, data=migrated_data)
58
- settings.write_config_file(migrated_config)
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
- raise TokenNotFoundError(f"Token file for {cred_name} not found")
36
+ return None
36
37
  return matches[0]
@@ -1,32 +1,67 @@
1
1
  from __future__ import annotations
2
2
 
3
- import dataclasses
4
- import json
5
- from dataclasses import dataclass
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 dacite import from_dict
7
+ from pydantic import BaseModel
10
8
 
11
9
  from cli.utils.time import parse_datetime
12
10
 
13
11
 
14
- @dataclass
15
- class StateFile:
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
- 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)
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()