Repository: dbt-labs/dbt-core

Issue:
Add Window Attribute for Metrics
resolves #5721 

### Description

Adding the attribute `window` to the metric definition as an optional string. No tests have been added yet as this is merely a draft.

### Produced Manifest

I tested this out in a development project and was able to confirm that `"window": "14 days"` was correctly represented in the single metric where it was defined. For all others `"window": ""` was present.

### Checklist

- [X] I have read [the contributing guide](https://github.com/dbt-labs/dbt-core/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] I have run this code in development and it appears to resolve the stated issue
- [x] This PR includes tests, or tests are not required/relevant for this PR
- [ ] I have [opened an issue to add/update docs](https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose), or docs changes are not required/relevant for this PR
- [x] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#Adding-CHANGELOG-Entry)



Discussion:
Thank you for your pull request! We could not find a changelog entry for this change. For details on how to document a change, see [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#adding-changelog-entry).
@callum-mcdata once you are ready for an Engineering team member to review (test passing, name solidified), feel free to add the `ready_for_review` label to the PR so we know to take a look
Hey @leahwicz this PR is ready for review! I updated the tests to ensure that they pass (ie adding `window` into the pre-created dicts that we compare against). If the PR gets the 🟢 from the core team I'll make the corresponding updates to the docs repo before we merge! 
Thanks @callum-mcdata! I just added @emmyoop as a reviewer since she is on support rotation right now
@emmyoop made the changes that you mentioned in above resolved conversations!
@gshank could I get approval to merge in this PR? Looks like I'm not an authorized user to merge into `main`

Resolving patch:
diff --git a/core/dbt/contracts/graph/parsed.py b/core/dbt/contracts/graph/parsed.py
--- a/core/dbt/contracts/graph/parsed.py
+++ b/core/dbt/contracts/graph/parsed.py
@@ -37,6 +37,7 @@
     ExposureType,
     MaturityType,
     MetricFilter,
+    MetricTime,
 )
 from dbt.contracts.util import Replaceable, AdditionalPropertiesMixin
 from dbt.exceptions import warn_or_error
@@ -812,7 +813,7 @@ class ParsedMetric(UnparsedBaseNode, HasUniqueID, HasFqn):
     filters: List[MetricFilter]
     time_grains: List[str]
     dimensions: List[str]
-    window: Optional[str]
+    window: Optional[MetricTime]
     model: Optional[str] = None
     model_unique_id: Optional[str] = None
     resource_type: NodeType = NodeType.Metric
diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py
--- a/core/dbt/contracts/graph/unparsed.py
+++ b/core/dbt/contracts/graph/unparsed.py
@@ -444,6 +444,25 @@ class MetricFilter(dbtClassMixin, Replaceable):
     value: str
 
 
+class MetricTimePeriod(StrEnum):
+    day = "day"
+    week = "week"
+    month = "month"
+    year = "year"
+
+    def plural(self) -> str:
+        return str(self) + "s"
+
+
+@dataclass
+class MetricTime(dbtClassMixin, Mergeable):
+    count: Optional[int] = None
+    period: Optional[MetricTimePeriod] = None
+
+    def __bool__(self):
+        return self.count is not None and self.period is not None
+
+
 @dataclass
 class UnparsedMetric(dbtClassMixin, Replaceable):
     # TODO : verify that this disallows metric names with spaces
@@ -457,8 +476,8 @@ class UnparsedMetric(dbtClassMixin, Replaceable):
     expression: Union[str, int] = ""
     time_grains: List[str] = field(default_factory=list)
     dimensions: List[str] = field(default_factory=list)
+    window: Optional[MetricTime] = None
     model: Optional[str] = None
-    window: Optional[str] = None
     filters: List[MetricFilter] = field(default_factory=list)
     meta: Dict[str, Any] = field(default_factory=dict)
     tags: List[str] = field(default_factory=list)


Test patch:
diff --git a/test/unit/test_contracts_graph_unparsed.py b/test/unit/test_contracts_graph_unparsed.py
--- a/test/unit/test_contracts_graph_unparsed.py
+++ b/test/unit/test_contracts_graph_unparsed.py
@@ -7,7 +7,7 @@
     FreshnessThreshold, Quoting, UnparsedSourceDefinition,
     UnparsedSourceTableDefinition, UnparsedDocumentationFile, UnparsedColumn,
     UnparsedNodeUpdate, Docs, UnparsedExposure, MaturityType, ExposureOwner,
-    ExposureType, UnparsedMetric, MetricFilter
+    ExposureType, UnparsedMetric, MetricFilter, MetricTime, MetricTimePeriod
 )
 from dbt.contracts.results import FreshnessStatus
 from dbt.node_types import NodeType
@@ -697,7 +697,11 @@ def get_ok_dict(self):
                     "operator": "=",
                 }
             ],
-            'window': '14 days',
+            'window': {
+                    "count": 14,
+                    "period": "day"
+                }
+            ,
             'tags': [],
             'meta': {
                 'is_okr': True
@@ -715,8 +719,8 @@ def get_ok_derived_dict(self):
             'timestamp': 'signup_date',
             'dimensions': [],
             'filters': [],
-            'window': '',
             'tags': [],
+            'window': {},
             'meta': {
                 'is_okr': True
             },
@@ -738,7 +742,10 @@ def test_ok(self):
                 value='True',
                 operator="=",
             )],
-            window="14 days",
+            window=MetricTime(
+                count=14,
+                period=MetricTimePeriod.day
+            ),
             meta={'is_okr': True},
         )
         dct = self.get_ok_dict()
@@ -756,8 +763,8 @@ def test_ok_metric_no_model(self):
             expression="{{ metric('revenue') }} / {{ metric('customers') }}",
             timestamp="signup_date",
             time_grains=['day', 'week', 'month'],
+            window=MetricTime(),
             dimensions=[],
-            window='',
             meta={'is_okr': True},
         )
         dct = self.get_ok_derived_dict()
diff --git a/test/unit/test_graph_selector_methods.py b/test/unit/test_graph_selector_methods.py
--- a/test/unit/test_graph_selector_methods.py
+++ b/test/unit/test_graph_selector_methods.py
@@ -23,7 +23,7 @@
     ColumnInfo,
 )
 from dbt.contracts.graph.manifest import Manifest
-from dbt.contracts.graph.unparsed import ExposureType, ExposureOwner, MetricFilter
+from dbt.contracts.graph.unparsed import ExposureType, ExposureOwner, MetricFilter,MetricTime
 from dbt.contracts.state import PreviousState
 from dbt.node_types import NodeType
 from dbt.graph.selector_methods import (
@@ -380,7 +380,7 @@ def make_metric(pkg, name, path=None):
            value=True,
            operator="=",
         )],
-        window='',
+        window=MetricTime(),
         meta={'is_okr': True},
         tags=['okrs'],
     )
diff --git a/test/unit/test_manifest.py b/test/unit/test_manifest.py
--- a/test/unit/test_manifest.py
+++ b/test/unit/test_manifest.py
@@ -29,7 +29,8 @@
     ExposureType,
     ExposureOwner,
     MaturityType,
-    MetricFilter
+    MetricFilter,
+    MetricTime
 )
 
 from dbt.contracts.graph.compiled import CompiledModelNode
@@ -115,7 +116,7 @@ def setUp(self):
                 )],
                 meta={'is_okr': True},
                 tags=['okrs'],
-                window='',
+                window=MetricTime(),
                 resource_type=NodeType.Metric,
                 depends_on=DependsOn(nodes=['model.root.multi']),
                 refs=[['multi']],
diff --git a/tests/functional/metrics/test_metric_deferral.py b/tests/functional/metrics/test_metric_deferral.py
--- a/tests/functional/metrics/test_metric_deferral.py
+++ b/tests/functional/metrics/test_metric_deferral.py
@@ -11,8 +11,8 @@
     label: Some Metric
     model: ref('model_a')
 
-    type: count
-    sql: id
+    calculation_method: count
+    expression: id
 
     timestamp: ts
     time_grains: [day]
@@ -26,8 +26,8 @@
     label: Some Metric
     model: ref('model_a')
 
-    type: count
-    sql: user_id
+    calculation_method: count
+    expression: user_id
 
     timestamp: ts
     time_grains: [day]
diff --git a/tests/functional/metrics/test_metrics.py b/tests/functional/metrics/test_metrics.py
--- a/tests/functional/metrics/test_metrics.py
+++ b/tests/functional/metrics/test_metrics.py
@@ -46,7 +46,9 @@
     expression: tenure
     timestamp: created_at
     time_grains: [day]
-    window: 14 days
+    window:
+      count: 14
+      period: day
     filters:
       - field: loves_dbt
         operator: 'is'