From 53152526cbb321dae2b5634ca2c6225b9ec42d13 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 15:03:10 +0000 Subject: [PATCH 01/15] Add params option to post and put --- simvue/api/objects/alert/base.py | 1 + simvue/api/objects/base.py | 7 +++++-- simvue/api/request.py | 10 +++++++++- simvue/run.py | 16 ++-------------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 0204f6d3..58bc7ccb 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -29,6 +29,7 @@ def new(cls, **kwargs): def __init__(self, identifier: str | None = None, **kwargs) -> None: """Retrieve an alert from the Simvue server by identifier""" self._label = "alert" + self._staging = {"deduplicate": True} super().__init__(identifier=identifier, **kwargs) def compare(self, other: "AlertBase") -> bool: diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index 7f43904f..840e8b66 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -169,6 +169,8 @@ def __init__( "User-Agent": _user_agent or f"Simvue Python client {__version__}", } + self._params: dict[str, str] = {} + self._staging: dict[str, typing.Any] = {} # If this object is read-only, but not a local construction, make an API call @@ -412,6 +414,7 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: _response = sv_post( url=f"{self._base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, + params=self._params, data=kwargs, is_json=is_json, ) @@ -423,7 +426,7 @@ def _post(self, is_json: bool = True, **kwargs) -> dict[str, typing.Any]: _json_response = get_json_from_response( response=_response, - expected_status=[http.HTTPStatus.OK], + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], scenario=f"Creation of {self._label}", ) @@ -452,7 +455,7 @@ def _put(self, **kwargs) -> dict[str, typing.Any]: return get_json_from_response( response=_response, - expected_status=[http.HTTPStatus.OK], + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.CONFLICT], scenario=f"Creation of {self._label} '{self._identifier}", ) diff --git a/simvue/api/request.py b/simvue/api/request.py index 3ebdb86b..8dd6a8bd 100644 --- a/simvue/api/request.py +++ b/simvue/api/request.py @@ -64,6 +64,7 @@ def is_retryable_exception(exception: Exception) -> bool: def post( url: str, headers: dict[str, str], + params: dict[str, str], data: typing.Any, is_json: bool = True, files: dict[str, typing.Any] | None = None, @@ -76,6 +77,8 @@ def post( URL to post to headers : dict[str, str] headers for the post request + params : dict[str, str] + query parameters for the post request data : dict[str, typing.Any] data to post is_json : bool, optional @@ -95,7 +98,12 @@ def post( logging.debug(f"POST: {url}\n\tdata={data_sent}") response = requests.post( - url, headers=headers, data=data_sent, timeout=DEFAULT_API_TIMEOUT, files=files + url, + headers=headers, + params=params, + data=data_sent, + timeout=DEFAULT_API_TIMEOUT, + files=files, ) if response.status_code == http.HTTPStatus.UNPROCESSABLE_ENTITY: diff --git a/simvue/run.py b/simvue/run.py index af7a5aa0..f4397a0b 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1651,20 +1651,8 @@ def add_alerts( return False def _attach_alert_to_run(self, alert: AlertBase) -> str | None: - # Check if the alert already exists - _alert_id: str | None = None - - for _, _existing_alert in Alert.get( - offline=self._user_config.run.mode == "offline" - ): - if _existing_alert.compare(alert): - _alert_id = _existing_alert.id - logger.info("Existing alert found with id: %s", _existing_alert.id) - break - - if not _alert_id: - alert.commit() - _alert_id = alert.id + alert.commit() + _alert_id: str = alert.id self._sv_obj.alerts = [_alert_id] From 0fb38ea57cd1c622a6cb40a33ccf9549e465d1c4 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 15:07:47 +0000 Subject: [PATCH 02/15] Add optional attach to run --- simvue/run.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index f4397a0b..c7c8c755 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1651,15 +1651,9 @@ def add_alerts( return False def _attach_alert_to_run(self, alert: AlertBase) -> str | None: - alert.commit() - _alert_id: str = alert.id - - self._sv_obj.alerts = [_alert_id] - + self._sv_obj.alerts = [alert.id] self._sv_obj.commit() - return _alert_id - @skip_if_failed("_aborted", "_suppress_errors", None) @check_run_initialised @pydantic.validate_call @@ -1679,6 +1673,7 @@ def create_metric_range_alert( ] = "average", notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates a metric range alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1708,6 +1703,8 @@ def create_metric_range_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1729,7 +1726,10 @@ def create_metric_range_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self._attach_alert_to_run(_alert) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @check_run_initialised @@ -1749,6 +1749,7 @@ def create_metric_threshold_alert( ] = "average", notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates a metric threshold alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1776,6 +1777,8 @@ def create_metric_threshold_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1796,7 +1799,10 @@ def create_metric_threshold_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self._attach_alert_to_run(_alert) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @check_run_initialised @@ -1810,6 +1816,7 @@ def create_event_alert( frequency: pydantic.PositiveInt = 1, notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> str | None: """Creates an events alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1827,6 +1834,8 @@ def create_event_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1843,7 +1852,10 @@ def create_event_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self._attach_alert_to_run(_alert) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @check_run_initialised @@ -1855,6 +1867,7 @@ def create_user_alert( description: str | None = None, notification: typing.Literal["email", "none"] = "none", trigger_abort: bool = False, + attach_to_run: bool = True, ) -> None: """Creates a user alert with the specified name (if it doesn't exist) and applies it to the current run. If alert already exists it will @@ -1870,6 +1883,8 @@ def create_user_alert( whether to notify on trigger, by default "none" trigger_abort : bool, optional whether this alert can trigger a run abort, default False + attach_to_run : bool, optional + whether to attach this alert to the current run, default True Returns ------- @@ -1884,7 +1899,10 @@ def create_user_alert( offline=self._user_config.run.mode == "offline", ) _alert.abort = trigger_abort - return self._attach_alert_to_run(_alert) + _alert.commit() + if attach_to_run: + self._attach_alert_to_run(_alert) + return _alert.id @skip_if_failed("_aborted", "_suppress_errors", False) @check_run_initialised From 8ba756fdf5d48839f9c94446168a5987fe141c99 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 15:42:20 +0000 Subject: [PATCH 03/15] Added params correctly in new of each alert --- simvue/api/objects/alert/base.py | 1 - simvue/api/objects/alert/events.py | 1 + simvue/api/objects/alert/metrics.py | 3 ++ simvue/api/objects/alert/user.py | 4 +- simvue/api/objects/artifact/fetch.py | 76 ++++++++++++++++++++++------ simvue/run.py | 1 + 6 files changed, 68 insertions(+), 18 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 58bc7ccb..0204f6d3 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -29,7 +29,6 @@ def new(cls, **kwargs): def __init__(self, identifier: str | None = None, **kwargs) -> None: """Retrieve an alert from the Simvue server by identifier""" self._label = "alert" - self._staging = {"deduplicate": True} super().__init__(identifier=identifier, **kwargs) def compare(self, other: "AlertBase") -> bool: diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 0d38b63f..00558ffb 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -87,6 +87,7 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} return _alert diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 2fb74f06..e9340873 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -105,6 +105,8 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} + return _alert @@ -194,6 +196,7 @@ def new( _offline=offline, ) _alert._staging |= _alert_definition + _alert._params = {"deduplicate": True} return _alert diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 9ddcd6e1..7fdcee43 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -57,7 +57,7 @@ def new( whether this alert should be created locally, default is False """ - return UserAlert( + _alert = UserAlert( name=name, description=description, notification=notification, @@ -66,6 +66,8 @@ def new( _read_only=False, _offline=offline, ) + _alert._params = {"deduplicate": True} + return _alert @classmethod def get( diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 88f582ce..1d571266 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -23,6 +23,62 @@ def __new__(cls, identifier: str | None = None, **kwargs): else: return ObjectArtifact(identifier=identifier, **kwargs) + @classmethod + def from_run( + cls, + run_id: str, + category: typing.Literal["input", "output", "code"] | None = None, + **kwargs, + ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]: + """Return artifacts associated with a given run. + + Parameters + ---------- + run_id : str + The ID of the run to retriece artifacts from + category : typing.Literal["input", "output", "code"] | None, optional + The category of artifacts to return, by default all artifacts are returned + + Returns + ------- + typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None] + The artifacts + + Yields + ------ + Iterator[typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]] + identifier for artifact + the artifact itself as a class instance + + Raises + ------ + ObjectNotFoundError + Raised if artifacts could not be found for that run + """ + _temp = ArtifactBase(**kwargs) + _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" + _response = sv_get( + url=f"{_url}", params={"category": category}, headers=_temp._headers + ) + _json_response = get_json_from_response( + expected_type=list, + response=_response, + expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NOT_FOUND], + scenario=f"Retrieval of artifacts for run '{run_id}'", + ) + + if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: + raise ObjectNotFoundError( + _temp._label, category, extra=f"for run '{run_id}'" + ) + + for _entry in _json_response: + _id = _entry.pop("id") + yield ( + _id, + Artifact(_local=True, _read_only=True, identifier=_id, **_entry), + ) + @classmethod def from_name( cls, run_id: str, name: str, **kwargs @@ -99,21 +155,9 @@ def get( if (_data := _json_response.get("data")) is None: raise RuntimeError(f"Expected key 'data' for retrieval of {_label}s") - _out_dict: dict[str, FileArtifact | ObjectArtifact] = {} - for _entry in _data: _id = _entry.pop("id") - if _entry["original_path"]: - yield ( - _id, - FileArtifact( - _local=True, _read_only=True, identifier=_id, **_entry - ), - ) - else: - yield ( - _id, - ObjectArtifact( - _local=True, _read_only=True, identifier=_id, **_entry - ), - ) + yield ( + _id, + Artifact(_local=True, _read_only=True, identifier=_id, **_entry), + ) diff --git a/simvue/run.py b/simvue/run.py index c7c8c755..9a3d2a9b 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1798,6 +1798,7 @@ def create_metric_threshold_alert( notification=notification, offline=self._user_config.run.mode == "offline", ) + _alert.abort = trigger_abort _alert.commit() if attach_to_run: From e2669b47bc34aec11a0c842344c67ac622f9907e Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 15:53:17 +0000 Subject: [PATCH 04/15] Fixed post in artifacts --- simvue/api/objects/artifact/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simvue/api/objects/artifact/base.py b/simvue/api/objects/artifact/base.py index dac009f5..0b5af04d 100644 --- a/simvue/api/objects/artifact/base.py +++ b/simvue/api/objects/artifact/base.py @@ -97,6 +97,7 @@ def _upload(self, file: io.BytesIO) -> None: _response = sv_post( url=_url, headers={}, + params={}, is_json=False, files={"file": file}, data=self._init_data.get("fields"), From 71c157e6c59bb1d65ec48fb8aa63f28e0eeed039 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 16:24:25 +0000 Subject: [PATCH 05/15] Drop requirement for initialized run for creating alerts and move to attach to run --- simvue/run.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index 9a3d2a9b..63369751 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1650,12 +1650,12 @@ def add_alerts( return False + @check_run_initialised def _attach_alert_to_run(self, alert: AlertBase) -> str | None: self._sv_obj.alerts = [alert.id] self._sv_obj.commit() @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_metric_range_alert( self, @@ -1732,7 +1732,6 @@ def create_metric_range_alert( return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_metric_threshold_alert( self, @@ -1806,7 +1805,6 @@ def create_metric_threshold_alert( return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_event_alert( self, @@ -1859,7 +1857,6 @@ def create_event_alert( return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) - @check_run_initialised @pydantic.validate_call def create_user_alert( self, From 95f5845872109952e5ee65bbb9c772b0b7f0f12d Mon Sep 17 00:00:00 2001 From: Matt Field Date: Thu, 13 Feb 2025 17:02:52 +0000 Subject: [PATCH 06/15] Change .alerts to return a list and fix add_alerts --- simvue/api/objects/run.py | 5 ++--- simvue/run.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 9841ded5..cbc470a3 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -225,9 +225,8 @@ def notifications(self, notifications: typing.Literal["none", "email"]) -> None: @property @staging_check - def alerts(self) -> typing.Generator[str, None, None]: - for alert in self.get_alert_details(): - yield alert["id"] + def alerts(self) -> list[str]: + return [alert["id"] for alert in self.get_alert_details()] def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, None]: """Retrieve the full details of alerts for this run""" diff --git a/simvue/run.py b/simvue/run.py index 63369751..12c006de 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1645,7 +1645,7 @@ def add_alerts( return False # Avoid duplication - self._sv_obj.alerts = list(set(self._sv_obj.alerts + [ids])) + self._sv_obj.alerts = list(set(self._sv_obj.alerts + ids)) self._sv_obj.commit() return False From 683c6f45572a8f95e618fcb00ac561b19d15df0f Mon Sep 17 00:00:00 2001 From: Matt Field Date: Fri, 14 Feb 2025 13:09:21 +0000 Subject: [PATCH 07/15] Remove attach_to_run as it isnt required, replace with add_alerts --- simvue/run.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index 12c006de..5f31ba21 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -29,7 +29,6 @@ import click import psutil -from simvue.api.objects.alert.base import AlertBase from simvue.api.objects.alert.fetch import Alert from simvue.api.objects.folder import Folder, get_folder_from_path from simvue.exception import ObjectNotFoundError, SimvueRunError @@ -1650,11 +1649,6 @@ def add_alerts( return False - @check_run_initialised - def _attach_alert_to_run(self, alert: AlertBase) -> str | None: - self._sv_obj.alerts = [alert.id] - self._sv_obj.commit() - @skip_if_failed("_aborted", "_suppress_errors", None) @pydantic.validate_call def create_metric_range_alert( @@ -1728,7 +1722,7 @@ def create_metric_range_alert( _alert.abort = trigger_abort _alert.commit() if attach_to_run: - self._attach_alert_to_run(_alert) + self.add_alerts(ids=[_alert.id]) return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @@ -1801,7 +1795,7 @@ def create_metric_threshold_alert( _alert.abort = trigger_abort _alert.commit() if attach_to_run: - self._attach_alert_to_run(_alert) + self.add_alerts(ids=[_alert.id]) return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @@ -1853,7 +1847,7 @@ def create_event_alert( _alert.abort = trigger_abort _alert.commit() if attach_to_run: - self._attach_alert_to_run(_alert) + self.add_alerts(ids=[_alert.id]) return _alert.id @skip_if_failed("_aborted", "_suppress_errors", None) @@ -1899,7 +1893,7 @@ def create_user_alert( _alert.abort = trigger_abort _alert.commit() if attach_to_run: - self._attach_alert_to_run(_alert) + self.add_alerts(ids=[_alert.id]) return _alert.id @skip_if_failed("_aborted", "_suppress_errors", False) From e4ac228df42f2de3a4eea8a87e7ed747a7123a3c Mon Sep 17 00:00:00 2001 From: Matt Field Date: Fri, 14 Feb 2025 13:46:10 +0000 Subject: [PATCH 08/15] Add initial sv_obj.alert as an empty list in init --- simvue/run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simvue/run.py b/simvue/run.py index 5f31ba21..8e821886 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -685,6 +685,7 @@ def init( self._sv_obj.tags = tags self._sv_obj.metadata = (metadata or {}) | git_info(os.getcwd()) | environment() self._sv_obj.heartbeat_timeout = timeout + self._sv_obj.alerts = [] if self._status == "running": self._sv_obj.system = get_system() From 030f85474b78476a2d6264acfd41490d17e565dc Mon Sep 17 00:00:00 2001 From: Matt Field Date: Fri, 14 Feb 2025 16:29:33 +0000 Subject: [PATCH 09/15] Fixed alert retrieval in offline --- simvue/api/objects/run.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index cbc470a3..60cad198 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -226,10 +226,17 @@ def notifications(self, notifications: typing.Literal["none", "email"]) -> None: @property @staging_check def alerts(self) -> list[str]: + if self._offline: + return self._get_attribute("alerts") + return [alert["id"] for alert in self.get_alert_details()] def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, None]: """Retrieve the full details of alerts for this run""" + if self._offline: + raise RuntimeError( + "Cannot get alert details from an offline run - use .alerts to access a list of IDs instead" + ) for alert in self._get_attribute("alerts"): yield alert["alert"] From 51fb0a47f7d14b2a19c97e29853ce715236cdc96 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Fri, 14 Feb 2025 17:00:17 +0000 Subject: [PATCH 10/15] Fixing add alerts wip --- simvue/run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index 8e821886..db260469 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1645,10 +1645,11 @@ def add_alerts( return False # Avoid duplication - self._sv_obj.alerts = list(set(self._sv_obj.alerts + ids)) + _deduplicated = list(set(self._sv_obj.alerts + ids)) + self._sv_obj.alerts = _deduplicated self._sv_obj.commit() - return False + return True @skip_if_failed("_aborted", "_suppress_errors", None) @pydantic.validate_call From 64075448850184dd8c2fb8e7f8127aee5883f486 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Fri, 14 Feb 2025 17:13:52 +0000 Subject: [PATCH 11/15] Still fixing add_alert --- simvue/run.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/simvue/run.py b/simvue/run.py index db260469..1c495e12 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -1632,14 +1632,14 @@ def add_alerts( try: if alerts := Alert.get(offline=self._user_config.run.mode == "offline"): for alert in alerts: - if alert.name in names: - ids.append(alert.id) + if alert[1].name in names: + ids.append(alert[1].id) + else: + self._error("No existing alerts") + return False except RuntimeError as e: self._error(f"{e.args[0]}") return False - else: - self._error("No existing alerts") - return False elif not names and not ids: self._error("Need to provide alert ids or alert names") return False From 475cb92a8f30052c604283fc66eecd1e7419f4f4 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Mon, 17 Feb 2025 14:06:33 +0000 Subject: [PATCH 12/15] Create add_alerts test --- tests/functional/test_run_class.py | 88 ++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/functional/test_run_class.py b/tests/functional/test_run_class.py index 5de51a8a..0e9d26ab 100644 --- a/tests/functional/test_run_class.py +++ b/tests/functional/test_run_class.py @@ -676,6 +676,94 @@ def test_save_object( save_obj = array([1, 2, 3, 4]) simvue_run.save_object(save_obj, "input", f"test_object_{object_type}") +@pytest.mark.run +def test_add_alerts() -> None: + _uuid = f"{uuid.uuid4()}".split("-")[0] + + run = sv_run.Run() + run.init( + name="test_add_alerts", + folder="/simvue_unit_tests", + retention_period="1 min", + tags=["test_add_alerts"], + visibility="tenant" + ) + + _expected_alerts = [] + + # Create alerts, have them attach to run automatically + _id = run.create_event_alert( + name=f"event_alert_{_uuid}", + pattern = "test", + ) + _expected_alerts.append(_id) + time.sleep(1) + # Retrieve run, check if alert has been added + _online_run = RunObject(identifier=run._id) + assert _id in _online_run.alerts + + # Create another alert and attach to run + _id = run.create_metric_range_alert( + name=f"metric_range_alert_{_uuid}", + metric="test", + range_low=10, + range_high=100, + rule="is inside range", + ) + _expected_alerts.append(_id) + time.sleep(1) + # Retrieve run, check both alerts have been added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Create another alert, do not attach to run + _id = run.create_metric_threshold_alert( + name=f"metric_threshold_alert_{_uuid}", + metric="test", + threshold=10, + rule="is above", + attach_to_run=False + ) + time.sleep(1) + # Retrieve run, check alert has NOT been added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Try adding all three alerts using add_alerts + _expected_alerts.append(_id) + run.add_alerts(names=[f"event_alert_{_uuid}", f"metric_range_alert_{_uuid}", f"metric_threshold_alert_{_uuid}"]) + time.sleep(1) + + # Check that there is no duplication + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Create another run without adding to run + _id = run.create_user_alert( + name=f"user_alert_{_uuid}", + attach_to_run=False + ) + time.sleep(1) + + # Check alert is not added + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + # Try adding alerts with IDs, check there is no duplication + _expected_alerts.append(_id) + run.add_alerts(ids=_expected_alerts) + time.sleep(1) + + _online_run.refresh() + assert sorted(_online_run.alerts) == sorted(_expected_alerts) + + run.close() + + client = sv_cl.Client() + client.delete_run(run._id) + for _id in _expected_alerts: + client.delete_alert(_id) + @pytest.mark.run def test_abort_on_alert_process(mocker: pytest_mock.MockerFixture) -> None: From f884daa86cd87547a37fe34144ab50e421a567e2 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Mon, 17 Feb 2025 14:08:28 +0000 Subject: [PATCH 13/15] Moved get status to under sleep in test --- tests/functional/test_run_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_run_class.py b/tests/functional/test_run_class.py index 0e9d26ab..7bb897b4 100644 --- a/tests/functional/test_run_class.py +++ b/tests/functional/test_run_class.py @@ -844,8 +844,8 @@ def testing_exit(status: int) -> None: run.add_process(identifier="forever_long", executable="bash", c="sleep 10") time.sleep(2) run.log_alert(alert_id, "critical") - _alert = Alert(identifier=alert_id) time.sleep(1) + _alert = Alert(identifier=alert_id) assert _alert.get_status(run.id) == "critical" counter = 0 while run._status != "terminated" and counter < 15: From 28b84b23047744960f1e03b2e978eba7894cd109 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Mon, 17 Feb 2025 14:21:56 +0000 Subject: [PATCH 14/15] =?UTF-8?q?Remove=20artifact=20changes=20which=20sho?= =?UTF-8?q?uldnt=20be=20in=20this=20PR=C2=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simvue/api/objects/artifact/fetch.py | 76 ++++++---------------------- 1 file changed, 16 insertions(+), 60 deletions(-) diff --git a/simvue/api/objects/artifact/fetch.py b/simvue/api/objects/artifact/fetch.py index 1d571266..88f582ce 100644 --- a/simvue/api/objects/artifact/fetch.py +++ b/simvue/api/objects/artifact/fetch.py @@ -23,62 +23,6 @@ def __new__(cls, identifier: str | None = None, **kwargs): else: return ObjectArtifact(identifier=identifier, **kwargs) - @classmethod - def from_run( - cls, - run_id: str, - category: typing.Literal["input", "output", "code"] | None = None, - **kwargs, - ) -> typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]: - """Return artifacts associated with a given run. - - Parameters - ---------- - run_id : str - The ID of the run to retriece artifacts from - category : typing.Literal["input", "output", "code"] | None, optional - The category of artifacts to return, by default all artifacts are returned - - Returns - ------- - typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None] - The artifacts - - Yields - ------ - Iterator[typing.Generator[tuple[str, FileArtifact | ObjectArtifact], None, None]] - identifier for artifact - the artifact itself as a class instance - - Raises - ------ - ObjectNotFoundError - Raised if artifacts could not be found for that run - """ - _temp = ArtifactBase(**kwargs) - _url = URL(_temp._user_config.server.url) / f"runs/{run_id}/artifacts" - _response = sv_get( - url=f"{_url}", params={"category": category}, headers=_temp._headers - ) - _json_response = get_json_from_response( - expected_type=list, - response=_response, - expected_status=[http.HTTPStatus.OK, http.HTTPStatus.NOT_FOUND], - scenario=f"Retrieval of artifacts for run '{run_id}'", - ) - - if _response.status_code == http.HTTPStatus.NOT_FOUND or not _json_response: - raise ObjectNotFoundError( - _temp._label, category, extra=f"for run '{run_id}'" - ) - - for _entry in _json_response: - _id = _entry.pop("id") - yield ( - _id, - Artifact(_local=True, _read_only=True, identifier=_id, **_entry), - ) - @classmethod def from_name( cls, run_id: str, name: str, **kwargs @@ -155,9 +99,21 @@ def get( if (_data := _json_response.get("data")) is None: raise RuntimeError(f"Expected key 'data' for retrieval of {_label}s") + _out_dict: dict[str, FileArtifact | ObjectArtifact] = {} + for _entry in _data: _id = _entry.pop("id") - yield ( - _id, - Artifact(_local=True, _read_only=True, identifier=_id, **_entry), - ) + if _entry["original_path"]: + yield ( + _id, + FileArtifact( + _local=True, _read_only=True, identifier=_id, **_entry + ), + ) + else: + yield ( + _id, + ObjectArtifact( + _local=True, _read_only=True, identifier=_id, **_entry + ), + ) From f1a1e69b3581271719deb6622d615c7c814737d5 Mon Sep 17 00:00:00 2001 From: Matt Field Date: Mon, 17 Feb 2025 14:54:10 +0000 Subject: [PATCH 15/15] Fix alerts setter --- simvue/api/objects/run.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 60cad198..371cf29c 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -244,9 +244,7 @@ def get_alert_details(self) -> typing.Generator[dict[str, typing.Any], None, Non @write_only @pydantic.validate_call def alerts(self, alerts: list[str]) -> None: - self._staging["alerts"] = [ - alert for alert in alerts if alert not in self._staging.get("alerts", []) - ] + self._staging["alerts"] = list(set(self._staging.get("alerts", []) + alerts)) @property @staging_check