Skip to content

Commit 722b849

Browse files
authored
feat: Add Client for Alarm API (#173)
1 parent 6256d42 commit 722b849

File tree

15 files changed

+1442
-0
lines changed

15 files changed

+1442
-0
lines changed

docs/api_reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ API Reference
88
:caption: Table of Contents
99

1010
api_reference/core
11+
api_reference/alarm
1112
api_reference/tag
1213
api_reference/product
1314
api_reference/testmonitor

docs/api_reference/alarm.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.. _api_alarm_page:
2+
3+
nisystemlink.clients.alarm
4+
==========================
5+
6+
.. autoclass:: nisystemlink.clients.alarm.AlarmClient
7+
:exclude-members: __init__
8+
9+
.. automethod:: __init__
10+
.. automethod:: create_or_update_alarm
11+
.. automethod:: get_alarm
12+
.. automethod:: delete_alarm
13+
.. automethod:: delete_alarms
14+
.. automethod:: acknowledge_alarms
15+
.. automethod:: query_alarms
16+
17+
.. automodule:: nisystemlink.clients.alarm.models
18+
:members:
19+
:imported-members:

docs/getting_started.rst

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,55 @@
33
Getting Started
44
===============
55

6+
Alarm API
7+
---------
8+
9+
Overview
10+
~~~~~~~~
11+
12+
The :class:`.AlarmClient` class is the primary entry point of the Alarm API.
13+
14+
When constructing an :class:`.AlarmClient`, you can pass an
15+
:class:`.HttpConfiguration` (like one retrieved from the
16+
:class:`.HttpConfigurationManager`), or let :class:`.AlarmClient` use the
17+
default connection. The default connection depends on your environment.
18+
19+
With an :class:`.AlarmClient` object, you can:
20+
21+
* Create and update alarm instances using :meth:`~.AlarmClient.create_or_update_alarm`
22+
23+
* Alarms have two key identifiers:
24+
25+
* ``alarm_id``: A user-defined identifier for the alarm type
26+
* ``instance_id``: A server-generated unique identifier for each alarm occurrence
27+
28+
* Create alarm transitions (SET, CLEAR) to track alarm state changes
29+
30+
* Query alarms with :meth:`~.AlarmClient.query_alarms`
31+
32+
* Filter alarms using Dynamic LINQ expressions
33+
* Control which transitions are returned (most recent only or all)
34+
* Sort and paginate results
35+
36+
* Get a specific alarm by its instance_id using :meth:`~.AlarmClient.get_alarm`
37+
38+
* Acknowledge alarms by its instance_id using :meth:`~.AlarmClient.acknowledge_alarms`
39+
40+
* Optionally force-clear alarms when acknowledging
41+
42+
* Delete alarms using :meth:`~.AlarmClient.delete_alarm` or
43+
:meth:`~.AlarmClient.delete_alarms`
44+
45+
Examples
46+
~~~~~~~~
47+
48+
Create, query, acknowledge, and delete alarms
49+
50+
.. literalinclude:: ../examples/alarm/alarm.py
51+
:language: python
52+
:linenos:
53+
54+
655
Tag API
756
-------
857

examples/alarm/alarm.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
from nisystemlink.clients.alarm import AlarmClient
5+
from nisystemlink.clients.alarm.models import (
6+
AlarmOrderBy,
7+
AlarmSeverityLevel,
8+
ClearAlarmTransition,
9+
CreateOrUpdateAlarmRequest,
10+
QueryAlarmsWithFilterRequest,
11+
SetAlarmTransition,
12+
TransitionInclusionOption,
13+
)
14+
from nisystemlink.clients.core import ApiException, HttpConfiguration
15+
16+
# Setup the server configuration to point to your instance of SystemLink Enterprise
17+
server_configuration = HttpConfiguration(
18+
server_uri="https://yourserver.yourcompany.com",
19+
api_key="YourAPIKeyGeneratedFromSystemLink",
20+
)
21+
client = AlarmClient(configuration=server_configuration)
22+
23+
# Create a unique alarm ID for this example
24+
# alarm_id is a user-defined identifier for this alarm
25+
alarm_id = f"example_alarm_{uuid.uuid1().hex}"
26+
27+
# Create an alarm with a SET transition
28+
create_request = CreateOrUpdateAlarmRequest(
29+
alarm_id=alarm_id,
30+
transition=SetAlarmTransition(
31+
occurred_at=datetime.now(),
32+
severity_level=AlarmSeverityLevel.HIGH,
33+
value="85",
34+
condition="Greater than 80",
35+
short_text="Temperature is high",
36+
detail_text="Temperature sensor reading is 85°C (higher than the configured threshold of 80°C)",
37+
),
38+
)
39+
# Returns instance_id - a server-generated unique identifier for this specific alarm occurrence
40+
id = client.create_or_update_alarm(create_request)
41+
42+
# Get the alarm by its instance ID (the unique occurrence identifier)
43+
alarm = client.get_alarm(instance_id=id)
44+
print(f"Retrieved alarm: {alarm.alarm_id}, Condition: {alarm.condition}")
45+
46+
# Update the alarm with a higher severity (same alarm_id, updates the same instance)
47+
update_request = CreateOrUpdateAlarmRequest(
48+
alarm_id=alarm_id,
49+
transition=SetAlarmTransition(
50+
occurred_at=datetime.now(),
51+
severity_level=AlarmSeverityLevel.CRITICAL,
52+
value="95",
53+
condition="Greater than 90",
54+
short_text="Temperature is critical",
55+
detail_text="Temperature sensor reading is 95°C (higher than the configured threshold of 90°C)",
56+
),
57+
)
58+
client.create_or_update_alarm(update_request)
59+
60+
# Query alarms with a filter (can filter by alarm_id to find all instances)
61+
# Include all transitions to see the full alarm history
62+
query_request = QueryAlarmsWithFilterRequest(
63+
filter="alarmId=@0",
64+
substitutions=[alarm_id],
65+
order_by=AlarmOrderBy.UPDATED_AT,
66+
order_by_descending=True,
67+
transition_inclusion_option=TransitionInclusionOption.ALL,
68+
return_count=True,
69+
)
70+
query_response = client.query_alarms(query_request)
71+
72+
# Display query results
73+
print(f"Total alarms found: {query_response.total_count}")
74+
for alarm in query_response.alarms:
75+
print(f" Alarm ID: {alarm.alarm_id}, Transitions: {len(alarm.transitions)}")
76+
for transition in alarm.transitions:
77+
print(f"- {transition.transition_type}: {transition.condition}")
78+
79+
# Acknowledge the alarm
80+
client.acknowledge_alarms(instance_ids=[id])
81+
82+
# Clear the alarm with 409 conflict handling - Method 1: Manual exception handling
83+
# A 409 Conflict response indicates that the requested transition would not change the alarm's state.
84+
# This allows stateless applications to simply attempt state transitions without first checking
85+
# the current state. For example, a monitoring system can repeatedly try to CLEAR an alarm
86+
# when conditions return to normal, and the API will return 409 if already cleared.
87+
clear_request = CreateOrUpdateAlarmRequest(
88+
alarm_id=alarm_id,
89+
transition=ClearAlarmTransition(
90+
occurred_at=datetime.now(),
91+
condition="Temperature returned to normal",
92+
),
93+
)
94+
try:
95+
client.create_or_update_alarm(clear_request)
96+
print("Alarm cleared successfully")
97+
except ApiException as e:
98+
if e.http_status_code == 409:
99+
print("Alarm is already in the requested state (409 Conflict)")
100+
else:
101+
raise
102+
103+
# Clear the alarm with 409 conflict handling - Method 2: Using ignore_conflict parameter
104+
# This approach is cleaner for stateless applications that don't care about 409 errors.
105+
# Returns None if the alarm is already in the requested state.
106+
result = client.create_or_update_alarm(clear_request, ignore_conflict=True)
107+
if result is None:
108+
print("No state change needed (alarm already in requested state)")
109+
else:
110+
print(f"Alarm cleared successfully: {result}")
111+
112+
# Delete the alarm by its instance ID
113+
client.delete_alarm(instance_id=id)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from nisystemlink.clients.alarm._alarm_client import AlarmClient
2+
3+
# flake8: noqa
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Implementation of Alarm Client"""
2+
3+
from typing import List, Literal, overload
4+
5+
from nisystemlink.clients import core
6+
from nisystemlink.clients.core._uplink._base_client import BaseClient
7+
from nisystemlink.clients.core._uplink._methods import delete, get, post
8+
from uplink import Field, Path, retry
9+
10+
from . import models
11+
12+
13+
@retry(
14+
when=retry.when.status(408, 429, 502, 503, 504),
15+
stop=retry.stop.after_attempt(5),
16+
on_exception=retry.CONNECTION_ERROR,
17+
)
18+
class AlarmClient(BaseClient):
19+
20+
def __init__(self, configuration: core.HttpConfiguration | None = None):
21+
"""Initialize an instance.
22+
23+
Args:
24+
configuration: Defines the web server to connect to and information about
25+
how to connect. If not provided, the
26+
:class:`HttpConfigurationManager <nisystemlink.clients.core.HttpConfigurationManager>`
27+
is used to obtain the configuration.
28+
29+
Raises:
30+
ApiException: if unable to communicate with the `/nialarm` Service.
31+
"""
32+
if configuration is None:
33+
configuration = core.HttpConfigurationManager.get_configuration()
34+
35+
super().__init__(configuration, base_path="/nialarm/v1/")
36+
37+
@post(
38+
"acknowledge-instances-by-instance-id",
39+
args=[Field("instanceIds"), Field("forceClear")],
40+
)
41+
def acknowledge_alarms(
42+
self, instance_ids: List[str], *, force_clear: bool = False
43+
) -> models.AcknowledgeAlarmsResponse:
44+
"""Acknowledges one or more alarm instances by their instance IDs.
45+
46+
Args:
47+
instance_ids: List of instance IDs (unique occurrence identifiers) of the alarms to acknowledge.
48+
These are the server-generated IDs returned when creating/updating alarms,
49+
not the user-defined alarm_id.
50+
force_clear: Whether or not the affected alarms should have their clear field set to true.
51+
Defaults to False.
52+
53+
Returns:
54+
A response containing the instance IDs that were successfully acknowledged,
55+
the instance IDs that failed, and error details for failures.
56+
57+
Raises:
58+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
59+
"""
60+
...
61+
62+
@overload
63+
def create_or_update_alarm( # noqa: E704
64+
self,
65+
request: models.CreateOrUpdateAlarmRequest,
66+
*,
67+
ignore_conflict: Literal[False] = False,
68+
) -> str: ...
69+
70+
@overload
71+
def create_or_update_alarm( # noqa: E704
72+
self,
73+
request: models.CreateOrUpdateAlarmRequest,
74+
*,
75+
ignore_conflict: Literal[True],
76+
) -> str | None: ...
77+
78+
def create_or_update_alarm(
79+
self,
80+
request: models.CreateOrUpdateAlarmRequest,
81+
*,
82+
ignore_conflict: bool = False,
83+
) -> str | None:
84+
"""Creates or updates an instance, or occurrence, of an alarm.
85+
86+
Creates or updates an alarm based on the requested transition and the state
87+
of the current active alarm with the given alarm_id (specified in the request).
88+
Multiple calls with the same alarm_id will update the same alarm instance.
89+
90+
Args:
91+
request: The request containing alarm_id (user-defined identifier),
92+
transition details, and other alarm properties.
93+
ignore_conflict: If True, 409 Conflict errors will be ignored and None will be returned.
94+
If False (default), 409 errors will raise an ApiException.
95+
Setting this to True is useful for stateless applications that want to
96+
attempt state transitions without checking the current alarm state first.
97+
98+
Returns:
99+
The instance_id (unique occurrence identifier) of the created or modified alarm.
100+
Use this ID for operations like get_alarm(), delete_alarm(), or acknowledge.
101+
Returns None if ignore_conflict is True and a 409 Conflict occurs.
102+
103+
Raises:
104+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
105+
A 409 Conflict error occurs when the request does not represent a valid transition
106+
for an existing alarm, such as attempting to clear an alarm which is already clear,
107+
or attempting to set an alarm which is already set at the given severity level.
108+
This error can be suppressed by setting ignore_conflict=True.
109+
"""
110+
try:
111+
return self._create_or_update_alarm(request)
112+
except core.ApiException as e:
113+
if ignore_conflict and e.http_status_code == 409:
114+
return None
115+
raise
116+
117+
@post("instances", return_key="instanceId")
118+
def _create_or_update_alarm(
119+
self, request: models.CreateOrUpdateAlarmRequest
120+
) -> str:
121+
"""Internal implementation of create_or_update_alarm."""
122+
...
123+
124+
@get("instances/{instance_id}", args=[Path("instance_id")])
125+
def get_alarm(self, instance_id: str) -> models.Alarm:
126+
"""Gets an alarm by its instance_id.
127+
128+
Args:
129+
instance_id: The unique instance ID (occurrence identifier) of the alarm to retrieve.
130+
This is the server-generated ID returned from create_or_update_alarm(),
131+
not the user-defined alarm_id.
132+
133+
Returns:
134+
The alarm with the specified instance_id.
135+
136+
Raises:
137+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
138+
"""
139+
...
140+
141+
@delete("instances/{instance_id}", args=[Path("instance_id")])
142+
def delete_alarm(self, instance_id: str) -> None:
143+
"""Deletes an alarm by its instance_id.
144+
145+
Args:
146+
instance_id: The unique instance ID (occurrence identifier) of the alarm to delete.
147+
This is the server-generated ID returned from create_or_update_alarm(),
148+
not the user-defined alarm_id.
149+
150+
Raises:
151+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
152+
"""
153+
...
154+
155+
@post("delete-instances-by-instance-id", args=[Field("instanceIds")])
156+
def delete_alarms(self, instance_ids: List[str]) -> models.DeleteAlarmsResponse:
157+
"""Deletes multiple alarm instances by their instance IDs.
158+
159+
Args:
160+
instance_ids: List of instance IDs (unique occurrence identifiers) of the alarms to delete.
161+
These are the server-generated IDs returned when creating/updating alarms,
162+
not the user-defined alarm_id.
163+
164+
Returns:
165+
A response containing lists of successfully deleted and failed instance IDs,
166+
along with error information for failures.
167+
168+
Raises:
169+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
170+
"""
171+
...
172+
173+
@post("query-instances-with-filter")
174+
def query_alarms(
175+
self, request: models.QueryAlarmsWithFilterRequest
176+
) -> models.QueryAlarmsWithFilterResponse:
177+
"""Queries for instances, or occurrences, of alarms using Dynamic LINQ.
178+
179+
Specifying an empty JSON object in the request body will result in all alarms being returned.
180+
181+
Args:
182+
request: The request containing filter information and query options.
183+
184+
Returns:
185+
A response containing the list of alarms that match the query, along with
186+
optional total count and continuation token for pagination.
187+
188+
Raises:
189+
ApiException: if unable to communicate with the `/nialarm` Service or provided invalid arguments.
190+
"""
191+
...

0 commit comments

Comments
 (0)