diff --git a/docs/README.md b/docs/README.md index a1980dc..5d39f64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -99,15 +99,20 @@ from bruin import context | Property | Type | Env Var | Description | |----------|------|---------|-------------| | `context.start_date` | `date \| None` | `BRUIN_START_DATE` | Pipeline run start date | -| `context.end_date` | `date \| None` | `BRUIN_END_DATE` | Pipeline run end date | | `context.start_datetime` | `datetime \| None` | `BRUIN_START_DATETIME` | Start date with time | +| `context.start_timestamp` | `datetime \| None` | `BRUIN_START_TIMESTAMP` | Start timestamp with timezone | +| `context.end_date` | `date \| None` | `BRUIN_END_DATE` | Pipeline run end date | | `context.end_datetime` | `datetime \| None` | `BRUIN_END_DATETIME` | End date with time | +| `context.end_timestamp` | `datetime \| None` | `BRUIN_END_TIMESTAMP` | End timestamp with timezone | | `context.execution_date` | `date \| None` | `BRUIN_EXECUTION_DATE` | Execution date | +| `context.execution_datetime` | `datetime \| None` | `BRUIN_EXECUTION_DATETIME` | Execution date with time | +| `context.execution_timestamp` | `datetime \| None` | `BRUIN_EXECUTION_TIMESTAMP` | Execution timestamp with timezone | | `context.run_id` | `str \| None` | `BRUIN_RUN_ID` | Unique run identifier | | `context.pipeline` | `str \| None` | `BRUIN_PIPELINE` | Pipeline name | | `context.asset_name` | `str \| None` | `BRUIN_ASSET` | Current asset name | | `context.connection` | `str \| None` | `BRUIN_CONNECTION` | Asset's default connection | | `context.is_full_refresh` | `bool` | `BRUIN_FULL_REFRESH` | `True` when `--full-refresh` flag is set | +| `context.commit_hash` | `str \| None` | `BRUIN_COMMIT_HASH` | Git commit hash of the pipeline's repository | | `context.vars` | `dict` | `BRUIN_VARS` | Pipeline variables (types preserved from JSON Schema) | All properties return `None` when the corresponding env var is missing (except `is_full_refresh` which returns `False`, and `vars` which returns `{}`). diff --git a/src/bruin/_context.py b/src/bruin/_context.py index f3a3770..393a133 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -27,9 +27,7 @@ def _parse_datetime(env_var: str) -> "datetime.datetime | None": try: return datetime.datetime.fromisoformat(val) except ValueError: - raise BruinError( - f"Invalid {env_var} value '{val}': expected ISO-8601 datetime (YYYY-MM-DDThh:mm:ss)." - ) + raise BruinError(f"Invalid {env_var} value '{val}': expected ISO-8601 datetime.") def _coerce_value(value, type_def: dict): @@ -95,22 +93,38 @@ class _BruinContext: def start_date(self) -> "datetime.date | None": return _parse_date("BRUIN_START_DATE") - @property - def end_date(self) -> "datetime.date | None": - return _parse_date("BRUIN_END_DATE") - @property def start_datetime(self) -> "datetime.datetime | None": return _parse_datetime("BRUIN_START_DATETIME") + @property + def start_timestamp(self) -> "datetime.datetime | None": + return _parse_datetime("BRUIN_START_TIMESTAMP") + + @property + def end_date(self) -> "datetime.date | None": + return _parse_date("BRUIN_END_DATE") + @property def end_datetime(self) -> "datetime.datetime | None": return _parse_datetime("BRUIN_END_DATETIME") + @property + def end_timestamp(self) -> "datetime.datetime | None": + return _parse_datetime("BRUIN_END_TIMESTAMP") + @property def execution_date(self) -> "datetime.date | None": return _parse_date("BRUIN_EXECUTION_DATE") + @property + def execution_datetime(self) -> "datetime.datetime | None": + return _parse_datetime("BRUIN_EXECUTION_DATETIME") + + @property + def execution_timestamp(self) -> "datetime.datetime | None": + return _parse_datetime("BRUIN_EXECUTION_TIMESTAMP") + @property def run_id(self) -> "str | None": return os.environ.get("BRUIN_RUN_ID") @@ -131,6 +145,10 @@ def connection(self) -> "str | None": def is_full_refresh(self) -> bool: return os.environ.get("BRUIN_FULL_REFRESH") == "1" + @property + def commit_hash(self) -> "str | None": + return os.environ.get("BRUIN_COMMIT_HASH") + @property def vars(self) -> dict: val = os.environ.get("BRUIN_VARS") diff --git a/tests/test_context.py b/tests/test_context.py index dbca7ee..51bf4ab 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -37,6 +37,23 @@ def test_returns_none_when_missing(self, monkeypatch): assert context.start_datetime is None +class TestStartTimestamp: + def test_returns_datetime_with_tz(self, monkeypatch): + monkeypatch.setenv("BRUIN_START_TIMESTAMP", "2024-01-15T10:30:00.000000+00:00") + result = context.start_timestamp + assert result == datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.timezone.utc) + + def test_returns_datetime_with_non_utc_tz(self, monkeypatch): + monkeypatch.setenv("BRUIN_START_TIMESTAMP", "2024-01-15T10:30:00.000000+05:30") + result = context.start_timestamp + tz = datetime.timezone(datetime.timedelta(hours=5, minutes=30)) + assert result == datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=tz) + + def test_returns_none_when_missing(self, monkeypatch): + monkeypatch.delenv("BRUIN_START_TIMESTAMP", raising=False) + assert context.start_timestamp is None + + class TestEndDatetime: def test_returns_datetime(self, monkeypatch): monkeypatch.setenv("BRUIN_END_DATETIME", "2024-06-30T23:59:59") @@ -47,6 +64,17 @@ def test_returns_none_when_missing(self, monkeypatch): assert context.end_datetime is None +class TestEndTimestamp: + def test_returns_datetime_with_tz(self, monkeypatch): + monkeypatch.setenv("BRUIN_END_TIMESTAMP", "2024-06-30T23:59:59.000000+00:00") + result = context.end_timestamp + assert result == datetime.datetime(2024, 6, 30, 23, 59, 59, tzinfo=datetime.timezone.utc) + + def test_returns_none_when_missing(self, monkeypatch): + monkeypatch.delenv("BRUIN_END_TIMESTAMP", raising=False) + assert context.end_timestamp is None + + class TestExecutionDate: def test_returns_date(self, monkeypatch): monkeypatch.setenv("BRUIN_EXECUTION_DATE", "2024-03-01") @@ -57,6 +85,27 @@ def test_returns_none_when_missing(self, monkeypatch): assert context.execution_date is None +class TestExecutionDatetime: + def test_returns_datetime(self, monkeypatch): + monkeypatch.setenv("BRUIN_EXECUTION_DATETIME", "2024-03-01T12:00:00") + assert context.execution_datetime == datetime.datetime(2024, 3, 1, 12, 0, 0) + + def test_returns_none_when_missing(self, monkeypatch): + monkeypatch.delenv("BRUIN_EXECUTION_DATETIME", raising=False) + assert context.execution_datetime is None + + +class TestExecutionTimestamp: + def test_returns_datetime_with_tz(self, monkeypatch): + monkeypatch.setenv("BRUIN_EXECUTION_TIMESTAMP", "2024-03-01T12:00:00.000000+00:00") + result = context.execution_timestamp + assert result == datetime.datetime(2024, 3, 1, 12, 0, 0, tzinfo=datetime.timezone.utc) + + def test_returns_none_when_missing(self, monkeypatch): + monkeypatch.delenv("BRUIN_EXECUTION_TIMESTAMP", raising=False) + assert context.execution_timestamp is None + + class TestRunId: def test_returns_string(self, monkeypatch): monkeypatch.setenv("BRUIN_RUN_ID", "abc-123") @@ -97,6 +146,16 @@ def test_returns_none_when_missing(self, monkeypatch): assert context.connection is None +class TestCommitHash: + def test_returns_string(self, monkeypatch): + monkeypatch.setenv("BRUIN_COMMIT_HASH", "abc1234def5678") + assert context.commit_hash == "abc1234def5678" + + def test_returns_none_when_missing(self, monkeypatch): + monkeypatch.delenv("BRUIN_COMMIT_HASH", raising=False) + assert context.commit_hash is None + + class TestIsFullRefresh: def test_true_when_set_to_1(self, monkeypatch): monkeypatch.setenv("BRUIN_FULL_REFRESH", "1")