From 05ac25d93eba747c39294bef42ffa5ba67bcb19a Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Sat, 14 Mar 2026 19:32:13 -0700 Subject: [PATCH 1/6] feat: add start_timestamp to context for BRUIN_START_TIMESTAMP Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 2 ++ src/bruin/_context.py | 8 ++++++++ tests/test_context.py | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/docs/README.md b/docs/README.md index a1980dc..f6a75eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -101,6 +101,7 @@ from bruin import context | `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_datetime` | `datetime \| None` | `BRUIN_END_DATETIME` | End date with time | | `context.execution_date` | `date \| None` | `BRUIN_EXECUTION_DATE` | Execution date | | `context.run_id` | `str \| None` | `BRUIN_RUN_ID` | Unique run identifier | @@ -108,6 +109,7 @@ from bruin import context | `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..0395aac 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -103,6 +103,10 @@ def end_date(self) -> "datetime.date | None": 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_datetime(self) -> "datetime.datetime | None": return _parse_datetime("BRUIN_END_DATETIME") @@ -131,6 +135,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..0f5a006 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -37,6 +37,17 @@ 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_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") @@ -97,6 +108,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") From 615c1faedd7c8f5a27e89609542dc21bd514ae71 Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Sat, 14 Mar 2026 19:32:32 -0700 Subject: [PATCH 2/6] feat: add end_timestamp to context for BRUIN_END_TIMESTAMP Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 1 + src/bruin/_context.py | 4 ++++ tests/test_context.py | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/docs/README.md b/docs/README.md index f6a75eb..aaa206c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -103,6 +103,7 @@ from bruin import context | `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_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.run_id` | `str \| None` | `BRUIN_RUN_ID` | Unique run identifier | | `context.pipeline` | `str \| None` | `BRUIN_PIPELINE` | Pipeline name | diff --git a/src/bruin/_context.py b/src/bruin/_context.py index 0395aac..ed34c2c 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -111,6 +111,10 @@ def start_timestamp(self) -> "datetime.datetime | None": 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") diff --git a/tests/test_context.py b/tests/test_context.py index 0f5a006..ce7d18e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -58,6 +58,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") From c06172fcf141c04393772e503464d413e118769b Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Sat, 14 Mar 2026 19:32:50 -0700 Subject: [PATCH 3/6] feat: add execution_datetime to context for BRUIN_EXECUTION_DATETIME Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 1 + src/bruin/_context.py | 4 ++++ tests/test_context.py | 10 ++++++++++ 3 files changed, 15 insertions(+) diff --git a/docs/README.md b/docs/README.md index aaa206c..2f13f89 100644 --- a/docs/README.md +++ b/docs/README.md @@ -105,6 +105,7 @@ from bruin import context | `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.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 | diff --git a/src/bruin/_context.py b/src/bruin/_context.py index ed34c2c..d44cc1b 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -119,6 +119,10 @@ def end_timestamp(self) -> "datetime.datetime | None": 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 run_id(self) -> "str | None": return os.environ.get("BRUIN_RUN_ID") diff --git a/tests/test_context.py b/tests/test_context.py index ce7d18e..0572a1d 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -79,6 +79,16 @@ 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 TestRunId: def test_returns_string(self, monkeypatch): monkeypatch.setenv("BRUIN_RUN_ID", "abc-123") From fc327c8935c7f627e7f7354a9904f6d6a0d53b9b Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Sat, 14 Mar 2026 19:33:13 -0700 Subject: [PATCH 4/6] feat: add execution_timestamp to context for BRUIN_EXECUTION_TIMESTAMP Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 1 + src/bruin/_context.py | 4 ++++ tests/test_context.py | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/docs/README.md b/docs/README.md index 2f13f89..732ab16 100644 --- a/docs/README.md +++ b/docs/README.md @@ -106,6 +106,7 @@ from bruin import context | `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 | diff --git a/src/bruin/_context.py b/src/bruin/_context.py index d44cc1b..8a1edf7 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -123,6 +123,10 @@ def execution_date(self) -> "datetime.date | None": 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") diff --git a/tests/test_context.py b/tests/test_context.py index 0572a1d..696aa16 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -89,6 +89,17 @@ def test_returns_none_when_missing(self, monkeypatch): 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") From 28d4484e2c9acf1556ee352b8b0e8db8ac4506a7 Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Sun, 15 Mar 2026 21:57:13 +0100 Subject: [PATCH 5/6] fix: reorder context properties by prefix and improve error message - Group start_*, end_*, execution_* properties together instead of interleaving date/datetime across prefixes - Simplify _parse_datetime error message to not hardcode a format that doesn't match timestamp variants - Add non-UTC timezone test for start_timestamp Co-Authored-By: Claude Opus 4.6 --- docs/README.md | 2 +- src/bruin/_context.py | 10 +++++----- tests/test_context.py | 6 ++++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/README.md b/docs/README.md index 732ab16..5d39f64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -99,9 +99,9 @@ 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 | diff --git a/src/bruin/_context.py b/src/bruin/_context.py index 8a1edf7..58f3803 100644 --- a/src/bruin/_context.py +++ b/src/bruin/_context.py @@ -28,7 +28,7 @@ def _parse_datetime(env_var: str) -> "datetime.datetime | None": return datetime.datetime.fromisoformat(val) except ValueError: raise BruinError( - f"Invalid {env_var} value '{val}': expected ISO-8601 datetime (YYYY-MM-DDThh:mm:ss)." + f"Invalid {env_var} value '{val}': expected ISO-8601 datetime." ) @@ -95,10 +95,6 @@ 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") @@ -107,6 +103,10 @@ def start_datetime(self) -> "datetime.datetime | None": 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") diff --git a/tests/test_context.py b/tests/test_context.py index 696aa16..51bf4ab 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -43,6 +43,12 @@ def test_returns_datetime_with_tz(self, monkeypatch): 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 From fe0a328f4d5fe18c579eadb652f8af428eb96fe7 Mon Sep 17 00:00:00 2001 From: Sabri Karagonen Date: Mon, 16 Mar 2026 08:42:21 +0100 Subject: [PATCH 6/6] style: fix ruff formatting Co-Authored-By: Claude Opus 4.6 --- src/bruin/_context.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/bruin/_context.py b/src/bruin/_context.py index 58f3803..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." - ) + raise BruinError(f"Invalid {env_var} value '{val}': expected ISO-8601 datetime.") def _coerce_value(value, type_def: dict):