From 30e9a029db8619641a3bc4aa22f7e47aef402afc Mon Sep 17 00:00:00 2001 From: Courtney Holcomb Date: Tue, 29 Aug 2023 10:50:59 -0700 Subject: [PATCH] Expose measures for metrics on MFEngine (#735) --- .../unreleased/Features-20230823-131738.yaml | 6 ++++ metricflow/engine/metricflow_engine.py | 30 +++++++++++++++++-- metricflow/engine/models.py | 14 +++++++++ metricflow/test/api/test_metricflow_client.py | 26 ++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Features-20230823-131738.yaml diff --git a/.changes/unreleased/Features-20230823-131738.yaml b/.changes/unreleased/Features-20230823-131738.yaml new file mode 100644 index 0000000000..7760c98446 --- /dev/null +++ b/.changes/unreleased/Features-20230823-131738.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Expose measures for metrics on MFEngine with agg_time_dimension +time: 2023-08-23T13:17:38.517068-07:00 +custom: + Author: courtneyholcomb + Issue: "735" diff --git a/metricflow/engine/metricflow_engine.py b/metricflow/engine/metricflow_engine.py index 92a34ee25f..a703fba8d9 100644 --- a/metricflow/engine/metricflow_engine.py +++ b/metricflow/engine/metricflow_engine.py @@ -10,7 +10,7 @@ import pandas as pd from dbt_semantic_interfaces.implementations.elements.dimension import PydanticDimensionTypeParams from dbt_semantic_interfaces.pretty_print import pformat_big_objects -from dbt_semantic_interfaces.references import EntityReference, MetricReference +from dbt_semantic_interfaces.references import EntityReference, MeasureReference, MetricReference from dbt_semantic_interfaces.type_enums import DimensionType from metricflow.assert_one_arg import assert_exactly_one_arg_set @@ -27,7 +27,7 @@ from metricflow.dataset.convert_semantic_model import SemanticModelToDataSetConverter from metricflow.dataset.dataset import DataSet from metricflow.dataset.semantic_model_adapter import SemanticModelDataSet -from metricflow.engine.models import Dimension, Entity, Metric +from metricflow.engine.models import Dimension, Entity, Measure, Metric from metricflow.engine.time_source import ServerTimeSource from metricflow.errors.errors import ExecutionException from metricflow.execution.execution_plan import ExecutionPlan, SqlQuery @@ -494,6 +494,32 @@ def _create_execution_plan(self, mf_query_request: MetricFlowQueryRequest) -> Me def explain(self, mf_request: MetricFlowQueryRequest) -> MetricFlowExplainResult: # noqa: D return self._create_execution_plan(mf_request) + def get_measures_for_metrics(self, metric_names: List[str]) -> List[Measure]: # noqa: D + metrics = self._semantic_manifest_lookup.metric_lookup.get_metrics( + metric_references=[MetricReference(element_name=metric_name) for metric_name in metric_names] + ) + semantic_model_lookup = self._semantic_manifest_lookup.semantic_model_lookup + + measures = set() + for metric in metrics: + for input_measure in metric.input_measures: + measure_reference = MeasureReference(element_name=input_measure.name) + # populate new obj + measure = semantic_model_lookup.get_measure(measure_reference=measure_reference) + measures.add( + Measure( + name=measure.name, + agg=measure.agg, + agg_time_dimension=semantic_model_lookup.get_agg_time_dimension_for_measure( + measure_reference=measure_reference + ).element_name, + description=measure.description, + expr=measure.expr, + agg_params=measure.agg_params, + ) + ) + return list(measures) + def simple_dimensions_for_metrics( # noqa: D self, metric_names: List[str], diff --git a/metricflow/engine/models.py b/metricflow/engine/models.py index 2b07ba7700..cefbf73e85 100644 --- a/metricflow/engine/models.py +++ b/metricflow/engine/models.py @@ -12,11 +12,13 @@ DimensionTypeParams, ) from dbt_semantic_interfaces.protocols.entity import Entity as SemanticManifestEntity +from dbt_semantic_interfaces.protocols.measure import MeasureAggregationParameters from dbt_semantic_interfaces.protocols.metadata import Metadata from dbt_semantic_interfaces.protocols.metric import Metric as SemanticManifestMetric from dbt_semantic_interfaces.protocols.metric import MetricInputMeasure, MetricType, MetricTypeParams from dbt_semantic_interfaces.protocols.where_filter import WhereFilter from dbt_semantic_interfaces.transformations.add_input_metric_measures import AddInputMetricMeasuresRule +from dbt_semantic_interfaces.type_enums.aggregation_type import AggregationType from dbt_semantic_interfaces.type_enums.entity_type import EntityType from metricflow.model.semantics.linkable_spec_resolver import ElementPathKey @@ -133,3 +135,15 @@ def from_pydantic(cls, pydantic_entity: SemanticManifestEntity) -> Entity: role=pydantic_entity.role, expr=pydantic_entity.expr, ) + + +@dataclass(frozen=True) +class Measure: + """Dataclass representation of a Measure.""" + + name: str + agg: AggregationType + agg_time_dimension: str + description: Optional[str] = None + expr: Optional[str] = None + agg_params: Optional[MeasureAggregationParameters] = None diff --git a/metricflow/test/api/test_metricflow_client.py b/metricflow/test/api/test_metricflow_client.py index 1588d4b059..fcc6cb7bb2 100644 --- a/metricflow/test/api/test_metricflow_client.py +++ b/metricflow/test/api/test_metricflow_client.py @@ -93,6 +93,32 @@ def test_list_dimensions(mf_client: MetricFlowClient) -> None: # noqa: D assert tuple(dim.name for dim in dimensions) == ("metric_time", "metric_time") +def test_get_measures_for_metrics(mf_client: MetricFlowClient) -> None: # noqa: D + measures = mf_client.engine.get_measures_for_metrics(["bookings"]) + assert len(measures) == 1 + measure = measures[0] + assert measure.name == "bookings" + assert measure.agg_time_dimension == "ds" + + # Multiple metrics + measures = mf_client.engine.get_measures_for_metrics(["bookings", "revenue"]) + assert len(measures) == 2 + assert {measure.name for measure in measures} == {"bookings", "txn_revenue"} + assert {measure.agg_time_dimension for measure in measures} == {"ds"} + + # Derived metric with multiple metric inputs + measures = mf_client.engine.get_measures_for_metrics(["views_times_booking_value"]) + assert len(measures) == 2 + assert {measure.name for measure in measures} == {"views", "booking_value"} + assert {measure.agg_time_dimension for measure in measures} == {"ds"} + + # Ratio metric with multiple measure inputs + measures = mf_client.engine.get_measures_for_metrics(["bookings_per_booker"]) + assert len(measures) == 2 + assert {measure.name for measure in measures} == {"bookings", "bookers"} + assert {measure.agg_time_dimension for measure in measures} == {"ds"} + + def test_get_dimension_values(mf_client: MetricFlowClient) -> None: # noqa: D dim_vals = mf_client.get_dimension_values( ["bookings"], "metric_time", start_time="2020-01-01", end_time="2024-01-01"