Files
scribe/scraibe/app/utils.py
T
2024-01-29 12:32:23 +00:00

298 lines
12 KiB
Python

"""
utils.py
This module contains two classes, ConfigLoader and AppConfig, which are used to manage application-specific configuration settings.
The ConfigLoader class provides methods for loading a configuration file, applying overrides, and restoring default values for specified keys. It also includes methods for recursively updating nested keys and getting the default configuration.
The AppConfig class extends ConfigLoader and provides additional methods for setting global variables, launch options, and layout options from the configuration. It also includes methods for checking and setting file paths, and getting layout options.
Classes:
ConfigLoader: Manages application-specific configuration settings.
AppConfig: Extends ConfigLoader to provide additional methods for managing application-specific configuration settings.
"""
import os
import warnings
import yaml
from typing import Any, Dict, Optional
import scraibe.app.global_var as gv
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
class ConfigLoader:
"""A class that extends ConfigLoader to manage application-specific configuration settings.
This class provides methods for setting global variables, launch options, and layout options from the configuration.
Attributes:
config (Dict[str, Any]): The current configuration settings.
launch (Dict[str, Any]): The launch configuration settings.
model (Dict[str, Any]): The model configuration settings.
advanced (Dict[str, Any]): The advanced configuration settings.
queue (Dict[str, Any]): The queue configuration settings.
layout (Dict[str, Any]): The layout configuration settings.
"""
def __init__(self, config: Dict[str, Any]):
"""Initializes a new instance of the ConfigLoader class.
Args:
config (dict): The configuration dictionary.
"""
self.config = config
def restore_defaults_for_keys(self, *args: str):
"""Restores specified keys to their default values, including nested keys.
Args:
*args (str): A list of keys or paths to keys (for nested dictionaries) to restore to default values.
Each key or path should be a list of keys leading to the desired key.
"""
default_config = self.get_default_config()
for key in args:
self.apply_overrides(self.config, default_config, key)
@classmethod
def load_config(cls, yaml_path: Optional[str] = None, **kwargs: Any) -> 'ConfigLoader':
"""Load the configuration file and apply overrides.
Args:
yaml_path (str, optional): Path to the YAML file containing overrides.
**kwargs: Additional overrides as keyword arguments.
Returns:
ConfigLoader: A ConfigLoader object with the loaded configuration.
"""
# Load the original configuration
config = cls.get_default_config()
# Override with another YAML file if provided
if yaml_path:
with open(yaml_path, 'r') as file:
override_config = yaml.safe_load(file)
cls.apply_overrides(config, override_config)
# Apply overrides from kwargs
cls.apply_overrides(config, kwargs)
return cls(config)
@staticmethod
def apply_overrides(orig_dict: Dict[str, Any], override_dict: Dict[str, Any], specific: Optional[str] = None):
"""Recursively apply overrides to the configuration, only for specific keys.
Args:
orig_dict (Dict[str, Any]): The original dictionary.
override_dict (Dict[str, Any]): The override dictionary.
specific (str, optional): The specific key to override.
"""
for key, value in override_dict.items():
if isinstance(value, dict):
# If the value is a dict, apply recursively
sub_dict = orig_dict.get(key, {})
ConfigLoader.apply_overrides(sub_dict, value, specific)
orig_dict[key] = sub_dict
else:
# Apply override for this key
if specific is None:
# If no specific keys are provided, update the key
# If the value is not a dict, search for the key and update
if ConfigLoader.update_nested_key(orig_dict, key, value):
continue # Key was found and updated
orig_dict[key] = value # Key not found, update at this level
elif key in specific:
# If specific keys are provided, only update if the key is in the list
if ConfigLoader.update_nested_key(orig_dict, specific, value):
continue # Key was found and updated
orig_dict[specific] = value
@staticmethod
def update_nested_key(d, key, value):
"""Recursively search and update the key in nested dictionary.
Args:
d (Dict[str, Any]): The dictionary.
key (str): The key to update.
value (Any): The new value.
Returns:
bool: True if the key was found and updated, False otherwise.
"""
if key in d:
d[key] = value
return True
for k, v in d.items():
if isinstance(v, dict) and ConfigLoader.update_nested_key(v, key, value):
return True
return False
@staticmethod
def get_default_config():
"""Return the default configuration.
Returns:
Dict[str, Any]: The default configuration.
"""
with open(gv.DEFAULT_APP_CONIFG_PATH , 'r') as file:
config = yaml.safe_load(file)
return config
class AppConfig(ConfigLoader):
"""A class that extends ConfigLoader to manage application-specific configuration settings.
This class provides methods for setting global variables, launch options, and layout options from the configuration.
Attributes:
config (dict): The current configuration settings.
launch (dict): The launch configuration settings.
model (dict): The model configuration settings.
advanced (dict): The advanced configuration settings.
queue (dict): The queue configuration settings.
layout (dict): The layout configuration settings.
"""
def __init__(self, config : Dict[str, Any]):
"""Initializes a new instance of the AppConfig class.
Args:
config (dict): The configuration dictionary.
"""
self.config = config
self.set_global_vars_from_config()
self.set_launch_options()
self.set_layout_options()
self.launch = self.config.get("launch")
self.model = self.config.get("model")
self.advanced = self.config.get("advanced")
self.queue = self.config.get("queue")
self.layout = self.config.get("layout")
def set_global_vars_from_config(self) -> None:
"""Sets the global variables from a configuration dictionary.
Args:
config (dict): A dictionary containing the parameters for the model. Modify the default parameters in the config.yml file.
Returns:
None
"""
gv.MODEL_PARAMS = self.config.get('model')
gv.TIMEOUT = self.config.get("advanced").get('timeout')
def set_launch_options(self) -> None:
"""Sets the launch options from a configuration dictionary.
Args:
None
Returns:
None
"""
launch_options = self.config.get("launch")
if launch_options.get('auth').pop('auth_enabled'):
self.config['launch']['auth'] = (launch_options.get('auth').pop('auth_username'),
launch_options.get('auth').pop('auth_password'))
else:
self.config['launch']['auth'] = None
def set_layout_options(self) -> None:
"""Sets the layout options from a configuration dictionary.
Args:
None
Returns:
None
"""
self.config['layout']['header'] = self.check_and_set_path(self.config['layout'], 'header')
self.config['layout']['footer'] = self.check_and_set_path(self.config['layout'], 'footer')
self.config['layout']['logo'] = self.check_and_set_path(self.config['layout'], 'logo')
def get_layout(self) -> Dict[str, str]:
"""Gets the layout options from a configuration dictionary.
Args:
None
Returns:
dict: A dictionary containing the header and footer layout options.
"""
if not os.path.exists(self.config['layout']['header']) and \
self.config['layout']['header'] == "scraibe/app/header.html":
hname = os.path.join(CURRENT_PATH, "header.html")
header = open(hname).read()
elif not os.path.exists(self.config['layout']['header']) and self.config['layout']['header'] != "scraibe/app/header.html":
warnings.warn(f"Header file not found: {self.config['layout']['header']} \n" \
"fall back to default.")
hname = os.path.join(CURRENT_PATH, "header.html")
header = open(hname).read()
elif os.path.exists(self.config['layout']['header']):
header = open(self.config['layout']['header']).read()
else:
warnings.warn(f"Header file not found: {self.config['layout']['header']}")
header = None
if header != None:
if self.config['layout']['logo'] == "scraibe/app/logo.svg":
header = header.replace("/file=logo.svg", f"/file={os.path.join(CURRENT_PATH, 'logo.svg')}")
elif self.config['layout']['logo'] != "scraibe/app/logo.svg":
header = header.replace("/file=logo.svg", f"/file={self.config['layout']['logo']}")
else:
warnings.warn(f"Logo file not found: {self.config['layout']['logo']}")
if self.config['layout']['footer'] != None:
if os.path.exists(self.config['layout']['footer']):
footer = open(self.config['layout']['footer']).read()
elif self.config['layout']['footer'] == None:
footer = None
else:
warnings.warn(f"Footer file not found: {self.config['layout']['footer']}")
else:
footer = None
return {'header' : header ,
'footer' : footer}
@staticmethod
def check_and_set_path(config_item: dict, key: str) -> Optional[str]:
"""Check if the file exists at the given path. If not, try with CURRENT_PATH.
Raise FileNotFoundError if the file still doesn't exist.
Args:
config_item (dict): The configuration item.
key (str): The key to check in the configuration item.
Returns:
str: The path to the file if it exists, None otherwise.
"""
_current_path = os.path.dirname(os.path.realpath(__file__)) # Define your CURRENT_PATH
file_path = config_item.get(key)
if file_path is None:
return None
if not os.path.exists(file_path):
new_path = os.path.join(_current_path, file_path)
if not os.path.exists(new_path):
warnings.warn(f"{key.capitalize()} file not found: {config_item[key]} \n" \
"fall back to default.")
else:
config_item[key] = new_path
return config_item[key]