| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | """This module provides functionality to manage and handle configuration for the EOS.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The module including loading, merging, and validating JSON configuration files. | 
					
						
							|  |  |  | It also provides utility functions for working directory setup and date handling. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Key features: | 
					
						
							|  |  |  | - Loading and merging configurations from default or custom JSON files | 
					
						
							|  |  |  | - Validating configurations using Pydantic models | 
					
						
							|  |  |  | - Managing directory setups for the application | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import shutil | 
					
						
							|  |  |  | from pathlib import Path | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  | from typing import Any, ClassVar, Optional, Type | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  | from platformdirs import user_config_dir, user_data_dir | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  | from pydantic import Field, computed_field | 
					
						
							|  |  |  | from pydantic_settings import ( | 
					
						
							|  |  |  |     BaseSettings, | 
					
						
							|  |  |  |     JsonConfigSettingsSource, | 
					
						
							|  |  |  |     PydanticBaseSettingsSource, | 
					
						
							|  |  |  |     SettingsConfigDict, | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | from pydantic_settings.sources import ConfigFileSourceMixin | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | # settings | 
					
						
							|  |  |  | from akkudoktoreos.config.configabc import SettingsBaseModel | 
					
						
							|  |  |  | from akkudoktoreos.core.coreabc import SingletonMixin | 
					
						
							| 
									
										
										
										
											2025-01-05 14:41:07 +01:00
										 |  |  | from akkudoktoreos.core.logging import get_logger | 
					
						
							|  |  |  | from akkudoktoreos.core.logsettings import LoggingCommonSettings | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  | from akkudoktoreos.core.pydantic import merge_models | 
					
						
							|  |  |  | from akkudoktoreos.devices.settings import DevicesCommonSettings | 
					
						
							| 
									
										
										
										
											2024-12-29 18:42:49 +01:00
										 |  |  | from akkudoktoreos.measurement.measurement import MeasurementCommonSettings | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | from akkudoktoreos.optimization.optimization import OptimizationCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.prediction.elecprice import ElecPriceCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.prediction.load import LoadCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.prediction.prediction import PredictionCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.prediction.pvforecast import PVForecastCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.prediction.weather import WeatherCommonSettings | 
					
						
							|  |  |  | from akkudoktoreos.server.server import ServerCommonSettings | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  | from akkudoktoreos.utils.utils import UtilsCommonSettings, classproperty | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | logger = get_logger(__name__) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  | def get_absolute_path( | 
					
						
							|  |  |  |     basepath: Optional[Path | str], subpath: Optional[Path | str] | 
					
						
							|  |  |  | ) -> Optional[Path]: | 
					
						
							|  |  |  |     """Get path based on base path.""" | 
					
						
							|  |  |  |     if isinstance(basepath, str): | 
					
						
							|  |  |  |         basepath = Path(basepath) | 
					
						
							|  |  |  |     if subpath is None: | 
					
						
							|  |  |  |         return basepath | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if isinstance(subpath, str): | 
					
						
							|  |  |  |         subpath = Path(subpath) | 
					
						
							|  |  |  |     if subpath.is_absolute(): | 
					
						
							|  |  |  |         return subpath | 
					
						
							|  |  |  |     if basepath is not None: | 
					
						
							|  |  |  |         return basepath.joinpath(subpath) | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | class ConfigCommonSettings(SettingsBaseModel): | 
					
						
							| 
									
										
										
										
											2025-01-15 00:54:45 +01:00
										 |  |  |     """Settings for common configuration.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     General configuration to set directories of cache and output files. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     data_folder_path: Optional[Path] = Field( | 
					
						
							| 
									
										
										
										
											2025-01-15 00:54:45 +01:00
										 |  |  |         default=None, description="Path to EOS data directory.", examples=[None, "/home/eos/data"] | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     data_output_subpath: Optional[Path] = Field( | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         default="output", description="Sub-path for the EOS output data directory." | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     data_cache_subpath: Optional[Path] = Field( | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         default="cache", description="Sub-path for the EOS cache data directory." | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Computed fields | 
					
						
							|  |  |  |     @computed_field  # type: ignore[prop-decorator] | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def data_output_path(self) -> Optional[Path]: | 
					
						
							|  |  |  |         """Compute data_output_path based on data_folder_path.""" | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |         return get_absolute_path(self.data_folder_path, self.data_output_subpath) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Computed fields | 
					
						
							|  |  |  |     @computed_field  # type: ignore[prop-decorator] | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def data_cache_path(self) -> Optional[Path]: | 
					
						
							|  |  |  |         """Compute data_cache_path based on data_folder_path.""" | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |         return get_absolute_path(self.data_folder_path, self.data_cache_subpath) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  | class SettingsEOS(BaseSettings): | 
					
						
							|  |  |  |     """Settings for all EOS.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Used by updating the configuration with specific settings only. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     general: Optional[ConfigCommonSettings] = None | 
					
						
							|  |  |  |     logging: Optional[LoggingCommonSettings] = None | 
					
						
							|  |  |  |     devices: Optional[DevicesCommonSettings] = None | 
					
						
							|  |  |  |     measurement: Optional[MeasurementCommonSettings] = None | 
					
						
							|  |  |  |     optimization: Optional[OptimizationCommonSettings] = None | 
					
						
							|  |  |  |     prediction: Optional[PredictionCommonSettings] = None | 
					
						
							|  |  |  |     elecprice: Optional[ElecPriceCommonSettings] = None | 
					
						
							|  |  |  |     load: Optional[LoadCommonSettings] = None | 
					
						
							|  |  |  |     pvforecast: Optional[PVForecastCommonSettings] = None | 
					
						
							|  |  |  |     weather: Optional[WeatherCommonSettings] = None | 
					
						
							|  |  |  |     server: Optional[ServerCommonSettings] = None | 
					
						
							|  |  |  |     utils: Optional[UtilsCommonSettings] = None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     model_config = SettingsConfigDict( | 
					
						
							|  |  |  |         env_nested_delimiter="__", nested_model_default_partial_update=True, env_prefix="EOS_" | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class SettingsEOSDefaults(SettingsEOS): | 
					
						
							|  |  |  |     """Settings for all of EOS with defaults.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Used by ConfigEOS instance to make all fields available. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     general: ConfigCommonSettings = ConfigCommonSettings() | 
					
						
							|  |  |  |     logging: LoggingCommonSettings = LoggingCommonSettings() | 
					
						
							|  |  |  |     devices: DevicesCommonSettings = DevicesCommonSettings() | 
					
						
							|  |  |  |     measurement: MeasurementCommonSettings = MeasurementCommonSettings() | 
					
						
							|  |  |  |     optimization: OptimizationCommonSettings = OptimizationCommonSettings() | 
					
						
							|  |  |  |     prediction: PredictionCommonSettings = PredictionCommonSettings() | 
					
						
							|  |  |  |     elecprice: ElecPriceCommonSettings = ElecPriceCommonSettings() | 
					
						
							|  |  |  |     load: LoadCommonSettings = LoadCommonSettings() | 
					
						
							|  |  |  |     pvforecast: PVForecastCommonSettings = PVForecastCommonSettings() | 
					
						
							|  |  |  |     weather: WeatherCommonSettings = WeatherCommonSettings() | 
					
						
							|  |  |  |     server: ServerCommonSettings = ServerCommonSettings() | 
					
						
							|  |  |  |     utils: UtilsCommonSettings = UtilsCommonSettings() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ConfigEOS(SingletonMixin, SettingsEOSDefaults): | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     """Singleton configuration handler for the EOS application.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     ConfigEOS extends `SettingsEOS` with support for  default configuration paths and automatic | 
					
						
							|  |  |  |     initialization. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     `ConfigEOS` ensures that only one instance of the class is created throughout the application, | 
					
						
							|  |  |  |     allowing consistent access to EOS configuration settings. This singleton instance loads | 
					
						
							|  |  |  |     configuration data from a predefined set of directories or creates a default configuration if | 
					
						
							|  |  |  |     none is found. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Initialization Process: | 
					
						
							|  |  |  |       - Upon instantiation, the singleton instance attempts to load a configuration file in this order: | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |         1. The directory specified by the `EOS_CONFIG_DIR` environment variable | 
					
						
							|  |  |  |         2. The directory specified by the `EOS_DIR` environment variable. | 
					
						
							|  |  |  |         3. A platform specific default directory for EOS. | 
					
						
							|  |  |  |         4. The current working directory. | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |       - The first available configuration file found in these directories is loaded. | 
					
						
							|  |  |  |       - If no configuration file is found, a default configuration file is created in the platform | 
					
						
							|  |  |  |         specific default directory, and default settings are loaded into it. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Attributes from the loaded configuration are accessible directly as instance attributes of | 
					
						
							|  |  |  |     `ConfigEOS`, providing a centralized, shared configuration object for EOS. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Singleton Behavior: | 
					
						
							|  |  |  |       - This class uses the `SingletonMixin` to ensure that all requests for `ConfigEOS` return | 
					
						
							|  |  |  |         the same instance, which contains the most up-to-date configuration. Modifying the configuration | 
					
						
							|  |  |  |         in one part of the application reflects across all references to this class. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Attributes: | 
					
						
							|  |  |  |         config_folder_path (Optional[Path]): Path to the configuration directory. | 
					
						
							|  |  |  |         config_file_path (Optional[Path]): Path to the configuration file. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Raises: | 
					
						
							|  |  |  |         FileNotFoundError: If no configuration file is found, and creating a default configuration fails. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Example: | 
					
						
							|  |  |  |         To initialize and access configuration attributes (only one instance is created): | 
					
						
							|  |  |  |         ```python | 
					
						
							|  |  |  |         config_eos = ConfigEOS()  # Always returns the same instance | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         print(config_eos.prediction.prediction_hours)  # Access a setting from the loaded configuration | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     APP_NAME: ClassVar[str] = "net.akkudoktor.eos"  # reverse order | 
					
						
							|  |  |  |     APP_AUTHOR: ClassVar[str] = "akkudoktor" | 
					
						
							|  |  |  |     EOS_DIR: ClassVar[str] = "EOS_DIR" | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |     EOS_CONFIG_DIR: ClassVar[str] = "EOS_CONFIG_DIR" | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     ENCODING: ClassVar[str] = "UTF-8" | 
					
						
							|  |  |  |     CONFIG_FILE_NAME: ClassVar[str] = "EOS.config.json" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     _config_folder_path: ClassVar[Optional[Path]] = None | 
					
						
							|  |  |  |     _config_file_path: ClassVar[Optional[Path]] = None | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def settings_customise_sources( | 
					
						
							|  |  |  |         cls, | 
					
						
							|  |  |  |         settings_cls: Type[BaseSettings], | 
					
						
							|  |  |  |         init_settings: PydanticBaseSettingsSource, | 
					
						
							|  |  |  |         env_settings: PydanticBaseSettingsSource, | 
					
						
							|  |  |  |         dotenv_settings: PydanticBaseSettingsSource, | 
					
						
							|  |  |  |         file_secret_settings: PydanticBaseSettingsSource, | 
					
						
							|  |  |  |     ) -> tuple[PydanticBaseSettingsSource, ...]: | 
					
						
							|  |  |  |         """Customizes the order and handling of settings sources for a Pydantic BaseSettings subclass.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         This method determines the sources for application configuration settings, including | 
					
						
							|  |  |  |         environment variables, dotenv files, JSON configuration files, and file secrets. | 
					
						
							|  |  |  |         It ensures that a default configuration file exists and creates one if necessary. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Args: | 
					
						
							|  |  |  |             settings_cls (Type[BaseSettings]): The Pydantic BaseSettings class for which sources are customized. | 
					
						
							|  |  |  |             init_settings (PydanticBaseSettingsSource): The initial settings source, typically passed at runtime. | 
					
						
							|  |  |  |             env_settings (PydanticBaseSettingsSource): Settings sourced from environment variables. | 
					
						
							|  |  |  |             dotenv_settings (PydanticBaseSettingsSource): Settings sourced from a dotenv file. | 
					
						
							|  |  |  |             file_secret_settings (PydanticBaseSettingsSource): Settings sourced from secret files. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Returns: | 
					
						
							|  |  |  |             tuple[PydanticBaseSettingsSource, ...]: A tuple of settings sources in the order they should be applied. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Behavior: | 
					
						
							|  |  |  |             1. Checks for the existence of a JSON configuration file in the expected location. | 
					
						
							|  |  |  |             2. If the configuration file does not exist, creates the directory (if needed) and attempts to copy a | 
					
						
							|  |  |  |                default configuration file to the location. If the copy fails, uses the default configuration file directly. | 
					
						
							|  |  |  |             3. Creates a `JsonConfigSettingsSource` for both the configuration file and the default configuration file. | 
					
						
							|  |  |  |             4. Updates class attributes `_config_folder_path` and `_config_file_path` to reflect the determined paths. | 
					
						
							|  |  |  |             5. Returns a tuple containing all provided and newly created settings sources in the desired order. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Notes: | 
					
						
							|  |  |  |             - This method logs a warning if the default configuration file cannot be copied. | 
					
						
							|  |  |  |             - It ensures that a fallback to the default configuration file is always possible. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         file_settings: Optional[ConfigFileSourceMixin] = None | 
					
						
							|  |  |  |         config_file, exists = cls._get_config_file_path() | 
					
						
							|  |  |  |         config_dir = config_file.parent | 
					
						
							|  |  |  |         if not exists: | 
					
						
							|  |  |  |             config_dir.mkdir(parents=True, exist_ok=True) | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 shutil.copy2(cls.config_default_file_path, config_file) | 
					
						
							|  |  |  |             except Exception as exc: | 
					
						
							|  |  |  |                 logger.warning(f"Could not copy default config: {exc}. Using default config...") | 
					
						
							|  |  |  |                 config_file = cls.config_default_file_path | 
					
						
							|  |  |  |                 config_dir = config_file.parent | 
					
						
							|  |  |  |         file_settings = JsonConfigSettingsSource(settings_cls, json_file=config_file) | 
					
						
							|  |  |  |         default_settings = JsonConfigSettingsSource( | 
					
						
							|  |  |  |             settings_cls, json_file=cls.config_default_file_path | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         cls._config_folder_path = config_dir | 
					
						
							|  |  |  |         cls._config_file_path = config_file | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return ( | 
					
						
							|  |  |  |             init_settings, | 
					
						
							|  |  |  |             env_settings, | 
					
						
							|  |  |  |             dotenv_settings, | 
					
						
							|  |  |  |             file_settings, | 
					
						
							|  |  |  |             file_secret_settings, | 
					
						
							|  |  |  |             default_settings, | 
					
						
							|  |  |  |         ) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |     @property | 
					
						
							|  |  |  |     def config_folder_path(self) -> Optional[Path]: | 
					
						
							|  |  |  |         """Path to EOS configuration directory.""" | 
					
						
							|  |  |  |         return self._config_folder_path | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @property | 
					
						
							|  |  |  |     def config_file_path(self) -> Optional[Path]: | 
					
						
							|  |  |  |         """Path to EOS configuration file.""" | 
					
						
							|  |  |  |         return self._config_file_path | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     @classproperty | 
					
						
							|  |  |  |     def config_default_file_path(cls) -> Path: | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         """Compute the default config file path.""" | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         return cls.package_root_path.joinpath("data/default.config.json") | 
					
						
							| 
									
										
										
										
											2025-01-05 14:41:07 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     @classproperty | 
					
						
							|  |  |  |     def package_root_path(cls) -> Path: | 
					
						
							| 
									
										
										
										
											2025-01-05 14:41:07 +01:00
										 |  |  |         """Compute the package root path.""" | 
					
						
							|  |  |  |         return Path(__file__).parent.parent.resolve() | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     def __init__(self, *args: Any, **kwargs: Any) -> None: | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         """Initializes the singleton ConfigEOS instance.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Configuration data is loaded from a configuration file or a default one is created if none | 
					
						
							|  |  |  |         exists. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         if hasattr(self, "_initialized"): | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         super().__init__(*args, **kwargs) | 
					
						
							|  |  |  |         self._create_initial_config_file() | 
					
						
							|  |  |  |         self._update_data_folder_path() | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     def _setup(self, *args: Any, **kwargs: Any) -> None: | 
					
						
							|  |  |  |         """Re-initialize global settings.""" | 
					
						
							|  |  |  |         SettingsEOSDefaults.__init__(self, *args, **kwargs) | 
					
						
							|  |  |  |         self._create_initial_config_file() | 
					
						
							|  |  |  |         self._update_data_folder_path() | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     def merge_settings(self, settings: SettingsEOS) -> None: | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         """Merges the provided settings into the global settings for EOS, with optional overwrite.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Args: | 
					
						
							|  |  |  |             settings (SettingsEOS): The settings to apply globally. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Raises: | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             ValueError: If the `settings` is not a `SettingsEOS` instance. | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         if not isinstance(settings, SettingsEOS): | 
					
						
							|  |  |  |             raise ValueError(f"Settings must be an instance of SettingsEOS: '{settings}'.") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         self.merge_settings_from_dict(settings.model_dump()) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def merge_settings_from_dict(self, data: dict) -> None: | 
					
						
							|  |  |  |         """Merges the provided dictionary data into the current instance.
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-29 18:42:49 +01:00
										 |  |  |         Creates a new settings instance, then applies the dictionary data through validation, | 
					
						
							|  |  |  |         and finally merges the validated settings into the current instance. None values | 
					
						
							|  |  |  |         are not merged. | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         Args: | 
					
						
							|  |  |  |             data (dict): Dictionary containing field values to merge into the | 
					
						
							|  |  |  |                 current settings instance. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Raises: | 
					
						
							|  |  |  |             ValidationError: If the data contains invalid values for the defined fields. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Example: | 
					
						
							|  |  |  |             >>> config = get_config() | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             >>> new_data = {"prediction": {"prediction_hours": 24}, "server": {"server_eos_port": 8000}} | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             >>> config.merge_settings_from_dict(new_data) | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         self._setup(**merge_models(self, data)) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def reset_settings(self) -> None: | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         """Reset all changed settings to environment/config file defaults.
 | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         This functions basically deletes the settings provided before. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         self._setup() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _create_initial_config_file(self) -> None: | 
					
						
							|  |  |  |         if self.config_file_path is not None and not self.config_file_path.exists(): | 
					
						
							|  |  |  |             self.config_file_path.parent.mkdir(parents=True, exist_ok=True) | 
					
						
							|  |  |  |             with open(self.config_file_path, "w") as f: | 
					
						
							|  |  |  |                 f.write(self.model_dump_json(indent=4)) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _update_data_folder_path(self) -> None: | 
					
						
							|  |  |  |         """Updates path to the data directory.""" | 
					
						
							|  |  |  |         # From Settings | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         if data_dir := self.general.data_folder_path: | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 data_dir.mkdir(parents=True, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |                 self.general.data_folder_path = data_dir | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             except Exception as e: | 
					
						
							|  |  |  |                 logger.warning(f"Could not setup data dir: {e}") | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         # From EOS_DIR env | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         if env_dir := os.getenv(self.EOS_DIR): | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             try: | 
					
						
							|  |  |  |                 data_dir = Path(env_dir).resolve() | 
					
						
							|  |  |  |                 data_dir.mkdir(parents=True, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |                 self.general.data_folder_path = data_dir | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             except Exception as e: | 
					
						
							|  |  |  |                 logger.warning(f"Could not setup data dir: {e}") | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         # From platform specific default path | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |             data_dir = Path(user_data_dir(self.APP_NAME, self.APP_AUTHOR)) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             if data_dir is not None: | 
					
						
							|  |  |  |                 data_dir.mkdir(parents=True, exist_ok=True) | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |                 self.general.data_folder_path = data_dir | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |                 return | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         except Exception as e: | 
					
						
							|  |  |  |             logger.warning(f"Could not setup data dir: {e}") | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         # Current working directory | 
					
						
							|  |  |  |         data_dir = Path.cwd() | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         self.general.data_folder_path = data_dir | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |     @classmethod | 
					
						
							|  |  |  |     def _get_config_file_path(cls) -> tuple[Path, bool]: | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |         """Finds the a valid configuration file or returns the desired path for a new config file.
 | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         Returns: | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |             tuple[Path, bool]: The path to the configuration directory and if there is already a config file there | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         config_dirs = [] | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         env_base_dir = os.getenv(cls.EOS_DIR) | 
					
						
							|  |  |  |         env_config_dir = os.getenv(cls.EOS_CONFIG_DIR) | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |         env_dir = get_absolute_path(env_base_dir, env_config_dir) | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         logger.debug(f"Environment config dir: '{env_dir}'") | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         if env_dir is not None: | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |             config_dirs.append(env_dir.resolve()) | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         config_dirs.append(Path(user_config_dir(cls.APP_NAME))) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |         config_dirs.append(Path.cwd()) | 
					
						
							|  |  |  |         for cdir in config_dirs: | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             cfile = cdir.joinpath(cls.CONFIG_FILE_NAME) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             if cfile.exists(): | 
					
						
							|  |  |  |                 logger.debug(f"Found config file: '{cfile}'") | 
					
						
							| 
									
										
										
										
											2024-12-30 13:41:39 +01:00
										 |  |  |                 return cfile, True | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         return config_dirs[0].joinpath(cls.CONFIG_FILE_NAME), False | 
					
						
							| 
									
										
										
										
											2025-01-05 14:41:07 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |     def to_config_file(self) -> None: | 
					
						
							|  |  |  |         """Saves the current configuration to the configuration file.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Also updates the configuration file settings. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Raises: | 
					
						
							|  |  |  |             ValueError: If the configuration file path is not specified or can not be written to. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         if not self.config_file_path: | 
					
						
							|  |  |  |             raise ValueError("Configuration file path unknown.") | 
					
						
							| 
									
										
										
										
											2024-12-29 18:42:49 +01:00
										 |  |  |         with self.config_file_path.open("w", encoding=self.ENCODING) as f_out: | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             json_str = super().model_dump_json() | 
					
						
							|  |  |  |             f_out.write(json_str) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def update(self) -> None: | 
					
						
							|  |  |  |         """Updates all configuration fields.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         This method updates all configuration fields using the following order for value retrieval: | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             1. Current settings. | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  |             2. Environment variables. | 
					
						
							|  |  |  |             3. EOS configuration file. | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |             4. Field default constants. | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |         The first non None value in priority order is taken. | 
					
						
							|  |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2025-01-12 05:19:37 +01:00
										 |  |  |         self._setup(**self.model_dump()) | 
					
						
							| 
									
										
										
										
											2024-12-15 14:40:03 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_config() -> ConfigEOS: | 
					
						
							|  |  |  |     """Gets the EOS configuration data.""" | 
					
						
							|  |  |  |     return ConfigEOS() |