Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## 0.25.0 (August 19th, 2025)
## 0.25.0 (August 28th, 2025)

ENHANCEMENTS:
* Add Usage Alerts (account): create/get/patch/delete/list; client-side validation; examples. Now accessible via alerts().usage.
* Adds support for new Alerts

## 0.24.0 (March 20th, 2025)

Expand Down
4 changes: 3 additions & 1 deletion examples/usage_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
# If no real API key is set, we'll get appropriate errors
# This is just an example to show the usage pattern
if not os.environ.get("NS1_APIKEY"):
print("Using a mock endpoint - for real usage, set the NS1_APIKEY environment variable")
print(
"Using a mock endpoint - for real usage, set the NS1_APIKEY environment variable"
)


# Usage Alerts API Examples
Expand Down
1 change: 0 additions & 1 deletion ns1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ def alerts(self):

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


def billing_usage(self):
"""
Return a new raw REST interface to BillingUsage resources
Expand Down
6 changes: 3 additions & 3 deletions ns1/alerting/usage_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def _validate(name: str, subtype: str, alert_at_percent: int) -> None:
class UsageAlertsAPI:
"""
Account-scoped usage alerts. Triggers when usage ≥ alert_at_percent.
Server rules:
- Always type='account'

Server rules:
- Always type='account'
- data.alert_at_percent must be in 1..100
- PATCH must not include type/subtype
- zone_names/notifier_list_ids may be empty ([])
Expand Down
26 changes: 13 additions & 13 deletions ns1/rest/alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,53 +16,53 @@ class Alerts(resource.BaseResource):
"record_ids",
"zone_names",
]

# Forward HTTP methods needed by UsageAlertsAPI
def _get(self, path, params=None):
"""Forward GET requests to make_request"""
# Fix path to start with /alerting/v1/ if needed
if path.startswith('/'):
if path.startswith("/"):
path = path[1:] # Remove leading slash
if not path.startswith("alerting/v1/"):
# Alerting endpoints should have this prefix
path = f"{self.ROOT}/{path.split('/')[-1]}"
return self._make_request("GET", path, params=params)

def _post(self, path, json=None):
"""Forward POST requests to make_request"""
if path.startswith('/'):
if path.startswith("/"):
path = path[1:] # Remove leading slash
if not path.startswith("alerting/v1/"):
path = f"{self.ROOT}"
return self._make_request("POST", path, body=json)

def _patch(self, path, json=None):
"""Forward PATCH requests to make_request"""
if path.startswith('/'):
if path.startswith("/"):
path = path[1:] # Remove leading slash
if not path.startswith("alerting/v1/"):
parts = path.split('/')
parts = path.split("/")
path = f"{self.ROOT}/{parts[-1]}"
return self._make_request("PATCH", path, body=json)

def _delete(self, path):
"""Forward DELETE requests to make_request"""
if path.startswith('/'):
if path.startswith("/"):
path = path[1:] # Remove leading slash
if not path.startswith("alerting/v1/"):
parts = path.split('/')
parts = path.split("/")
path = f"{self.ROOT}/{parts[-1]}"
return self._make_request("DELETE", path)

def __init__(self, config):
super(Alerts, self).__init__(config)
self._usage_api = None

@property
def usage(self):
"""
Return interface to usage alerts operations

:return: :py:class:`ns1.alerting.UsageAlertsAPI`
"""
if self._usage_api is None:
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ def test_rest_alert_list(alerts_config):
"data": {"min": 20, "max": 80},
},
),
(
"sso_alert",
"account",
"saml_certificate_expired",
"../alerting/v1/alerts",
{
"notifier_list_ids": ["6707da567cd4f300012cd7e4"],
},
),
(
"redirect_alert",
"redirects",
"certificate_renewal_failed",
"../alerting/v1/alerts",
{
"notifier_list_ids": ["6707da567cd4f300012cd7e4"],
},
),
],
)
def test_rest_alert_create(
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def datasets_config(config):
return config


@pytest.mark.parametrize("url", [("datasets")])
@pytest.mark.parametrize("url", ["datasets"])
def test_rest_datasets_list(datasets_config, url):
z = NS1(config=datasets_config).datasets()
z._make_request = mock.MagicMock()
Expand Down Expand Up @@ -62,7 +62,7 @@ def test_rest_dataset_retrieve(datasets_config, dtId, url):
)


@pytest.mark.parametrize("url", [("datasets")])
@pytest.mark.parametrize("url", ["datasets"])
def test_rest_dataset_create(datasets_config, url):
z = NS1(config=datasets_config).datasets()
z._make_request = mock.MagicMock()
Expand Down
20 changes: 10 additions & 10 deletions tests/unit/test_usage_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@ def test_create_usage_alert(usage_alerts_client):
# Get the usage API and directly set its client
usage_api = client.alerts().usage
usage_api._c = client

# Make the API call
alert = usage_api.create(
name="Test Alert",
subtype="query_usage",
alert_at_percent=85,
notifier_list_ids=["n1"],
)
name="Test Alert",
subtype="query_usage",
alert_at_percent=85,
notifier_list_ids=["n1"],
)

# Verify _post was called with correct arguments
expected_body = {
Expand Down Expand Up @@ -102,7 +102,7 @@ def test_get_usage_alert(usage_alerts_client):
# Get the usage API and directly set its client
usage_api = client.alerts().usage
usage_api._c = client

# Make the API call
alert = usage_api.get(alert_id)

Expand Down Expand Up @@ -135,7 +135,7 @@ def test_patch_usage_alert(usage_alerts_client):
# Get the usage API and directly set its client
usage_api = client.alerts().usage
usage_api._c = client

# Make the API call
alert = usage_api.patch(
alert_id, name="Updated Alert", alert_at_percent=90
Expand Down Expand Up @@ -169,7 +169,7 @@ def test_delete_usage_alert(usage_alerts_client):
# Get the usage API and directly set its client
usage_api = client.alerts().usage
usage_api._c = client

# Make the API call
usage_api.delete(alert_id)

Expand Down Expand Up @@ -201,7 +201,7 @@ def test_list_usage_alerts(usage_alerts_client):
# Get the usage API and directly set its client
usage_api = client.alerts().usage
usage_api._c = client

# Make the API call
response = usage_api.list(limit=1, order_descending=True)

Expand Down