-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
391 additions
and
2 deletions.
There are no files selected for viewing
224 changes: 224 additions & 0 deletions
224
dbt_semantic_interfaces/parsing/text_input/rendering_helper.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
from __future__ import annotations | ||
|
||
import typing | ||
from typing import Callable, FrozenSet, Optional, Sequence | ||
|
||
from typing_extensions import override | ||
|
||
from dbt_semantic_interfaces.parsing.text_input.ti_description import ( | ||
ObjectBuilderMethod, | ||
QueryItemDescription, | ||
QueryItemType, | ||
) | ||
from dbt_semantic_interfaces.parsing.text_input.ti_exceptions import ( | ||
InvalidBuilderMethodException, | ||
) | ||
|
||
if typing.TYPE_CHECKING: | ||
from dbt_semantic_interfaces.parsing.text_input.ti_processor import ( | ||
QueryItemDescriptionProcessor, | ||
) | ||
|
||
from dbt_semantic_interfaces.parsing.text_input.valid_method import ValidMethodMapping | ||
|
||
|
||
class ObjectBuilderJinjaRenderHelper: | ||
"""Helps to build the methods that go into the Jinja template `.render()` call. | ||
e.g. | ||
SandboxedEnvironment(undefined=StrictUndefined) | ||
.from_string(jinja_template) | ||
.render( | ||
Dimension=render_helper.get_function_for_dimension(), | ||
TimeDimension=render_helper.get_function_for_time_dimension(), | ||
Entity=render_helper.get_function_for_entity(), | ||
Metric=render_helper.get_function_for_metric(), | ||
) | ||
) | ||
""" | ||
|
||
def __init__( # noqa: D107 | ||
self, | ||
description_processor: QueryItemDescriptionProcessor, | ||
valid_method_mapping: ValidMethodMapping, | ||
) -> None: | ||
self._description_processor = description_processor | ||
self._valid_method_mapping = valid_method_mapping | ||
|
||
def get_function_for_dimension(self) -> Callable: | ||
"""Returns the function that should be passed in to `.render(Dimension=...)`.""" | ||
description_processor = self._description_processor | ||
item_type = QueryItemType.DIMENSION | ||
allowed_methods = self._valid_method_mapping[item_type] | ||
|
||
def _create(name: str, entity_path: Sequence[str] = ()) -> _RenderingClassForJinjaTemplate: | ||
return _RenderingClassForJinjaTemplate( | ||
description_processor=description_processor, | ||
allowed_methods=allowed_methods, | ||
initial_item_description=QueryItemDescription( | ||
item_type=item_type, | ||
item_name=name, | ||
entity_path=tuple(entity_path), | ||
time_granularity_name=None, | ||
date_part_name=None, | ||
group_by_for_metric_item=(), | ||
descending=None, | ||
), | ||
) | ||
|
||
return _create | ||
|
||
def get_function_for_time_dimension(self) -> Callable: | ||
"""Returns the function that should be passed in to `.render(TimeDimension=...)`.""" | ||
description_processor = self._description_processor | ||
item_type = QueryItemType.TIME_DIMENSION | ||
allowed_methods = self._valid_method_mapping[item_type] | ||
|
||
def _create( | ||
time_dimension_name: str, | ||
time_granularity_name: Optional[str] = None, | ||
entity_path: Sequence[str] = (), | ||
descending: Optional[bool] = None, | ||
date_part_name: Optional[str] = None, | ||
) -> _RenderingClassForJinjaTemplate: | ||
return _RenderingClassForJinjaTemplate( | ||
description_processor=description_processor, | ||
allowed_methods=allowed_methods, | ||
initial_item_description=QueryItemDescription( | ||
item_type=item_type, | ||
item_name=time_dimension_name, | ||
entity_path=tuple(entity_path), | ||
time_granularity_name=time_granularity_name, | ||
date_part_name=date_part_name, | ||
group_by_for_metric_item=(), | ||
descending=descending, | ||
), | ||
) | ||
|
||
return _create | ||
|
||
def get_function_for_entity(self) -> Callable: | ||
"""Returns the function that should be passed in to `.render(Entity=...)`.""" | ||
description_processor = self._description_processor | ||
item_type = QueryItemType.ENTITY | ||
allowed_methods = self._valid_method_mapping[item_type] | ||
|
||
def _create(entity_name: str, entity_path: Sequence[str] = ()) -> _RenderingClassForJinjaTemplate: | ||
return _RenderingClassForJinjaTemplate( | ||
description_processor=description_processor, | ||
allowed_methods=allowed_methods, | ||
initial_item_description=QueryItemDescription( | ||
item_type=item_type, | ||
item_name=entity_name, | ||
entity_path=tuple(entity_path), | ||
time_granularity_name=None, | ||
date_part_name=None, | ||
group_by_for_metric_item=(), | ||
descending=None, | ||
), | ||
) | ||
|
||
return _create | ||
|
||
def get_function_for_metric(self) -> Callable: | ||
"""Returns the function that should be passed in to `.render(Metric=...)`.""" | ||
description_processor = self._description_processor | ||
item_type = QueryItemType.METRIC | ||
allowed_methods = self._valid_method_mapping[item_type] | ||
|
||
def _create(metric_name: str, group_by: Sequence[str] = ()) -> _RenderingClassForJinjaTemplate: | ||
return _RenderingClassForJinjaTemplate( | ||
description_processor=description_processor, | ||
allowed_methods=allowed_methods, | ||
initial_item_description=QueryItemDescription( | ||
item_type=item_type, | ||
item_name=metric_name, | ||
entity_path=(), | ||
time_granularity_name=None, | ||
date_part_name=None, | ||
group_by_for_metric_item=tuple(group_by), | ||
descending=None, | ||
), | ||
) | ||
|
||
return _create | ||
|
||
|
||
class _RenderingClassForJinjaTemplate: | ||
"""Helper class that behaves like a builder object as used in a Jinja template. | ||
e.g. in the Jinja template: | ||
{{ Dimension('listing__created_at').grain('day').date_part('month') }} | ||
The `Dimension('listing__created_at')` is an instance of this class and when builder methods like `.grain()` are | ||
called on it, the state of the instance is updated and returns itself so that additional builder methods can be | ||
chained. | ||
""" | ||
|
||
def __init__( | ||
self, | ||
description_processor: QueryItemDescriptionProcessor, | ||
allowed_methods: FrozenSet[ObjectBuilderMethod], | ||
initial_item_description: QueryItemDescription, | ||
) -> None: | ||
"""Initializer. | ||
Args: | ||
description_processor: The description processor that will run using the query-item description described | ||
in the builder call. It will run after all builder methods are called. | ||
allowed_methods: Builder methods that can be used. Otherwise, an `InvalidBuilderMethodException` is raised. | ||
initial_item_description: The starting description. Usually it contains the element name and entity path. | ||
""" | ||
self._description_processor = description_processor | ||
self._allowed_builder_methods = allowed_methods | ||
self._current_description = initial_item_description | ||
|
||
def _update_current_description( | ||
self, | ||
time_granularity_name: Optional[str] = None, | ||
date_part_name: Optional[str] = None, | ||
descending: Optional[bool] = None, | ||
) -> None: | ||
args = (time_granularity_name, date_part_name, descending) | ||
assert sum(1 for arg in args if arg is not None) == 1, f"Expected exactly 1 argument set, but got {args}" | ||
|
||
builder_method: Optional[ObjectBuilderMethod] | ||
if time_granularity_name is not None: | ||
builder_method = ObjectBuilderMethod.GRAIN | ||
elif date_part_name is not None: | ||
builder_method = ObjectBuilderMethod.DATE_PART | ||
elif descending is not None: | ||
builder_method = ObjectBuilderMethod.DESCENDING | ||
else: | ||
assert False, "Exactly 1 argument should have been set as previously checked." | ||
|
||
if builder_method not in self._allowed_builder_methods: | ||
raise InvalidBuilderMethodException( | ||
f"`{builder_method.value}` can't be used with `{self._current_description.item_type.value}`" | ||
f" in this context.", | ||
item_type=self._current_description.item_type, | ||
invalid_builder_method=builder_method, | ||
) | ||
self._current_description = self._current_description.create_modified( | ||
time_granularity_name=time_granularity_name, | ||
date_part_name=date_part_name, | ||
descending=descending, | ||
) | ||
|
||
def grain(self, time_granularity: str) -> _RenderingClassForJinjaTemplate: | ||
self._update_current_description(time_granularity_name=time_granularity) | ||
return self | ||
|
||
def descending(self, _is_descending: bool) -> _RenderingClassForJinjaTemplate: | ||
self._update_current_description(descending=_is_descending) | ||
return self | ||
|
||
def date_part(self, date_part_name: str) -> _RenderingClassForJinjaTemplate: | ||
self._update_current_description(date_part_name=date_part_name) | ||
return self | ||
|
||
@override | ||
def __str__(self) -> str: | ||
return self._description_processor.process_description(self._current_description) |
38 changes: 38 additions & 0 deletions
38
dbt_semantic_interfaces/parsing/text_input/ti_exceptions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
from __future__ import annotations | ||
|
||
from dbt_semantic_interfaces.errors import InvalidQuerySyntax | ||
from dbt_semantic_interfaces.parsing.text_input.ti_description import ( | ||
ObjectBuilderMethod, | ||
QueryItemType, | ||
) | ||
|
||
|
||
class QueryItemJinjaException(Exception): | ||
"""Raised when there is an exception when calling Jinja package methods on the query item input.""" | ||
|
||
pass | ||
|
||
|
||
class InvalidBuilderMethodException(InvalidQuerySyntax): | ||
"""Raised when a query item using the object-builder format uses a disallowed method. | ||
For example, `Entity('listing').grain('day')` should raise this exception since `grain` is only applicable to | ||
`Dimension()`. | ||
""" | ||
|
||
def __init__( # noqa: D107 | ||
self, message: str, item_type: QueryItemType, invalid_builder_method: ObjectBuilderMethod | ||
) -> None: | ||
super().__init__(message) | ||
self._item_type = item_type | ||
self._invalid_builder_method = invalid_builder_method | ||
|
||
@property | ||
def item_type(self) -> QueryItemType: | ||
"""Return the item that was used with the invalid method.""" | ||
return self._item_type | ||
|
||
@property | ||
def invalid_builder_method(self) -> ObjectBuilderMethod: | ||
"""Return the invalid builder method that was used.""" | ||
return self._invalid_builder_method |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.