Skip to content

Commit 727a88c

Browse files
committed
feat: add audit_pass notification event
Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
1 parent db84671 commit 727a88c

5 files changed

Lines changed: 59 additions & 3 deletions

File tree

docs/guides/notifications.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ This example stops all notifications other than those for `User1`:
130130

131131
SQLMesh notifications are triggered by events. The events that should trigger a notification are specified in the notification target's `notify_on` field.
132132

133-
Notifications are supported for [`plan` application](../concepts/plans.md) start/end/failure, [`run`](../reference/cli.md#run) start/end/failure, and [`audit`](../concepts/audits.md) failures.
133+
Notifications are supported for [`plan` application](../concepts/plans.md) start/end/failure, [`run`](../reference/cli.md#run) start/end/failure, and [`audit`](../concepts/audits.md) passes and failures.
134134

135135
For `plan` and `run` start/end, the target environment name is included in the notification message. For failures, the Python exception or error text is included in the notification message.
136136

@@ -145,6 +145,7 @@ This table lists each event, its associated `notify_on` value, and its notificat
145145
| SQLMesh run end | run_end | "SQLMesh run finished for environment `{environment}`." |
146146
| SQLMesh run failure | run_failure | "Failed to run SQLMesh.\n{exception}" |
147147
| Audit failure | audit_failure | "{audit_error}" |
148+
| Audit pass | audit_pass | "Audit `{audit_name}` passed for model `{model_name}`." |
148149

149150
Any combination of these events can be specified in a notification target's `notify_on` field.
150151

@@ -269,6 +270,7 @@ Each of those notification target classes is a subclass of `BaseNotificationTarg
269270
| notify_run_end | Environment name: `env` |
270271
| notify_run_failure | Exception stack trace: `exc` |
271272
| notify_audit_failure | Audit error trace: `audit_error` |
273+
| notify_audit_pass | Audit name: `audit_name`, model name: `model_name` |
272274

273275
This example creates a new notification target class `CustomSMTPNotificationTarget`.
274276

sqlmesh/core/notification_target.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class NotificationEvent(str, Enum):
6464
APPLY_FAILURE = "apply_failure"
6565
RUN_FAILURE = "run_failure"
6666
AUDIT_FAILURE = "audit_failure"
67+
AUDIT_PASS = "audit_pass"
6768
MIGRATION_FAILURE = "migration_failure"
6869

6970

@@ -172,6 +173,21 @@ def notify_audit_failure(self, audit_error: AuditError, *args: t.Any, **kwargs:
172173
"""
173174
self.send(NotificationStatus.FAILURE, "Audit failure.", audit_error=audit_error)
174175

176+
def notify_audit_pass(
177+
self, audit_name: str, model_name: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any
178+
) -> None:
179+
"""Notify when an audit passes.
180+
181+
Args:
182+
audit_name: The name of the audit that passed.
183+
model_name: The name of the model the audit ran against, if any.
184+
"""
185+
if model_name:
186+
msg = f"Audit `{audit_name}` passed for model `{model_name}`."
187+
else:
188+
msg = f"Audit `{audit_name}` passed."
189+
self.send(NotificationStatus.SUCCESS, msg)
190+
175191
def notify_migration_failure(self, exc: str, *args: t.Any, **kwargs: t.Any) -> None:
176192
"""Notify in the case of a migration failure.
177193

sqlmesh/core/scheduler.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,20 @@ def _audit_snapshot(
930930
else:
931931
audit_errors_to_warn.append(error)
932932

933+
model = snapshot.model_or_none
934+
model_name = model.name if model else None
935+
for audit_result in audit_results:
936+
if audit_result.skipped or audit_result.count:
937+
continue
938+
audit_name = audit_result.audit.name
939+
self.notification_target_manager.notify(
940+
NotificationEvent.AUDIT_PASS, audit_name, model_name
941+
)
942+
if is_deployable and snapshot.node.owner:
943+
self.notification_target_manager.notify_user(
944+
NotificationEvent.AUDIT_PASS, snapshot.node.owner, audit_name, model_name
945+
)
946+
933947
if audit_errors_to_raise:
934948
raise NodeAuditsErrors(audit_errors_to_raise)
935949

tests/core/test_notification_target.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def notification_target_manager_with_spy(mocker) -> tuple[NotificationTargetMana
2424
notification_targets={
2525
NotificationEvent.APPLY_START: {console_notification_target},
2626
NotificationEvent.APPLY_END: {console_notification_target},
27+
NotificationEvent.AUDIT_PASS: {console_notification_target},
2728
},
2829
user_notification_targets={
2930
"test_user": {test_user_console_notification_target},
@@ -57,6 +58,25 @@ def test_notify(notification_target_manager_with_spy):
5758
spy.assert_not_called()
5859

5960

61+
def test_notify_audit_pass(notification_target_manager_with_spy):
62+
notification_target_manager, spy = notification_target_manager_with_spy
63+
notification_target_manager.notify(NotificationEvent.AUDIT_PASS, "not_null", "sushi.orders")
64+
spy.assert_called_once_with(
65+
mock.ANY,
66+
NotificationStatus.SUCCESS,
67+
"Audit `not_null` passed for model `sushi.orders`.",
68+
)
69+
70+
# Without a model name the message omits the model reference
71+
spy.reset_mock()
72+
notification_target_manager.notify(NotificationEvent.AUDIT_PASS, "not_null")
73+
spy.assert_called_once_with(
74+
mock.ANY,
75+
NotificationStatus.SUCCESS,
76+
"Audit `not_null` passed.",
77+
)
78+
79+
6080
def test_notify_user(notification_target_manager_with_spy):
6181
notification_target_manager, spy = notification_target_manager_with_spy
6282
notification_target_manager.notify_user(

tests/core/test_scheduler.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ def _evaluate():
588588
0,
589589
)
590590

591+
# A passing audit notifies AUDIT_PASS.
591592
evaluator_audit_mock.return_value = [
592593
AuditResult(
593594
audit=audit,
@@ -599,9 +600,12 @@ def _evaluate():
599600
)
600601
]
601602
_evaluate()
602-
assert notify_user_mock.call_count == 0
603-
assert notify_mock.call_count == 0
603+
assert notify_user_mock.call_count == 1
604+
assert notify_mock.call_count == 1
605+
notify_user_mock.reset_mock()
606+
notify_mock.reset_mock()
604607

608+
# A skipped audit is neither a pass nor a failure, so nothing fires.
605609
evaluator_audit_mock.return_value = [
606610
AuditResult(
607611
audit=audit,

0 commit comments

Comments
 (0)