from copy import deepcopy from dataclasses import dataclass from typing import List, Iterator, Dict, Any, TypeVar, Union from dbt.config import RuntimeConfig, Project from dbt.contracts.graph.model_config import BaseConfig, get_config_for from dbt.exceptions import InternalException from dbt.legacy_config_updater import ConfigUpdater, IsFQNResource from dbt.node_types import NodeType from dbt.utils import fqn_search @dataclass class ModelParts(IsFQNResource): fqn: List[str] resource_type: NodeType package_name: str class LegacyContextConfig: def __init__( self, active_project: RuntimeConfig, own_project: Project, fqn: List[str], node_type: NodeType, ): self._config = None self.active_project: RuntimeConfig = active_project self.own_project: Project = own_project self.model = ModelParts( fqn=fqn, resource_type=node_type, package_name=self.own_project.project_name, ) self.updater = ConfigUpdater(active_project.credentials.type) # the config options defined within the model self.in_model_config: Dict[str, Any] = {} def get_default(self) -> Dict[str, Any]: defaults = {"enabled": True, "materialized": "view"} if self.model.resource_type == NodeType.Seed: defaults['materialized'] = 'seed' elif self.model.resource_type == NodeType.Snapshot: defaults['materialized'] = 'snapshot' if self.model.resource_type == NodeType.Test: defaults['severity'] = 'ERROR' return defaults def build_config_dict(self, base: bool = False) -> Dict[str, Any]: defaults = self.get_default() active_config = self.load_config_from_active_project() if self.active_project.project_name == self.own_project.project_name: cfg = self.updater.merge( defaults, active_config, self.in_model_config ) else: own_config = self.load_config_from_own_project() cfg = self.updater.merge( defaults, own_config, self.in_model_config, active_config ) return cfg def _translate_adapter_aliases(self, config: Dict[str, Any]): return self.active_project.credentials.translate_aliases(config) def update_in_model_config(self, config: Dict[str, Any]) -> None: config = self._translate_adapter_aliases(config) self.updater.update_into(self.in_model_config, config) def load_config_from_own_project(self) -> Dict[str, Any]: return self.updater.get_project_config(self.model, self.own_project) def load_config_from_active_project(self) -> Dict[str, Any]: return self.updater.get_project_config(self.model, self.active_project) T = TypeVar('T', bound=BaseConfig) class ContextConfigGenerator: def __init__(self, active_project: RuntimeConfig): self.active_project = active_project def get_node_project(self, project_name: str): if project_name == self.active_project.project_name: return self.active_project dependencies = self.active_project.load_dependencies() if project_name not in dependencies: raise InternalException( f'Project name {project_name} not found in dependencies ' f'(found {list(dependencies)})' ) return dependencies[project_name] def project_configs( self, project: Project, fqn: List[str], resource_type: NodeType ) -> Iterator[Dict[str, Any]]: if resource_type == NodeType.Seed: model_configs = project.seeds elif resource_type == NodeType.Snapshot: model_configs = project.snapshots elif resource_type == NodeType.Source: model_configs = project.sources else: model_configs = project.models for level_config in fqn_search(model_configs, fqn): result = {} for key, value in level_config.items(): if key.startswith('+'): result[key[1:]] = deepcopy(value) elif not isinstance(value, dict): result[key] = deepcopy(value) yield result def active_project_configs( self, fqn: List[str], resource_type: NodeType ) -> Iterator[Dict[str, Any]]: return self.project_configs(self.active_project, fqn, resource_type) def _update_from_config( self, result: T, partial: Dict[str, Any], validate: bool = False ) -> T: return result.update_from( partial, self.active_project.credentials.type, validate=validate ) def calculate_node_config( self, config_calls: List[Dict[str, Any]], fqn: List[str], resource_type: NodeType, project_name: str, base: bool, ) -> BaseConfig: own_config = self.get_node_project(project_name) # defaults, own_config, config calls, active_config (if != own_config) config_cls = get_config_for(resource_type, base=base) # Calculate the defaults. We don't want to validate the defaults, # because it might be invalid in the case of required config members # (such as on snapshots!) result = config_cls.from_dict({}, validate=False) for fqn_config in self.project_configs(own_config, fqn, resource_type): result = self._update_from_config(result, fqn_config) for config_call in config_calls: result = self._update_from_config(result, config_call) if own_config.project_name != self.active_project.project_name: for fqn_config in self.active_project_configs(fqn, resource_type): result = self._update_from_config(result, fqn_config) # this is mostly impactful in the snapshot config case return result.finalize_and_validate() class ContextConfig: def __init__( self, active_project: RuntimeConfig, fqn: List[str], resource_type: NodeType, project_name: str, ) -> None: self.config_calls: List[Dict[str, Any]] = [] self.cfg_source = ContextConfigGenerator(active_project) self.fqn = fqn self.resource_type = resource_type self.project_name = project_name def update_in_model_config(self, opts: Dict[str, Any]) -> None: self.config_calls.append(opts) def build_config_dict(self, base: bool = False) -> Dict[str, Any]: return self.cfg_source.calculate_node_config( config_calls=self.config_calls, fqn=self.fqn, resource_type=self.resource_type, project_name=self.project_name, base=base, ).to_dict() ContextConfigType = Union[LegacyContextConfig, ContextConfig]