2# +==== BEGIN CatFeeder =================+
5# ...............)..(.')
7# ...............\(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
14# CREATION DATE: 04-12-2025
15# LAST Modified: 22:14:17 14-01-2026
17# This is the backend server in charge of making the actual website work.
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: Centralized configuration management with intelligent path discovery
22# +==== END CatFeeder =================+
26from pathlib
import Path
27from typing
import Optional, Dict, Any
28from display_tty
import Disp, initialise_logger
29from .env_loader
import EnvLoader
31if sys.version_info >= (3, 11):
33 import tomllib
as tomli
34 except ImportError
as e:
36 "Python 3.11+ should have tomllib in stdlib. Something is wrong with your Python installation."
41 except ImportError
as e:
43 "No TOML library found. Install tomli for Python < 3.11: pip install tomli"
52 Singleton configuration loader with smart path resolution.
54 Loads config.toml files once and caches them for application lifecycle.
55 Supports dynamic path discovery and multiple configuration sources.
58 _instance: Optional[
'TOMLLoader'] =
None
59 _initialized: bool =
False
63 debug = _EI.get_environment_variable(
"DEBUG").lower() ==
"true"
66 disp: Disp = initialise_logger(__qualname__,
False)
69 """Ensure only one instance exists."""
74 def __init__(self, debug: Optional[bool] =
None) ->
None:
75 """Initialize the TOMLLoader singleton."""
77 if TOMLLoader._initialized:
79 "An instance already exists, returning early"
84 if isinstance(debug, bool):
90 self.
disp.log_info(
"Initialising...")
97 TOMLLoader._initialized =
True
99 self.
disp.log_info(
"Initialised")
102 """Find project root by looking for marker files/directories."""
106 current = Path(__file__).resolve().parent
107 cwd = Path.cwd().resolve()
108 search_paths = [cwd, current]
111 'docker-compose.yaml',
119 for start_path
in search_paths:
120 for level
in range(5):
121 check_path = start_path
122 for _
in range(level):
123 check_path = check_path.parent
125 for marker
in markers:
126 marker_path = check_path / marker
127 if marker_path.exists():
136 Recursively search directory for file up to max_depth.
139 directory: Directory to search in
140 filename: Name of file to find
141 current_depth: Current recursion depth
142 max_depth: Maximum recursion depth
145 Path to found file or None if not found
147 if current_depth > max_depth:
150 target = directory / filename
151 if target.exists()
and target.is_file():
154 if current_depth < max_depth:
156 for subdir
in directory.iterdir():
157 if subdir.is_dir()
and not subdir.name.startswith(
'.'):
159 subdir, filename, current_depth + 1, max_depth)
162 except PermissionError:
167 def _find_file(self, filename: str, max_depth: int = 3) -> Optional[Path]:
169 Find a file by searching dynamically in directory tree.
172 filename: Name of file to find
173 max_depth: Maximum directory levels to search (default: 3)
176 Path to found file or None if not found
183 Update the debug status of the loader.
186 debug: New debug status
190 _EI.update_debug(debug)
191 self.
disp.log_info(f
"Debug mode set to {self.debug}")
193 def load_toml(self, force_reload: bool =
False, custom_path: Optional[str] =
None) -> Dict[str, Any]:
195 Load config.toml file and return as dictionary.
198 force_reload: Force reload even if cached (default: False)
199 custom_path: Custom path to config file (default: None)
202 Dictionary containing parsed TOML configuration
205 FileNotFoundError: If config.toml not found
206 OSError: If file cannot be read
207 IOError: If file read operation fails
215 config_path = Path(custom_path)
217 if '--config' in sys.argv:
219 idx = sys.argv.index(
'--config')
220 if idx + 1 < len(sys.argv):
221 config_path = Path(sys.argv[idx + 1])
222 except (ValueError, IndexError):
225 if config_path
is None:
226 env_config = os.environ.get(
'CONFIG_FILE')
228 config_path = Path(env_config)
230 if config_path
is None:
233 if config_path
is None or not config_path.exists():
234 error_message = f
"config.toml not found in {os.getcwd()}"
235 self.
disp.log_critical(error_message)
236 raise FileNotFoundError(
237 "config.toml not found. Searched in project root and subdirectories. "
238 "Set CONFIG_FILE environment variable or pass custom_path to specify location."
242 with open(config_path,
'rb')
as f:
244 except (OSError, IOError)
as e:
245 self.
disp.log_warning(f
"{e}",
"load_config_toml")
250 def load_config_toml(self, force_reload: bool =
False, custom_path: Optional[str] =
None) -> Dict[str, Any]:
252 Alias function for the load_toml function.
253 Load config.toml file and return as dictionary.
256 force_reload: Force reload even if cached (default: False)
257 custom_path: Custom path to config file (default: None)
260 Dictionary containing parsed TOML configuration
263 FileNotFoundError: If config.toml not found
264 OSError: If file cannot be read
265 IOError: If file read operation fails
267 return self.
load_toml(force_reload, custom_path)
271 Get a value from config.toml using nested keys.
274 *keys: Nested keys to traverse (e.g., 'database', 'host')
275 default: Default value if key not found (default: None)
278 Value at specified key path or default if not found
284 if isinstance(result, dict)
and key
in result:
293 Alias function of get_toml_value
294 Get a value from config.toml using nested keys.
297 *keys: Nested keys to traverse (e.g., 'database', 'host')
298 default: Default value if key not found (default: None)
301 Value at specified key path or default if not found
306 """Clear cached configuration."""
311 """Get the project root directory."""
315 """Make sure tha the internal toml file is loaded and ready to be used
318 RuntimeError: If the internal toml configuration could not be loaded.
320 error_message: str =
"No toml configuration loaded"
324 except (FileNotFoundError, OSError, IOError)
as e:
325 self.
disp.log_error(error_message)
326 raise RuntimeError(error_message)
from e
328 self.
disp.log_error(error_message)
329 raise RuntimeError(error_message)
331 def get_toml_variable(self, section: str, key: str, default=
None, *, toml_conf: Optional[Dict[str, Optional[str]]] =
None) -> Any:
333 Get the value of a configuration variable from the TOML file.
336 section (str): The section of the TOML file to search in.
337 key (str): The key within the section to fetch.
338 default: The default value to return if the key is not found. Defaults to None.
339 toml_conf (dict, optional): The loaded TOML configuration as a dictionary. Defaults to None.
342 str: The value of the configuration variable, or the default value if the key is not found.
345 KeyError: If the section is not found in the TOML configuration.
346 RuntimeError: If the internally determined toml file has not been loaded and not alternate configuration is specified.
352 current_section = toml_conf
354 keys = section.split(
'.')
357 if k
in current_section:
358 current_section = current_section[k]
360 error_message: str = f
"Section '{section}' not found in TOML configuration."
361 self.
disp.log_error(error_message)
362 raise KeyError(error_message)
364 if key
in current_section:
365 msg = f
"current_section[{key}] = {current_section[key]} : "
366 msg += f
"{type(current_section[key])}"
367 self.
disp.log_debug(msg)
368 if current_section[key] ==
"none":
370 "The value none has been converted to None."
373 return current_section[key]
375 msg = f
"Key '{key}' not found in section '{section}' "
376 msg +=
"of TOML configuration."
377 self.
disp.log_error(msg)
381 except KeyError
as e:
382 self.
disp.log_warning(f
"{e}")
388 Load config.toml (cached).
391 Dictionary containing parsed TOML configuration
394 FileNotFoundError: If config.toml not found
397 return loader.load_config_toml()
402 Get config.toml value using nested keys.
405 *keys: Nested keys to traverse (e.g., 'database', 'host')
406 default: Default value if key not found (default: None)
409 Value at specified key path or default if not found
412 return loader.get_config_value(*keys, default=default)
416 """Get the project root directory."""
418 return loader.get_project_root()
422 """Refresh the debug status from environment variable."""
423 env_vars: list[str] = [
"DEBUG",
"DEBUG_MODE"]
424 toml_locations: list[str] = [
425 "Server_configuration.debug_mode",
426 "Server_configuration.debug",
427 "Server_configuration",
432 toml_vars: list[str] = [
437 debug: bool = loader.debug
438 if debug_mode_default
is True:
439 loader.update_debug(
True)
440 _EI.update_debug(
True)
441 loader.disp.log_debug(
"Debug mode enabled by default.")
444 loader.disp.log_debug(
"Debug mode already enabled, no refresh needed.")
446 loader.disp.log_info(
"Refreshing debug mode from configuration.")
447 for env_var
in env_vars:
449 debug = _EI.get_environment_variable(env_var).lower() ==
"true"
451 f
"Debug mode from ENV: {debug}, var name: '{env_var}'"
456 _EI.update_debug(debug)
457 loader.update_debug(debug)
458 loader.disp.log_debug(
"Debug mode enabled from ENV.")
460 for location
in toml_locations:
461 for toml_var_node
in toml_vars:
462 tmp_debug = loader.get_toml_variable(
463 location, toml_var_node,
None
465 if isinstance(tmp_debug, bool)
and tmp_debug
is True:
467 loader.update_debug(debug)
468 _EI.update_debug(debug)
470 f
"Debug mode found in TOML at {location}.{toml_var_node}"
473 tmp_debug = loader.get_toml_variable(
474 location, toml_var_node,
None
476 if isinstance(tmp_debug, bool)
and tmp_debug
is True:
478 loader.update_debug(debug)
479 _EI.update_debug(debug)
481 f
"Debug mode found in TOML at {location}.{toml_var_node}"
Any get_toml_value(self, *str keys, Any default=None)
None update_debug(self, bool debug)
Optional[Path] _search_directory_for_file(self, Path directory, str filename, int current_depth, int max_depth)
Optional[Dict[str, Any]] _config_toml
Any get_toml_variable(self, str section, str key, default=None, *, Optional[Dict[str, Optional[str]]] toml_conf=None)
Optional[Path] _find_file(self, str filename, int max_depth=3)
Dict[str, Any] load_toml(self, bool force_reload=False, Optional[str] custom_path=None)
None __init__(self, Optional[bool] debug=None)
Dict[str, Any] load_config_toml(self, bool force_reload=False, Optional[str] custom_path=None)
Any get_config_value(self, *str keys, Any default=None)
Path _find_project_root(self)
Optional[Path] _project_root
None _ensure_loaded(self)
bool refresh_debug(bool debug_mode_default=False)
Any get_config(*str keys, Any default=None)
Dict[str, Any] load_config()