redfetch 1.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.
- redfetch/__about__.py +24 -0
- redfetch/__init__.py +0 -0
- redfetch/api.py +134 -0
- redfetch/auth.py +573 -0
- redfetch/config.py +483 -0
- redfetch/config_firstrun.py +455 -0
- redfetch/desktop_shortcut.py +81 -0
- redfetch/detecteq.py +116 -0
- redfetch/download.py +312 -0
- redfetch/listener.py +216 -0
- redfetch/main.py +744 -0
- redfetch/meta.py +561 -0
- redfetch/navmesh.py +371 -0
- redfetch/net.py +109 -0
- redfetch/processes.py +118 -0
- redfetch/push.py +246 -0
- redfetch/redfetch.ico +0 -0
- redfetch/runtime_errors.py +96 -0
- redfetch/settings.toml +303 -0
- redfetch/special.py +81 -0
- redfetch/store.py +505 -0
- redfetch/sync.py +261 -0
- redfetch/sync_discovery.py +352 -0
- redfetch/sync_executor.py +164 -0
- redfetch/sync_planner.py +196 -0
- redfetch/sync_remote.py +182 -0
- redfetch/sync_types.py +348 -0
- redfetch/terminal_ui.py +2318 -0
- redfetch/terminal_ui.tcss +569 -0
- redfetch/unloadmq.py +148 -0
- redfetch/update_status.py +62 -0
- redfetch/utils.py +256 -0
- redfetch-1.3.0.dist-info/METADATA +257 -0
- redfetch-1.3.0.dist-info/RECORD +37 -0
- redfetch-1.3.0.dist-info/WHEEL +4 -0
- redfetch-1.3.0.dist-info/entry_points.txt +2 -0
- redfetch-1.3.0.dist-info/licenses/LICENSE +674 -0
redfetch/config.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# standard
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
|
|
8
|
+
# third-party
|
|
9
|
+
import tomlkit
|
|
10
|
+
from dynaconf import Dynaconf, Validator, ValidationError
|
|
11
|
+
from platformdirs import user_config_dir, user_data_dir
|
|
12
|
+
|
|
13
|
+
# Parent Category to folder
|
|
14
|
+
CATEGORY_MAP = {
|
|
15
|
+
8: "macros",
|
|
16
|
+
11: "plugins",
|
|
17
|
+
25: "lua"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Resource to MQ version
|
|
21
|
+
VANILLA_MAP = {
|
|
22
|
+
1974: "LIVE",
|
|
23
|
+
2218: "TEST",
|
|
24
|
+
60: "EMU"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
MYSEQ_MAP = {
|
|
28
|
+
151: "LIVE",
|
|
29
|
+
164: "TEST"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# to make settings.local.toml easier to read, names are added in comments
|
|
33
|
+
RESOURCE_NAMES = {
|
|
34
|
+
"1974": "Very Vanilla MQ Live",
|
|
35
|
+
"2218": "Very Vanilla MQ Test",
|
|
36
|
+
"60": "Very Vanilla MQ Emu",
|
|
37
|
+
"4": "KissAssist",
|
|
38
|
+
"2539": "Lua Event Manager",
|
|
39
|
+
"151": "MySEQ Live",
|
|
40
|
+
"164": "MySEQ Test",
|
|
41
|
+
"153": "Brewall's EverQuest Maps",
|
|
42
|
+
"303": "Good's EverQuest Maps",
|
|
43
|
+
"2318": "guildclicky",
|
|
44
|
+
"2174": "buttonmaster",
|
|
45
|
+
"2062": "alertmaster",
|
|
46
|
+
"3040": "rgmercs",
|
|
47
|
+
"2196": "lootly",
|
|
48
|
+
"2088": "boxhud",
|
|
49
|
+
"2391": "scriber",
|
|
50
|
+
"3001": "bazaar / auction helper",
|
|
51
|
+
"2937": "skill skillup: spells and others",
|
|
52
|
+
"2675": "lootnscoot",
|
|
53
|
+
"973": "Ninjadvloot.inc",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
BREADCRUMB_FILENAME = "last_command.json"
|
|
57
|
+
DEFAULT_CONFIG_DIR = user_config_dir("redfetch", "RedGuides")
|
|
58
|
+
|
|
59
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
60
|
+
os.environ['REDFETCH_SCRIPT_DIR'] = script_dir
|
|
61
|
+
|
|
62
|
+
# Populated by initialize_config()
|
|
63
|
+
config_dir = None
|
|
64
|
+
env_file_path = None
|
|
65
|
+
settings = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalize_and_create_path(path):
|
|
69
|
+
if not path:
|
|
70
|
+
raise ValidationError("Path is not set.")
|
|
71
|
+
normalized_path = os.path.normpath(path)
|
|
72
|
+
if not os.path.exists(normalized_path):
|
|
73
|
+
try:
|
|
74
|
+
os.makedirs(normalized_path, exist_ok=True)
|
|
75
|
+
print(f"Created directory: {normalized_path}")
|
|
76
|
+
except OSError as e:
|
|
77
|
+
raise ValidationError(f"Failed to create the directory '{normalized_path}': {e}")
|
|
78
|
+
return normalized_path
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def normalize_category_paths(data):
|
|
82
|
+
"""Normalize and validate absolute paths in CATEGORY_PATHS."""
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
return data
|
|
85
|
+
valid_names = set(CATEGORY_MAP.values())
|
|
86
|
+
for key, value in list(data.items()):
|
|
87
|
+
if key not in valid_names:
|
|
88
|
+
raise ValidationError(
|
|
89
|
+
f"Unknown category '{key}' in CATEGORY_PATHS. "
|
|
90
|
+
f"Valid categories: {', '.join(sorted(valid_names))}"
|
|
91
|
+
)
|
|
92
|
+
if isinstance(value, str) and value:
|
|
93
|
+
normalized = os.path.normpath(value)
|
|
94
|
+
data[key] = normalized
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_paths_in_dict(data, parent_key=None):
|
|
99
|
+
"""Dynaconf validator for SPECIAL_RESOURCE paths."""
|
|
100
|
+
if isinstance(data, dict):
|
|
101
|
+
for key, value in data.items():
|
|
102
|
+
if isinstance(value, dict):
|
|
103
|
+
normalize_paths_in_dict(value, parent_key=key)
|
|
104
|
+
elif isinstance(value, list):
|
|
105
|
+
for index, item in enumerate(value):
|
|
106
|
+
normalize_paths_in_dict(item, parent_key=key)
|
|
107
|
+
elif key in ['default_path', 'custom_path'] and isinstance(value, str):
|
|
108
|
+
normalized_value = os.path.normpath(value) if value else value
|
|
109
|
+
data[key] = normalized_value
|
|
110
|
+
elif isinstance(data, list):
|
|
111
|
+
for index, item in enumerate(data):
|
|
112
|
+
normalize_paths_in_dict(item, parent_key=parent_key)
|
|
113
|
+
return data
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def initialize_config():
|
|
117
|
+
"""Initialize configuration settings."""
|
|
118
|
+
from redfetch.config_firstrun import first_run_setup
|
|
119
|
+
|
|
120
|
+
global config_dir, env_file_path, settings # Declare globals to modify them
|
|
121
|
+
|
|
122
|
+
# Perform first-run setup
|
|
123
|
+
config_dir = first_run_setup()
|
|
124
|
+
os.environ['REDFETCH_CONFIG_DIR'] = config_dir
|
|
125
|
+
|
|
126
|
+
# Data dir: Linux default uses XDG data dir (~/.local/share), else same as config
|
|
127
|
+
is_linux_default = platform.system() == "Linux" and config_dir == DEFAULT_CONFIG_DIR
|
|
128
|
+
data_dir = user_data_dir("redfetch", "RedGuides") if is_linux_default else config_dir
|
|
129
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
130
|
+
os.environ['REDFETCH_DATA_DIR'] = data_dir
|
|
131
|
+
|
|
132
|
+
# Path to the .env file
|
|
133
|
+
env_file_path = os.path.join(config_dir, '.env')
|
|
134
|
+
|
|
135
|
+
# Check if the .env file exists
|
|
136
|
+
if not os.path.exists(env_file_path):
|
|
137
|
+
# If not, create it and set the default environment to 'LIVE'
|
|
138
|
+
atomic_write_text(env_file_path, 'REDFETCH_ENV=LIVE\n')
|
|
139
|
+
print(f".env file created with default environment set to 'LIVE' at {env_file_path}")
|
|
140
|
+
|
|
141
|
+
# Initialize Dynaconf settings
|
|
142
|
+
settings = Dynaconf(
|
|
143
|
+
envvar_prefix="REDFETCH",
|
|
144
|
+
settings_files=[
|
|
145
|
+
os.path.join(script_dir, 'settings.toml'),
|
|
146
|
+
os.path.join(config_dir, 'settings.local.toml')
|
|
147
|
+
],
|
|
148
|
+
load_dotenv=True,
|
|
149
|
+
dotenv_path=env_file_path,
|
|
150
|
+
dotenv_override=True,
|
|
151
|
+
env_switcher="REDFETCH_ENV",
|
|
152
|
+
merge_enabled=True,
|
|
153
|
+
lazy_load=True,
|
|
154
|
+
environments=True,
|
|
155
|
+
validate_on_update=True,
|
|
156
|
+
validators=[
|
|
157
|
+
Validator("DOWNLOAD_FOLDER", cast=normalize_and_create_path),
|
|
158
|
+
# Separate validator for EQPATH to avoid triggering eqgame.exe check
|
|
159
|
+
Validator("EQPATH", default=None, cast=lambda x: os.path.normpath(x) if x else None),
|
|
160
|
+
Validator("SPECIAL_RESOURCES", cast=normalize_paths_in_dict),
|
|
161
|
+
Validator("CATEGORY_PATHS", default={}, cast=normalize_category_paths)
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
write_breadcrumb()
|
|
166
|
+
|
|
167
|
+
# Return the settings object for potential use
|
|
168
|
+
return settings
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _resolve_redfetch_executable():
|
|
172
|
+
"""PYAPP will give a path when built with PYAPP_PASS_LOCATION=1"""
|
|
173
|
+
pyapp = os.environ.get("PYAPP")
|
|
174
|
+
if pyapp and "redfetch" in os.path.basename(pyapp).lower() and os.path.exists(pyapp):
|
|
175
|
+
return os.path.abspath(pyapp)
|
|
176
|
+
|
|
177
|
+
cmd = shutil.which("redfetch")
|
|
178
|
+
if cmd:
|
|
179
|
+
return os.path.abspath(cmd)
|
|
180
|
+
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def atomic_write_text(path: str, text: str) -> None:
|
|
185
|
+
"""Write UTF-8 text to `path` via a temp file + os.replace() so readers never see a partial write."""
|
|
186
|
+
directory = os.path.dirname(path)
|
|
187
|
+
if directory:
|
|
188
|
+
os.makedirs(directory, exist_ok=True)
|
|
189
|
+
tmp_path = f"{path}.tmp"
|
|
190
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
191
|
+
f.write(text)
|
|
192
|
+
os.replace(tmp_path, path)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def atomic_write_json(path: str, data) -> None:
|
|
196
|
+
"""Atomically write `data` as UTF-8 JSON (ensure_ascii=False keeps non-ASCII paths/titles verbatim)."""
|
|
197
|
+
atomic_write_text(path, json.dumps(data, ensure_ascii=False, indent=2))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def write_breadcrumb() -> None:
|
|
201
|
+
"""A breadcrumb in the user config dir to track the most recently used redfetch binary's location."""
|
|
202
|
+
try:
|
|
203
|
+
program = _resolve_redfetch_executable()
|
|
204
|
+
if program is None:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
breadcrumb_path = os.path.join(DEFAULT_CONFIG_DIR, BREADCRUMB_FILENAME)
|
|
208
|
+
atomic_write_json(breadcrumb_path, {"program": program})
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def remove_breadcrumb() -> None:
|
|
214
|
+
breadcrumb_path = os.path.join(DEFAULT_CONFIG_DIR, BREADCRUMB_FILENAME)
|
|
215
|
+
try:
|
|
216
|
+
os.remove(breadcrumb_path)
|
|
217
|
+
except FileNotFoundError:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def switch_environment(new_env):
|
|
222
|
+
"""Switch the environment and update the settings."""
|
|
223
|
+
if settings is None:
|
|
224
|
+
raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
|
|
225
|
+
|
|
226
|
+
# Update the .env file first
|
|
227
|
+
write_env_to_file(new_env)
|
|
228
|
+
|
|
229
|
+
# Set the Dynaconf environment so subsequent `from_env` calls use the new env
|
|
230
|
+
settings.setenv(new_env)
|
|
231
|
+
|
|
232
|
+
# Keep a simple attribute around for convenience (used throughout the app)
|
|
233
|
+
settings.ENV = new_env
|
|
234
|
+
|
|
235
|
+
# Re-validate settings after environment switch
|
|
236
|
+
try:
|
|
237
|
+
settings.validators.validate()
|
|
238
|
+
print(f"Server type: {new_env}")
|
|
239
|
+
except ValidationError as e:
|
|
240
|
+
print(f"Validation error after switching to {new_env}: {e}")
|
|
241
|
+
|
|
242
|
+
return settings
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def select_environment_in_memory(new_env):
|
|
246
|
+
"""Select `new_env` for this process only, without persisting to the .env file."""
|
|
247
|
+
if settings is None:
|
|
248
|
+
raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
|
|
249
|
+
|
|
250
|
+
settings.setenv(new_env)
|
|
251
|
+
settings.ENV = new_env
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
settings.validators.validate()
|
|
255
|
+
except ValidationError as e:
|
|
256
|
+
print(f"Validation error after selecting {new_env}: {e}")
|
|
257
|
+
|
|
258
|
+
return settings
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def ensure_config_file_exists(file_path):
|
|
262
|
+
"""Ensure the configuration file exists."""
|
|
263
|
+
if not os.path.exists(file_path):
|
|
264
|
+
atomic_write_text(file_path, tomlkit.dumps({}))
|
|
265
|
+
print(f"Created new configuration file: {file_path}")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def load_config(file_path):
|
|
269
|
+
"""Load the TOML configuration file, creating an empty document if it doesn't exist."""
|
|
270
|
+
if not os.path.exists(file_path):
|
|
271
|
+
return tomlkit.document()
|
|
272
|
+
try:
|
|
273
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
274
|
+
return tomlkit.parse(f.read())
|
|
275
|
+
except Exception as e:
|
|
276
|
+
raise ValidationError(f"Error loading config file {file_path}: {e}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _annotate_special_resource_comments(toml_text: str) -> str:
|
|
280
|
+
"""Add a `# friendly-name` comment above each known SPECIAL_RESOURCES section."""
|
|
281
|
+
section_pattern = re.compile(r"^\[(?:DEFAULT|LIVE|TEST|EMU)\.SPECIAL_RESOURCES\.(\d+)\]\s*$")
|
|
282
|
+
|
|
283
|
+
new_lines = []
|
|
284
|
+
for line in toml_text.splitlines():
|
|
285
|
+
match = section_pattern.match(line)
|
|
286
|
+
if match:
|
|
287
|
+
friendly_name = RESOURCE_NAMES.get(match.group(1))
|
|
288
|
+
if friendly_name and not (new_lines and new_lines[-1] == f"# {friendly_name}"):
|
|
289
|
+
new_lines.append(f"# {friendly_name}")
|
|
290
|
+
new_lines.append(line)
|
|
291
|
+
|
|
292
|
+
ending = "\n" if toml_text.endswith("\n") else ""
|
|
293
|
+
return "\n".join(new_lines) + ending
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# Header for settings.local.toml, which redfetch rewrites on every save.
|
|
297
|
+
SETTINGS_LOCAL_HEADER = (
|
|
298
|
+
"# Managed by redfetch: stores only your changes from settings.toml defaults.\n"
|
|
299
|
+
"# Editable by hand, but redfetch rewrites on save, so comments may be dropped\n"
|
|
300
|
+
"# and values matching a default are removed. See settings.toml for all options.\n"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Path-valued keys, compared with path-aware equality (slash vs backslash).
|
|
304
|
+
_PATH_LIKE_KEYS = {"EQPATH", "DOWNLOAD_FOLDER", "custom_path", "default_path"}
|
|
305
|
+
|
|
306
|
+
_MISSING = object()
|
|
307
|
+
_base_settings_cache = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _base_settings():
|
|
311
|
+
"""Cached Dynaconf view of the bundled settings.toml defaults (no overrides)."""
|
|
312
|
+
global _base_settings_cache
|
|
313
|
+
if _base_settings_cache is None:
|
|
314
|
+
_base_settings_cache = Dynaconf(
|
|
315
|
+
settings_files=[os.path.join(script_dir, "settings.toml")],
|
|
316
|
+
environments=True,
|
|
317
|
+
merge_enabled=True,
|
|
318
|
+
env_switcher="REDFETCH_ENV",
|
|
319
|
+
)
|
|
320
|
+
return _base_settings_cache
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _to_plain(data):
|
|
324
|
+
"""Convert a tomlkit document/table (or any nested mapping) to plain dicts/lists."""
|
|
325
|
+
if hasattr(data, "unwrap"):
|
|
326
|
+
return data.unwrap()
|
|
327
|
+
if isinstance(data, dict):
|
|
328
|
+
return {k: _to_plain(v) for k, v in data.items()}
|
|
329
|
+
if isinstance(data, list):
|
|
330
|
+
return [_to_plain(v) for v in data]
|
|
331
|
+
return data
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _base_lookup(base, key):
|
|
335
|
+
"""Fetch key from base defaults, tolerating Dynaconf case folding."""
|
|
336
|
+
if not isinstance(base, dict):
|
|
337
|
+
return _MISSING
|
|
338
|
+
if key in base:
|
|
339
|
+
return base[key]
|
|
340
|
+
lowered = key.lower()
|
|
341
|
+
for bkey, bval in base.items():
|
|
342
|
+
if isinstance(bkey, str) and bkey.lower() == lowered:
|
|
343
|
+
return bval
|
|
344
|
+
return _MISSING
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _equals_default(value, default, key):
|
|
348
|
+
"""True if value matches the default (and can be dropped)."""
|
|
349
|
+
if value == default:
|
|
350
|
+
return True
|
|
351
|
+
if (
|
|
352
|
+
key in _PATH_LIKE_KEYS
|
|
353
|
+
and isinstance(value, str) and value
|
|
354
|
+
and isinstance(default, str) and default
|
|
355
|
+
):
|
|
356
|
+
return os.path.normpath(value) == os.path.normpath(default)
|
|
357
|
+
return False
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _prune_branch(local, base):
|
|
361
|
+
"""Recursively drop leaves equal to their default and tables left empty."""
|
|
362
|
+
for key in list(local.keys()):
|
|
363
|
+
value = local[key]
|
|
364
|
+
base_value = _base_lookup(base, key)
|
|
365
|
+
if isinstance(value, dict):
|
|
366
|
+
_prune_branch(value, base_value if isinstance(base_value, dict) else {})
|
|
367
|
+
if not value:
|
|
368
|
+
del local[key]
|
|
369
|
+
elif base_value is not _MISSING and _equals_default(value, base_value, key):
|
|
370
|
+
del local[key]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _prune_to_deltas(data):
|
|
374
|
+
"""Drop entries equal to the defaults, leaving only deltas.
|
|
375
|
+
|
|
376
|
+
Each top-level table is an environment (LIVE/TEST/EMU/DEFAULT), compared
|
|
377
|
+
against the same environment's defaults from the bundled settings.toml.
|
|
378
|
+
"""
|
|
379
|
+
base = _base_settings()
|
|
380
|
+
for env in list(data.keys()):
|
|
381
|
+
if not isinstance(data[env], dict):
|
|
382
|
+
continue
|
|
383
|
+
try:
|
|
384
|
+
base_env = base.from_env(env).as_dict()
|
|
385
|
+
except Exception:
|
|
386
|
+
continue # defaults unresolvable; keep env verbatim
|
|
387
|
+
_prune_branch(data[env], base_env)
|
|
388
|
+
if not data[env]:
|
|
389
|
+
del data[env]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def save_config(file_path, config_data):
|
|
393
|
+
"""Regenerate settings.local.toml, keeping only deltas from the defaults.
|
|
394
|
+
|
|
395
|
+
Accepts a tomlkit document or a plain dict.
|
|
396
|
+
"""
|
|
397
|
+
data = _to_plain(config_data)
|
|
398
|
+
_prune_to_deltas(data)
|
|
399
|
+
|
|
400
|
+
body = _annotate_special_resource_comments(tomlkit.dumps(data)).strip("\n")
|
|
401
|
+
|
|
402
|
+
if body:
|
|
403
|
+
toml_text = f"{SETTINGS_LOCAL_HEADER}\n{body}\n"
|
|
404
|
+
else:
|
|
405
|
+
toml_text = SETTINGS_LOCAL_HEADER
|
|
406
|
+
atomic_write_text(file_path, toml_text)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def update_setting(setting_path, setting_value, env=None):
|
|
410
|
+
"""Update a specific setting in the settings.local.toml file and in memory,
|
|
411
|
+
optionally within a specific environment."""
|
|
412
|
+
if settings is None or config_dir is None:
|
|
413
|
+
raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
|
|
414
|
+
|
|
415
|
+
config_file = os.path.join(config_dir, 'settings.local.toml')
|
|
416
|
+
ensure_config_file_exists(config_file)
|
|
417
|
+
config_data = load_config(config_file)
|
|
418
|
+
|
|
419
|
+
# Use the specified environment or, if None, the current environment
|
|
420
|
+
env = env or settings.current_env
|
|
421
|
+
|
|
422
|
+
# Ensure the environment exists in the configuration
|
|
423
|
+
if env not in config_data:
|
|
424
|
+
config_data[env] = tomlkit.table()
|
|
425
|
+
|
|
426
|
+
# Navigate to the correct setting based on the path within the specified environment
|
|
427
|
+
current_data = config_data[env]
|
|
428
|
+
for key in setting_path[:-1]:
|
|
429
|
+
if key not in current_data:
|
|
430
|
+
current_data[key] = tomlkit.table()
|
|
431
|
+
current_data = current_data[key]
|
|
432
|
+
|
|
433
|
+
# Debugging output
|
|
434
|
+
config_key = '.'.join(setting_path)
|
|
435
|
+
print(f"Updating config key: {config_key}")
|
|
436
|
+
print(f"Old Value: {current_data.get(setting_path[-1], 'Not set')}")
|
|
437
|
+
|
|
438
|
+
# Convert 'true'/'false' strings to Boolean values
|
|
439
|
+
if isinstance(setting_value, str) and setting_value.lower() in ('true', 'false'):
|
|
440
|
+
setting_value = setting_value.lower() == 'true'
|
|
441
|
+
|
|
442
|
+
# None means "unset", TOML can't store None, so remove the key
|
|
443
|
+
if setting_value is None:
|
|
444
|
+
current_data.pop(setting_path[-1], None)
|
|
445
|
+
else:
|
|
446
|
+
current_data[setting_path[-1]] = setting_value
|
|
447
|
+
|
|
448
|
+
# Update the environment using from_env to target the correct environment
|
|
449
|
+
settings.from_env(env).set(config_key, setting_value)
|
|
450
|
+
# Update general settings object to keep it in sync
|
|
451
|
+
settings.set(config_key, setting_value)
|
|
452
|
+
|
|
453
|
+
print(f"New Value: {setting_value}")
|
|
454
|
+
|
|
455
|
+
save_config(config_file, config_data)
|
|
456
|
+
settings.reload()
|
|
457
|
+
|
|
458
|
+
print("Configuration saved.")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def write_env_to_file(new_env):
|
|
462
|
+
"""Update the environment setting in the .env file."""
|
|
463
|
+
if env_file_path is None:
|
|
464
|
+
raise RuntimeError("Configuration has not been initialized. Call initialize_config() first.")
|
|
465
|
+
|
|
466
|
+
# Read the existing content of the .env file
|
|
467
|
+
with open(env_file_path, 'r') as file:
|
|
468
|
+
lines = file.readlines()
|
|
469
|
+
|
|
470
|
+
# Update the environment line
|
|
471
|
+
updated = False
|
|
472
|
+
for i, line in enumerate(lines):
|
|
473
|
+
if line.startswith('REDFETCH_ENV='):
|
|
474
|
+
lines[i] = f'REDFETCH_ENV={new_env}\n'
|
|
475
|
+
updated = True
|
|
476
|
+
break
|
|
477
|
+
|
|
478
|
+
# If the environment line was not found, add it
|
|
479
|
+
if not updated:
|
|
480
|
+
lines.append(f'REDFETCH_ENV={new_env}\n')
|
|
481
|
+
|
|
482
|
+
# Write the updated content back to the .env file
|
|
483
|
+
atomic_write_text(env_file_path, ''.join(lines))
|