config.py
1 import contextlib 2 from typing import Dict 3 from typing import Iterator 4 from typing import List 5 from typing import Type 6 from typing import TypeVar 7 8 import dynaconf 9 from dynaconf import LazySettings 10 from dynaconf.utils.boxing import DynaBox 11 from litestar import Litestar 12 from litestar.di import Provide 13 14 from evidently._pydantic_compat import BaseModel 15 from evidently._pydantic_compat import PrivateAttr 16 from evidently._pydantic_compat import parse_obj_as 17 from evidently.legacy.ui.components.base import SECTION_COMPONENT_TYPE_MAPPING 18 from evidently.legacy.ui.components.base import AppBuilder 19 from evidently.legacy.ui.components.base import Component 20 from evidently.legacy.ui.components.base import ComponentContext 21 from evidently.legacy.ui.components.base import ServiceComponent 22 from evidently.legacy.ui.components.base import T 23 from evidently.legacy.ui.components.security import SecurityComponent 24 25 26 def _convert_keys(box): 27 if isinstance(box, (DynaBox, LazySettings)): 28 return {k.lower(): _convert_keys(v) for k, v in box.items()} 29 return box 30 31 32 class ConfigContext(ComponentContext): 33 def __init__(self, config: "Config", components_mapping: Dict[Type[Component], Component]): 34 self.config = config 35 self.components_mapping = components_mapping 36 37 def get_component(self, type_: Type[T]) -> T: 38 for cls in self.components_mapping: 39 if issubclass(cls, type_): 40 return self.components_mapping[cls] # type: ignore[return-value] 41 raise ValueError(f"Component of type {type_.__name__} not found") 42 43 @property 44 def components(self) -> List[Component]: 45 return list(sorted(self.components_mapping.values(), key=lambda x: -x.get_priority())) 46 47 def get_dependencies(self) -> Dict[str, Provide]: 48 res = {} 49 for c in self.components: 50 dependencies = c.get_dependencies(self) 51 print(f"{c.__class__.__name__} deps: " + ", ".join(dependencies)) 52 res.update(dependencies) 53 return res 54 55 def get_middlewares(self): 56 res = [] 57 for c in self.components: 58 res.extend(c.get_middlewares(self)) 59 return res 60 61 def apply(self, builder: AppBuilder): 62 for c in self.components: 63 c.apply(self, builder) 64 65 def finalize(self, app: Litestar): 66 for c in self.components: 67 c.finalize(self, app) 68 69 def validate(self): 70 for c in self.components_mapping.values(): 71 reqs = c.get_requirements() 72 for r in reqs: 73 try: 74 self.get_component(r) 75 except ValueError as e: 76 raise ValueError(f"Component {c.__class__.__name__} missing {r.__name__} requirement") from e 77 78 79 class Config(BaseModel): 80 additional_components: Dict[str, Component] = {} 81 82 _components: List[Component] = PrivateAttr(default_factory=list) 83 _ctx: ComponentContext = PrivateAttr() 84 85 @property 86 def components(self) -> List[Component]: 87 return [getattr(self, name) for name in self.__fields__ if isinstance(getattr(self, name), Component)] + list( 88 self.additional_components.values() 89 ) 90 91 @contextlib.contextmanager 92 def context(self) -> Iterator[ConfigContext]: 93 ctx = ConfigContext(self, {type(c): c for c in self.components}) 94 ctx.validate() 95 self._ctx = ctx 96 yield ctx 97 del self._ctx 98 99 100 class AppConfig(Config): 101 security: SecurityComponent 102 service: ServiceComponent 103 104 105 TConfig = TypeVar("TConfig", bound=Config) 106 107 108 def load_config(config_type: Type[TConfig], box: dict) -> TConfig: 109 new_box = _convert_keys(box) 110 components = {} 111 named_components = {} 112 for section, component_dict in new_box.items(): 113 # todo 114 if not isinstance(component_dict, dict): 115 continue 116 if section.endswith("for_dynaconf"): 117 continue 118 if section in ("renamed_vars", "dict_itemiterator"): 119 continue 120 if section == "additional_components": 121 for subsection, compoennt_subdict in component_dict.items(): 122 component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING.get(subsection, Component), compoennt_subdict) 123 components[subsection] = component 124 elif section in config_type.__fields__: 125 type_ = config_type.__fields__[section].type_ 126 component = parse_obj_as(type_, component_dict) 127 named_components[section] = component 128 elif section in SECTION_COMPONENT_TYPE_MAPPING: 129 component = parse_obj_as(SECTION_COMPONENT_TYPE_MAPPING[section], component_dict) 130 components[section] = component 131 else: 132 raise ValueError(f"unknown config section {section}") 133 134 # todo: we will get validation error if not all components configured, but we can wrap it more nicely 135 return config_type(additional_components=components, **named_components) 136 137 138 def load_config_from_file(cls: Type[TConfig], path: str, envvar_prefix: str = "EVIDENTLY") -> TConfig: 139 dc = dynaconf.Dynaconf( 140 envvar_prefix=envvar_prefix, 141 ) 142 dc.configure(settings_module=path) 143 config = load_config(cls, dc) 144 return config 145 146 147 settings = dynaconf.Dynaconf( 148 envvar_prefix="EVIDENTLY", 149 )