mlx-stack 0.1.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.
- mlx_stack/__init__.py +5 -0
- mlx_stack/_version.py +24 -0
- mlx_stack/cli/__init__.py +5 -0
- mlx_stack/cli/bench.py +221 -0
- mlx_stack/cli/config.py +166 -0
- mlx_stack/cli/down.py +109 -0
- mlx_stack/cli/init.py +180 -0
- mlx_stack/cli/install.py +165 -0
- mlx_stack/cli/logs.py +234 -0
- mlx_stack/cli/main.py +187 -0
- mlx_stack/cli/models.py +304 -0
- mlx_stack/cli/profile.py +65 -0
- mlx_stack/cli/pull.py +134 -0
- mlx_stack/cli/recommend.py +397 -0
- mlx_stack/cli/status.py +111 -0
- mlx_stack/cli/up.py +163 -0
- mlx_stack/cli/watch.py +252 -0
- mlx_stack/core/__init__.py +1 -0
- mlx_stack/core/benchmark.py +1182 -0
- mlx_stack/core/catalog.py +560 -0
- mlx_stack/core/config.py +471 -0
- mlx_stack/core/deps.py +323 -0
- mlx_stack/core/hardware.py +304 -0
- mlx_stack/core/launchd.py +531 -0
- mlx_stack/core/litellm_gen.py +188 -0
- mlx_stack/core/log_rotation.py +231 -0
- mlx_stack/core/log_viewer.py +386 -0
- mlx_stack/core/models.py +639 -0
- mlx_stack/core/paths.py +79 -0
- mlx_stack/core/process.py +887 -0
- mlx_stack/core/pull.py +815 -0
- mlx_stack/core/scoring.py +611 -0
- mlx_stack/core/stack_down.py +317 -0
- mlx_stack/core/stack_init.py +524 -0
- mlx_stack/core/stack_status.py +229 -0
- mlx_stack/core/stack_up.py +856 -0
- mlx_stack/core/watchdog.py +744 -0
- mlx_stack/data/__init__.py +1 -0
- mlx_stack/data/catalog/__init__.py +1 -0
- mlx_stack/data/catalog/deepseek-r1-32b.yaml +46 -0
- mlx_stack/data/catalog/deepseek-r1-8b.yaml +45 -0
- mlx_stack/data/catalog/gemma3-12b.yaml +45 -0
- mlx_stack/data/catalog/gemma3-27b.yaml +45 -0
- mlx_stack/data/catalog/gemma3-4b.yaml +45 -0
- mlx_stack/data/catalog/llama3.3-8b.yaml +44 -0
- mlx_stack/data/catalog/nemotron-49b.yaml +41 -0
- mlx_stack/data/catalog/nemotron-8b.yaml +44 -0
- mlx_stack/data/catalog/qwen3-8b.yaml +45 -0
- mlx_stack/data/catalog/qwen3.5-0.8b.yaml +45 -0
- mlx_stack/data/catalog/qwen3.5-14b.yaml +46 -0
- mlx_stack/data/catalog/qwen3.5-32b.yaml +45 -0
- mlx_stack/data/catalog/qwen3.5-3b.yaml +44 -0
- mlx_stack/data/catalog/qwen3.5-72b.yaml +42 -0
- mlx_stack/data/catalog/qwen3.5-8b.yaml +45 -0
- mlx_stack/py.typed +1 -0
- mlx_stack/utils/__init__.py +1 -0
- mlx_stack-0.1.0.dist-info/METADATA +397 -0
- mlx_stack-0.1.0.dist-info/RECORD +61 -0
- mlx_stack-0.1.0.dist-info/WHEEL +4 -0
- mlx_stack-0.1.0.dist-info/entry_points.txt +2 -0
- mlx_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
mlx_stack/core/config.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Configuration management module for mlx-stack.
|
|
2
|
+
|
|
3
|
+
Handles persistent configuration in ~/.mlx-stack/config.yaml with
|
|
4
|
+
type validation, default values, and masking of sensitive data.
|
|
5
|
+
|
|
6
|
+
Supports 8 configuration keys:
|
|
7
|
+
- openrouter-key: OpenRouter API key (masked in display)
|
|
8
|
+
- default-quant: Default quantization level (int4, int8, bf16)
|
|
9
|
+
- memory-budget-pct: Memory budget percentage (1-100)
|
|
10
|
+
- litellm-port: LiteLLM proxy port (1-65535)
|
|
11
|
+
- model-dir: Model storage directory path
|
|
12
|
+
- auto-health-check: Auto health check on startup (true/false)
|
|
13
|
+
- log-max-size-mb: Maximum log file size in MB before rotation (1+)
|
|
14
|
+
- log-max-files: Maximum number of rotated log archives to keep (1+)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
from mlx_stack.core.paths import ensure_data_home, get_config_path, get_models_dir
|
|
25
|
+
|
|
26
|
+
# --------------------------------------------------------------------------- #
|
|
27
|
+
# Exceptions
|
|
28
|
+
# --------------------------------------------------------------------------- #
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigError(Exception):
|
|
32
|
+
"""Raised when configuration operations fail."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfigValidationError(ConfigError):
|
|
36
|
+
"""Raised when a configuration value fails validation."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConfigCorruptError(ConfigError):
|
|
40
|
+
"""Raised when the config file is corrupt or unreadable."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --------------------------------------------------------------------------- #
|
|
44
|
+
# Config key definitions
|
|
45
|
+
# --------------------------------------------------------------------------- #
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class ConfigKeyDef:
|
|
50
|
+
"""Definition of a configuration key with its type and validation."""
|
|
51
|
+
|
|
52
|
+
name: str
|
|
53
|
+
description: str
|
|
54
|
+
default: Any
|
|
55
|
+
value_type: str # "string", "int", "bool", "path"
|
|
56
|
+
validator: str | None = None # name of validation function
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# The 6 supported configuration keys with their defaults
|
|
60
|
+
CONFIG_KEYS: dict[str, ConfigKeyDef] = {
|
|
61
|
+
"openrouter-key": ConfigKeyDef(
|
|
62
|
+
name="openrouter-key",
|
|
63
|
+
description="OpenRouter API key for cloud fallback",
|
|
64
|
+
default="",
|
|
65
|
+
value_type="string",
|
|
66
|
+
),
|
|
67
|
+
"default-quant": ConfigKeyDef(
|
|
68
|
+
name="default-quant",
|
|
69
|
+
description="Default quantization level",
|
|
70
|
+
default="int4",
|
|
71
|
+
value_type="string",
|
|
72
|
+
validator="quant",
|
|
73
|
+
),
|
|
74
|
+
"memory-budget-pct": ConfigKeyDef(
|
|
75
|
+
name="memory-budget-pct",
|
|
76
|
+
description="Memory budget percentage (1-100)",
|
|
77
|
+
default=40,
|
|
78
|
+
value_type="int",
|
|
79
|
+
validator="memory_pct",
|
|
80
|
+
),
|
|
81
|
+
"litellm-port": ConfigKeyDef(
|
|
82
|
+
name="litellm-port",
|
|
83
|
+
description="LiteLLM proxy port",
|
|
84
|
+
default=4000,
|
|
85
|
+
value_type="int",
|
|
86
|
+
validator="port",
|
|
87
|
+
),
|
|
88
|
+
"model-dir": ConfigKeyDef(
|
|
89
|
+
name="model-dir",
|
|
90
|
+
description="Model storage directory",
|
|
91
|
+
default="~/.mlx-stack/models",
|
|
92
|
+
value_type="path",
|
|
93
|
+
),
|
|
94
|
+
"auto-health-check": ConfigKeyDef(
|
|
95
|
+
name="auto-health-check",
|
|
96
|
+
description="Auto health check on startup",
|
|
97
|
+
default=True,
|
|
98
|
+
value_type="bool",
|
|
99
|
+
),
|
|
100
|
+
"log-max-size-mb": ConfigKeyDef(
|
|
101
|
+
name="log-max-size-mb",
|
|
102
|
+
description="Maximum log file size in MB before rotation",
|
|
103
|
+
default=50,
|
|
104
|
+
value_type="int",
|
|
105
|
+
validator="positive_int",
|
|
106
|
+
),
|
|
107
|
+
"log-max-files": ConfigKeyDef(
|
|
108
|
+
name="log-max-files",
|
|
109
|
+
description="Maximum number of rotated log archives to keep",
|
|
110
|
+
default=5,
|
|
111
|
+
value_type="int",
|
|
112
|
+
validator="positive_int",
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
VALID_KEYS: list[str] = sorted(CONFIG_KEYS.keys())
|
|
117
|
+
|
|
118
|
+
# Valid quantization values
|
|
119
|
+
_VALID_QUANTS: set[str] = {"int4", "int8", "bf16"}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------- #
|
|
123
|
+
# Validation
|
|
124
|
+
# --------------------------------------------------------------------------- #
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def validate_key(key: str) -> ConfigKeyDef:
|
|
128
|
+
"""Validate that a key is a known configuration key.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
key: The configuration key name.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The ConfigKeyDef for the key.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ConfigError: If the key is not recognized.
|
|
138
|
+
"""
|
|
139
|
+
if key not in CONFIG_KEYS:
|
|
140
|
+
valid_list = ", ".join(VALID_KEYS)
|
|
141
|
+
msg = f"Unknown config key '{key}'. Valid keys: {valid_list}"
|
|
142
|
+
raise ConfigError(msg)
|
|
143
|
+
return CONFIG_KEYS[key]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _validate_quant(value: str) -> str:
|
|
147
|
+
"""Validate a quantization value.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
ConfigValidationError: If the value is not a valid quantization.
|
|
151
|
+
"""
|
|
152
|
+
if value not in _VALID_QUANTS:
|
|
153
|
+
valid = ", ".join(sorted(_VALID_QUANTS))
|
|
154
|
+
msg = f"Invalid quantization '{value}'. Valid values: {valid}"
|
|
155
|
+
raise ConfigValidationError(msg)
|
|
156
|
+
return value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _validate_memory_pct(value: int) -> int:
|
|
160
|
+
"""Validate a memory budget percentage.
|
|
161
|
+
|
|
162
|
+
Raises:
|
|
163
|
+
ConfigValidationError: If the value is not in 1-100 range.
|
|
164
|
+
"""
|
|
165
|
+
if not (1 <= value <= 100):
|
|
166
|
+
msg = f"Invalid memory budget '{value}'. Must be between 1 and 100."
|
|
167
|
+
raise ConfigValidationError(msg)
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _validate_port(value: int) -> int:
|
|
172
|
+
"""Validate a TCP port number.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ConfigValidationError: If the value is not in 1-65535 range.
|
|
176
|
+
"""
|
|
177
|
+
if not (1 <= value <= 65535):
|
|
178
|
+
msg = f"Invalid port '{value}'. Must be between 1 and 65535."
|
|
179
|
+
raise ConfigValidationError(msg)
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _validate_positive_int(key_name: str, value: int) -> int:
|
|
184
|
+
"""Validate that a value is a positive integer (>= 1).
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ConfigValidationError: If the value is less than 1.
|
|
188
|
+
"""
|
|
189
|
+
if value < 1:
|
|
190
|
+
msg = f"Invalid value '{value}' for '{key_name}'. Must be at least 1."
|
|
191
|
+
raise ConfigValidationError(msg)
|
|
192
|
+
return value
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def parse_value(key_def: ConfigKeyDef, raw_value: str) -> Any:
|
|
196
|
+
"""Parse a raw string value into the appropriate type for a config key.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key_def: The configuration key definition.
|
|
200
|
+
raw_value: The raw string value from CLI input.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The parsed and validated value.
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ConfigValidationError: If the value fails type conversion or validation.
|
|
207
|
+
"""
|
|
208
|
+
value: Any
|
|
209
|
+
|
|
210
|
+
if key_def.value_type == "int":
|
|
211
|
+
try:
|
|
212
|
+
value = int(raw_value)
|
|
213
|
+
except ValueError:
|
|
214
|
+
msg = f"Invalid value '{raw_value}' for '{key_def.name}'. Expected an integer."
|
|
215
|
+
raise ConfigValidationError(msg) from None
|
|
216
|
+
|
|
217
|
+
elif key_def.value_type == "bool":
|
|
218
|
+
lower = raw_value.lower()
|
|
219
|
+
if lower in ("true", "1", "yes", "on"):
|
|
220
|
+
value = True
|
|
221
|
+
elif lower in ("false", "0", "no", "off"):
|
|
222
|
+
value = False
|
|
223
|
+
else:
|
|
224
|
+
msg = (
|
|
225
|
+
f"Invalid value '{raw_value}' for '{key_def.name}'. "
|
|
226
|
+
f"Expected true/false, yes/no, 1/0, or on/off."
|
|
227
|
+
)
|
|
228
|
+
raise ConfigValidationError(msg)
|
|
229
|
+
|
|
230
|
+
elif key_def.value_type == "path":
|
|
231
|
+
value = raw_value
|
|
232
|
+
|
|
233
|
+
else:
|
|
234
|
+
# string type
|
|
235
|
+
value = raw_value
|
|
236
|
+
|
|
237
|
+
# Run specific validators
|
|
238
|
+
if key_def.validator == "quant":
|
|
239
|
+
_validate_quant(str(value))
|
|
240
|
+
elif key_def.validator == "memory_pct":
|
|
241
|
+
_validate_memory_pct(int(value))
|
|
242
|
+
elif key_def.validator == "port":
|
|
243
|
+
_validate_port(int(value))
|
|
244
|
+
elif key_def.validator == "positive_int":
|
|
245
|
+
_validate_positive_int(key_def.name, int(value))
|
|
246
|
+
|
|
247
|
+
return value
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# --------------------------------------------------------------------------- #
|
|
251
|
+
# Masking
|
|
252
|
+
# --------------------------------------------------------------------------- #
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def mask_value(key: str, value: Any) -> str:
|
|
256
|
+
"""Mask sensitive values for display.
|
|
257
|
+
|
|
258
|
+
The openrouter-key is masked to show only the first 3 and last 3
|
|
259
|
+
characters. All other values are returned as-is.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
key: The configuration key name.
|
|
263
|
+
value: The value to potentially mask.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The masked or unmasked string representation.
|
|
267
|
+
"""
|
|
268
|
+
str_value = str(value)
|
|
269
|
+
if key == "openrouter-key" and str_value and str_value != "":
|
|
270
|
+
if len(str_value) <= 6:
|
|
271
|
+
return "****"
|
|
272
|
+
return f"{str_value[:3]}****{str_value[-3:]}"
|
|
273
|
+
return str_value
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# --------------------------------------------------------------------------- #
|
|
277
|
+
# File I/O
|
|
278
|
+
# --------------------------------------------------------------------------- #
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _load_raw_config() -> dict[str, Any]:
|
|
282
|
+
"""Load the raw config dictionary from disk.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
A dictionary of config key-value pairs. Empty dict if file
|
|
286
|
+
is missing or empty.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
ConfigCorruptError: If the config file contains invalid YAML.
|
|
290
|
+
"""
|
|
291
|
+
config_path = get_config_path()
|
|
292
|
+
|
|
293
|
+
if not config_path.exists():
|
|
294
|
+
return {}
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
content = config_path.read_text(encoding="utf-8")
|
|
298
|
+
except OSError as exc:
|
|
299
|
+
msg = f"Could not read config file: {exc}"
|
|
300
|
+
raise ConfigCorruptError(msg) from None
|
|
301
|
+
|
|
302
|
+
if not content.strip():
|
|
303
|
+
return {}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
data = yaml.safe_load(content)
|
|
307
|
+
except yaml.YAMLError as exc:
|
|
308
|
+
msg = (
|
|
309
|
+
f"Config file is corrupt ({config_path}): {exc}\n"
|
|
310
|
+
f"Run 'mlx-stack config reset --yes' to restore defaults."
|
|
311
|
+
)
|
|
312
|
+
raise ConfigCorruptError(msg) from None
|
|
313
|
+
|
|
314
|
+
if data is None:
|
|
315
|
+
return {}
|
|
316
|
+
|
|
317
|
+
if not isinstance(data, dict):
|
|
318
|
+
msg = (
|
|
319
|
+
f"Config file has invalid format ({config_path}): "
|
|
320
|
+
f"expected a mapping, got {type(data).__name__}.\n"
|
|
321
|
+
f"Run 'mlx-stack config reset --yes' to restore defaults."
|
|
322
|
+
)
|
|
323
|
+
raise ConfigCorruptError(msg) from None
|
|
324
|
+
|
|
325
|
+
return data
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _save_raw_config(data: dict[str, Any]) -> None:
|
|
329
|
+
"""Save the raw config dictionary to disk.
|
|
330
|
+
|
|
331
|
+
Creates the data directory if it doesn't exist.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
data: The config dictionary to write.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
ConfigError: If the file cannot be written.
|
|
338
|
+
"""
|
|
339
|
+
ensure_data_home()
|
|
340
|
+
config_path = get_config_path()
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
content = yaml.dump(data, default_flow_style=False, sort_keys=True)
|
|
344
|
+
config_path.write_text(content, encoding="utf-8")
|
|
345
|
+
except OSError as exc:
|
|
346
|
+
msg = f"Could not write config file: {exc}"
|
|
347
|
+
raise ConfigError(msg) from None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# --------------------------------------------------------------------------- #
|
|
351
|
+
# Public API
|
|
352
|
+
# --------------------------------------------------------------------------- #
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def get_default_value(key: str) -> Any:
|
|
356
|
+
"""Return the default value for a config key.
|
|
357
|
+
|
|
358
|
+
For model-dir, expands ~ to the actual models directory path.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
key: The configuration key name.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The default value.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
ConfigError: If the key is not recognized.
|
|
368
|
+
"""
|
|
369
|
+
key_def = validate_key(key)
|
|
370
|
+
if key == "model-dir":
|
|
371
|
+
return str(get_models_dir())
|
|
372
|
+
return key_def.default
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_value(key: str) -> Any:
|
|
376
|
+
"""Get the current value of a config key.
|
|
377
|
+
|
|
378
|
+
Returns the user-set value if present, otherwise the default.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
key: The configuration key name.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The current value (user-set or default).
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ConfigError: If the key is not recognized.
|
|
388
|
+
ConfigCorruptError: If the config file is corrupt.
|
|
389
|
+
"""
|
|
390
|
+
validate_key(key)
|
|
391
|
+
data = _load_raw_config()
|
|
392
|
+
|
|
393
|
+
if key in data:
|
|
394
|
+
return data[key]
|
|
395
|
+
|
|
396
|
+
return get_default_value(key)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def set_value(key: str, raw_value: str) -> Any:
|
|
400
|
+
"""Set a config key to a new value.
|
|
401
|
+
|
|
402
|
+
Validates the key and value, then persists to disk.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
key: The configuration key name.
|
|
406
|
+
raw_value: The raw string value from CLI input.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
The parsed and validated value that was stored.
|
|
410
|
+
|
|
411
|
+
Raises:
|
|
412
|
+
ConfigError: If the key is not recognized.
|
|
413
|
+
ConfigValidationError: If the value fails validation.
|
|
414
|
+
ConfigCorruptError: If the config file is corrupt.
|
|
415
|
+
"""
|
|
416
|
+
key_def = validate_key(key)
|
|
417
|
+
value = parse_value(key_def, raw_value)
|
|
418
|
+
|
|
419
|
+
data = _load_raw_config()
|
|
420
|
+
data[key] = value
|
|
421
|
+
_save_raw_config(data)
|
|
422
|
+
|
|
423
|
+
return value
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def get_all_config() -> list[dict[str, Any]]:
|
|
427
|
+
"""Get all configuration keys with their current values and metadata.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
A list of dicts, each with keys: name, value, default, is_default,
|
|
431
|
+
description, masked_value.
|
|
432
|
+
|
|
433
|
+
Raises:
|
|
434
|
+
ConfigCorruptError: If the config file is corrupt.
|
|
435
|
+
"""
|
|
436
|
+
data = _load_raw_config()
|
|
437
|
+
result: list[dict[str, Any]] = []
|
|
438
|
+
|
|
439
|
+
for key, key_def in CONFIG_KEYS.items():
|
|
440
|
+
default = get_default_value(key)
|
|
441
|
+
is_default = key not in data
|
|
442
|
+
value = data.get(key, default)
|
|
443
|
+
|
|
444
|
+
result.append({
|
|
445
|
+
"name": key,
|
|
446
|
+
"value": value,
|
|
447
|
+
"default": default,
|
|
448
|
+
"is_default": is_default,
|
|
449
|
+
"description": key_def.description,
|
|
450
|
+
"masked_value": mask_value(key, value),
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return result
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def reset_config() -> None:
|
|
457
|
+
"""Reset all user-set config values by removing the config file.
|
|
458
|
+
|
|
459
|
+
After reset, all keys return their default values.
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ConfigError: If the config file cannot be removed.
|
|
463
|
+
"""
|
|
464
|
+
config_path = get_config_path()
|
|
465
|
+
|
|
466
|
+
if config_path.exists():
|
|
467
|
+
try:
|
|
468
|
+
config_path.unlink()
|
|
469
|
+
except OSError as exc:
|
|
470
|
+
msg = f"Could not remove config file: {exc}"
|
|
471
|
+
raise ConfigError(msg) from None
|