218 lines
6.5 KiB
Python
218 lines
6.5 KiB
Python
|
import os
|
||
|
import re
|
||
|
from abc import ABCMeta, abstractmethod
|
||
|
from dataclasses import dataclass
|
||
|
|
||
|
from typing import (
|
||
|
Set, Iterator, List, Optional, Dict, Union, Any, Iterable, Tuple
|
||
|
)
|
||
|
from .graph import UniqueId
|
||
|
from .selector_methods import MethodName
|
||
|
from dbt.exceptions import RuntimeException, InvalidSelectorException
|
||
|
|
||
|
|
||
|
RAW_SELECTOR_PATTERN = re.compile(
|
||
|
r'\A'
|
||
|
r'(?P<childrens_parents>(\@))?'
|
||
|
r'(?P<parents>((?P<parents_depth>(\d*))\+))?'
|
||
|
r'((?P<method>([\w.]+)):)?(?P<value>(.*?))'
|
||
|
r'(?P<children>(\+(?P<children_depth>(\d*))))?'
|
||
|
r'\Z'
|
||
|
)
|
||
|
SELECTOR_METHOD_SEPARATOR = '.'
|
||
|
|
||
|
|
||
|
def _probably_path(value: str):
|
||
|
"""Decide if value is probably a path. Windows has two path separators, so
|
||
|
we should check both sep ('\\') and altsep ('/') there.
|
||
|
"""
|
||
|
if os.path.sep in value:
|
||
|
return True
|
||
|
elif os.path.altsep is not None and os.path.altsep in value:
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
|
||
|
def _match_to_int(match: Dict[str, str], key: str) -> Optional[int]:
|
||
|
raw = match.get(key)
|
||
|
# turn the empty string into None, too.
|
||
|
if not raw:
|
||
|
return None
|
||
|
try:
|
||
|
return int(raw)
|
||
|
except ValueError as exc:
|
||
|
raise RuntimeException(
|
||
|
f'Invalid node spec - could not handle parent depth {raw}'
|
||
|
) from exc
|
||
|
|
||
|
|
||
|
SelectionSpec = Union[
|
||
|
'SelectionCriteria',
|
||
|
'SelectionIntersection',
|
||
|
'SelectionDifference',
|
||
|
'SelectionUnion',
|
||
|
]
|
||
|
|
||
|
|
||
|
@dataclass
|
||
|
class SelectionCriteria:
|
||
|
raw: Any
|
||
|
method: MethodName
|
||
|
method_arguments: List[str]
|
||
|
value: Any
|
||
|
childrens_parents: bool
|
||
|
parents: bool
|
||
|
parents_depth: Optional[int]
|
||
|
children: bool
|
||
|
children_depth: Optional[int]
|
||
|
greedy: bool = False
|
||
|
greedy_warning: bool = False # do not raise warning for yaml selectors
|
||
|
|
||
|
def __post_init__(self):
|
||
|
if self.children and self.childrens_parents:
|
||
|
raise RuntimeException(
|
||
|
f'Invalid node spec {self.raw} - "@" prefix and "+" suffix '
|
||
|
'are incompatible'
|
||
|
)
|
||
|
|
||
|
@classmethod
|
||
|
def default_method(cls, value: str) -> MethodName:
|
||
|
if _probably_path(value):
|
||
|
return MethodName.Path
|
||
|
else:
|
||
|
return MethodName.FQN
|
||
|
|
||
|
@classmethod
|
||
|
def parse_method(
|
||
|
cls, groupdict: Dict[str, Any]
|
||
|
) -> Tuple[MethodName, List[str]]:
|
||
|
raw_method = groupdict.get('method')
|
||
|
if raw_method is None:
|
||
|
return cls.default_method(groupdict['value']), []
|
||
|
|
||
|
method_parts: List[str] = raw_method.split(SELECTOR_METHOD_SEPARATOR)
|
||
|
try:
|
||
|
method_name = MethodName(method_parts[0])
|
||
|
except ValueError as exc:
|
||
|
raise InvalidSelectorException(
|
||
|
f"'{method_parts[0]}' is not a valid method name"
|
||
|
) from exc
|
||
|
|
||
|
method_arguments: List[str] = method_parts[1:]
|
||
|
|
||
|
return method_name, method_arguments
|
||
|
|
||
|
@classmethod
|
||
|
def selection_criteria_from_dict(
|
||
|
cls, raw: Any, dct: Dict[str, Any], greedy: bool = False
|
||
|
) -> 'SelectionCriteria':
|
||
|
if 'value' not in dct:
|
||
|
raise RuntimeException(
|
||
|
f'Invalid node spec "{raw}" - no search value!'
|
||
|
)
|
||
|
method_name, method_arguments = cls.parse_method(dct)
|
||
|
|
||
|
parents_depth = _match_to_int(dct, 'parents_depth')
|
||
|
children_depth = _match_to_int(dct, 'children_depth')
|
||
|
return cls(
|
||
|
raw=raw,
|
||
|
method=method_name,
|
||
|
method_arguments=method_arguments,
|
||
|
value=dct['value'],
|
||
|
childrens_parents=bool(dct.get('childrens_parents')),
|
||
|
parents=bool(dct.get('parents')),
|
||
|
parents_depth=parents_depth,
|
||
|
children=bool(dct.get('children')),
|
||
|
children_depth=children_depth,
|
||
|
greedy=(greedy or bool(dct.get('greedy'))),
|
||
|
)
|
||
|
|
||
|
@classmethod
|
||
|
def dict_from_single_spec(cls, raw: str):
|
||
|
result = RAW_SELECTOR_PATTERN.match(raw)
|
||
|
if result is None:
|
||
|
return {'error': 'Invalid selector spec'}
|
||
|
dct: Dict[str, Any] = result.groupdict()
|
||
|
method_name, method_arguments = cls.parse_method(dct)
|
||
|
meth_name = str(method_name)
|
||
|
if method_arguments:
|
||
|
meth_name = meth_name + '.' + '.'.join(method_arguments)
|
||
|
dct['method'] = meth_name
|
||
|
dct = {k: v for k, v in dct.items() if (v is not None and v != '')}
|
||
|
if 'childrens_parents' in dct:
|
||
|
dct['childrens_parents'] = bool(dct.get('childrens_parents'))
|
||
|
if 'parents' in dct:
|
||
|
dct['parents'] = bool(dct.get('parents'))
|
||
|
if 'children' in dct:
|
||
|
dct['children'] = bool(dct.get('children'))
|
||
|
if 'greedy' in dct:
|
||
|
dct['greedy'] = bool(dct.get('greedy'))
|
||
|
return dct
|
||
|
|
||
|
@classmethod
|
||
|
def from_single_spec(cls, raw: str, greedy: bool = False) -> 'SelectionCriteria':
|
||
|
result = RAW_SELECTOR_PATTERN.match(raw)
|
||
|
if result is None:
|
||
|
# bad spec!
|
||
|
raise RuntimeException(f'Invalid selector spec "{raw}"')
|
||
|
|
||
|
return cls.selection_criteria_from_dict(raw, result.groupdict(), greedy=greedy)
|
||
|
|
||
|
|
||
|
class BaseSelectionGroup(Iterable[SelectionSpec], metaclass=ABCMeta):
|
||
|
def __init__(
|
||
|
self,
|
||
|
components: Iterable[SelectionSpec],
|
||
|
expect_exists: bool = False,
|
||
|
greedy_warning: bool = True,
|
||
|
raw: Any = None,
|
||
|
):
|
||
|
self.components: List[SelectionSpec] = list(components)
|
||
|
self.expect_exists = expect_exists
|
||
|
self.greedy_warning = greedy_warning
|
||
|
self.raw = raw
|
||
|
|
||
|
def __iter__(self) -> Iterator[SelectionSpec]:
|
||
|
for component in self.components:
|
||
|
yield component
|
||
|
|
||
|
@abstractmethod
|
||
|
def combine_selections(
|
||
|
self,
|
||
|
selections: List[Set[UniqueId]],
|
||
|
) -> Set[UniqueId]:
|
||
|
raise NotImplementedError(
|
||
|
'_combine_selections not implemented!'
|
||
|
)
|
||
|
|
||
|
def combined(self, selections: List[Set[UniqueId]]) -> Set[UniqueId]:
|
||
|
if not selections:
|
||
|
return set()
|
||
|
|
||
|
return self.combine_selections(selections)
|
||
|
|
||
|
|
||
|
class SelectionIntersection(BaseSelectionGroup):
|
||
|
def combine_selections(
|
||
|
self,
|
||
|
selections: List[Set[UniqueId]],
|
||
|
) -> Set[UniqueId]:
|
||
|
return set.intersection(*selections)
|
||
|
|
||
|
|
||
|
class SelectionDifference(BaseSelectionGroup):
|
||
|
def combine_selections(
|
||
|
self,
|
||
|
selections: List[Set[UniqueId]],
|
||
|
) -> Set[UniqueId]:
|
||
|
return set.difference(*selections)
|
||
|
|
||
|
|
||
|
class SelectionUnion(BaseSelectionGroup):
|
||
|
def combine_selections(
|
||
|
self,
|
||
|
selections: List[Set[UniqueId]],
|
||
|
) -> Set[UniqueId]:
|
||
|
return set.union(*selections)
|