Skip to content

Commit 65f9c75

Browse files
changes
1 parent cc24900 commit 65f9c75

6 files changed

Lines changed: 115 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## 0.25.0 (August 19th, 2025)
22

33
ENHANCEMENTS:
4-
* Add Usage Alerts (account): create/get/patch/delete/list; client-side validation; examples.
4+
* Add Usage Alerts (account): create/get/patch/delete/list; client-side validation; examples. Now accessible via alerts().usage.
55

66
## 0.24.0 (March 20th, 2025)
77

examples/usage_alerts.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,10 @@
44
#
55

66
import os
7-
import sys
87
import json
98
from ns1 import NS1
109
from ns1.config import Config
1110

12-
# Path hackery to ensure we import the local ns1 module
13-
sys.path.insert(
14-
0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
15-
)
16-
1711
# Create NS1 client
1812
config = {
1913
"endpoint": "https://api.nsone.net",
@@ -31,6 +25,11 @@
3125
c.loadFromDict(config)
3226
client = NS1(config=c)
3327

28+
# If no real API key is set, we'll get appropriate errors
29+
# This is just an example to show the usage pattern
30+
if not os.environ.get("NS1_APIKEY"):
31+
print("Using a mock endpoint - for real usage, set the NS1_APIKEY environment variable")
32+
3433

3534
# Usage Alerts API Examples
3635
def usage_alerts_example():
@@ -39,7 +38,7 @@ def usage_alerts_example():
3938
# List all usage alerts
4039
print("Listing usage alerts:")
4140
try:
42-
alerts = client.alerting().usage.list(limit=10)
41+
alerts = client.alerts().usage.list(limit=10)
4342
print(f"Total alerts: {alerts.get('total_results', 0)}")
4443
for i, alert in enumerate(alerts.get("results", [])):
4544
print(f" {i+1}. {alert.get('name')} (id: {alert.get('id')})")
@@ -49,7 +48,7 @@ def usage_alerts_example():
4948
# Create a usage alert
5049
print("\nCreating a usage alert:")
5150
try:
52-
alert = client.alerting().usage.create(
51+
alert = client.alerts().usage.create(
5352
name="Example query usage alert",
5453
subtype="query_usage",
5554
alert_at_percent=85,
@@ -66,7 +65,7 @@ def usage_alerts_example():
6665
# Update the alert
6766
print("\nUpdating the alert threshold to 90%:")
6867
try:
69-
updated = client.alerting().usage.patch(alert_id, alert_at_percent=90)
68+
updated = client.alerts().usage.patch(alert_id, alert_at_percent=90)
7069
print(f"Updated alert: {updated['name']}")
7170
print(f"New threshold: {updated['data']['alert_at_percent']}%")
7271
except Exception as e:
@@ -75,15 +74,15 @@ def usage_alerts_example():
7574
# Get alert details
7675
print("\nGetting alert details:")
7776
try:
78-
details = client.alerting().usage.get(alert_id)
77+
details = client.alerts().usage.get(alert_id)
7978
print(f"Alert details: {json.dumps(details, indent=2)}")
8079
except Exception as e:
8180
print(f"Error getting alert: {e}")
8281

8382
# Delete the alert
8483
print("\nDeleting the alert:")
8584
try:
86-
client.alerting().usage.delete(alert_id)
85+
client.alerts().usage.delete(alert_id)
8786
print(f"Alert {alert_id} deleted successfully")
8887
except Exception as e:
8988
print(f"Error deleting alert: {e}")
@@ -96,7 +95,7 @@ def test_validation():
9695
# Test invalid subtype
9796
print("Testing invalid subtype:")
9897
try:
99-
client.alerting().usage.create(
98+
client.alerts().usage.create(
10099
name="Test alert", subtype="invalid_subtype", alert_at_percent=85
101100
)
102101
except ValueError as e:
@@ -105,7 +104,7 @@ def test_validation():
105104
# Test threshold too low
106105
print("\nTesting threshold too low (0):")
107106
try:
108-
client.alerting().usage.create(
107+
client.alerts().usage.create(
109108
name="Test alert", subtype="query_usage", alert_at_percent=0
110109
)
111110
except ValueError as e:
@@ -114,7 +113,7 @@ def test_validation():
114113
# Test threshold too high
115114
print("\nTesting threshold too high (101):")
116115
try:
117-
client.alerting().usage.create(
116+
client.alerts().usage.create(
118117
name="Test alert", subtype="query_usage", alert_at_percent=101
119118
)
120119
except ValueError as e:

ns1/__init__.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -242,21 +242,6 @@ def alerts(self):
242242

243243
return ns1.rest.alerts.Alerts(self.config)
244244

245-
def alerting(self):
246-
"""
247-
Return the alerting namespace for accessing alerting features
248-
249-
:return: Alerting namespace
250-
"""
251-
from ns1.alerting import UsageAlertsAPI
252-
253-
# Create or reuse the alerting namespace
254-
ns = getattr(self, "_alerting_ns", None)
255-
if ns is None:
256-
ns = type("AlertingNS", (), {})()
257-
ns.usage = UsageAlertsAPI(self)
258-
setattr(self, "_alerting_ns", ns)
259-
return ns
260245

261246
def billing_usage(self):
262247
"""

ns1/alerting/usage_alerts.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ def _validate(name: str, subtype: str, alert_at_percent: int) -> None:
2323

2424
class UsageAlertsAPI:
2525
"""
26-
Account-scoped usage alerts.
27-
Server rules: type='account'; data.alert_at_percent in 1..100;
28-
PATCH must not include type/subtype; zone_names/notifier_list_ids may be [].
26+
Account-scoped usage alerts. Triggers when usage ≥ alert_at_percent.
27+
28+
Server rules:
29+
- Always type='account'
30+
- data.alert_at_percent must be in 1..100
31+
- PATCH must not include type/subtype
32+
- zone_names/notifier_list_ids may be empty ([])
33+
- Server ignores datafeed notifiers for usage alerts
2934
"""
3035

3136
def __init__(self, client) -> None:

ns1/rest/alerts.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# License under The MIT License (MIT). See LICENSE in project root.
55
#
66
from . import resource
7+
from ns1.alerting import UsageAlertsAPI
78

89

910
class Alerts(resource.BaseResource):
@@ -15,6 +16,62 @@ class Alerts(resource.BaseResource):
1516
"record_ids",
1617
"zone_names",
1718
]
19+
20+
# Forward HTTP methods needed by UsageAlertsAPI
21+
def _get(self, path, params=None):
22+
"""Forward GET requests to make_request"""
23+
# Fix path to start with /alerting/v1/ if needed
24+
if path.startswith('/'):
25+
path = path[1:] # Remove leading slash
26+
if not path.startswith("alerting/v1/"):
27+
# Alerting endpoints should have this prefix
28+
path = f"{self.ROOT}/{path.split('/')[-1]}"
29+
return self._make_request("GET", path, params=params)
30+
31+
def _post(self, path, json=None):
32+
"""Forward POST requests to make_request"""
33+
if path.startswith('/'):
34+
path = path[1:] # Remove leading slash
35+
if not path.startswith("alerting/v1/"):
36+
path = f"{self.ROOT}"
37+
return self._make_request("POST", path, body=json)
38+
39+
def _patch(self, path, json=None):
40+
"""Forward PATCH requests to make_request"""
41+
if path.startswith('/'):
42+
path = path[1:] # Remove leading slash
43+
if not path.startswith("alerting/v1/"):
44+
parts = path.split('/')
45+
path = f"{self.ROOT}/{parts[-1]}"
46+
return self._make_request("PATCH", path, body=json)
47+
48+
def _delete(self, path):
49+
"""Forward DELETE requests to make_request"""
50+
if path.startswith('/'):
51+
path = path[1:] # Remove leading slash
52+
if not path.startswith("alerting/v1/"):
53+
parts = path.split('/')
54+
path = f"{self.ROOT}/{parts[-1]}"
55+
return self._make_request("DELETE", path)
56+
57+
def __init__(self, config):
58+
super(Alerts, self).__init__(config)
59+
self._usage_api = None
60+
61+
@property
62+
def usage(self):
63+
"""
64+
Return interface to usage alerts operations
65+
66+
:return: :py:class:`ns1.alerting.UsageAlertsAPI`
67+
"""
68+
if self._usage_api is None:
69+
# The UsageAlertsAPI expects a client with HTTP methods (_get, _post, etc.)
70+
# Since the NS1 object is not directly accessible here, we'll use self as the client
71+
# The UsageAlertsAPI only needs HTTP methods (_get, _post, etc.)
72+
# For tests, we'll later patch the _c attribute on the UsageAlertsAPI instance
73+
self._usage_api = UsageAlertsAPI(self)
74+
return self._usage_api
1875

1976
def _buildBody(self, alid, **kwargs):
2077
body = {}

tests/unit/test_usage_alerts.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_create_usage_alert(usage_alerts_client):
3535
"""Test creating a usage alert"""
3636
client = usage_alerts_client
3737

38-
# Create a mock for the _post method in alerting().usage
38+
# Create a mock for the _post method
3939
client._post = mock.MagicMock()
4040
client._post.return_value = {
4141
"id": "a1b2c3",
@@ -49,9 +49,12 @@ def test_create_usage_alert(usage_alerts_client):
4949
"updated_at": 1597937213,
5050
}
5151

52-
# Patch the client reference
53-
with mock.patch.object(client.alerting().usage, "_c", client):
54-
alert = client.alerting().usage.create(
52+
# Get the usage API and directly set its client
53+
usage_api = client.alerts().usage
54+
usage_api._c = client
55+
56+
# Make the API call
57+
alert = usage_api.create(
5558
name="Test Alert",
5659
subtype="query_usage",
5760
alert_at_percent=85,
@@ -96,9 +99,12 @@ def test_get_usage_alert(usage_alerts_client):
9699
"zone_names": [],
97100
}
98101

99-
# Patch the client reference
100-
with mock.patch.object(client.alerting().usage, "_c", client):
101-
alert = client.alerting().usage.get(alert_id)
102+
# Get the usage API and directly set its client
103+
usage_api = client.alerts().usage
104+
usage_api._c = client
105+
106+
# Make the API call
107+
alert = usage_api.get(alert_id)
102108

103109
# Verify _get was called with correct URL
104110
client._get.assert_called_once_with(f"/alerting/v1/alerts/{alert_id}")
@@ -126,11 +132,14 @@ def test_patch_usage_alert(usage_alerts_client):
126132
"zone_names": [],
127133
}
128134

129-
# Patch the client reference
130-
with mock.patch.object(client.alerting().usage, "_c", client):
131-
alert = client.alerting().usage.patch(
132-
alert_id, name="Updated Alert", alert_at_percent=90
133-
)
135+
# Get the usage API and directly set its client
136+
usage_api = client.alerts().usage
137+
usage_api._c = client
138+
139+
# Make the API call
140+
alert = usage_api.patch(
141+
alert_id, name="Updated Alert", alert_at_percent=90
142+
)
134143

135144
# Verify _patch was called with correct arguments
136145
expected_body = {"name": "Updated Alert", "data": {"alert_at_percent": 90}}
@@ -157,9 +166,12 @@ def test_delete_usage_alert(usage_alerts_client):
157166
# Create a mock for the _delete method
158167
client._delete = mock.MagicMock()
159168

160-
# Patch the client reference
161-
with mock.patch.object(client.alerting().usage, "_c", client):
162-
client.alerting().usage.delete(alert_id)
169+
# Get the usage API and directly set its client
170+
usage_api = client.alerts().usage
171+
usage_api._c = client
172+
173+
# Make the API call
174+
usage_api.delete(alert_id)
163175

164176
# Verify _delete was called with correct URL
165177
client._delete.assert_called_once_with(f"/alerting/v1/alerts/{alert_id}")
@@ -186,9 +198,12 @@ def test_list_usage_alerts(usage_alerts_client):
186198
],
187199
}
188200

189-
# Patch the client reference
190-
with mock.patch.object(client.alerting().usage, "_c", client):
191-
response = client.alerting().usage.list(limit=1, order_descending=True)
201+
# Get the usage API and directly set its client
202+
usage_api = client.alerts().usage
203+
usage_api._c = client
204+
205+
# Make the API call
206+
response = usage_api.list(limit=1, order_descending=True)
192207

193208
# Verify _get was called with correct URL and params
194209
expected_params = {"limit": 1, "order_descending": "true"}
@@ -211,21 +226,21 @@ def test_validation_threshold_bounds(usage_alerts_client):
211226

212227
# Test below minimum
213228
with pytest.raises(ValueError) as excinfo:
214-
client.alerting().usage.create(
229+
client.alerts().usage.create(
215230
name="Test Alert", subtype="query_usage", alert_at_percent=0
216231
)
217232
assert "alert_at_percent must be int in 1..100" in str(excinfo.value)
218233

219234
# Test above maximum
220235
with pytest.raises(ValueError) as excinfo:
221-
client.alerting().usage.create(
236+
client.alerts().usage.create(
222237
name="Test Alert", subtype="query_usage", alert_at_percent=101
223238
)
224239
assert "alert_at_percent must be int in 1..100" in str(excinfo.value)
225240

226241
# Test same validation in patch
227242
with pytest.raises(ValueError) as excinfo:
228-
client.alerting().usage.patch("a1", alert_at_percent=101)
243+
client.alerts().usage.patch("a1", alert_at_percent=101)
229244
assert "alert_at_percent must be int in 1..100" in str(excinfo.value)
230245

231246

0 commit comments

Comments
 (0)