239 lines
6.7 KiB
Python
239 lines
6.7 KiB
Python
from typing import Dict, Any, Tuple, Optional, Union, Callable
|
|
|
|
from dbt.clients.jinja import get_rendered, catch_jinja
|
|
|
|
from dbt.exceptions import (
|
|
DbtProjectError, CompilationException, RecursionException
|
|
)
|
|
from dbt.node_types import NodeType
|
|
from dbt.utils import deep_map
|
|
|
|
|
|
Keypath = Tuple[Union[str, int], ...]
|
|
|
|
|
|
class BaseRenderer:
|
|
def __init__(self, context: Dict[str, Any]) -> None:
|
|
self.context = context
|
|
|
|
@property
|
|
def name(self):
|
|
return 'Rendering'
|
|
|
|
def should_render_keypath(self, keypath: Keypath) -> bool:
|
|
return True
|
|
|
|
def render_entry(self, value: Any, keypath: Keypath) -> Any:
|
|
if not self.should_render_keypath(keypath):
|
|
return value
|
|
|
|
return self.render_value(value, keypath)
|
|
|
|
def render_value(
|
|
self, value: Any, keypath: Optional[Keypath] = None
|
|
) -> Any:
|
|
# keypath is ignored.
|
|
# if it wasn't read as a string, ignore it
|
|
if not isinstance(value, str):
|
|
return value
|
|
try:
|
|
with catch_jinja():
|
|
return get_rendered(value, self.context, native=True)
|
|
except CompilationException as exc:
|
|
msg = f'Could not render {value}: {exc.msg}'
|
|
raise CompilationException(msg) from exc
|
|
|
|
def render_data(
|
|
self, data: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
try:
|
|
return deep_map(self.render_entry, data)
|
|
except RecursionException:
|
|
raise DbtProjectError(
|
|
f'Cycle detected: {self.name} input has a reference to itself',
|
|
project=data
|
|
)
|
|
|
|
|
|
def _list_if_none(value):
|
|
if value is None:
|
|
value = []
|
|
return value
|
|
|
|
|
|
def _dict_if_none(value):
|
|
if value is None:
|
|
value = {}
|
|
return value
|
|
|
|
|
|
def _list_if_none_or_string(value):
|
|
value = _list_if_none(value)
|
|
if isinstance(value, str):
|
|
return [value]
|
|
return value
|
|
|
|
|
|
class ProjectPostprocessor(Dict[Keypath, Callable[[Any], Any]]):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
self[('on-run-start',)] = _list_if_none_or_string
|
|
self[('on-run-end',)] = _list_if_none_or_string
|
|
|
|
for k in ('models', 'seeds', 'snapshots'):
|
|
self[(k,)] = _dict_if_none
|
|
self[(k, 'vars')] = _dict_if_none
|
|
self[(k, 'pre-hook')] = _list_if_none_or_string
|
|
self[(k, 'post-hook')] = _list_if_none_or_string
|
|
self[('seeds', 'column_types')] = _dict_if_none
|
|
|
|
def postprocess(self, value: Any, key: Keypath) -> Any:
|
|
if key in self:
|
|
handler = self[key]
|
|
return handler(value)
|
|
|
|
return value
|
|
|
|
|
|
class DbtProjectYamlRenderer(BaseRenderer):
|
|
_KEYPATH_HANDLERS = ProjectPostprocessor()
|
|
|
|
@property
|
|
def name(self):
|
|
'Project config'
|
|
|
|
def get_package_renderer(self) -> BaseRenderer:
|
|
return PackageRenderer(self.context)
|
|
|
|
def get_selector_renderer(self) -> BaseRenderer:
|
|
return SelectorRenderer(self.context)
|
|
|
|
def render_project(
|
|
self,
|
|
project: Dict[str, Any],
|
|
project_root: str,
|
|
) -> Dict[str, Any]:
|
|
"""Render the project and insert the project root after rendering."""
|
|
rendered_project = self.render_data(project)
|
|
rendered_project['project-root'] = project_root
|
|
return rendered_project
|
|
|
|
def render_packages(self, packages: Dict[str, Any]):
|
|
"""Render the given packages dict"""
|
|
package_renderer = self.get_package_renderer()
|
|
return package_renderer.render_data(packages)
|
|
|
|
def render_selectors(self, selectors: Dict[str, Any]):
|
|
selector_renderer = self.get_selector_renderer()
|
|
return selector_renderer.render_data(selectors)
|
|
|
|
def render_entry(self, value: Any, keypath: Keypath) -> Any:
|
|
result = super().render_entry(value, keypath)
|
|
return self._KEYPATH_HANDLERS.postprocess(result, keypath)
|
|
|
|
def should_render_keypath(self, keypath: Keypath) -> bool:
|
|
if not keypath:
|
|
return True
|
|
|
|
first = keypath[0]
|
|
# run hooks are not rendered
|
|
if first in {'on-run-start', 'on-run-end', 'query-comment'}:
|
|
return False
|
|
|
|
# don't render vars blocks until runtime
|
|
if first == 'vars':
|
|
return False
|
|
|
|
if first in {'seeds', 'models', 'snapshots', 'tests'}:
|
|
keypath_parts = {
|
|
(k.lstrip('+ ') if isinstance(k, str) else k)
|
|
for k in keypath
|
|
}
|
|
# model-level hooks
|
|
if 'pre-hook' in keypath_parts or 'post-hook' in keypath_parts:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class ProfileRenderer(BaseRenderer):
|
|
@property
|
|
def name(self):
|
|
'Profile'
|
|
|
|
|
|
class SchemaYamlRenderer(BaseRenderer):
|
|
DOCUMENTABLE_NODES = frozenset(
|
|
n.pluralize() for n in NodeType.documentable()
|
|
)
|
|
|
|
@property
|
|
def name(self):
|
|
return 'Rendering yaml'
|
|
|
|
def _is_norender_key(self, keypath: Keypath) -> bool:
|
|
"""
|
|
models:
|
|
- name: blah
|
|
- description: blah
|
|
tests: ...
|
|
- columns:
|
|
- name:
|
|
- description: blah
|
|
tests: ...
|
|
|
|
Return True if it's tests or description - those aren't rendered
|
|
"""
|
|
if len(keypath) >= 2 and keypath[1] in ('tests', 'description'):
|
|
return True
|
|
|
|
if (
|
|
len(keypath) >= 4 and
|
|
keypath[1] == 'columns' and
|
|
keypath[3] in ('tests', 'description')
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
# don't render descriptions or test keyword arguments
|
|
def should_render_keypath(self, keypath: Keypath) -> bool:
|
|
if len(keypath) < 2:
|
|
return True
|
|
|
|
if keypath[0] not in self.DOCUMENTABLE_NODES:
|
|
return True
|
|
|
|
if len(keypath) < 3:
|
|
return True
|
|
|
|
if keypath[0] == NodeType.Source.pluralize():
|
|
if keypath[2] == 'description':
|
|
return False
|
|
if keypath[2] == 'tables':
|
|
if self._is_norender_key(keypath[3:]):
|
|
return False
|
|
elif keypath[0] == NodeType.Macro.pluralize():
|
|
if keypath[2] == 'arguments':
|
|
if self._is_norender_key(keypath[3:]):
|
|
return False
|
|
elif self._is_norender_key(keypath[1:]):
|
|
return False
|
|
else: # keypath[0] in self.DOCUMENTABLE_NODES:
|
|
if self._is_norender_key(keypath[1:]):
|
|
return False
|
|
return True
|
|
|
|
|
|
class PackageRenderer(BaseRenderer):
|
|
@property
|
|
def name(self):
|
|
return 'Packages config'
|
|
|
|
|
|
class SelectorRenderer(BaseRenderer):
|
|
@property
|
|
def name(self):
|
|
return 'Selector config'
|