Skip to content

Commit e5af7e7

Browse files
NoyaArieclaude
andauthored
Core 645 generic alert types (#2192)
* refactor: widen alert type annotations to AlertModel base class Widens the hard-coded three-way Union[TestAlertModel, ModelAlertModel, SourceFreshnessAlertModel] to the common AlertModel base across alert group, message builder, and integration APIs, and widens PendingAlertSchema.data to BaseAlertDataSchema. Enables downstream packages to extend the alert hierarchy (e.g. pipeline alerts) without needing type: ignore workarounds. No behavioral changes — type-level only. mypy passes and unit tests are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: keep smart_union = True on PendingAlertSchema Config Removed by mistake in the previous commit. It's a class-level setting that still affects other Union/Optional fields on the schema, so keeping it preserves the pre-existing parsing behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: make asset_type and concise_name part of the AlertModel contract - AlertModel.asset_type: new mandatory @Property (NotImplementedError on base) - AlertModel.concise_name: now raises NotImplementedError on base (was "Alert") - TestAlertModel.asset_type -> "test" - ModelAlertModel.asset_type -> "snapshot" | "model" (based on materialization) - ModelAlertModel.concise_name -> self.alias (was "dbt {type} alert - {alias}") - SourceFreshnessAlertModel.asset_type -> "source" - SourceFreshnessAlertModel.concise_name -> "{source_name}.{identifier}" (was "source freshness alert - {source_name}.{identifier}") - AlertMessageBuilder._get_run_alert_subtitle_blocks now consumes alert.asset_type / alert.concise_name instead of an isinstance chain, so downstream subclasses (e.g. pipeline alerts) work without edits here. - Widened _get_run_alert_subtitle_block's `type` param from Literal to str. - Added unit tests for asset_type/concise_name on every concrete subclass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c642a36 commit e5af7e7

15 files changed

Lines changed: 159 additions & 145 deletions

File tree

elementary/monitor/alerts/alert.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,12 @@ def data(self) -> Dict:
9999
raise NotImplementedError
100100

101101
@property
102-
def concise_name(self):
103-
return "Alert"
102+
def concise_name(self) -> str:
103+
raise NotImplementedError
104+
105+
@property
106+
def asset_type(self) -> str:
107+
raise NotImplementedError
104108

105109
@property
106110
def summary(self) -> str:

elementary/monitor/alerts/alert_messages/builder.py

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import timedelta
2-
from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple, Union
2+
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
33

44
from elementary.messages.block_builders import (
55
BoldTextBlock,
@@ -33,7 +33,7 @@
3333
TextStyle,
3434
)
3535
from elementary.messages.message_body import Color, MessageBlock, MessageBody
36-
from elementary.monitor.alerts.alert import OrchestratorInfo
36+
from elementary.monitor.alerts.alert import AlertModel, OrchestratorInfo
3737
from elementary.monitor.alerts.alert_messages.alert_fields import AlertField
3838
from elementary.monitor.alerts.alerts_groups.alerts_group import AlertsGroup
3939
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
@@ -51,12 +51,7 @@
5151
)
5252
from elementary.utils.pydantic_shim import BaseModel
5353

54-
AlertType = Union[
55-
TestAlertModel,
56-
ModelAlertModel,
57-
SourceFreshnessAlertModel,
58-
BaseAlertsGroup,
59-
]
54+
AlertType = Union[AlertModel, BaseAlertsGroup]
6055

6156

6257
class MessageBuilderConfig(BaseModel):
@@ -104,7 +99,7 @@ def _get_alert_title(
10499

105100
def _get_run_alert_subtitle_block(
106101
self,
107-
type: Literal["test", "snapshot", "model", "source"],
102+
type: str,
108103
name: str,
109104
status: Optional[str] = None,
110105
detected_at_str: Optional[str] = None,
@@ -172,7 +167,7 @@ def _get_run_alert_subtitle_block(
172167

173168
def _get_run_alert_subtitle_links(
174169
self,
175-
alert: Union[TestAlertModel, SourceFreshnessAlertModel, ModelAlertModel],
170+
alert: AlertModel,
176171
) -> List[ReportLinkData]:
177172
report_link = alert.get_report_link()
178173
if report_link:
@@ -181,25 +176,14 @@ def _get_run_alert_subtitle_links(
181176

182177
def _get_run_alert_subtitle_blocks(
183178
self,
184-
alert: Union[TestAlertModel, SourceFreshnessAlertModel, ModelAlertModel],
179+
alert: AlertModel,
185180
) -> List[MessageBlock]:
186-
asset_type: Literal["test", "snapshot", "model", "source"]
187-
asset_name: str
188-
if isinstance(alert, TestAlertModel):
189-
asset_type = "test"
190-
asset_name = alert.concise_name
191-
elif isinstance(alert, SourceFreshnessAlertModel):
192-
asset_type = "source"
193-
asset_name = f"{alert.source_name}.{alert.identifier}"
194-
elif isinstance(alert, ModelAlertModel):
195-
asset_type = "snapshot" if alert.materialization == "snapshot" else "model"
196-
asset_name = alert.alias
197181
links = self._get_run_alert_subtitle_links(alert)
198182
orchestrator_info = alert.orchestrator_info
199183
return [
200184
self._get_run_alert_subtitle_block(
201-
type=asset_type,
202-
name=asset_name,
185+
type=alert.asset_type,
186+
name=alert.concise_name,
203187
status=alert.status,
204188
detected_at_str=alert.detected_at_str,
205189
suppression_interval=alert.suppression_interval,
@@ -472,11 +456,7 @@ def _get_source_freshness_alert_config_blocks(
472456

473457
def _get_alert_list_line(
474458
self,
475-
alert: Union[
476-
TestAlertModel,
477-
ModelAlertModel,
478-
SourceFreshnessAlertModel,
479-
],
459+
alert: AlertModel,
480460
) -> LineBlock:
481461
inlines: List[InlineBlock] = [
482462
TextBlock(text=alert.summary, style=TextStyle.BOLD),
@@ -507,13 +487,7 @@ def _get_alert_list_blocks(
507487
self,
508488
title: str,
509489
bullet_icon: Icon,
510-
alerts: Sequence[
511-
Union[
512-
TestAlertModel,
513-
ModelAlertModel,
514-
SourceFreshnessAlertModel,
515-
]
516-
],
490+
alerts: Sequence[AlertModel],
517491
) -> List[MessageBlock]:
518492
blocks: List[MessageBlock] = []
519493
if not alerts:
@@ -526,10 +500,10 @@ def _get_alert_list_blocks(
526500

527501
def _get_sub_alert_groups_blocks(
528502
self,
529-
test_errors: List[Union[TestAlertModel, SourceFreshnessAlertModel]],
530-
test_warnings: List[Union[TestAlertModel, SourceFreshnessAlertModel]],
531-
test_failures: List[Union[TestAlertModel, SourceFreshnessAlertModel]],
532-
model_errors: List[ModelAlertModel],
503+
test_errors: Sequence[AlertModel],
504+
test_warnings: Sequence[AlertModel],
505+
test_failures: Sequence[AlertModel],
506+
model_errors: Sequence[AlertModel],
533507
) -> List[MessageBlock]:
534508
blocks: List[MessageBlock] = []
535509
model_errors_alert_list_blocks = self._get_alert_list_blocks(

elementary/monitor/alerts/alerts_groups/alerts_group.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1-
from typing import List, Optional, Union
1+
from typing import List, Optional, Sequence
22

3+
from elementary.monitor.alerts.alert import AlertModel
34
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
45
from elementary.monitor.alerts.model_alert import ModelAlertModel
5-
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel
6-
from elementary.monitor.alerts.test_alert import TestAlertModel
76

87

98
class AlertsGroup(BaseAlertsGroup):
10-
test_errors: List[Union[TestAlertModel, SourceFreshnessAlertModel]]
11-
test_warnings: List[Union[TestAlertModel, SourceFreshnessAlertModel]]
12-
test_failures: List[Union[TestAlertModel, SourceFreshnessAlertModel]]
9+
test_errors: List[AlertModel]
10+
test_warnings: List[AlertModel]
11+
test_failures: List[AlertModel]
1312
model_errors: List[ModelAlertModel]
1413

1514
def __init__(
1615
self,
17-
alerts: List[Union[TestAlertModel, ModelAlertModel, SourceFreshnessAlertModel]],
16+
alerts: Sequence[AlertModel],
1817
env: Optional[str] = None,
1918
) -> None:
2019
super().__init__(alerts, env=env)

elementary/monitor/alerts/alerts_groups/base_alerts_group.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
from abc import ABC, abstractmethod
22
from datetime import datetime
3-
from typing import Dict, List, Optional, Sequence, Union
3+
from typing import Dict, List, Optional, Sequence
44

5-
from elementary.monitor.alerts.model_alert import ModelAlertModel
6-
from elementary.monitor.alerts.source_freshness_alert import SourceFreshnessAlertModel
7-
from elementary.monitor.alerts.test_alert import TestAlertModel
5+
from elementary.monitor.alerts.alert import AlertModel
86

97

108
class BaseAlertsGroup(ABC):
119
def __init__(
1210
self,
13-
alerts: Sequence[
14-
Union[TestAlertModel, ModelAlertModel, SourceFreshnessAlertModel]
15-
],
11+
alerts: Sequence[AlertModel],
1612
env: Optional[str] = None,
1713
) -> None:
1814
self.alerts = alerts

elementary/monitor/alerts/model_alert.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,13 @@ def data(self) -> Dict:
100100
full_refresh=self.full_refresh,
101101
)
102102

103+
@property
104+
def asset_type(self) -> str:
105+
return "snapshot" if self.materialization == "snapshot" else "model"
106+
103107
@property
104108
def concise_name(self) -> str:
105-
if self.materialization == "snapshot":
106-
dbt_type = "snapshot"
107-
else:
108-
dbt_type = "model"
109-
return f"dbt {dbt_type} alert - {self.alias}"
109+
return self.alias
110110

111111
@property
112112
def summary(self) -> str:

elementary/monitor/alerts/source_freshness_alert.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,13 @@ def data(self) -> Dict:
155155
freshness_description=self.freshness_description,
156156
)
157157

158+
@property
159+
def asset_type(self) -> str:
160+
return "source"
161+
158162
@property
159163
def concise_name(self) -> str:
160-
return f"source freshness alert - {self.source_name}.{self.identifier}"
164+
return f"{self.source_name}.{self.identifier}"
161165

162166
@property
163167
def error_message(self) -> str:

elementary/monitor/alerts/test_alert.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ def data(self) -> Dict:
166166
env=self.env,
167167
)
168168

169+
@property
170+
def asset_type(self) -> str:
171+
return "test"
172+
169173
@property
170174
def concise_name(self) -> str:
171175
if self.test_sub_type_display_name.lower() not in (

elementary/monitor/api/alerts/alert_filters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def _get_alert_node_name(alert: PendingAlertSchema) -> Optional[str]:
2929
alert_node_name = None
3030
alert_type = AlertTypes(alert.type)
3131
if alert_type is AlertTypes.TEST:
32-
alert_node_name = alert.data.test_name # type: ignore[union-attr]
32+
alert_node_name = alert.data.test_name # type: ignore[attr-defined]
3333
elif alert_type is AlertTypes.MODEL or alert_type is AlertTypes.SOURCE_FRESHNESS:
3434
alert_node_name = alert.data.model_unique_id
3535
else:

elementary/monitor/data_monitoring/alerts/data_monitoring_alerts.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from elementary.messages.messaging_integrations.exceptions import (
2323
MessagingIntegrationError,
2424
)
25+
from elementary.monitor.alerts.alert import AlertModel
2526
from elementary.monitor.alerts.alert_messages.builder import (
2627
AlertMessageBuilder,
2728
MessageBuilderConfig,
@@ -385,13 +386,7 @@ def _send_alerts(
385386
self.execution_properties["sent_alert_count"] = self.sent_alert_count
386387
return
387388

388-
sent_successfully_alerts: List[
389-
Union[
390-
TestAlertModel,
391-
ModelAlertModel,
392-
SourceFreshnessAlertModel,
393-
]
394-
] = []
389+
sent_successfully_alerts: List[AlertModel] = []
395390

396391
with alive_bar(len(alerts), title="Sending alerts") as bar:
397392
for alert in alerts:

elementary/monitor/data_monitoring/alerts/integrations/base_integration.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from abc import ABC, abstractmethod
22
from typing import Union
33

4+
from elementary.monitor.alerts.alert import AlertModel
45
from elementary.monitor.alerts.alerts_groups import GroupedByTableAlerts
56
from elementary.monitor.alerts.alerts_groups.base_alerts_group import BaseAlertsGroup
67
from elementary.monitor.alerts.model_alert import ModelAlertModel
@@ -21,13 +22,7 @@ def _initial_client(self, *args, **kwargs):
2122

2223
def _get_alert_template(
2324
self,
24-
alert: Union[
25-
TestAlertModel,
26-
ModelAlertModel,
27-
SourceFreshnessAlertModel,
28-
GroupedByTableAlerts,
29-
BaseAlertsGroup,
30-
],
25+
alert: Union[AlertModel, GroupedByTableAlerts, BaseAlertsGroup],
3126
*args,
3227
**kwargs,
3328
):
@@ -83,12 +78,7 @@ def _get_alerts_group_template(self, alert: BaseAlertsGroup, *args, **kwargs):
8378
@abstractmethod
8479
def _get_fallback_template(
8580
self,
86-
alert: Union[
87-
TestAlertModel,
88-
ModelAlertModel,
89-
SourceFreshnessAlertModel,
90-
GroupedByTableAlerts,
91-
],
81+
alert: Union[AlertModel, GroupedByTableAlerts],
9282
*args,
9383
**kwargs,
9484
):
@@ -97,13 +87,7 @@ def _get_fallback_template(
9787
@abstractmethod
9888
def send_alert(
9989
self,
100-
alert: Union[
101-
TestAlertModel,
102-
ModelAlertModel,
103-
SourceFreshnessAlertModel,
104-
GroupedByTableAlerts,
105-
BaseAlertsGroup,
106-
],
90+
alert: Union[AlertModel, GroupedByTableAlerts, BaseAlertsGroup],
10791
*args,
10892
**kwargs,
10993
) -> bool:

0 commit comments

Comments
 (0)