Skip to content

Commit

Permalink
Upgrade DSI to get new granularity options
Browse files Browse the repository at this point in the history
  • Loading branch information
courtneyholcomb committed May 31, 2024
1 parent 3d26700 commit cd6b9d9
Show file tree
Hide file tree
Showing 7 changed files with 12,148 additions and 5,490 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,19 @@ class DateutilTimePeriodAdjuster(TimePeriodAdjuster):

def _relative_delta_for_window(self, time_granularity: TimeGranularity, count: int) -> relativedelta:
"""Relative-delta to cover time windows specified at different grains."""
if time_granularity is TimeGranularity.DAY:
if time_granularity is TimeGranularity.NANOSECOND:
raise ValueError("`relativedelta` does not support nanoseconds.")
elif time_granularity is TimeGranularity.MICROSECOND:
return relativedelta(microseconds=count)
elif time_granularity is TimeGranularity.MILLISECOND:
return relativedelta(microseconds=count * 1000)
elif time_granularity is TimeGranularity.SECOND:
return relativedelta(seconds=count)
elif time_granularity is TimeGranularity.MINUTE:
return relativedelta(minutes=count)
elif time_granularity is TimeGranularity.HOUR:
return relativedelta(hours=count)
elif time_granularity is TimeGranularity.DAY:
return relativedelta(days=count)
elif time_granularity is TimeGranularity.WEEK:
return relativedelta(weeks=count)
Expand Down Expand Up @@ -53,8 +65,18 @@ def expand_time_constraint_to_fill_granularity(
def adjust_to_start_of_period(
self, time_granularity: TimeGranularity, date_to_adjust: datetime.datetime
) -> datetime.datetime:
if time_granularity is TimeGranularity.DAY:
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
return date_to_adjust

elif time_granularity is TimeGranularity.WEEK:
return date_to_adjust + relativedelta(weekday=dateutil.relativedelta.MO(-1))
elif time_granularity is TimeGranularity.MONTH:
Expand All @@ -77,8 +99,18 @@ def adjust_to_start_of_period(
def adjust_to_end_of_period(
self, time_granularity: TimeGranularity, date_to_adjust: datetime.datetime
) -> datetime.datetime:
if time_granularity is TimeGranularity.DAY:
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
return date_to_adjust

elif time_granularity is TimeGranularity.WEEK:
return date_to_adjust + relativedelta(weekday=dateutil.relativedelta.SU(1))
elif time_granularity is TimeGranularity.MONTH:
Expand Down
71 changes: 61 additions & 10 deletions metricflow-semantics/metricflow_semantics/time/pandas_adjuster.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,18 @@ def expand_time_constraint_for_cumulative_metric(
def is_period_start(time_granularity: TimeGranularity, date: Union[pd.Timestamp, date]) -> bool: # noqa: D103
pd_date = pd.Timestamp(date)

if time_granularity is TimeGranularity.DAY:
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
return True

elif time_granularity is TimeGranularity.WEEK:
return ISOWeekDay.from_pandas_timestamp(pd_date).is_week_start
elif time_granularity is TimeGranularity.MONTH:
Expand All @@ -106,8 +116,18 @@ def is_period_start(time_granularity: TimeGranularity, date: Union[pd.Timestamp,
def is_period_end(time_granularity: TimeGranularity, date: Union[pd.Timestamp, date]) -> bool: # noqa: D103
pd_date = pd.Timestamp(date)

if time_granularity is TimeGranularity.DAY:
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
return True

elif time_granularity is TimeGranularity.WEEK:
return ISOWeekDay.from_pandas_timestamp(pd_date).is_week_end
elif time_granularity is TimeGranularity.MONTH:
Expand All @@ -123,8 +143,18 @@ def is_period_end(time_granularity: TimeGranularity, date: Union[pd.Timestamp, d
def period_begin_offset( # noqa: D103
time_granularity: TimeGranularity,
) -> Union[pd.offsets.MonthBegin, pd.offsets.QuarterBegin, pd.offsets.Week, pd.offsets.YearBegin]:
if time_granularity is TimeGranularity.DAY:
raise ValueError(f"Can't get period start offset for TimeGranularity.{time_granularity.name}.")
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
raise ValueError(f"Can't get period start offset for TimeGranularity: {time_granularity.name}.")

elif time_granularity is TimeGranularity.WEEK:
return pd.offsets.Week(weekday=ISOWeekDay.MONDAY.pandas_value)
elif time_granularity is TimeGranularity.MONTH:
Expand All @@ -140,8 +170,18 @@ def period_begin_offset( # noqa: D103
def period_end_offset( # noqa: D103
time_granularity: TimeGranularity,
) -> Union[pd.offsets.MonthEnd, pd.offsets.QuarterEnd, pd.offsets.Week, pd.offsets.YearEnd]:
if time_granularity is TimeGranularity.DAY:
# TODO: update these options once time constraints support a full timestamp
if (
time_granularity is TimeGranularity.NANOSECOND
or time_granularity is TimeGranularity.MICROSECOND
or time_granularity is TimeGranularity.MILLISECOND
or time_granularity is TimeGranularity.SECOND
or time_granularity is TimeGranularity.MINUTE
or time_granularity is TimeGranularity.HOUR
or time_granularity is TimeGranularity.DAY
):
raise ValueError(f"Can't get period end offset for TimeGranularity.{time_granularity.name}.")

elif time_granularity == TimeGranularity.WEEK:
return pd.offsets.Week(weekday=ISOWeekDay.SUNDAY.pandas_value)
elif time_granularity is TimeGranularity.MONTH:
Expand Down Expand Up @@ -176,17 +216,28 @@ def adjust_to_end_of_period(

def offset_period(time_granularity: TimeGranularity) -> pd.offsets.DateOffset:
"""Offset object to use for adjusting by one granularity period."""
# The type checker is throwing errors for some of those arguments, but they are valid.
if time_granularity is TimeGranularity.DAY:
return pd.offsets.DateOffset(days=1) # type: ignore
if time_granularity is TimeGranularity.NANOSECOND:
return pd.offsets.DateOffset(nanoseconds=1)
elif time_granularity is TimeGranularity.MICROSECOND:
return pd.offsets.DateOffset(microseconds=1)
elif time_granularity is TimeGranularity.MILLISECOND:
return pd.offsets.DateOffset(milliseconds=1)
elif time_granularity is TimeGranularity.SECOND:
return pd.offsets.DateOffset(seconds=1)
elif time_granularity is TimeGranularity.MINUTE:
return pd.offsets.DateOffset(minutes=1)
elif time_granularity is TimeGranularity.HOUR:
return pd.offsets.DateOffset(hours=1)
elif time_granularity is TimeGranularity.DAY:
return pd.offsets.DateOffset(days=1)
elif time_granularity is TimeGranularity.WEEK:
return pd.offsets.DateOffset(weeks=1) # type: ignore
return pd.offsets.DateOffset(weeks=1)
elif time_granularity is TimeGranularity.MONTH:
return pd.offsets.DateOffset(months=1)
elif time_granularity is TimeGranularity.QUARTER:
return pd.offsets.DateOffset(months=3)
elif time_granularity is TimeGranularity.YEAR:
return pd.offsets.DateOffset(years=1) # type: ignore
return pd.offsets.DateOffset(years=1)
else:
assert_values_exhausted(time_granularity)

Expand Down
2 changes: 1 addition & 1 deletion metricflow-semantics/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ classifiers = [
]

dependencies = [
"dbt-semantic-interfaces>=0.5.1, <0.6.0",
"dbt-semantic-interfaces==0.6.0.dev0",
"pandas>=1.5.0, <1.6.0",
"rapidfuzz>=3.0, <4.0",
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,15 @@
description: Derived offset metric with 2 different agg_time_dimensions
type: derived
type_params:
expr: revenue - revenue_daily
expr: revenue - revenue_last_7_days
metrics:
- name: revenue
- name: revenue_daily
offset_window: 1 week
alias: revenue_last_7_days
"""
)
# TODO: is this validation going to be a breaking change? Will users hit validation errors when querying?


@pytest.fixture
Expand Down
Loading

0 comments on commit cd6b9d9

Please sign in to comment.