Skip to content

Commit 3639a4f

Browse files
authored
Merge pull request #688 from simvue-io/wk9874/alert_deduplication
Improved Alert Handling
2 parents b608c5e + 1fac4cb commit 3639a4f

File tree

9 files changed

+158
-48
lines changed

9 files changed

+158
-48
lines changed

simvue/api/objects/alert/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def new(
8787
_offline=offline,
8888
)
8989
_alert._staging |= _alert_definition
90+
_alert._params = {"deduplicate": True}
9091
return _alert
9192

9293

simvue/api/objects/alert/metrics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def new(
105105
_offline=offline,
106106
)
107107
_alert._staging |= _alert_definition
108+
_alert._params = {"deduplicate": True}
109+
108110
return _alert
109111

110112

@@ -194,6 +196,7 @@ def new(
194196
_offline=offline,
195197
)
196198
_alert._staging |= _alert_definition
199+
_alert._params = {"deduplicate": True}
197200
return _alert
198201

199202

simvue/api/objects/alert/user.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def new(
5757
whether this alert should be created locally, default is False
5858
5959
"""
60-
return UserAlert(
60+
_alert = UserAlert(
6161
name=name,
6262
description=description,
6363
notification=notification,
@@ -66,6 +66,8 @@ def new(
6666
_read_only=False,
6767
_offline=offline,
6868
)
69+
_alert._params = {"deduplicate": True}
70+
return _alert
6971

7072
@classmethod
7173
def get(

simvue/api/objects/artifact/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def _upload(self, file: io.BytesIO) -> None:
9797
_response = sv_post(
9898
url=_url,
9999
headers={},
100+
params={},
100101
is_json=False,
101102
files={"file": file},
102103
data=self._init_data.get("fields"),

simvue/api/objects/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ def __init__(
174174
else {}
175175
)
176176

177+
self._params: dict[str, str] = {}
178+
177179
self._staging: dict[str, typing.Any] = {}
178180

179181
# If this object is read-only, but not a local construction, make an API call
@@ -417,6 +419,7 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]:
417419
_response = sv_post(
418420
url=f"{self._base_url}",
419421
headers=self._headers | {"Content-Type": "application/msgpack"},
422+
params=self._params,
420423
data=kwargs,
421424
is_json=is_json,
422425
)
@@ -457,7 +460,7 @@ def _put(self, **kwargs) -> dict[str, typing.Any]:
457460

458461
return get_json_from_response(
459462
response=_response,
460-
expected_status=[http.HTTPStatus.OK],
463+
expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT],
461464
scenario=f"Creation of {self._label} '{self._identifier}",
462465
)
463466

simvue/api/objects/run.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,26 @@ def notifications(
227227

228228
@property
229229
@staging_check
230-
def alerts(self) -> typing.Generator[str, None, None]:
231-
for alert in self.get_alert_details():
232-
yield alert["id"]
230+
def alerts(self) -> list[str]:
231+
if self._offline:
232+
return self._get_attribute("alerts")
233+
234+
return [alert["id"] for alert in self.get_alert_details()]
233235

234236
def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, None]:
235237
"""Retrieve the full details of alerts for this run"""
238+
if self._offline:
239+
raise RuntimeError(
240+
"Cannot get alert details from an offline run - use .alerts to access a list of IDs instead"
241+
)
236242
for alert in self._get_attribute("alerts"):
237243
yield alert["alert"]
238244

239245
@alerts.setter
240246
@write_only
241247
@pydantic.validate_call
242248
def alerts(self, alerts: list[str]) -> None:
243-
self._staging["alerts"] = [
244-
alert for alert in alerts if alert not in self._staging.get("alerts", [])
245-
]
249+
self._staging["alerts"] = list(set(self._staging.get("alerts", []) + alerts))
246250

247251
@property
248252
@staging_check

simvue/api/request.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def is_retryable_exception(exception: Exception) -> bool:
6464
def post(
6565
url: str,
6666
headers: dict[str, str],
67+
params: dict[str, str],
6768
data: typing.Any,
6869
is_json: bool = True,
6970
files: dict[str, typing.Any] | None = None,
@@ -76,6 +77,8 @@ def post(
7677
URL to post to
7778
headers : dict[str, str]
7879
headers for the post request
80+
params : dict[str, str]
81+
query parameters for the post request
7982
data : dict[str, typing.Any]
8083
data to post
8184
is_json : bool, optional
@@ -95,7 +98,12 @@ def post(
9598

9699
logging.debug(f"POST: {url}\n\tdata={data_sent}")
97100
response = requests.post(
98-
url, headers=headers, data=data_sent, timeout=DEFAULT_API_TIMEOUT, files=files
101+
url,
102+
headers=headers,
103+
params=params,
104+
data=data_sent,
105+
timeout=DEFAULT_API_TIMEOUT,
106+
files=files,
99107
)
100108

101109
if response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY:

simvue/run.py

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import click
3030
import psutil
3131

32-
from simvue.api.objects.alert.base import AlertBase
3332
from simvue.api.objects.alert.fetch import Alert
3433
from simvue.api.objects.folder import Folder
3534
from simvue.exception import SimvueRunError
@@ -695,6 +694,7 @@ def init(
695694
self._sv_obj.tags = tags
696695
self._sv_obj.metadata = (metadata or {}) | git_info(os.getcwd()) | environment()
697696
self._sv_obj.heartbeat_timeout = timeout
697+
self._sv_obj.alerts = []
698698
self._sv_obj.notifications = notification
699699

700700
if self._status == "running":
@@ -1642,48 +1642,26 @@ def add_alerts(
16421642
try:
16431643
if alerts := Alert.get(offline=self._user_config.run.mode == "offline"):
16441644
for alert in alerts:
1645-
if alert.name in names:
1646-
ids.append(alert.id)
1645+
if alert[1].name in names:
1646+
ids.append(alert[1].id)
1647+
else:
1648+
self._error("No existing alerts")
1649+
return False
16471650
except RuntimeError as e:
16481651
self._error(f"{e.args[0]}")
16491652
return False
1650-
else:
1651-
self._error("No existing alerts")
1652-
return False
16531653
elif not names and not ids:
16541654
self._error("Need to provide alert ids or alert names")
16551655
return False
16561656

16571657
# Avoid duplication
1658-
self._sv_obj.alerts = list(set(self._sv_obj.alerts + [ids]))
1658+
_deduplicated = list(set(self._sv_obj.alerts + ids))
1659+
self._sv_obj.alerts = _deduplicated
16591660
self._sv_obj.commit()
16601661

1661-
return False
1662-
1663-
def _attach_alert_to_run(self, alert: AlertBase) -> str | None:
1664-
# Check if the alert already exists
1665-
_alert_id: str | None = None
1666-
1667-
for _, _existing_alert in Alert.get(
1668-
offline=self._user_config.run.mode == "offline"
1669-
):
1670-
if _existing_alert.compare(alert):
1671-
_alert_id = _existing_alert.id
1672-
logger.info("Existing alert found with id: %s", _existing_alert.id)
1673-
break
1674-
1675-
if not _alert_id:
1676-
alert.commit()
1677-
_alert_id = alert.id
1678-
1679-
self._sv_obj.alerts = [_alert_id]
1680-
1681-
self._sv_obj.commit()
1682-
1683-
return _alert_id
1662+
return True
16841663

16851664
@skip_if_failed("_aborted", "_suppress_errors", None)
1686-
@check_run_initialised
16871665
@pydantic.validate_call
16881666
def create_metric_range_alert(
16891667
self,
@@ -1701,6 +1679,7 @@ def create_metric_range_alert(
17011679
] = "average",
17021680
notification: typing.Literal["email", "none"] = "none",
17031681
trigger_abort: bool = False,
1682+
attach_to_run: bool = True,
17041683
) -> str | None:
17051684
"""Creates a metric range alert with the specified name (if it doesn't exist)
17061685
and applies it to the current run. If alert already exists it will
@@ -1730,6 +1709,8 @@ def create_metric_range_alert(
17301709
whether to notify on trigger, by default "none"
17311710
trigger_abort : bool, optional
17321711
whether this alert can trigger a run abort, default False
1712+
attach_to_run : bool, optional
1713+
whether to attach this alert to the current run, default True
17331714
17341715
Returns
17351716
-------
@@ -1751,10 +1732,12 @@ def create_metric_range_alert(
17511732
offline=self._user_config.run.mode == "offline",
17521733
)
17531734
_alert.abort = trigger_abort
1754-
return self._attach_alert_to_run(_alert)
1735+
_alert.commit()
1736+
if attach_to_run:
1737+
self.add_alerts(ids=[_alert.id])
1738+
return _alert.id
17551739

17561740
@skip_if_failed("_aborted", "_suppress_errors", None)
1757-
@check_run_initialised
17581741
@pydantic.validate_call
17591742
def create_metric_threshold_alert(
17601743
self,
@@ -1771,6 +1754,7 @@ def create_metric_threshold_alert(
17711754
] = "average",
17721755
notification: typing.Literal["email", "none"] = "none",
17731756
trigger_abort: bool = False,
1757+
attach_to_run: bool = True,
17741758
) -> str | None:
17751759
"""Creates a metric threshold alert with the specified name (if it doesn't exist)
17761760
and applies it to the current run. If alert already exists it will
@@ -1798,6 +1782,8 @@ def create_metric_threshold_alert(
17981782
whether to notify on trigger, by default "none"
17991783
trigger_abort : bool, optional
18001784
whether this alert can trigger a run abort, default False
1785+
attach_to_run : bool, optional
1786+
whether to attach this alert to the current run, default True
18011787
18021788
Returns
18031789
-------
@@ -1817,11 +1803,14 @@ def create_metric_threshold_alert(
18171803
notification=notification,
18181804
offline=self._user_config.run.mode == "offline",
18191805
)
1806+
18201807
_alert.abort = trigger_abort
1821-
return self._attach_alert_to_run(_alert)
1808+
_alert.commit()
1809+
if attach_to_run:
1810+
self.add_alerts(ids=[_alert.id])
1811+
return _alert.id
18221812

18231813
@skip_if_failed("_aborted", "_suppress_errors", None)
1824-
@check_run_initialised
18251814
@pydantic.validate_call
18261815
def create_event_alert(
18271816
self,
@@ -1832,6 +1821,7 @@ def create_event_alert(
18321821
frequency: pydantic.PositiveInt = 1,
18331822
notification: typing.Literal["email", "none"] = "none",
18341823
trigger_abort: bool = False,
1824+
attach_to_run: bool = True,
18351825
) -> str | None:
18361826
"""Creates an events alert with the specified name (if it doesn't exist)
18371827
and applies it to the current run. If alert already exists it will
@@ -1849,6 +1839,8 @@ def create_event_alert(
18491839
whether to notify on trigger, by default "none"
18501840
trigger_abort : bool, optional
18511841
whether this alert can trigger a run abort
1842+
attach_to_run : bool, optional
1843+
whether to attach this alert to the current run, default True
18521844
18531845
Returns
18541846
-------
@@ -1865,10 +1857,12 @@ def create_event_alert(
18651857
offline=self._user_config.run.mode == "offline",
18661858
)
18671859
_alert.abort = trigger_abort
1868-
return self._attach_alert_to_run(_alert)
1860+
_alert.commit()
1861+
if attach_to_run:
1862+
self.add_alerts(ids=[_alert.id])
1863+
return _alert.id
18691864

18701865
@skip_if_failed("_aborted", "_suppress_errors", None)
1871-
@check_run_initialised
18721866
@pydantic.validate_call
18731867
def create_user_alert(
18741868
self,
@@ -1877,6 +1871,7 @@ def create_user_alert(
18771871
description: str | None = None,
18781872
notification: typing.Literal["email", "none"] = "none",
18791873
trigger_abort: bool = False,
1874+
attach_to_run: bool = True,
18801875
) -> None:
18811876
"""Creates a user alert with the specified name (if it doesn't exist)
18821877
and applies it to the current run. If alert already exists it will
@@ -1892,6 +1887,8 @@ def create_user_alert(
18921887
whether to notify on trigger, by default "none"
18931888
trigger_abort : bool, optional
18941889
whether this alert can trigger a run abort, default False
1890+
attach_to_run : bool, optional
1891+
whether to attach this alert to the current run, default True
18951892
18961893
Returns
18971894
-------
@@ -1906,7 +1903,10 @@ def create_user_alert(
19061903
offline=self._user_config.run.mode == "offline",
19071904
)
19081905
_alert.abort = trigger_abort
1909-
return self._attach_alert_to_run(_alert)
1906+
_alert.commit()
1907+
if attach_to_run:
1908+
self.add_alerts(ids=[_alert.id])
1909+
return _alert.id
19101910

19111911
@skip_if_failed("_aborted", "_suppress_errors", False)
19121912
@check_run_initialised

0 commit comments

Comments
 (0)