diff --git a/.changes/unreleased/Features-20241003-170529.yaml b/.changes/unreleased/Features-20241003-170529.yaml new file mode 100644 index 00000000000..8b4ed1b8d74 --- /dev/null +++ b/.changes/unreleased/Features-20241003-170529.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Ensure `--event-time-start` is before `--event-time-end` +time: 2024-10-03T17:05:29.984398-05:00 +custom: + Author: QMalcolm + Issue: "10786" diff --git a/core/dbt/cli/flags.py b/core/dbt/cli/flags.py index d3dabded7b0..649c24fb3e0 100644 --- a/core/dbt/cli/flags.py +++ b/core/dbt/cli/flags.py @@ -1,11 +1,13 @@ import os import sys from dataclasses import dataclass +from datetime import datetime from importlib import import_module from pathlib import Path from pprint import pformat as pf from typing import Any, Callable, Dict, List, Optional, Set, Union +import pytz from click import Context, Parameter, get_current_context from click.core import Command as ClickCommand from click.core import Group, ParameterSource @@ -313,6 +315,9 @@ def _assign_params( self._assert_mutually_exclusive(params_assigned_from_default, ["SELECT", "INLINE"]) self._assert_mutually_exclusive(params_assigned_from_default, ["SELECTOR", "INLINE"]) + # Check event_time configs for validity + self._validate_event_time_configs() + # Support lower cased access for legacy code. params = set( x for x in dir(self) if not callable(getattr(self, x)) and not x.startswith("__") @@ -349,6 +354,27 @@ def _assert_mutually_exclusive( elif flag_set_by_user: set_flag = flag + def _validate_event_time_configs(self) -> None: + event_time_start: datetime = ( + getattr(self, "EVENT_TIME_START") if hasattr(self, "EVENT_TIME_START") else None + ) + event_time_end: datetime = ( + getattr(self, "EVENT_TIME_END") if hasattr(self, "EVENT_TIME_END") else None + ) + + if event_time_start is not None and event_time_end is not None: + if event_time_start >= event_time_end: + raise DbtUsageException( + "Value for `--event-time-start` must be less than `--event-time-end`" + ) + elif event_time_start is not None: + utc_start = event_time_start.replace(tzinfo=pytz.UTC) + current_time = datetime.now(pytz.UTC) + if utc_start >= current_time: + raise DbtUsageException( + f"Value for `--event-time-start` ({utc_start}) must be less than the current time ({current_time}) if `--event-time-end` is not specififed" + ) + def fire_deprecations(self): """Fires events for deprecated env_var usage.""" [dep_fn() for dep_fn in self.deprecated_env_var_warnings] diff --git a/tests/functional/cli/test_option_interaction_validations.py b/tests/functional/cli/test_option_interaction_validations.py new file mode 100644 index 00000000000..6a57820b026 --- /dev/null +++ b/tests/functional/cli/test_option_interaction_validations.py @@ -0,0 +1,52 @@ +from datetime import datetime + +import pytest +from freezegun import freeze_time + +from dbt.tests.util import run_dbt + + +class TestEventTimeEndEventTimeStart: + @pytest.mark.parametrize( + "event_time_start,event_time_end,expect_pass", + [ + ("2024-10-01", "2024-10-02", True), + ("2024-10-02", "2024-10-01", False), + ], + ) + def test_option_combo(self, project, event_time_start, event_time_end, expect_pass): + try: + run_dbt( + [ + "build", + "--event-time-start", + event_time_start, + "--event-time-end", + event_time_end, + ] + ) + assert expect_pass + except Exception as e: + assert ( + "Value for `--event-time-start` must be less than `--event-time-end`" + in e.__str__() + ) + assert not expect_pass + + +class TestEventTimeStartCurrent_time: + @pytest.mark.parametrize( + "event_time_start,current_time,expect_pass", + [ + ("2024-10-01", "2024-10-02", True), + ("2024-10-02", "2024-10-01", False), + ], + ) + def test_option_combo(self, project, event_time_start, current_time, expect_pass): + with freeze_time(datetime.fromisoformat(current_time)): + try: + run_dbt(["build", "--event-time-start", event_time_start]) + assert expect_pass + except Exception as e: + assert "must be less than the current time" in e.__str__() + assert not expect_pass