Skip to content

Commit ae4e9c1

Browse files
feat: Add Notification Client (#195)
1 parent 26a2e86 commit ae4e9c1

16 files changed

+706
-0
lines changed

docs/api_reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ API Reference
2222
api_reference/work_item
2323
api_reference/test_plan
2424
api_reference/artifact
25+
api_reference/notification
2526

2627
Indices and tables
2728
------------------
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.. _api_notification_page:
2+
3+
nisystemlink.clients.notification
4+
==========================
5+
6+
.. autoclass:: nisystemlink.clients.notification.NotificationClient
7+
:exclude-members: __init__
8+
9+
.. automethod:: __init__
10+
.. automethod:: apply_dynamic_notification_strategy
11+
12+
.. automodule:: nisystemlink.clients.notification.models
13+
:members:
14+
:imported-members:

docs/getting_started.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,3 +506,29 @@ Create, query and delete test plan templates.
506506
.. literalinclude:: ../examples/test_plan/test_plan_templates.py
507507
:language: python
508508
:linenos:
509+
510+
Notification API
511+
----------------
512+
513+
Overview
514+
~~~~~~~~
515+
516+
The :class:`.NotificationClient` class is the primary entry point of the Notification API.
517+
518+
When constructing a :class:`.NotificationClient`, you can pass an
519+
:class:`.HttpConfiguration` (like one retrieved from the
520+
:class:`.HttpConfigurationManager`), or let :class:`.NotificationClient` use the
521+
default connection. The default connection depends on your environment.
522+
523+
With a :class:`.NotificationClient` object, you can:
524+
525+
* Apply dynamic notification strategy using :meth:`~.NotificationClient.apply_dynamic_notification_strategy`
526+
527+
Examples
528+
~~~~~~~~
529+
530+
Apply a notification strategy
531+
532+
.. literalinclude:: ../examples/notification/notification.py
533+
:language: python
534+
:linenos:
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import uuid
2+
from datetime import datetime
3+
4+
from nisystemlink.clients.alarm import AlarmClient
5+
from nisystemlink.clients.alarm.models._alarm import Alarm, AlarmSeverityLevel
6+
from nisystemlink.clients.alarm.models._create_or_update_alarm_request import (
7+
CreateOrUpdateAlarmRequest,
8+
SetAlarmTransition,
9+
)
10+
from nisystemlink.clients.core import HttpConfiguration
11+
from nisystemlink.clients.notification import NotificationClient
12+
from nisystemlink.clients.notification.models import (
13+
DynamicNotificationConfiguration,
14+
DynamicNotificationStrategy,
15+
DynamicStrategyRequest,
16+
SmtpAddressFields,
17+
SmtpAddressGroup,
18+
SmtpMessageTemplate,
19+
SmtpMessageTemplateFields,
20+
)
21+
22+
# Server configuration is not required when used with SystemLink Client or run through Jupyter on SystemLink
23+
server_configuration: HttpConfiguration | None = None
24+
25+
# To set up the server configuration to point to your instance of SystemLink Enterprise, uncomment
26+
# the following lines and provide your server URI and API key.
27+
# server_configuration = HttpConfiguration(
28+
# server_uri="https://yourserver.yourcompany.com",
29+
# api_key="",
30+
# )
31+
32+
33+
# Create request for applying strategy
34+
def create_notification_request_for_alarm(
35+
alarm: Alarm,
36+
address_group: SmtpAddressGroup,
37+
message_template: SmtpMessageTemplate,
38+
) -> DynamicStrategyRequest:
39+
"""Creates and returns a dynamic strategy request."""
40+
occurred_at = alarm.most_recent_transition_occurred_at
41+
42+
return DynamicStrategyRequest(
43+
message_template_substitution_fields={
44+
"alarm_id": alarm.alarm_id,
45+
"alarm_condition": alarm.condition,
46+
"alarm_description": alarm.description,
47+
"alarm_severity": str(alarm.current_severity_level),
48+
"alarm_occurred_at": occurred_at.isoformat() if occurred_at else "",
49+
},
50+
notification_strategy=DynamicNotificationStrategy(
51+
notification_configurations=[
52+
DynamicNotificationConfiguration(
53+
address_group=address_group,
54+
message_template=message_template,
55+
)
56+
]
57+
),
58+
)
59+
60+
61+
# Create clients for Notification and Alarm services
62+
notification_client = NotificationClient(configuration=server_configuration)
63+
alarm_client = AlarmClient(configuration=server_configuration)
64+
65+
# Create a unique alarm ID for this example
66+
alarm_id = f"example_alarm_{uuid.uuid1().hex}"
67+
68+
# Create an alarm with a SET transition
69+
create_alarm_request = CreateOrUpdateAlarmRequest(
70+
alarm_id=alarm_id,
71+
transition=SetAlarmTransition(
72+
occurred_at=datetime.now(),
73+
severity_level=AlarmSeverityLevel.HIGH,
74+
value="85",
75+
condition="Greater than 80",
76+
short_text="Temperature is high",
77+
detail_text="Temperature sensor reading is 85°C (higher than the configured threshold of 80°C)",
78+
),
79+
description="Example alarm for notification",
80+
)
81+
id = alarm_client.create_or_update_alarm(create_alarm_request)
82+
print("Alarm created successfully")
83+
84+
# Get the alarm by its instance ID (the unique occurrence identifier)
85+
retrieved_alarm = alarm_client.get_alarm(instance_id=id)
86+
87+
# Define recipients to notify
88+
recipients = SmtpAddressFields(toAddresses=["sample1@example.com"])
89+
90+
# Create address group
91+
address_group = SmtpAddressGroup(
92+
display_name="Alarm Notification Recipients",
93+
properties={"address group": "Alarm"},
94+
fields=recipients,
95+
)
96+
97+
# Create mail template for alarm creation notification
98+
alarm_creation_template = SmtpMessageTemplate(
99+
display_name="Alarm Creation Template",
100+
fields=SmtpMessageTemplateFields(
101+
subject_template="Alarm Created: <alarm_id>",
102+
body_template="An alarm with ID <alarm_id> has been created.\n"
103+
"Condition: <alarm_condition>\n"
104+
"Description: <alarm_description>\n"
105+
"Current severity: <alarm_severity>\n"
106+
"Occurred At: <alarm_occurred_at>",
107+
),
108+
)
109+
110+
# Send notification for alarm creation
111+
notification_for_alarm_creation = create_notification_request_for_alarm(
112+
alarm=retrieved_alarm,
113+
address_group=address_group,
114+
message_template=alarm_creation_template,
115+
)
116+
notification_client.apply_dynamic_notification_strategy(
117+
request=notification_for_alarm_creation
118+
)
119+
print("Notification sent for alarm creation")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._notification_client import NotificationClient
2+
3+
# flake8: noqa
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Implementation of Notification Client"""
2+
3+
from nisystemlink.clients import core
4+
from nisystemlink.clients.core._uplink._base_client import BaseClient
5+
from nisystemlink.clients.core._uplink._methods import post
6+
from uplink import retry
7+
8+
from . import models
9+
10+
11+
@retry(
12+
when=retry.when.status(408, 429, 502, 503, 504),
13+
stop=retry.stop.after_attempt(1),
14+
on_exception=retry.CONNECTION_ERROR,
15+
)
16+
class NotificationClient(BaseClient):
17+
def __init__(self, configuration: core.HttpConfiguration | None = None):
18+
"""Initialize an instance.
19+
20+
Args:
21+
configuration: Defines the web server to connect to and information about
22+
how to connect. If not provided, the
23+
:class:`HttpConfigurationManager <nisystemlink.clients.core.HttpConfigurationManager>`
24+
is used to obtain the configuration.
25+
26+
Raises:
27+
ApiException: if unable to communicate with the `/ninotification` service.
28+
"""
29+
if configuration is None:
30+
configuration = core.HttpConfigurationManager.get_configuration()
31+
32+
super().__init__(configuration, base_path="/ninotification/v1/")
33+
34+
@post("apply-dynamic-strategy")
35+
def apply_dynamic_notification_strategy(
36+
self, request: models.DynamicStrategyRequest
37+
) -> None:
38+
"""Applies the notification strategy from the given request.
39+
40+
Args:
41+
request: Request with message template substitution fields and notification strategies.
42+
43+
Returns:
44+
None.
45+
46+
Raises:
47+
ApiException: if unable to communicate with the `/ninotification` service or provided invalid arguments.
48+
"""
49+
...
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from ._address_group import AddressGroup
2+
from ._dynamic_strategy_request import DynamicStrategyRequest
3+
from ._message_template import MessageTemplate
4+
from ._dynamic_notification_configuration import DynamicNotificationConfiguration
5+
from ._dynamic_notification_strategy import DynamicNotificationStrategy
6+
from ._smtp_address_group import SmtpAddressFields, SmtpAddressGroup
7+
from ._smtp_message_template import SmtpMessageTemplateFields, SmtpMessageTemplate
8+
9+
# flake8: noqa
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from typing import Dict, List
2+
3+
from ._base_notification_metadata import BaseNotificationMetadata
4+
5+
6+
class AddressGroup(BaseNotificationMetadata):
7+
"""Model defining notification recipients for generic service."""
8+
9+
interpreting_service_name: str
10+
"""Name of the interpreting service."""
11+
12+
fields: Dict[str, List[str]]
13+
"""Address group's fields. Requires at least one valid recipient."""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Dict, List
2+
3+
from nisystemlink.clients.core._uplink._json_model import JsonModel
4+
5+
6+
class BaseNotificationMetadata(JsonModel):
7+
id: str | None = None
8+
"""Identifier of the notification metadata"""
9+
10+
display_name: str | None = None
11+
"""Display name of the object.
12+
13+
Example: "name"
14+
"""
15+
16+
properties: Dict[str, str] | None = None
17+
"""Additional properties for the base metadata.
18+
19+
Example: { "property": "value" }
20+
"""
21+
22+
referencing_notification_strategies: List[str] | None = None
23+
"""List of notification strategies referencing the notification metadata"""
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from __future__ import annotations
2+
3+
from nisystemlink.clients.core._uplink._json_model import JsonModel
4+
from nisystemlink.clients.notification.models._address_group import AddressGroup
5+
from nisystemlink.clients.notification.models._message_template import MessageTemplate
6+
from nisystemlink.clients.notification.models._smtp_address_group import (
7+
SmtpAddressGroup,
8+
)
9+
from nisystemlink.clients.notification.models._smtp_message_template import (
10+
SmtpMessageTemplate,
11+
)
12+
from pydantic import Field, model_validator
13+
14+
15+
class DynamicNotificationConfiguration(JsonModel):
16+
"""Model for notification configuration defining address groups and message template for the notification.
17+
18+
Requires at least one of addressGroupId or addressGroup, and one of messageTemplateId or messageTemplate.
19+
"""
20+
21+
address_group_id: str | None = None
22+
"""ID referencing the associated address group."""
23+
24+
message_template_id: str | None = None
25+
"""ID referencing the associated message template."""
26+
27+
address_group: AddressGroup | SmtpAddressGroup | None = Field(default=None)
28+
"""Address group defining notification recipients."""
29+
30+
message_template: MessageTemplate | SmtpMessageTemplate | None = Field(default=None)
31+
"""Message template defining notification content structure"""
32+
33+
@model_validator(mode="after")
34+
def validate_required_pairs(self) -> DynamicNotificationConfiguration:
35+
"""Validator to check at least one of address_group_id or address_group, and
36+
one of message_template_id or message_template is present.
37+
"""
38+
if self.address_group_id is None and self.address_group is None:
39+
raise ValueError(
40+
"One of either AddressGroupId or AddressGroup is required."
41+
)
42+
43+
if self.message_template_id is None and self.message_template is None:
44+
raise ValueError(
45+
"One of either MessageTemplateId or MessageTemplate is required."
46+
)
47+
48+
return self

0 commit comments

Comments
 (0)