Skip to content

Commit 9efa708

Browse files
committed
Merge branch 'dev' into kzscisoft/client-refactor
2 parents 7a4fb6d + 495b15b commit 9efa708

23 files changed

Lines changed: 1308 additions & 937 deletions

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
args: [--branch, main, --branch, dev]
2424
- id: check-added-large-files
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.15.4
26+
rev: v0.15.9
2727
hooks:
2828
- id: ruff
2929
args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ]

‎CHANGELOG.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
# Change Log
22

3-
## Unreleased
3+
## [v2.4.1](https://github.com/simvue-io/python-api/releases/tag/v2.4.1) - 2026-03-31
4+
5+
- Moved to using `threading.Event` as termination trigger events and added deprecation notice for `multiprocessing.Event`.
6+
- Fixed creation of duplicate alerts.
7+
- Fixed case where alert specifications being `0` led to incorrect falsification.
8+
9+
## [v2.4.0](https://github.com/simvue-io/python-api/releases/tag/v2.4.0) - 2026-03-23
410

511
- Added search for Simvue configuration within the parent file hierarchy of the current directory.
612
- Added advanced filtering LLAPI functionality allowing users to construct filters programmatically.
13+
- Allow user of LLAPI to force download of duplicate artifacts.
714

815
## [v2.3.6](https://github.com/simvue-io/python-api/releases/tag/v2.3.6) - 2026-02-27
916

‎CITATION.cff‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ keywords:
4242
- alerting
4343
- simulation
4444
license: Apache-2.0
45-
commit: cb55da4b1b5c3deb67107571965f887cf48dc756
46-
version: 2.3.6
47-
date-released: '2026-02-27'
45+
commit: 98d894b048c5dbb157324a0fc1562d1a5f72a774
46+
version: 2.4.1
47+
date-released: '2026-03-31'

‎examples/Geant4/geant4_simvue.py‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@
66
monitoring the yield of key particles of interest
77
"""
88

9-
import multiparser
10-
import multiparser.parsing.file as mp_file_parse
9+
import threading
1110
import simvue
12-
import uproot
13-
import multiprocessing
1411
import typing
1512
import click
1613
import pathlib
17-
import os
18-
import tempfile
1914

2015
from particle import Particle
2116

@@ -77,7 +72,7 @@ def root_file_parser(
7772

7873
for i in range(events):
7974
# Create new multiprocessing Trigger which will register when the simulation is complete
80-
_trigger = multiprocessing.Event()
75+
_trigger = threading.Event()
8176

8277
if i % 10 == 0:
8378
click.secho(

‎poetry.lock‎

Lines changed: 890 additions & 838 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "simvue"
3-
version = "2.3.6"
3+
version = "2.4.1"
44
description = "Simulation tracking and monitoring"
55
authors = [{ name = "Simvue Development Team", email = "info@simvue.io" }]
66
license = "Apache v2"
@@ -55,6 +55,7 @@ dependencies = [
5555
"pydantic-extra-types (>=2.10.5,<3.0.0)",
5656
"pyyaml (>=6.0.2,<7.0.0)",
5757
"flatdict (==4.0.0)",
58+
"pytest (>=9.0.3,<10.0.0)",
5859
]
5960

6061
[project.urls]
@@ -69,7 +70,6 @@ plot = ["plotly (>=6.0.0,<7.0.0)", "matplotlib (>=3.10.0,<4.0.0)"]
6970
simvue-sender = "simvue.bin.sender:run"
7071

7172
[tool.poetry.group.dev.dependencies]
72-
pytest = "^8.0.0"
7373
ruff = ">=0.5.0,<0.10.0"
7474
pytest-cov = ">=4.1,<7.0"
7575
pytest-mock = "^3.14.0"
@@ -93,6 +93,7 @@ extend-exclude = ["tests", "examples", "notebooks"]
9393
addopts = "-p no:warnings --no-cov -n 0"
9494
testpaths = ["tests"]
9595
markers = [
96+
"cli: Sender CLI tests",
9697
"eco: tests for emission metrics",
9798
"client: tests of Simvue client",
9899
"dispatch: test data dispatcher",

‎simvue/api/objects/alert/base.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
from simvue.api.url import URL
1616
from simvue.models import NAME_REGEX, DATETIME_FORMAT
1717

18+
try:
19+
from typing import override
20+
except ImportError:
21+
from typing_extensions import override # noqa: UP035
22+
1823

1924
class AlertBase(SimvueObject):
2025
"""Class for interfacing with Simvue alerts
@@ -23,7 +28,7 @@ class AlertBase(SimvueObject):
2328
"""
2429

2530
@classmethod
26-
def new(cls, **kwargs):
31+
def new(cls, read_only: bool = False, **kwargs):
2732
"""Create a new alert"""
2833
pass
2934

@@ -38,6 +43,38 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None:
3843
"status",
3944
]
4045

46+
def _compare_objects(self, other: "AlertBase") -> bool:
47+
return all(
48+
[
49+
self.name == other.name,
50+
self.description == other.description,
51+
self.source == other.source,
52+
self.notification == other.notification,
53+
]
54+
)
55+
56+
@override
57+
def __eq__(self, other: "AlertBase") -> bool:
58+
"""Check if alerts are the same."""
59+
60+
# Need to ensure objects are read-only for this
61+
# operation as we do not want staging to alter
62+
_self_is_read_only: bool = self._read_only
63+
_other_is_read_only: bool = other._read_only
64+
self.read_only(True)
65+
other.read_only(True)
66+
67+
_comparison = self._compare_objects(other)
68+
69+
# Restore to write allowed unless the input object
70+
# was read-only to begin with
71+
if not _self_is_read_only:
72+
self.read_only(False, clear_staged=False)
73+
if not _other_is_read_only:
74+
other.read_only(False, clear_staged=False)
75+
76+
return _comparison
77+
4178
def compare(self, other: "AlertBase") -> bool:
4279
"""Compare this alert to another"""
4380
return type(self) is type(other) and self.name == other.name

‎simvue/api/objects/alert/events.py‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import pydantic
1111

1212
try:
13-
from typing import Self
13+
from typing import Self, override
1414
except ImportError:
15-
from typing_extensions import Self
15+
from typing_extensions import Self, override
1616
from simvue.api.objects.base import write_only
1717
from .base import AlertBase, staging_check
1818
from simvue.models import NAME_REGEX
@@ -109,6 +109,13 @@ def new(
109109
_alert._params = {"deduplicate": True}
110110
return _alert
111111

112+
@override
113+
def _compare_objects(self, other: "AlertBase") -> bool:
114+
"""Compare Events Alerts."""
115+
if not isinstance(other, EventsAlert):
116+
return False
117+
return super()._compare_objects(other) and self.alert == other.alert
118+
112119

113120
class EventAlertDefinition:
114121
"""Event alert definition sub-class"""
@@ -117,11 +124,8 @@ def __init__(self, alert: EventsAlert) -> None:
117124
"""Initialise an alert definition with its parent alert"""
118125
self._sv_obj = alert
119126

120-
def compare(self, other: "EventAlertDefinition") -> bool:
127+
def __eq__(self, other: "EventAlertDefinition") -> bool:
121128
"""Compare this definition with that of another EventAlert"""
122-
if not isinstance(other, EventAlertDefinition):
123-
return False
124-
125129
return all(
126130
[
127131
self.frequency == other.frequency,

‎simvue/api/objects/alert/metrics.py‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
Aggregate = typing.Literal["average", "sum", "at least one", "all"]
2323
Rule = typing.Literal["is above", "is below", "is inside range", "is outside range"]
2424

25+
try:
26+
from typing import override
27+
except ImportError:
28+
from typing_extensions import override # noqa: UP035
29+
2530

2631
class MetricsThresholdAlert(AlertBase):
2732
"""
@@ -134,6 +139,12 @@ def new(
134139

135140
return _alert
136141

142+
@override
143+
def _compare_objects(self, other: "AlertBase") -> bool:
144+
if not isinstance(other, MetricsThresholdAlert):
145+
return False
146+
return super()._compare_objects(other) and self.alert == other.alert
147+
137148

138149
class MetricsRangeAlert(AlertBase):
139150
"""
@@ -169,9 +180,12 @@ def __init__(self, identifier: str | None = None, **kwargs) -> None:
169180
"range_high",
170181
]
171182

172-
def compare(self, other: "MetricsRangeAlert") -> bool:
183+
@override
184+
def _compare_objects(self, other: "AlertBase") -> bool:
173185
"""Compare two MetricRangeAlerts"""
174-
return self.alert.compare(other) if super().compare(other) else False
186+
if not isinstance(other, MetricsRangeAlert):
187+
return False
188+
return super()._compare_objects(other) and self.alert == other.alert
175189

176190
@classmethod
177191
@pydantic.validate_call
@@ -258,7 +272,7 @@ def __init__(self, alert: MetricsRangeAlert) -> None:
258272
"""Initialise definition with target alert"""
259273
self._sv_obj = alert
260274

261-
def compare(self, other: "MetricsAlertDefinition") -> bool:
275+
def __eq__(self, other: "MetricsAlertDefinition") -> bool:
262276
"""Compare a MetricsAlertDefinition with another"""
263277
return all(
264278
[
@@ -272,7 +286,7 @@ def compare(self, other: "MetricsAlertDefinition") -> bool:
272286
@property
273287
def aggregation(self) -> Aggregate:
274288
"""Retrieve the aggregation strategy for this alert"""
275-
if not (_aggregation := self._sv_obj.get_alert().get("aggregation")):
289+
if (_aggregation := self._sv_obj.get_alert().get("aggregation")) is None:
276290
raise RuntimeError(
277291
"Expected key 'aggregation' in alert definition retrieval"
278292
)
@@ -281,14 +295,14 @@ def aggregation(self) -> Aggregate:
281295
@property
282296
def rule(self) -> Rule:
283297
"""Retrieve the rule for this alert"""
284-
if not (_rule := self._sv_obj.get_alert().get("rule")):
298+
if (_rule := self._sv_obj.get_alert().get("rule")) is None:
285299
raise RuntimeError("Expected key 'rule' in alert definition retrieval")
286300
return _rule
287301

288302
@property
289303
def window(self) -> int:
290304
"""Retrieve the aggregation window for this alert"""
291-
if not (_window := self._sv_obj.get_alert().get("window")):
305+
if (_window := self._sv_obj.get_alert().get("window")) is None:
292306
raise RuntimeError("Expected key 'window' in alert definition retrieval")
293307
return _window
294308

@@ -315,32 +329,31 @@ def frequency(self, frequency: int) -> None:
315329
class MetricThresholdAlertDefinition(MetricsAlertDefinition):
316330
"""Alert definition for metric threshold alerts"""
317331

318-
def compare(self, other: "MetricThresholdAlertDefinition") -> bool:
332+
def __eq__(self, other: "MetricThresholdAlertDefinition") -> bool:
319333
"""Compare this MetricThresholdAlertDefinition with another"""
320-
if not isinstance(other, MetricThresholdAlertDefinition):
334+
if not super().__eq__(other):
321335
return False
322336

323-
return all([super().compare(other), self.threshold == other.threshold])
337+
return self.threshold == other.threshold
324338

325339
@property
326340
def threshold(self) -> float:
327341
"""Retrieve the threshold value for this alert"""
328-
if not (threshold_l := self._sv_obj.get_alert().get("threshold")):
342+
if (threshold_l := self._sv_obj.get_alert().get("threshold")) is None:
329343
raise RuntimeError("Expected key 'threshold' in alert definition retrieval")
330344
return threshold_l
331345

332346

333347
class MetricRangeAlertDefinition(MetricsAlertDefinition):
334348
"""Alert definition for metric range alerts"""
335349

336-
def compare(self, other: "MetricRangeAlertDefinition") -> bool:
350+
def __eq__(self, other: "MetricRangeAlertDefinition") -> bool:
337351
"""Compare a MetricRangeAlertDefinition with another"""
338-
if not isinstance(other, MetricRangeAlertDefinition):
352+
if not super().__eq__(other):
339353
return False
340354

341355
return all(
342356
[
343-
super().compare(other),
344357
self.range_high == other.range_high,
345358
self.range_low == other.range_low,
346359
]
@@ -349,14 +362,14 @@ def compare(self, other: "MetricRangeAlertDefinition") -> bool:
349362
@property
350363
def range_low(self) -> float:
351364
"""Retrieve the lower limit for metric range"""
352-
if not (range_l := self._sv_obj.get_alert().get("range_low")):
365+
if (range_l := self._sv_obj.get_alert().get("range_low")) is None:
353366
raise RuntimeError("Expected key 'range_low' in alert definition retrieval")
354367
return range_l
355368

356369
@property
357370
def range_high(self) -> float:
358371
"""Retrieve upper limit for metric range"""
359-
if not (range_u := self._sv_obj.get_alert().get("range_high")):
372+
if (range_u := self._sv_obj.get_alert().get("range_high")) is None:
360373
raise RuntimeError(
361374
"Expected key 'range_high' in alert definition retrieval"
362375
)

‎simvue/api/objects/alert/user.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
import typing
1111

1212
try:
13-
from typing import Self
13+
from typing import Self, override
1414
except ImportError:
15-
from typing_extensions import Self
15+
from typing_extensions import Self, override
1616
import http
1717

1818
from simvue.api.request import get_json_from_response, put as sv_put
@@ -89,6 +89,12 @@ def new(
8989
_alert._params = {"deduplicate": True}
9090
return _alert
9191

92+
@override
93+
def _compare_objects(self, other: "AlertBase") -> bool:
94+
if not isinstance(other, UserAlert):
95+
return False
96+
return super()._compare_objects(other)
97+
9298
@classmethod
9399
def get(
94100
cls, count: int | None = None, offset: int | None = None

0 commit comments

Comments
 (0)