-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Finalize validations for custom granularities #370
Changes from 5 commits
a968ffd
2083a24
00abab6
9151740
825622c
e0fa2b1
99c6043
cbdef95
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,12 @@ | ||
import traceback | ||
from typing import Dict, Generic, List, Optional, Sequence | ||
|
||
from dbt_semantic_interfaces.errors import ParsingException | ||
from dbt_semantic_interfaces.implementations.metric import ( | ||
PydanticMetric, | ||
PydanticMetricTimeWindow, | ||
) | ||
from dbt_semantic_interfaces.implementations.metric import PydanticMetric | ||
from dbt_semantic_interfaces.protocols import ( | ||
ConversionTypeParams, | ||
Dimension, | ||
Metric, | ||
MetricInputMeasure, | ||
MetricTimeWindow, | ||
SemanticManifest, | ||
SemanticManifestT, | ||
SemanticModel, | ||
|
@@ -42,20 +38,23 @@ | |
WhereFiltersAreParseable, # noQa | ||
) | ||
|
||
TEMP_CUSTOM_GRAIN_MSG = "Custom granularities are not supported for this field yet." | ||
|
||
|
||
class CumulativeMetricRule(SemanticManifestValidationRule[SemanticManifestT], Generic[SemanticManifestT]): | ||
"""Checks that cumulative metrics are configured properly.""" | ||
|
||
@staticmethod | ||
@classmethod | ||
@validate_safely(whats_being_done="running model validation ensuring cumulative metrics are valid") | ||
def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D | ||
def validate_manifest(cls, semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D | ||
issues: List[ValidationIssue] = [] | ||
|
||
custom_granularity_names = [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit / premature optimization - why not make this a set here, rather than making it a set inside validate_metric_time_window, which is called every iteration of the for loop in this function? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good idea! |
||
granularity.name | ||
for time_spine in semantic_manifest.project_configuration.time_spines | ||
for granularity in time_spine.custom_granularities | ||
] | ||
standard_granularities = {item.value.lower() for item in TimeGranularity} | ||
|
||
for metric in semantic_manifest.metrics or []: | ||
if metric.type != MetricType.CUMULATIVE: | ||
|
@@ -100,6 +99,18 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati | |
grain_to_date = metric.type_params.grain_to_date.value if metric.type_params.grain_to_date else None | ||
if metric.type_params.cumulative_type_params and metric.type_params.cumulative_type_params.grain_to_date: | ||
grain_to_date = metric.type_params.cumulative_type_params.grain_to_date | ||
|
||
if grain_to_date and grain_to_date not in standard_granularities: | ||
issues.append( | ||
ValidationError( | ||
context=metric_context, | ||
message=( | ||
f"Invalid time granularity found in `grain_to_date`: '{grain_to_date}'. " | ||
f"{TEMP_CUSTOM_GRAIN_MSG}" | ||
), | ||
) | ||
) | ||
|
||
if window and grain_to_date: | ||
issues.append( | ||
ValidationError( | ||
|
@@ -109,19 +120,44 @@ def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[Validati | |
) | ||
|
||
if window: | ||
try: | ||
# TODO: Should not call an implementation class. | ||
PydanticMetricTimeWindow.parse( | ||
window=window.window_string, custom_granularity_names=custom_granularity_names | ||
) | ||
except ParsingException as e: | ||
issues.append( | ||
ValidationError( | ||
context=metric_context, | ||
message="".join(traceback.format_exception_only(type(e), value=e)), | ||
extra_detail="".join(traceback.format_tb(e.__traceback__)), | ||
) | ||
) | ||
issues += cls.validate_metric_time_window( | ||
metric_context=metric_context, window=window, custom_granularities=custom_granularity_names | ||
) | ||
|
||
return issues | ||
|
||
@classmethod | ||
def validate_metric_time_window( # noqa: D | ||
cls, | ||
metric_context: MetricContext, | ||
window: MetricTimeWindow, | ||
custom_granularities: Sequence[str], | ||
allow_custom: bool = False, | ||
) -> Sequence[ValidationIssue]: | ||
issues: List[ValidationIssue] = [] | ||
|
||
standard_granularities = {item.value.lower() for item in TimeGranularity} | ||
valid_granularities = set(custom_granularities) | standard_granularities | ||
window_granularity = window.granularity | ||
if window_granularity.endswith("s") and window_granularity[:-1] in valid_granularities: | ||
# months -> month | ||
window_granularity = window_granularity[:-1] | ||
|
||
msg = f"Invalid time granularity '{window_granularity}' in window: '{window.window_string}'" | ||
if window_granularity not in valid_granularities: | ||
issues.append( | ||
ValidationError( | ||
context=metric_context, | ||
message=msg, | ||
) | ||
) | ||
elif not allow_custom and (window_granularity not in standard_granularities): | ||
issues.append( | ||
ValidationError( | ||
context=metric_context, | ||
message=msg + " " + TEMP_CUSTOM_GRAIN_MSG, | ||
) | ||
) | ||
|
||
return issues | ||
|
||
|
@@ -188,17 +224,37 @@ def _validate_input_metrics_exist(semantic_manifest: SemanticManifest) -> List[V | |
|
||
@staticmethod | ||
@validate_safely(whats_being_done="checking that input metric time offset params are valid") | ||
def _validate_time_offset_params(metric: Metric) -> List[ValidationIssue]: | ||
def _validate_time_offset_params(metric: Metric, custom_granularities: Sequence[str]) -> List[ValidationIssue]: | ||
issues: List[ValidationIssue] = [] | ||
|
||
standard_granularities = {item.value.lower() for item in TimeGranularity} | ||
|
||
metric_context = MetricContext( | ||
file_context=FileContext.from_metadata(metadata=metric.metadata), | ||
metric=MetricModelReference(metric_name=metric.name), | ||
) | ||
for input_metric in metric.type_params.metrics or []: | ||
if input_metric.offset_window and input_metric.offset_to_grain: | ||
if input_metric.offset_window: | ||
issues += CumulativeMetricRule.validate_metric_time_window( | ||
metric_context=metric_context, | ||
window=input_metric.offset_window, | ||
custom_granularities=custom_granularities, | ||
allow_custom=True, | ||
) | ||
if input_metric.offset_to_grain and input_metric.offset_to_grain not in standard_granularities: | ||
issues.append( | ||
ValidationError( | ||
context=MetricContext( | ||
file_context=FileContext.from_metadata(metadata=metric.metadata), | ||
metric=MetricModelReference(metric_name=metric.name), | ||
context=metric_context, | ||
message=( | ||
f"Invalid time granularity found in `offset_to_grain`: '{input_metric.offset_to_grain}'. " | ||
f"{TEMP_CUSTOM_GRAIN_MSG}" | ||
), | ||
) | ||
) | ||
if input_metric.offset_window and input_metric.offset_to_grain: | ||
issues.append( | ||
ValidationError( | ||
context=metric_context, | ||
message=f"Both offset_window and offset_to_grain set for derived metric '{metric.name}' on " | ||
f"input metric '{input_metric.name}'. Please set one or the other.", | ||
) | ||
|
@@ -247,10 +303,18 @@ def _validate_expr(metric: Metric) -> List[ValidationIssue]: | |
def validate_manifest(semantic_manifest: SemanticManifestT) -> Sequence[ValidationIssue]: # noqa: D | ||
issues: List[ValidationIssue] = [] | ||
|
||
custom_granularity_names = [ | ||
granularity.name | ||
for time_spine in semantic_manifest.project_configuration.time_spines | ||
for granularity in time_spine.custom_granularities | ||
] | ||
|
||
issues += DerivedMetricRule._validate_input_metrics_exist(semantic_manifest=semantic_manifest) | ||
for metric in semantic_manifest.metrics or []: | ||
issues += DerivedMetricRule._validate_alias_collision(metric=metric) | ||
issues += DerivedMetricRule._validate_time_offset_params(metric=metric) | ||
issues += DerivedMetricRule._validate_time_offset_params( | ||
metric=metric, custom_granularities=custom_granularity_names | ||
) | ||
issues += DerivedMetricRule._validate_expr(metric=metric) | ||
return issues | ||
|
||
|
@@ -267,20 +331,15 @@ def _validate_type_params( | |
|
||
window = conversion_type_params.window | ||
if window: | ||
try: | ||
window_str = f"{window.count} {window.granularity}" | ||
PydanticMetricTimeWindow.parse(window=window_str, custom_granularity_names=custom_granularity_names) | ||
except ParsingException as e: | ||
issues.append( | ||
ValidationError( | ||
context=MetricContext( | ||
file_context=FileContext.from_metadata(metadata=metric.metadata), | ||
metric=MetricModelReference(metric_name=metric.name), | ||
), | ||
message="".join(traceback.format_exception_only(type(e), value=e)), | ||
extra_detail="".join(traceback.format_tb(e.__traceback__)), | ||
) | ||
) | ||
issues += CumulativeMetricRule.validate_metric_time_window( | ||
metric_context=MetricContext( | ||
file_context=FileContext.from_metadata(metadata=metric.metadata), | ||
metric=MetricModelReference(metric_name=metric.name), | ||
), | ||
window=window, | ||
custom_granularities=custom_granularity_names, | ||
) | ||
|
||
return issues | ||
|
||
@staticmethod | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we lowercase window here too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could but I think the way it's implemented now is less hacky! This one feels less optimal because we have to go through these dict keys that are not typed. But I couldn't use the same strategy as we did for window because grain is just a string, not an object.