286 lines
9.2 KiB
Python
286 lines
9.2 KiB
Python
|
# special support for CLI argument parsing.
|
||
|
from dbt import flags
|
||
|
import itertools
|
||
|
from dbt.clients.yaml_helper import yaml, Loader, Dumper # noqa: F401
|
||
|
|
||
|
from typing import (
|
||
|
Dict, List, Optional, Tuple, Any, Union
|
||
|
)
|
||
|
|
||
|
from dbt.contracts.selection import SelectorDefinition, SelectorFile
|
||
|
from dbt.exceptions import InternalException, ValidationException
|
||
|
|
||
|
from .selector_spec import (
|
||
|
SelectionUnion,
|
||
|
SelectionSpec,
|
||
|
SelectionIntersection,
|
||
|
SelectionDifference,
|
||
|
SelectionCriteria,
|
||
|
)
|
||
|
|
||
|
INTERSECTION_DELIMITER = ','
|
||
|
|
||
|
DEFAULT_INCLUDES: List[str] = ['fqn:*', 'source:*', 'exposure:*']
|
||
|
DEFAULT_EXCLUDES: List[str] = []
|
||
|
DATA_TEST_SELECTOR: str = 'test_type:data'
|
||
|
SCHEMA_TEST_SELECTOR: str = 'test_type:schema'
|
||
|
|
||
|
|
||
|
def parse_union(
|
||
|
components: List[str], expect_exists: bool, greedy: bool = False
|
||
|
) -> SelectionUnion:
|
||
|
# turn ['a b', 'c'] -> ['a', 'b', 'c']
|
||
|
raw_specs = itertools.chain.from_iterable(
|
||
|
r.split(' ') for r in components
|
||
|
)
|
||
|
union_components: List[SelectionSpec] = []
|
||
|
|
||
|
# ['a', 'b', 'c,d'] -> union('a', 'b', intersection('c', 'd'))
|
||
|
for raw_spec in raw_specs:
|
||
|
intersection_components: List[SelectionSpec] = [
|
||
|
SelectionCriteria.from_single_spec(part, greedy=greedy)
|
||
|
for part in raw_spec.split(INTERSECTION_DELIMITER)
|
||
|
]
|
||
|
union_components.append(SelectionIntersection(
|
||
|
components=intersection_components,
|
||
|
expect_exists=expect_exists,
|
||
|
raw=raw_spec,
|
||
|
))
|
||
|
return SelectionUnion(
|
||
|
components=union_components,
|
||
|
expect_exists=False,
|
||
|
raw=components,
|
||
|
)
|
||
|
|
||
|
|
||
|
def parse_union_from_default(
|
||
|
raw: Optional[List[str]], default: List[str], greedy: bool = False
|
||
|
) -> SelectionUnion:
|
||
|
components: List[str]
|
||
|
expect_exists: bool
|
||
|
if raw is None:
|
||
|
return parse_union(components=default, expect_exists=False, greedy=greedy)
|
||
|
else:
|
||
|
return parse_union(components=raw, expect_exists=True, greedy=greedy)
|
||
|
|
||
|
|
||
|
def parse_difference(
|
||
|
include: Optional[List[str]], exclude: Optional[List[str]]
|
||
|
) -> SelectionDifference:
|
||
|
included = parse_union_from_default(include, DEFAULT_INCLUDES, greedy=bool(flags.GREEDY))
|
||
|
excluded = parse_union_from_default(exclude, DEFAULT_EXCLUDES, greedy=True)
|
||
|
return SelectionDifference(components=[included, excluded])
|
||
|
|
||
|
|
||
|
def parse_test_selectors(
|
||
|
data: bool, schema: bool, base: SelectionSpec
|
||
|
) -> SelectionSpec:
|
||
|
union_components = []
|
||
|
|
||
|
if data:
|
||
|
union_components.append(
|
||
|
SelectionCriteria.from_single_spec(DATA_TEST_SELECTOR)
|
||
|
)
|
||
|
if schema:
|
||
|
union_components.append(
|
||
|
SelectionCriteria.from_single_spec(SCHEMA_TEST_SELECTOR)
|
||
|
)
|
||
|
|
||
|
intersect_with: SelectionSpec
|
||
|
if not union_components:
|
||
|
return base
|
||
|
elif len(union_components) == 1:
|
||
|
intersect_with = union_components[0]
|
||
|
else: # data and schema tests
|
||
|
intersect_with = SelectionUnion(
|
||
|
components=union_components,
|
||
|
expect_exists=True,
|
||
|
raw=[DATA_TEST_SELECTOR, SCHEMA_TEST_SELECTOR],
|
||
|
)
|
||
|
|
||
|
return SelectionIntersection(
|
||
|
components=[base, intersect_with], expect_exists=True
|
||
|
)
|
||
|
|
||
|
|
||
|
RawDefinition = Union[str, Dict[str, Any]]
|
||
|
|
||
|
|
||
|
def _get_list_dicts(
|
||
|
dct: Dict[str, Any], key: str
|
||
|
) -> List[RawDefinition]:
|
||
|
result: List[RawDefinition] = []
|
||
|
if key not in dct:
|
||
|
raise InternalException(
|
||
|
f'Expected to find key {key} in dict, only found {list(dct)}'
|
||
|
)
|
||
|
values = dct[key]
|
||
|
if not isinstance(values, list):
|
||
|
raise ValidationException(
|
||
|
f'Invalid value for key "{key}". Expected a list.'
|
||
|
)
|
||
|
for value in values:
|
||
|
if isinstance(value, dict):
|
||
|
for value_key in value:
|
||
|
if not isinstance(value_key, str):
|
||
|
raise ValidationException(
|
||
|
f'Expected all keys to "{key}" dict to be strings, '
|
||
|
f'but "{value_key}" is a "{type(value_key)}"'
|
||
|
)
|
||
|
result.append(value)
|
||
|
elif isinstance(value, str):
|
||
|
result.append(value)
|
||
|
else:
|
||
|
raise ValidationException(
|
||
|
f'Invalid value type {type(value)} in key "{key}", expected '
|
||
|
f'dict or str (value: {value}).'
|
||
|
)
|
||
|
|
||
|
return result
|
||
|
|
||
|
|
||
|
def _parse_exclusions(definition) -> Optional[SelectionSpec]:
|
||
|
exclusions = _get_list_dicts(definition, 'exclude')
|
||
|
parsed_exclusions = [
|
||
|
parse_from_definition(excl) for excl in exclusions
|
||
|
]
|
||
|
if len(parsed_exclusions) == 1:
|
||
|
return parsed_exclusions[0]
|
||
|
elif len(parsed_exclusions) > 1:
|
||
|
return SelectionUnion(
|
||
|
components=parsed_exclusions,
|
||
|
raw=exclusions
|
||
|
)
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
def _parse_include_exclude_subdefs(
|
||
|
definitions: List[RawDefinition]
|
||
|
) -> Tuple[List[SelectionSpec], Optional[SelectionSpec]]:
|
||
|
include_parts: List[SelectionSpec] = []
|
||
|
diff_arg: Optional[SelectionSpec] = None
|
||
|
|
||
|
for definition in definitions:
|
||
|
if isinstance(definition, dict) and 'exclude' in definition:
|
||
|
# do not allow multiple exclude: defs at the same level
|
||
|
if diff_arg is not None:
|
||
|
yaml_sel_cfg = yaml.dump(definition)
|
||
|
raise ValidationException(
|
||
|
f"You cannot provide multiple exclude arguments to the "
|
||
|
f"same selector set operator:\n{yaml_sel_cfg}"
|
||
|
)
|
||
|
diff_arg = _parse_exclusions(definition)
|
||
|
else:
|
||
|
include_parts.append(parse_from_definition(definition))
|
||
|
|
||
|
return (include_parts, diff_arg)
|
||
|
|
||
|
|
||
|
def parse_union_definition(definition: Dict[str, Any]) -> SelectionSpec:
|
||
|
union_def_parts = _get_list_dicts(definition, 'union')
|
||
|
include, exclude = _parse_include_exclude_subdefs(union_def_parts)
|
||
|
|
||
|
union = SelectionUnion(components=include, greedy_warning=False)
|
||
|
|
||
|
if exclude is None:
|
||
|
union.raw = definition
|
||
|
return union
|
||
|
else:
|
||
|
return SelectionDifference(
|
||
|
components=[union, exclude],
|
||
|
raw=definition,
|
||
|
greedy_warning=False
|
||
|
)
|
||
|
|
||
|
|
||
|
def parse_intersection_definition(
|
||
|
definition: Dict[str, Any]
|
||
|
) -> SelectionSpec:
|
||
|
intersection_def_parts = _get_list_dicts(definition, 'intersection')
|
||
|
include, exclude = _parse_include_exclude_subdefs(intersection_def_parts)
|
||
|
intersection = SelectionIntersection(components=include, greedy_warning=False)
|
||
|
|
||
|
if exclude is None:
|
||
|
intersection.raw = definition
|
||
|
return intersection
|
||
|
else:
|
||
|
return SelectionDifference(
|
||
|
components=[intersection, exclude],
|
||
|
raw=definition,
|
||
|
greedy_warning=False
|
||
|
)
|
||
|
|
||
|
|
||
|
def parse_dict_definition(definition: Dict[str, Any]) -> SelectionSpec:
|
||
|
diff_arg: Optional[SelectionSpec] = None
|
||
|
if len(definition) == 1:
|
||
|
key = list(definition)[0]
|
||
|
value = definition[key]
|
||
|
if not isinstance(key, str):
|
||
|
raise ValidationException(
|
||
|
f'Expected definition key to be a "str", got one of type '
|
||
|
f'"{type(key)}" ({key})'
|
||
|
)
|
||
|
dct = {
|
||
|
'method': key,
|
||
|
'value': value,
|
||
|
}
|
||
|
elif 'method' in definition and 'value' in definition:
|
||
|
dct = definition
|
||
|
if 'exclude' in definition:
|
||
|
diff_arg = _parse_exclusions(definition)
|
||
|
dct = {k: v for k, v in dct.items() if k != 'exclude'}
|
||
|
else:
|
||
|
raise ValidationException(
|
||
|
f'Expected either 1 key or else "method" '
|
||
|
f'and "value" keys, but got {list(definition)}'
|
||
|
)
|
||
|
|
||
|
# if key isn't a valid method name, this will raise
|
||
|
base = SelectionCriteria.selection_criteria_from_dict(definition, dct)
|
||
|
if diff_arg is None:
|
||
|
return base
|
||
|
else:
|
||
|
return SelectionDifference(components=[base, diff_arg], greedy_warning=False)
|
||
|
|
||
|
|
||
|
def parse_from_definition(
|
||
|
definition: RawDefinition, rootlevel=False
|
||
|
) -> SelectionSpec:
|
||
|
|
||
|
if (isinstance(definition, dict) and
|
||
|
('union' in definition or 'intersection' in definition) and
|
||
|
rootlevel and len(definition) > 1):
|
||
|
keys = ",".join(definition.keys())
|
||
|
raise ValidationException(
|
||
|
f"Only a single 'union' or 'intersection' key is allowed "
|
||
|
f"in a root level selector definition; found {keys}."
|
||
|
)
|
||
|
if isinstance(definition, str):
|
||
|
return SelectionCriteria.from_single_spec(definition)
|
||
|
elif 'union' in definition:
|
||
|
return parse_union_definition(definition)
|
||
|
elif 'intersection' in definition:
|
||
|
return parse_intersection_definition(definition)
|
||
|
elif isinstance(definition, dict):
|
||
|
return parse_dict_definition(definition)
|
||
|
else:
|
||
|
raise ValidationException(
|
||
|
f'Expected to find union, intersection, str or dict, instead '
|
||
|
f'found {type(definition)}: {definition}'
|
||
|
)
|
||
|
|
||
|
|
||
|
def parse_from_selectors_definition(
|
||
|
source: SelectorFile
|
||
|
) -> Dict[str, Dict[str, Union[SelectionSpec, bool]]]:
|
||
|
result: Dict[str, Dict[str, Union[SelectionSpec, bool]]] = {}
|
||
|
selector: SelectorDefinition
|
||
|
for selector in source.selectors:
|
||
|
result[selector.name] = {
|
||
|
"default": selector.default,
|
||
|
"definition": parse_from_definition(selector.definition, rootlevel=True)
|
||
|
}
|
||
|
return result
|