diff --git a/Makefile b/Makefile index ebb91ee9..7127e3cc 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ test: export FORMAT_JSON_LOGS="1" && hatch -v run dev-env:pytest -n auto tests lint: - hatch run dev-env:pre-commit run --show-diff-on-failure --color=always --all-files + hatch run dev-env:pre-commit run --color=always --all-files json_schema: hatch run dev-env:python dbt_semantic_interfaces/parsing/generate_json_schema_file.py diff --git a/dbt_semantic_interfaces/parsing/where_filter/parameter_set_factory.py b/dbt_semantic_interfaces/parsing/where_filter/parameter_set_factory.py index ce40343a..ab969bc6 100644 --- a/dbt_semantic_interfaces/parsing/where_filter/parameter_set_factory.py +++ b/dbt_semantic_interfaces/parsing/where_filter/parameter_set_factory.py @@ -1,4 +1,4 @@ -from typing import Optional, Sequence +from typing import Optional, Sequence, Union from dbt_semantic_interfaces.call_parameter_sets import ( DimensionCallParameterSet, @@ -12,6 +12,9 @@ METRIC_TIME_ELEMENT_NAME, is_metric_time_name, ) +from dbt_semantic_interfaces.parsing.where_filter.where_filter_objects import ( + WhereFilterEntity, +) from dbt_semantic_interfaces.references import ( DimensionReference, EntityReference, @@ -106,7 +109,9 @@ def create_entity(entity_name: str, entity_path: Sequence[str] = ()) -> EntityCa ) @staticmethod - def create_metric(metric_name: str, group_by: Sequence[str] = ()) -> MetricCallParameterSet: + def create_metric( + metric_name: str, group_by: Sequence[Union[str, WhereFilterEntity]] = () + ) -> MetricCallParameterSet: """Gets called by Jinja when rendering {{ Metric(...) }}.""" if not group_by: raise ParseWhereFilterException( @@ -115,5 +120,15 @@ def create_metric(metric_name: str, group_by: Sequence[str] = ()) -> MetricCallP ) return MetricCallParameterSet( metric_reference=MetricReference(element_name=metric_name), - group_by=tuple([LinkableElementReference(element_name=group_by_name) for group_by_name in group_by]), + group_by=tuple( + [ + LinkableElementReference( + # TODO: add entity_links + group_by_item + if isinstance(group_by_item, str) + else group_by_item.element_name + ) + for group_by_item in group_by + ] + ), ) diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_dimension.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_dimension.py deleted file mode 100644 index b03a7eab..00000000 --- a/dbt_semantic_interfaces/parsing/where_filter/where_filter_dimension.py +++ /dev/null @@ -1,64 +0,0 @@ -from __future__ import annotations - -from typing import List, Optional, Sequence - -from typing_extensions import override - -from dbt_semantic_interfaces.errors import InvalidQuerySyntax -from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint -from dbt_semantic_interfaces.protocols.query_interface import ( - QueryInterfaceDimension, - QueryInterfaceDimensionFactory, -) - - -class WhereFilterDimension(ProtocolHint[QueryInterfaceDimension]): - """A dimension that is passed in through the where filter parameter.""" - - @override - def _implements_protocol(self) -> QueryInterfaceDimension: - return self - - def __init__( # noqa - self, - name: str, - entity_path: Sequence[str], - ) -> None: - self.name = name - self.entity_path = entity_path - self.time_granularity_name: Optional[str] = None - self.date_part_name: Optional[str] = None - - def grain(self, time_granularity: str) -> QueryInterfaceDimension: - """The time granularity.""" - self.time_granularity_name = time_granularity - return self - - def descending(self, _is_descending: bool) -> QueryInterfaceDimension: - """Set the sort order for order-by.""" - raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec") - - def date_part(self, date_part_name: str) -> QueryInterfaceDimension: - """Date part to extract from the dimension.""" - self.date_part_name = date_part_name - return self - - -class WhereFilterDimensionFactory(ProtocolHint[QueryInterfaceDimensionFactory]): - """Creates a WhereFilterDimension. - - Each call to `create` adds a WhereFilterDimension to `created`. - """ - - @override - def _implements_protocol(self) -> QueryInterfaceDimensionFactory: - return self - - def __init__(self) -> None: # noqa - self.created: List[WhereFilterDimension] = [] - - def create(self, dimension_name: str, entity_path: Sequence[str] = ()) -> WhereFilterDimension: - """Gets called by Jinja when rendering {{ Dimension(...) }}.""" - dimension = WhereFilterDimension(dimension_name, entity_path) - self.created.append(dimension) - return dimension diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_entity.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_entity.py deleted file mode 100644 index b4d346a2..00000000 --- a/dbt_semantic_interfaces/parsing/where_filter/where_filter_entity.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -from typing import List, Sequence - -from typing_extensions import override - -from dbt_semantic_interfaces.call_parameter_sets import ( - EntityCallParameterSet, - MetricCallParameterSet, -) -from dbt_semantic_interfaces.errors import InvalidQuerySyntax -from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( - ParameterSetFactory, -) -from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint -from dbt_semantic_interfaces.protocols.query_interface import ( - QueryInterfaceEntity, - QueryInterfaceEntityFactory, - QueryInterfaceMetric, - QueryInterfaceMetricFactory, -) - - -class EntityStub(ProtocolHint[QueryInterfaceEntity]): - """An Entity implementation that just satisfies the protocol. - - QueryInterfaceEntity currently has no methods and the parameter set is created in the factory. - So, there is nothing to do here. - """ - - @override - def _implements_protocol(self) -> QueryInterfaceEntity: - return self - - -class MetricStub(ProtocolHint[QueryInterfaceMetric]): - """A Metric implementation that just satisfies the protocol. - - QueryInterfaceMetric currently has no methods and the parameter set is created in the factory. - """ - - @override - def _implements_protocol(self) -> QueryInterfaceMetric: - return self - - def descending(self, _is_descending: bool) -> QueryInterfaceMetric: # noqa: D - raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec") - - -class WhereFilterEntityFactory(ProtocolHint[QueryInterfaceEntityFactory]): - """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" - - @override - def _implements_protocol(self) -> QueryInterfaceEntityFactory: - return self - - def __init__(self) -> None: # noqa - self.entity_call_parameter_sets: List[EntityCallParameterSet] = [] - - def create(self, entity_name: str, entity_path: Sequence[str] = ()) -> EntityStub: - """Gets called by Jinja when rendering {{ Entity(...) }}.""" - self.entity_call_parameter_sets.append(ParameterSetFactory.create_entity(entity_name, entity_path)) - return EntityStub() - - -class WhereFilterMetricFactory(ProtocolHint[QueryInterfaceMetricFactory]): - """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" - - @override - def _implements_protocol(self) -> QueryInterfaceMetricFactory: - return self - - def __init__(self) -> None: # noqa: D - self.metric_call_parameter_sets: List[MetricCallParameterSet] = [] - - def create(self, metric_name: str, group_by: Sequence[str] = ()) -> MetricStub: # noqa: D - self.metric_call_parameter_sets.append( - ParameterSetFactory.create_metric(metric_name=metric_name, group_by=group_by) - ) - return MetricStub() diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_factories.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_factories.py new file mode 100644 index 00000000..ae937176 --- /dev/null +++ b/dbt_semantic_interfaces/parsing/where_filter/where_filter_factories.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +from typing import List, Optional, Sequence + +from dbt_semantic_interfaces.call_parameter_sets import ( + EntityCallParameterSet, + MetricCallParameterSet, + TimeDimensionCallParameterSet, +) +from dbt_semantic_interfaces.errors import InvalidQuerySyntax +from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( + ParameterSetFactory, +) +from dbt_semantic_interfaces.parsing.where_filter.where_filter_objects import ( + WhereFilterDimension, + WhereFilterEntity, + WhereFilterMetric, + WhereFilterTimeDimension, +) +from dbt_semantic_interfaces.type_enums import DatePart, TimeGranularity + + +# Rename these factories: RenderedWhereFilterEntityFactory (MF) vs. ParsedWhereFilterParser (DSI) +# Add protocols back. Rename them to: WhereFilterEntity (JinjaWhereFilterEntity? Can we reuse them in the JDBC interface & saved queries?), +# JinjaEntityFactory (do we even need a protocol for this? I guess to make sure the create() method is aligned?) +# Can we use an ABC instead of a protocol? Something that lets me add functional methods to the base class to avoid duplication. +class WhereFilterEntityFactory: + """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" + + def __init__(self) -> None: # noqa + self.entity_call_parameter_sets: List[EntityCallParameterSet] = [] + + def create(self, entity_name: str, entity_path: Sequence[str] = ()) -> WhereFilterEntity: + """Gets called by Jinja when rendering {{ Entity(...) }}.""" + self.entity_call_parameter_sets.append(ParameterSetFactory.create_entity(entity_name, entity_path)) + return WhereFilterEntity(element_name=entity_name, entity_links=entity_path) + + +class WhereFilterMetricFactory: + """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" + + def __init__(self) -> None: # noqa: D + self.metric_call_parameter_sets: List[MetricCallParameterSet] = [] + + def create(self, metric_name: str, group_by: Sequence[str] = ()) -> WhereFilterMetric: + """Create a WhereFilterMetric. + + Note that group_by is required, but uses a default arg here so that we can return a readable error to the user + if they leave it out. + """ + self.metric_call_parameter_sets.append( + ParameterSetFactory.create_metric(metric_name=metric_name, group_by=group_by) + ) + return WhereFilterMetric(element_name=metric_name, group_by=group_by) + + +class WhereFilterDimensionFactory: + """Creates a WhereFilterDimension. + + Each call to `create` adds a WhereFilterDimension to `created`. + """ + + def __init__(self) -> None: # noqa + self.created: List[WhereFilterDimension] = [] + + def create(self, dimension_name: str, entity_path: Sequence[str] = ()) -> WhereFilterDimension: + """Gets called by Jinja when rendering {{ Dimension(...) }}.""" + dimension = WhereFilterDimension(dimension_name, entity_path) + self.created.append(dimension) + return dimension + + +class WhereFilterTimeDimensionFactory: + """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" + + def __init__(self) -> None: # noqa + self.time_dimension_call_parameter_sets: List[TimeDimensionCallParameterSet] = [] + + def create( + self, + time_dimension_name: str, + time_granularity_name: Optional[str] = None, + entity_path: Sequence[str] = (), + descending: Optional[bool] = None, + date_part_name: Optional[str] = None, + ) -> WhereFilterTimeDimension: + """Gets called by Jinja when rendering {{ TimeDimension(...) }}.""" + if descending is not None: + raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec") + self.time_dimension_call_parameter_sets.append( + ParameterSetFactory.create_time_dimension( + time_dimension_name=time_dimension_name, + time_granularity_name=time_granularity_name, + entity_path=entity_path, + date_part_name=date_part_name, + ) + ) + return WhereFilterTimeDimension( + element_name=time_dimension_name, + time_granularity=TimeGranularity(time_granularity_name.lower()) if time_granularity_name else None, + entity_path=entity_path, + date_part_name=DatePart(date_part_name.lower()) if date_part_name else None, + ) diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_objects.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_objects.py new file mode 100644 index 00000000..4fc0fac2 --- /dev/null +++ b/dbt_semantic_interfaces/parsing/where_filter/where_filter_objects.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional, Sequence + +from dbt_semantic_interfaces.type_enums import DatePart, TimeGranularity + + +@dataclass +class WhereFilterEntity: + """A dimension that is passed in through the where filter parameter using Jinja syntax.""" + + element_name: str + entity_links: Sequence[str] = () + + +@dataclass +class WhereFilterMetric: + """A metric that is passed in through the where filter parameter using Jinja syntax.""" + + element_name: str + group_by: Sequence[str] + + +@dataclass +class WhereFilterDimension: + """A dimension that is passed in through the where filter parameter using Jinja syntax.""" + + name: str + entity_path: Sequence[str] = () # Default is new here - consistent with TimeDimension + # Behavior change: allows passing these params on init (why shouldn't we allow that?) + # don't love the names, though. Copy MFS jinja object? + time_granularity_name: Optional[str] = None + date_part_name: Optional[str] = None + + def grain(self, time_granularity: str) -> WhereFilterDimension: # noqa: D + if self.time_granularity_name: + raise RuntimeError("Grain was already set in the Dimension object parameters.") + self.time_granularity_name = time_granularity + return self + + def date_part(self, date_part: str) -> WhereFilterDimension: # noqa: D + if self.date_part_name: + raise RuntimeError("Date part was already set in the Dimension object parameters.") + self.date_part_name = date_part + return self + + +@dataclass +class WhereFilterTimeDimension: + """A time dimension that is passed in through the where filter parameter using Jinja syntax.""" + + element_name: str + time_granularity: Optional[TimeGranularity] = None # not str? + entity_path: Sequence[str] = () + # Can we change the name below? Breaking change bo one is using date part anyway, right? And it's not documented? + date_part_name: Optional[DatePart] = None + + def grain(self, time_granularity: str) -> WhereFilterTimeDimension: # noqa: D + if self.time_granularity: + raise RuntimeError("Grain was already set in the Dimension object parameters.") + self.time_granularity = TimeGranularity(time_granularity.lower()) + return self + + def date_part(self, date_part: str) -> WhereFilterTimeDimension: # noqa: D + if self.date_part_name: + raise RuntimeError("Date part was already set in the Dimension object parameters.") + self.date_part_name = DatePart(date_part.lower()) + return self diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_parser.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_parser.py index df81590d..e2e01388 100644 --- a/dbt_semantic_interfaces/parsing/where_filter/where_filter_parser.py +++ b/dbt_semantic_interfaces/parsing/where_filter/where_filter_parser.py @@ -11,14 +11,10 @@ from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( ParameterSetFactory, ) -from dbt_semantic_interfaces.parsing.where_filter.where_filter_dimension import ( +from dbt_semantic_interfaces.parsing.where_filter.where_filter_factories import ( WhereFilterDimensionFactory, -) -from dbt_semantic_interfaces.parsing.where_filter.where_filter_entity import ( WhereFilterEntityFactory, WhereFilterMetricFactory, -) -from dbt_semantic_interfaces.parsing.where_filter.where_filter_time_dimension import ( WhereFilterTimeDimensionFactory, ) diff --git a/dbt_semantic_interfaces/parsing/where_filter/where_filter_time_dimension.py b/dbt_semantic_interfaces/parsing/where_filter/where_filter_time_dimension.py deleted file mode 100644 index 693c8344..00000000 --- a/dbt_semantic_interfaces/parsing/where_filter/where_filter_time_dimension.py +++ /dev/null @@ -1,57 +0,0 @@ -from __future__ import annotations - -from typing import List, Optional, Sequence - -from typing_extensions import override - -from dbt_semantic_interfaces.call_parameter_sets import TimeDimensionCallParameterSet -from dbt_semantic_interfaces.errors import InvalidQuerySyntax -from dbt_semantic_interfaces.parsing.where_filter.parameter_set_factory import ( - ParameterSetFactory, -) -from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint -from dbt_semantic_interfaces.protocols.query_interface import ( - QueryInterfaceTimeDimension, - QueryInterfaceTimeDimensionFactory, -) - - -class TimeDimensionStub(ProtocolHint[QueryInterfaceTimeDimension]): - """A TimeDimension implementation that just satisfies the protocol. - - QueryInterfaceTimeDimension currently has no methods and the parameter set is created in the factory. - So, there is nothing to do here. - """ - - @override - def _implements_protocol(self) -> QueryInterfaceTimeDimension: - return self - - -class WhereFilterTimeDimensionFactory(ProtocolHint[QueryInterfaceTimeDimensionFactory]): - """Executes in the Jinja sandbox to produce parameter sets and append them to a list.""" - - @override - def _implements_protocol(self) -> QueryInterfaceTimeDimensionFactory: - return self - - def __init__(self) -> None: # noqa - self.time_dimension_call_parameter_sets: List[TimeDimensionCallParameterSet] = [] - - def create( - self, - time_dimension_name: str, - time_granularity_name: Optional[str] = None, - entity_path: Sequence[str] = (), - descending: Optional[bool] = None, - date_part_name: Optional[str] = None, - ) -> TimeDimensionStub: - """Gets called by Jinja when rendering {{ TimeDimension(...) }}.""" - if descending is not None: - raise InvalidQuerySyntax("descending is invalid in the where parameter and filter spec") - self.time_dimension_call_parameter_sets.append( - ParameterSetFactory.create_time_dimension( - time_dimension_name, time_granularity_name, entity_path, date_part_name - ) - ) - return TimeDimensionStub() diff --git a/dbt_semantic_interfaces/protocols/query_interface.py b/dbt_semantic_interfaces/protocols/query_interface.py deleted file mode 100644 index 70225450..00000000 --- a/dbt_semantic_interfaces/protocols/query_interface.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from typing import Optional, Protocol, Sequence - - -class QueryInterfaceMetric(Protocol): - """Represents the interface for Metric in the query interface.""" - - @abstractmethod - def descending(self, _is_descending: bool) -> QueryInterfaceMetric: - """Set the sort order for order-by.""" - pass - - -class QueryInterfaceDimension(Protocol): - """Represents the interface for Dimension in the query interface.""" - - @abstractmethod - def grain(self, _grain: str) -> QueryInterfaceDimension: - """The time granularity.""" - pass - - @abstractmethod - def descending(self, _is_descending: bool) -> QueryInterfaceDimension: - """Set the sort order for order-by.""" - pass - - @abstractmethod - def date_part(self, _date_part: str) -> QueryInterfaceDimension: - """Date part to extract from the dimension.""" - pass - - -class QueryInterfaceDimensionFactory(Protocol): - """Creates a Dimension for the query interface. - - Represented as the Dimension constructor in the Jinja sandbox. - """ - - @abstractmethod - def create(self, name: str, entity_path: Sequence[str] = ()) -> QueryInterfaceDimension: - """Create a QueryInterfaceDimension.""" - pass - - -class QueryInterfaceTimeDimension(Protocol): - """Represents the interface for TimeDimension in the query interface.""" - - pass - - -class QueryInterfaceTimeDimensionFactory(Protocol): - """Creates a TimeDimension for the query interface. - - Represented as the TimeDimension constructor in the Jinja sandbox. - """ - - @abstractmethod - def create( - self, - time_dimension_name: str, - time_granularity_name: str, - entity_path: Sequence[str] = (), - descending: Optional[bool] = None, - date_part_name: Optional[str] = None, - ) -> QueryInterfaceTimeDimension: - """Create a TimeDimension.""" - pass - - -class QueryInterfaceEntity(Protocol): - """Represents the interface for Entity in the query interface.""" - - pass - - -class QueryInterfaceEntityFactory(Protocol): - """Creates an Entity for the query interface. - - Represented as the Entity constructor in the Jinja sandbox. - """ - - @abstractmethod - def create(self, entity_name: str, entity_path: Sequence[str] = ()) -> QueryInterfaceEntity: - """Create an Entity.""" - pass - - -class QueryInterfaceMetricFactory(Protocol): - """Creates an Metric for the query interface. - - Represented as the Metric constructor in the Jinja sandbox. - """ - - @abstractmethod - def create(self, metric_name: str, group_by: Sequence[str] = ()) -> QueryInterfaceMetric: - """Create a Metric.""" - pass diff --git a/dbt_semantic_interfaces/type_enums/__init__.py b/dbt_semantic_interfaces/type_enums/__init__.py index 0389d2c4..727c34cb 100644 --- a/dbt_semantic_interfaces/type_enums/__init__.py +++ b/dbt_semantic_interfaces/type_enums/__init__.py @@ -4,6 +4,7 @@ from dbt_semantic_interfaces.type_enums.conversion_calculation_type import ( # noqa:F401 ConversionCalculationType, ) +from dbt_semantic_interfaces.type_enums.date_part import DatePart # noqa:F401 from dbt_semantic_interfaces.type_enums.dimension_type import DimensionType # noqa:F401 from dbt_semantic_interfaces.type_enums.entity_type import EntityType # noqa:F401 from dbt_semantic_interfaces.type_enums.metric_type import MetricType # noqa:F401 diff --git a/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/metrics.yaml b/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/metrics.yaml index 9b834a34..c9a4408d 100644 --- a/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/metrics.yaml +++ b/tests/fixtures/semantic_manifest_yamls/simple_semantic_manifest/metrics.yaml @@ -544,7 +544,7 @@ metric: type_params: measure: name: listings - filter: "{{ Metric('bookings', group_by=['listing']) }} > 2" + filter: "{{ Metric('bookings', sup=['listing']) }} > 2" --- metric: name: "active_listings"