From c833632025d30455149d0b6b24311c650639a16b Mon Sep 17 00:00:00 2001 From: Devon Fulcher <24593113+DevonFulcher@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:36:25 -0600 Subject: [PATCH] Add meta to measures, entities, and dimensions (#358) ### Description This PR is modeled off of [this](https://github.com/dbt-labs/dbt-semantic-interfaces/pull/250) prior PR. This PR adds meta to measures, entities, and dimensions. ### Checklist - [x] I have read [the contributing guide](https://github.com/dbt-labs/dbt-semantic-interfaces/blob/main/CONTRIBUTING.md) and understand what's expected of me - [x] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements) - [x] This PR includes tests, or tests are not required/relevant for this PR - [x] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-semantic-interfaces/blob/main/CONTRIBUTING.md#adding-a-changelog-entry) --- .../unreleased/Features-20241023-113450.yaml | 6 +++ .../implementations/element_config.py | 18 +++++++ .../implementations/elements/dimension.py | 4 ++ .../implementations/elements/entity.py | 4 ++ .../implementations/elements/measure.py | 4 ++ .../default_explicit_schema.json | 48 +++++++++++++++++++ dbt_semantic_interfaces/parsing/schemas.py | 34 +++++++++++++ .../protocols/dimension.py | 6 +++ dbt_semantic_interfaces/protocols/entity.py | 6 +++ dbt_semantic_interfaces/protocols/measure.py | 6 +++ dbt_semantic_interfaces/protocols/meta.py | 12 +++++ tests/parsing/test_semantic_model_parsing.py | 11 ++++- tests/test_implements_satisfy_protocols.py | 7 +++ 13 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 .changes/unreleased/Features-20241023-113450.yaml create mode 100644 dbt_semantic_interfaces/implementations/element_config.py create mode 100644 dbt_semantic_interfaces/protocols/meta.py diff --git a/.changes/unreleased/Features-20241023-113450.yaml b/.changes/unreleased/Features-20241023-113450.yaml new file mode 100644 index 00000000..0eda8b80 --- /dev/null +++ b/.changes/unreleased/Features-20241023-113450.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Support meta for dimensions, entities, and measures +time: 2024-10-23T11:34:50.577294-05:00 +custom: + Author: DevonFulcher + Issue: None diff --git a/dbt_semantic_interfaces/implementations/element_config.py b/dbt_semantic_interfaces/implementations/element_config.py new file mode 100644 index 00000000..d8ec41dd --- /dev/null +++ b/dbt_semantic_interfaces/implementations/element_config.py @@ -0,0 +1,18 @@ +from typing import Any, Dict + +from pydantic import Field +from typing_extensions import override + +from dbt_semantic_interfaces.implementations.base import HashableBaseModel +from dbt_semantic_interfaces.protocols.meta import SemanticLayerElementConfig +from dbt_semantic_interfaces.protocols.protocol_hint import ProtocolHint + + +class PydanticSemanticLayerElementConfig(HashableBaseModel, ProtocolHint[SemanticLayerElementConfig]): + """PydanticDimension config.""" + + @override + def _implements_protocol(self) -> SemanticLayerElementConfig: # noqa: D + return self + + meta: Dict[str, Any] = Field(default_factory=dict) diff --git a/dbt_semantic_interfaces/implementations/elements/dimension.py b/dbt_semantic_interfaces/implementations/elements/dimension.py index c3cfa2e4..a4cb5a03 100644 --- a/dbt_semantic_interfaces/implementations/elements/dimension.py +++ b/dbt_semantic_interfaces/implementations/elements/dimension.py @@ -6,6 +6,9 @@ HashableBaseModel, ModelWithMetadataParsing, ) +from dbt_semantic_interfaces.implementations.element_config import ( + PydanticSemanticLayerElementConfig, +) from dbt_semantic_interfaces.implementations.metadata import PydanticMetadata from dbt_semantic_interfaces.references import ( DimensionReference, @@ -48,6 +51,7 @@ class PydanticDimension(HashableBaseModel, ModelWithMetadataParsing): expr: Optional[str] = None metadata: Optional[PydanticMetadata] label: Optional[str] = None + config: Optional[PydanticSemanticLayerElementConfig] @property def reference(self) -> DimensionReference: # noqa: D diff --git a/dbt_semantic_interfaces/implementations/elements/entity.py b/dbt_semantic_interfaces/implementations/elements/entity.py index ac98e4db..9a6557e8 100644 --- a/dbt_semantic_interfaces/implementations/elements/entity.py +++ b/dbt_semantic_interfaces/implementations/elements/entity.py @@ -6,6 +6,9 @@ HashableBaseModel, ModelWithMetadataParsing, ) +from dbt_semantic_interfaces.implementations.element_config import ( + PydanticSemanticLayerElementConfig, +) from dbt_semantic_interfaces.implementations.metadata import PydanticMetadata from dbt_semantic_interfaces.references import EntityReference from dbt_semantic_interfaces.type_enums import EntityType @@ -21,6 +24,7 @@ class PydanticEntity(HashableBaseModel, ModelWithMetadataParsing): expr: Optional[str] = None metadata: Optional[PydanticMetadata] = None label: Optional[str] = None + config: Optional[PydanticSemanticLayerElementConfig] @property def reference(self) -> EntityReference: # noqa: D diff --git a/dbt_semantic_interfaces/implementations/elements/measure.py b/dbt_semantic_interfaces/implementations/elements/measure.py index 5da1da4f..f3c88c00 100644 --- a/dbt_semantic_interfaces/implementations/elements/measure.py +++ b/dbt_semantic_interfaces/implementations/elements/measure.py @@ -6,6 +6,9 @@ HashableBaseModel, ModelWithMetadataParsing, ) +from dbt_semantic_interfaces.implementations.element_config import ( + PydanticSemanticLayerElementConfig, +) from dbt_semantic_interfaces.implementations.metadata import PydanticMetadata from dbt_semantic_interfaces.references import MeasureReference from dbt_semantic_interfaces.type_enums import AggregationType @@ -46,6 +49,7 @@ class PydanticMeasure(HashableBaseModel, ModelWithMetadataParsing): non_additive_dimension: Optional[PydanticNonAdditiveDimensionParameters] = None agg_time_dimension: Optional[str] = None label: Optional[str] = None + config: Optional[PydanticSemanticLayerElementConfig] = None @property def reference(self) -> MeasureReference: # noqa: D diff --git a/dbt_semantic_interfaces/parsing/generated_json_schemas/default_explicit_schema.json b/dbt_semantic_interfaces/parsing/generated_json_schemas/default_explicit_schema.json index 62b682a6..5e247886 100644 --- a/dbt_semantic_interfaces/parsing/generated_json_schemas/default_explicit_schema.json +++ b/dbt_semantic_interfaces/parsing/generated_json_schemas/default_explicit_schema.json @@ -112,6 +112,19 @@ ], "type": "object" }, + "dimension_config_schema": { + "$id": "dimension_config_schema", + "additionalProperties": false, + "properties": { + "meta": { + "propertyNames": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "dimension_schema": { "$id": "dimension_schema", "additionalProperties": false, @@ -128,6 +141,9 @@ } ], "properties": { + "config": { + "$ref": "#/definitions/dimension_config_schema" + }, "description": { "type": "string" }, @@ -204,10 +220,26 @@ ], "type": "object" }, + "entity_config_schema": { + "$id": "entity_config_schema", + "additionalProperties": false, + "properties": { + "meta": { + "propertyNames": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "entity_schema": { "$id": "entity_schema", "additionalProperties": false, "properties": { + "config": { + "$ref": "#/definitions/entity_config_schema" + }, "entity": { "type": "string" }, @@ -314,6 +346,19 @@ "type" ] }, + "measure_config_schema": { + "$id": "measure_config_schema", + "additionalProperties": false, + "properties": { + "meta": { + "propertyNames": { + "type": "string" + }, + "type": "object" + } + }, + "type": "object" + }, "measure_schema": { "$id": "measure_schema", "additionalProperties": false, @@ -347,6 +392,9 @@ "pattern": "(?!.*__).*^[a-z][a-z0-9_]*[a-z0-9]$", "type": "string" }, + "config": { + "$ref": "#/definitions/measure_config_schema" + }, "create_metric": { "type": "boolean" }, diff --git a/dbt_semantic_interfaces/parsing/schemas.py b/dbt_semantic_interfaces/parsing/schemas.py index 16d1e8bd..21ff8d3e 100644 --- a/dbt_semantic_interfaces/parsing/schemas.py +++ b/dbt_semantic_interfaces/parsing/schemas.py @@ -161,6 +161,16 @@ "additionalProperties": False, } + +entity_config_schema = { + "$id": "entity_config_schema", + "type": "object", + "properties": { + "meta": {"type": "object", "propertyNames": {"type": "string"}}, + }, + "additionalProperties": False, +} + entity_schema = { "$id": "entity_schema", "type": "object", @@ -174,6 +184,7 @@ "expr": {"type": ["string", "boolean"]}, "entity": {"type": "string"}, "label": {"type": "string"}, + "config": {"$ref": "entity_config_schema"}, }, "additionalProperties": False, "required": ["name", "type"], @@ -226,6 +237,15 @@ "additionalProperties": False, } +measure_config_schema = { + "$id": "measure_config_schema", + "type": "object", + "properties": { + "meta": {"type": "object", "propertyNames": {"type": "string"}}, + }, + "additionalProperties": False, +} + measure_schema = { "$id": "measure_schema", "type": "object", @@ -248,11 +268,21 @@ }, "description": {"type": "string"}, "label": {"type": "string"}, + "config": {"$ref": "measure_config_schema"}, }, "additionalProperties": False, "required": ["name", "agg"], } +dimension_config_schema = { + "$id": "dimension_config_schema", + "type": "object", + "properties": { + "meta": {"type": "object", "propertyNames": {"type": "string"}}, + }, + "additionalProperties": False, +} + dimension_schema = { "$id": "dimension_schema", "type": "object", @@ -267,6 +297,7 @@ "expr": {"type": ["string", "boolean"]}, "type_params": {"$ref": "dimension_type_params_schema"}, "label": {"type": "string"}, + "config": {"$ref": "dimension_config_schema"}, }, # dimension must have type_params if its a time dimension "anyOf": [{"not": {"$ref": "#/definitions/is-time-dimension"}}, {"required": ["type_params"]}], @@ -529,6 +560,9 @@ saved_query_query_params_schema["$id"]: saved_query_query_params_schema, semantic_model_config_schema["$id"]: semantic_model_config_schema, metric_config_schema["$id"]: metric_config_schema, + dimension_config_schema["$id"]: dimension_config_schema, + entity_config_schema["$id"]: entity_config_schema, + measure_config_schema["$id"]: measure_config_schema, } resources: List[Tuple[str, Resource]] = [(str(k), DRAFT7.create_resource(v)) for k, v in schema_store.items()] diff --git a/dbt_semantic_interfaces/protocols/dimension.py b/dbt_semantic_interfaces/protocols/dimension.py index 5cf8cbfe..f0ac3b67 100644 --- a/dbt_semantic_interfaces/protocols/dimension.py +++ b/dbt_semantic_interfaces/protocols/dimension.py @@ -3,6 +3,7 @@ from abc import abstractmethod from typing import Optional, Protocol +from dbt_semantic_interfaces.protocols.meta import SemanticLayerElementConfig from dbt_semantic_interfaces.protocols.metadata import Metadata from dbt_semantic_interfaces.references import ( DimensionReference, @@ -107,3 +108,8 @@ def validity_params(self) -> Optional[DimensionValidityParams]: def label(self) -> Optional[str]: """Returns a string representing a human readable label for the dimension.""" pass + + @property + @abstractmethod + def config(self) -> Optional[SemanticLayerElementConfig]: # noqa: D + pass diff --git a/dbt_semantic_interfaces/protocols/entity.py b/dbt_semantic_interfaces/protocols/entity.py index 356e6803..27719d1a 100644 --- a/dbt_semantic_interfaces/protocols/entity.py +++ b/dbt_semantic_interfaces/protocols/entity.py @@ -3,6 +3,7 @@ from abc import abstractmethod from typing import Optional, Protocol +from dbt_semantic_interfaces.protocols.meta import SemanticLayerElementConfig from dbt_semantic_interfaces.references import EntityReference from dbt_semantic_interfaces.type_enums import EntityType @@ -60,3 +61,8 @@ def is_linkable_entity_type(self) -> bool: def label(self) -> Optional[str]: """Returns a string representing a human readable label for the entity.""" pass + + @property + @abstractmethod + def config(self) -> Optional[SemanticLayerElementConfig]: # noqa: D + pass diff --git a/dbt_semantic_interfaces/protocols/measure.py b/dbt_semantic_interfaces/protocols/measure.py index a7588cfb..7d8606f8 100644 --- a/dbt_semantic_interfaces/protocols/measure.py +++ b/dbt_semantic_interfaces/protocols/measure.py @@ -3,6 +3,7 @@ from abc import abstractmethod from typing import Optional, Protocol, Sequence +from dbt_semantic_interfaces.protocols.meta import SemanticLayerElementConfig from dbt_semantic_interfaces.references import MeasureReference from dbt_semantic_interfaces.type_enums import AggregationType @@ -98,3 +99,8 @@ def reference(self) -> MeasureReference: def label(self) -> Optional[str]: """Returns a string representing a human readable label for the measure.""" pass + + @property + @abstractmethod + def config(self) -> Optional[SemanticLayerElementConfig]: # noqa: D + pass diff --git a/dbt_semantic_interfaces/protocols/meta.py b/dbt_semantic_interfaces/protocols/meta.py new file mode 100644 index 00000000..08a1eef8 --- /dev/null +++ b/dbt_semantic_interfaces/protocols/meta.py @@ -0,0 +1,12 @@ +from abc import abstractmethod +from typing import Any, Dict, Protocol + + +class SemanticLayerElementConfig(Protocol): # noqa: D + """The config property allows you to configure additional resources/metadata.""" + + @property + @abstractmethod + def meta(self) -> Dict[str, Any]: + """The meta field can be used to set metadata for a resource.""" + pass diff --git a/tests/parsing/test_semantic_model_parsing.py b/tests/parsing/test_semantic_model_parsing.py index 8faec41f..34ad851f 100644 --- a/tests/parsing/test_semantic_model_parsing.py +++ b/tests/parsing/test_semantic_model_parsing.py @@ -110,7 +110,9 @@ def test_base_semantic_model_entity_parsing() -> None: role: test_role expr: example_id label: {label} - + config: + meta: + random: metadata """ ) file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents) @@ -206,7 +208,9 @@ def test_base_semantic_model_measure_parsing() -> None: expr: example_input description: {description} label: {label} - + config: + meta: + random: metadata """ ) file = YamlConfigFile(filepath="test_dir/inline_for_test", contents=yaml_contents) @@ -325,6 +329,9 @@ def test_semantic_model_categorical_dimension_parsing() -> None: - name: example_categorical_dimension type: categorical expr: dimension_input + config: + meta: + random: metadata """ ) file = YamlConfigFile(filepath="inline_for_test", contents=yaml_contents) diff --git a/tests/test_implements_satisfy_protocols.py b/tests/test_implements_satisfy_protocols.py index b37b5ca7..0c4ec8e7 100644 --- a/tests/test_implements_satisfy_protocols.py +++ b/tests/test_implements_satisfy_protocols.py @@ -3,6 +3,9 @@ from hypothesis import given from hypothesis.strategies import booleans, builds, from_type, just, lists, none, text +from dbt_semantic_interfaces.implementations.element_config import ( + PydanticSemanticLayerElementConfig, +) from dbt_semantic_interfaces.implementations.elements.dimension import ( PydanticDimension, PydanticDimensionTypeParams, @@ -66,6 +69,7 @@ expr=OPTIONAL_STR_STRATEGY, metadata=OPTIONAL_METADATA_STRATEGY, label=OPTIONAL_STR_STRATEGY, + config=builds(PydanticSemanticLayerElementConfig), ) DIMENSION_VALIDITY_PARAMS_STRATEGY = builds( @@ -82,6 +86,7 @@ expr=OPTIONAL_STR_STRATEGY, metadata=OPTIONAL_METADATA_STRATEGY, label=OPTIONAL_STR_STRATEGY, + config=builds(PydanticSemanticLayerElementConfig), ) DIMENSION_STRATEGY = TIME_DIMENSION_STRATEGY | CATEGORICAL_DIMENSION_STRATEGY @@ -93,6 +98,7 @@ expr=OPTIONAL_STR_STRATEGY, metadata=OPTIONAL_METADATA_STRATEGY, label=OPTIONAL_STR_STRATEGY, + config=builds(PydanticSemanticLayerElementConfig), ) MEASURE_STRATEGY = builds( @@ -104,6 +110,7 @@ non_additive_dimesnion=builds(PydanticNonAdditiveDimensionParameters) | none(), agg_time_dimension=OPTIONAL_STR_STRATEGY, label=OPTIONAL_STR_STRATEGY, + config=builds(PydanticSemanticLayerElementConfig), ) SEMANTIC_MODEL_STRATEGY = builds(