From ddb06103641b261b2a351a67a4e18906487cee86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 27 May 2026 15:39:24 +0100 Subject: [PATCH 1/6] Fixed executor in offline mode --- simvue/api/objects/alert/base.py | 20 ++++++- simvue/api/objects/alert/events.py | 9 +++- simvue/api/objects/alert/metrics.py | 10 ++++ simvue/api/objects/alert/user.py | 5 ++ simvue/api/objects/run.py | 8 ++- simvue/executor.py | 12 ++--- simvue/run.py | 55 ++++++++++---------- simvue/sender/actions.py | 4 ++ tests/functional/test_run_execute_process.py | 2 +- 9 files changed, 84 insertions(+), 41 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index f22c02d8..0c86f603 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -8,6 +8,7 @@ import pydantic import datetime import typing +import abc from simvue.api.objects.base import SimvueObject, staging_check, write_only from simvue.api.request import get as sv_get, get_json_from_response from simvue.api.url import URL @@ -19,7 +20,7 @@ from typing_extensions import Self, override # noqa: UP035 -class AlertBase(SimvueObject): +class AlertBase(SimvueObject, abc.ABC): """Class for interfacing with Simvue alerts Contains properties common to all alert types. @@ -29,7 +30,20 @@ class AlertBase(SimvueObject): @override @classmethod - def new(cls, *_, **__) -> Self: + @abc.abstractmethod + def new( + cls, + *, + name: typing.Annotated[str, pydantic.Field(pattern=NAME_REGEX)], + description: str | None, + notification: typing.Literal["none", "email"], + enabled: bool, + allow_duplicates: bool, + offline: bool, + server_url: str | None, + server_token: pydantic.SecretStr | None, + **_, + ) -> Self: raise NotImplementedError @override @@ -211,6 +225,8 @@ def set_status(self, run_id: str, status: typing.Literal["ok", "critical"]) -> N def get_status(self, run_id: str) -> typing.Literal["ok", "critical"]: """Retrieve the status of this alert for a given run""" + _offline_run: bool = run_id.startswith("offline") + if not self._offline and run_id.startswith("offline"): raise ValueError( f"Cannot retrieve status of online alert '{self.id}' for offline run '{run_id}'" diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 2833b82c..5d278825 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -81,10 +81,11 @@ def new( notification: typing.Literal["none", "email"], pattern: str, frequency: pydantic.PositiveInt, - server_url: str | None = None, - server_token: pydantic.SecretStr | None = None, enabled: bool = True, + allow_duplicates: bool = True, offline: bool = False, + server_url: str | None = None, + server_token: pydantic.SecretStr | None = None, **_, ) -> Self: """Create a new event-based alert @@ -105,6 +106,9 @@ def new( how often to check for updates enabled : bool, optional enable this alert upon creation, default is True + allow_duplicates : bool, optional + whether to raise exception if duplicate alert requested, + default of True will allow duplicates offline : bool, optional create alert locally, default is False server_url: str | None, optional @@ -127,6 +131,7 @@ def new( source="events", alert=_alert_definition, enabled=enabled, + deduplicate=not allow_duplicates, server_url=server_url, server_token=server_token, _read_only=False, diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index 43ce4843..be94ede2 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -100,6 +100,7 @@ def new( threshold: float | int, frequency: pydantic.PositiveInt, enabled: bool = True, + allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -131,6 +132,9 @@ def new( how often to monitor the metric enabled : bool, optional whether this alert is enabled upon creation, default is True + allow_duplicates : bool, optional + whether to raise exception if duplicate alert requested, + default of True will allow duplicates offline : bool, optional whether to create the alert locally, default is False server_url: str | None, optional @@ -160,6 +164,7 @@ def new( enabled=enabled, server_url=server_url, server_token=server_token, + deduplicate=not allow_duplicates, _read_only=False, _offline=offline, ) @@ -243,6 +248,7 @@ def new( range_low: float, frequency: pydantic.PositiveInt, enabled: bool = True, + allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -276,6 +282,9 @@ def new( how often to monitor the metric enabled : bool, optional whether this alert is enabled upon creation, default is True + allow_duplicates : bool, optional + whether to raise exception if duplicate alert requested, + default of True will allow duplicates offline : bool, optional whether to create the alert locally, default is False server_url: str | None, optional @@ -305,6 +314,7 @@ def new( alert=_alert_definition, server_url=server_url, server_token=server_token, + deduplicate=not allow_duplicates, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 5461d40e..1d6dd836 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -66,6 +66,7 @@ def new( description: str | None, notification: typing.Literal["none", "email"], enabled: bool = True, + allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -85,6 +86,9 @@ def new( configure notification settings for this alert enabled : bool, optional whether this alert is enabled upon creation, default is True + allow_duplicates : bool, optional + whether to raise exception if duplicate alert requested, + default of True will allow duplicates offline : bool, optional whether this alert should be created locally, default is False server_url: str | None, optional @@ -101,6 +105,7 @@ def new( enabled=enabled, server_url=server_url, server_token=server_token, + deduplicate=not allow_duplicates, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/run.py b/simvue/api/objects/run.py index 79edd31a..b4d894f8 100644 --- a/simvue/api/objects/run.py +++ b/simvue/api/objects/run.py @@ -795,8 +795,12 @@ def on_reconnect(self, id_mapping: dict[str, str]) -> None: id_mapping: dict[str, str] A mapping from offline identifier to online identifier. """ - online_alert_ids: list[str] = list( - set(id_mapping.get(_id) for _id in self._staging.get("alerts", [])) + online_alert_ids: list[str | None] = list( + set( + id_mapping.get(_id) + for _id in self._staging.get("alerts", []) + if _id.startswith("offline") + ) ) if not all(online_alert_ids): raise KeyError("Could not find alert ID in offline to online ID mapping.") diff --git a/simvue/executor.py b/simvue/executor.py index 7b7f1a9a..ff8be2fc 100644 --- a/simvue/executor.py +++ b/simvue/executor.py @@ -420,7 +420,10 @@ def _update_alerts(self) -> None: server_url=self._runner._user_config.server.url, server_token=self._runner._user_config.server.token, ) - _is_set = _alert.get_status(run_id=self._runner.id) + _is_set: bool = False + + if self._runner.mode == "online": + _is_set = _alert.get_status(run_id=self._runner.id) is not None if process.returncode != 0: # If the process fails then purge the dispatcher event queue @@ -431,11 +434,8 @@ def _update_alerts(self) -> None: self._runner.log_alert( identifier=self._alert_ids[proc_id], state="critical" ) - else: - if not _is_set: - self._runner.log_alert( - identifier=self._alert_ids[proc_id], state="ok" - ) + elif self._runner.mode == "online" and not _is_set: + self._runner.log_alert(identifier=self._alert_ids[proc_id], state="ok") _current_time: float = 0 while ( diff --git a/simvue/run.py b/simvue/run.py index 6f73531f..d4c5b06f 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -285,9 +285,7 @@ def __exit__( ) -> None: logger.debug( "Automatically closing run '%s' in status %s", - self.id - if self._user_config.run.mode == "online" and self._sv_obj - else "unregistered", + self.id if self.mode == "online" and self._sv_obj else "unregistered", self._status, ) @@ -301,6 +299,11 @@ def duration(self) -> float: """Return current run duration""" return time.time() - self._start_time + @property + def mode(self) -> typing.Literal["offline", "online", "disabled"]: + """Return whether this run is offline.""" + return self._user_config.run.mode + @property def processes(self) -> list[psutil.Process]: """Create an array containing a list of processes""" @@ -500,7 +503,7 @@ def _dispatch_callback( if category == "events": _events = Events.new( run=self.id, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, events=buffer, @@ -512,13 +515,13 @@ def _dispatch_callback( data=buffer, server_url=self._user_config.server.url, server_token=self._user_config.server.token, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", ) return _grid_metrics.commit() else: _metrics = Metrics.new( run=self.id, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, metrics=buffer, @@ -550,7 +553,7 @@ def _start(self) -> bool: if self._sv_obj.status != "running": self._sv_obj.status = self._status _changed = True - if self._user_config.run.mode == "offline": + if self.mode == "offline": self._sv_obj.started = self._start_time _changed = True if _changed: @@ -721,7 +724,7 @@ def init( self._folder = Folder.new( path=folder, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -742,7 +745,7 @@ def init( if name and not re.match(r"^[a-zA-Z0-9\-\_\s\/\.:]+$", name): self._error("specified name is invalid") return False - elif not name and self._user_config.run.mode == "offline": + elif not name and self.mode == "offline": name = randomname.get_name() self._status = "running" if running else "created" @@ -1219,7 +1222,7 @@ def config( "Emissions metrics require resource metrics collection - make sure resource metrics are enabled!" ) return False - if self._user_config.run.mode == "offline": + if self.mode == "offline": # Create an emissions monitor with no API calls self._emissions_monitor = CO2Monitor( intensity_refresh_interval=None, @@ -1627,7 +1630,7 @@ def assign_metric_to_grid( name=grid_name, grid=axes_ticks, labels=axes_labels, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -1650,7 +1653,7 @@ def assign_metric_to_grid( try: _grid_attach = Grid( identifier=self._grids[grid_name]["id"], - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -1859,7 +1862,7 @@ def save_object( allow_pickling=allow_pickle, storage=self._storage_id, metadata=metadata, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -1931,7 +1934,7 @@ def save_file( name=name or stored_file_name, storage=self._storage_id, file_path=file_path, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", mime_type=file_type, metadata=metadata, snapshot=snapshot, @@ -2096,11 +2099,7 @@ def _tidy_run(self) -> None: self._heartbeat_termination_trigger.set() self._heartbeat_thread.join() - if ( - self._sv_obj - and self._user_config.run.mode == "offline" - and self._status != "created" - ): + if self._sv_obj and self.mode == "offline" and self._status != "created": self._user_config.offline.cache.joinpath( "runs", f"{self.id}.closed" ).touch() @@ -2231,14 +2230,14 @@ def add_alerts( names = names or [] if names and not ids: - if self._user_config.run.mode == "offline": + if self.mode == "offline": self._error( "Cannot retrieve alerts based on names in offline mode - please use IDs instead." ) return False try: if alerts := Alert.get( - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ): @@ -2264,7 +2263,7 @@ def _check_if_alert_exists(self, alert: "AlertBase") -> str | None: """Check if an existing alert matches definition.""" # If the alert already exists just add the existing one for _id, _existing_alert in Alert.get( - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ): @@ -2348,7 +2347,7 @@ def create_metric_range_alert( range_low=range_low, range_high=range_high, frequency=frequency or 60, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -2438,7 +2437,7 @@ def create_metric_threshold_alert( frequency=frequency, aggregation=aggregation, notification=notification, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -2501,7 +2500,7 @@ def create_event_alert( pattern=pattern, notification=notification, frequency=frequency, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -2561,7 +2560,7 @@ def create_user_alert( name=name, notification=notification, description=description, - offline=self._user_config.run.mode == "offline", + offline=self.mode == "offline", server_url=self._user_config.server.url, server_token=self._user_config.server.token, ) @@ -2613,7 +2612,7 @@ def log_alert( self._error("Please specify alert to update either by ID or by name.") return False - if name and self._user_config.run.mode == "offline": + if name and self.mode == "offline": self._error( "Cannot retrieve alerts based on names in offline mode - please use IDs instead." ) @@ -2621,7 +2620,7 @@ def log_alert( if name: try: - if alerts := Alert.get(offline=self._user_config.run.mode == "offline"): + if alerts := Alert.get(offline=self.mode == "offline"): identifier = next( (id for id, alert in alerts if alert.name == name), None ) diff --git a/simvue/sender/actions.py b/simvue/sender/actions.py index bc975f46..a01949b1 100644 --- a/simvue/sender/actions.py +++ b/simvue/sender/actions.py @@ -725,6 +725,10 @@ def initialise_object(cls, online_id: ObjectID | None, **data) -> AlertType: if not online_id: _source: str = data["source"] + # We need to make sure the ID of an existing alert is returned + # the server will return 409 with an ID if this is the case + data["allow_duplicates"] = False + if _source == "events": return EventsAlert.new(**data) elif _source == "metrics" and data.get("threshold"): diff --git a/tests/functional/test_run_execute_process.py b/tests/functional/test_run_execute_process.py index 66b8a4c9..11aed318 100644 --- a/tests/functional/test_run_execute_process.py +++ b/tests/functional/test_run_execute_process.py @@ -25,7 +25,7 @@ def test_monitor_processes(create_plain_run_offline: tuple[Run, dict]): _run.add_process(f"process_2_{os.environ.get('PYTEST_XDIST_WORKER', 0)}", Command="Get-ChildItem", executable="powershell") _run.add_process(f"process_3_{os.environ.get('PYTEST_XDIST_WORKER', 0)}", Command="exit 0", executable="powershell") _sender = Sender(_run._sv_obj._local_staging_file.parents[1], 1, 10, throw_exceptions=True) - _sender.upload(["folders", "runs", "alerts"], ) + _sender.upload(["folders", "alerts", "runs"], ) @pytest.mark.executor From f78b638cc6dea3dc44024c5566cb2ce34699dc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 27 May 2026 16:25:15 +0100 Subject: [PATCH 2/6] Move duplicates argument to parameters --- simvue/api/objects/alert/base.py | 4 ++++ simvue/api/objects/base.py | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 0c86f603..c16696d1 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -56,10 +56,14 @@ def __init__( **kwargs, ) -> None: """Retrieve an alert from the Simvue server by identifier""" + _params: dict[str, str | bool] = { + "deduplicate": not kwargs.get("allow_duplicates", True) + } super().__init__( identifier=identifier, server_url=server_url, server_token=server_token, + _params=_params, **kwargs, ) self._local_only_args += [ diff --git a/simvue/api/objects/base.py b/simvue/api/objects/base.py index fb979103..b3a0970e 100644 --- a/simvue/api/objects/base.py +++ b/simvue/api/objects/base.py @@ -185,6 +185,7 @@ def __init__( *, server_url: str | None, server_token: pydantic.SecretStr | None, + _params: dict[str, str | bool] | None = None, _read_only: bool = True, _local: bool = False, _user_agent: str | None = None, @@ -231,7 +232,7 @@ def __init__( self._user_config.headers if not self._offline else {} ) - self._params: dict[str, str | bool] = {} + self._params: dict[str, str | bool] | None = _params self._staging: dict[str, typing.Any] = {} @@ -655,7 +656,7 @@ def _post_batch( _response = sv_post( url=f"{self._base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, - params=self._params, + params=self._params or {}, data=batch_data, is_json=True, ) @@ -694,7 +695,7 @@ def _post_single( _response = sv_post( url=f"{self._base_url}", headers=self._headers | {"Content-Type": "application/msgpack"}, - params=self._params, + params=self._params or {}, data=data or kwargs, is_json=is_json, ) From 7c7b4eddb76b39f758158c8e1dd1018e31ad73de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 27 May 2026 16:32:58 +0100 Subject: [PATCH 3/6] Keep duplication parameters as internal --- simvue/api/objects/alert/events.py | 5 ----- simvue/api/objects/alert/metrics.py | 10 ---------- simvue/api/objects/alert/user.py | 7 +------ 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/simvue/api/objects/alert/events.py b/simvue/api/objects/alert/events.py index 5d278825..f4a504d3 100644 --- a/simvue/api/objects/alert/events.py +++ b/simvue/api/objects/alert/events.py @@ -82,7 +82,6 @@ def new( pattern: str, frequency: pydantic.PositiveInt, enabled: bool = True, - allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -106,9 +105,6 @@ def new( how often to check for updates enabled : bool, optional enable this alert upon creation, default is True - allow_duplicates : bool, optional - whether to raise exception if duplicate alert requested, - default of True will allow duplicates offline : bool, optional create alert locally, default is False server_url: str | None, optional @@ -131,7 +127,6 @@ def new( source="events", alert=_alert_definition, enabled=enabled, - deduplicate=not allow_duplicates, server_url=server_url, server_token=server_token, _read_only=False, diff --git a/simvue/api/objects/alert/metrics.py b/simvue/api/objects/alert/metrics.py index be94ede2..43ce4843 100644 --- a/simvue/api/objects/alert/metrics.py +++ b/simvue/api/objects/alert/metrics.py @@ -100,7 +100,6 @@ def new( threshold: float | int, frequency: pydantic.PositiveInt, enabled: bool = True, - allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -132,9 +131,6 @@ def new( how often to monitor the metric enabled : bool, optional whether this alert is enabled upon creation, default is True - allow_duplicates : bool, optional - whether to raise exception if duplicate alert requested, - default of True will allow duplicates offline : bool, optional whether to create the alert locally, default is False server_url: str | None, optional @@ -164,7 +160,6 @@ def new( enabled=enabled, server_url=server_url, server_token=server_token, - deduplicate=not allow_duplicates, _read_only=False, _offline=offline, ) @@ -248,7 +243,6 @@ def new( range_low: float, frequency: pydantic.PositiveInt, enabled: bool = True, - allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -282,9 +276,6 @@ def new( how often to monitor the metric enabled : bool, optional whether this alert is enabled upon creation, default is True - allow_duplicates : bool, optional - whether to raise exception if duplicate alert requested, - default of True will allow duplicates offline : bool, optional whether to create the alert locally, default is False server_url: str | None, optional @@ -314,7 +305,6 @@ def new( alert=_alert_definition, server_url=server_url, server_token=server_token, - deduplicate=not allow_duplicates, _read_only=False, _offline=offline, ) diff --git a/simvue/api/objects/alert/user.py b/simvue/api/objects/alert/user.py index 1d6dd836..3e9dedac 100644 --- a/simvue/api/objects/alert/user.py +++ b/simvue/api/objects/alert/user.py @@ -66,7 +66,6 @@ def new( description: str | None, notification: typing.Literal["none", "email"], enabled: bool = True, - allow_duplicates: bool = True, offline: bool = False, server_url: str | None = None, server_token: pydantic.SecretStr | None = None, @@ -86,9 +85,6 @@ def new( configure notification settings for this alert enabled : bool, optional whether this alert is enabled upon creation, default is True - allow_duplicates : bool, optional - whether to raise exception if duplicate alert requested, - default of True will allow duplicates offline : bool, optional whether this alert should be created locally, default is False server_url: str | None, optional @@ -105,11 +101,10 @@ def new( enabled=enabled, server_url=server_url, server_token=server_token, - deduplicate=not allow_duplicates, + _params={"deduplicate": True}, _read_only=False, _offline=offline, ) - _alert._params = {"deduplicate": True} return _alert @override From 0548f8f7c5a6d02d7b1a3b528f9fa1f273a955d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Wed, 27 May 2026 16:36:33 +0100 Subject: [PATCH 4/6] Fix parameter merge in User alerts --- simvue/api/objects/alert/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index c16696d1..dfe0abd3 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -63,7 +63,7 @@ def __init__( identifier=identifier, server_url=server_url, server_token=server_token, - _params=_params, + _params=kwargs.get("_params", {}) | _params, **kwargs, ) self._local_only_args += [ From 32fa6c518ee084b463a4cd492c8f835aefb74ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 28 May 2026 08:06:32 +0100 Subject: [PATCH 5/6] Remove abc from alert --- simvue/api/objects/alert/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index dfe0abd3..1a2f6442 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -8,7 +8,6 @@ import pydantic import datetime import typing -import abc from simvue.api.objects.base import SimvueObject, staging_check, write_only from simvue.api.request import get as sv_get, get_json_from_response from simvue.api.url import URL @@ -20,7 +19,7 @@ from typing_extensions import Self, override # noqa: UP035 -class AlertBase(SimvueObject, abc.ABC): +class AlertBase(SimvueObject): """Class for interfacing with Simvue alerts Contains properties common to all alert types. @@ -30,7 +29,6 @@ class AlertBase(SimvueObject, abc.ABC): @override @classmethod - @abc.abstractmethod def new( cls, *, From 876f0ddbfb61a8bb672ef12a5ff47cffff5c08c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Zar=C4=99bski?= Date: Thu, 28 May 2026 08:07:48 +0100 Subject: [PATCH 6/6] Fix params arg duplication --- simvue/api/objects/alert/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simvue/api/objects/alert/base.py b/simvue/api/objects/alert/base.py index 1a2f6442..8d1258a0 100644 --- a/simvue/api/objects/alert/base.py +++ b/simvue/api/objects/alert/base.py @@ -54,14 +54,14 @@ def __init__( **kwargs, ) -> None: """Retrieve an alert from the Simvue server by identifier""" - _params: dict[str, str | bool] = { + _params: dict[str, str | bool] = kwargs.pop("_params", {}) | { "deduplicate": not kwargs.get("allow_duplicates", True) } super().__init__( identifier=identifier, server_url=server_url, server_token=server_token, - _params=kwargs.get("_params", {}) | _params, + _params=_params, **kwargs, ) self._local_only_args += [