diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e2817f..6e295c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +ENHANCEMENTS: +* Add `getRedirectsUsage` to `BillingUsage` for the `billing-usage/v1/redirects` endpoint +* Deprecate `tsig` and `views` DDI resources +* Deprecate `pulsar/query/decision/customer` endpoints + ## 0.29.0 (July 15th, 2026) ENHANCEMENTS: diff --git a/README.md b/README.md index a0e02dd..8bef9e5 100644 --- a/README.md +++ b/README.md @@ -44,14 +44,14 @@ See the [examples directory](https://github.com/ns1/ns1-python/tree/master/examp Documentation ============= -If you don't yet have an NS1 account, [signup here (free)](https://ns1.com/signup/) +If you don't yet have an NS1 account, [signup here (free)](https://www.ibm.com/account/reg/us-en/signup?formid=urx-54055) You'll need an API Key. To create one, login to [the portal](https://my.nsone.net/) and click on the Account button in the top right. Select Settings & Users, then add a new API Key at the bottom. * [Documentation at ReadTheDocs](https://ns1-python.readthedocs.org/en/latest/) -* [NS1 REST API Documentation](https://ns1.com/api/) +* [NS1 REST API Documentation](https://developer.ibm.com/apis/catalog/ns1--ibm-ns1-connect-api/api/API--ns1--ibm-ns1-connect-api) Tests ===== diff --git a/doc/index.rst b/doc/index.rst index f5c7e88..88f6d36 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -2,7 +2,7 @@ Python SDK for NS1 DNS Platform =============================== .. image:: _static/ns1.png - :target: https://ns1.com/ + :target: https://www.ibm.com/products/ns1-connect About diff --git a/ns1/__init__.py b/ns1/__init__.py index 2c2934c..222420a 100644 --- a/ns1/__init__.py +++ b/ns1/__init__.py @@ -165,12 +165,21 @@ def user(self): def tsig(self): """ - Return a new raw REST interface to tsgi resources + Return a new raw REST interface to tsig resources. - :rtype: :py:class:`ns1.rest.tsig.Tsgi` + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + + :rtype: :py:class:`ns1.rest.tsig.Tsig` """ + import warnings import ns1.rest.tsig + warnings.warn( + "NS1.tsig() is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return ns1.rest.tsig.Tsig(self.config) def apikey(self): @@ -205,12 +214,21 @@ def acls(self): def views(self): """ - Return a new raw REST interface to View resources + Return a new raw REST interface to View resources. + + .. deprecated:: + The Views API is deprecated and will be removed in a future release. :rtype: :py:class:`ns1.rest.views.Views` """ + import warnings import ns1.rest.views + warnings.warn( + "NS1.views() is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return ns1.rest.views.Views(self.config) def datasets(self): diff --git a/ns1/rest/billing_usage.py b/ns1/rest/billing_usage.py index 8efb11b..8ef7587 100644 --- a/ns1/rest/billing_usage.py +++ b/ns1/rest/billing_usage.py @@ -62,6 +62,15 @@ def getFilterChainsUsage(self, callback=None, errback=None): params={}, ) + def getRedirectsUsage(self, callback=None, errback=None): + return self._make_request( + "GET", + f"{self.ROOT}/redirects", + callback=callback, + errback=errback, + params={}, + ) + def getLimits(self, from_unix, to_unix, callback=None, errback=None): return self._make_request( "GET", diff --git a/ns1/rest/pulsar_decisions.py b/ns1/rest/pulsar_decisions.py index def62b9..8fd54b8 100644 --- a/ns1/rest/pulsar_decisions.py +++ b/ns1/rest/pulsar_decisions.py @@ -3,6 +3,8 @@ # # License under The MIT License (MIT). See LICENSE in project root. # +import warnings + from . import resource try: @@ -134,6 +136,17 @@ def get_filters_time(self, callback=None, errback=None, **kwargs): def get_decision_customer( self, customer_id, callback=None, errback=None, **kwargs ): + """ + .. deprecated:: + The ``pulsar/query/decision/customer`` API is deprecated and will + be removed in a future release. + """ + warnings.warn( + "Decisions.get_decision_customer is deprecated and will be" + " removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) path = self._make_query_url( "decision/customer/%s" % customer_id, **kwargs ) @@ -147,6 +160,17 @@ def get_decision_customer( def get_decision_customer_undetermined( self, customer_id, callback=None, errback=None, **kwargs ): + """ + .. deprecated:: + The ``pulsar/query/decision/customer`` API is deprecated and will + be removed in a future release. + """ + warnings.warn( + "Decisions.get_decision_customer_undetermined is deprecated and" + " will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) path = self._make_query_url( "decision/customer/%s/undetermined" % customer_id, **kwargs ) @@ -166,6 +190,17 @@ def get_decision_record( errback=None, **kwargs ): + """ + .. deprecated:: + The ``pulsar/query/decision/customer`` API is deprecated and will + be removed in a future release. + """ + warnings.warn( + "Decisions.get_decision_record is deprecated and will be" + " removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) path = self._make_query_url( "decision/customer/%s/record/%s/%s" % (customer_id, domain, rec_type), @@ -187,6 +222,17 @@ def get_decision_record_undetermined( errback=None, **kwargs ): + """ + .. deprecated:: + The ``pulsar/query/decision/customer`` API is deprecated and will + be removed in a future release. + """ + warnings.warn( + "Decisions.get_decision_record_undetermined is deprecated and" + " will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) path = self._make_query_url( "decision/customer/%s/record/%s/%s/undetermined" % (customer_id, domain, rec_type), @@ -202,6 +248,17 @@ def get_decision_record_undetermined( def get_decision_total( self, customer_id, callback=None, errback=None, **kwargs ): + """ + .. deprecated:: + The ``pulsar/query/decision/customer`` API is deprecated and will + be removed in a future release. + """ + warnings.warn( + "Decisions.get_decision_total is deprecated and will be" + " removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) path = self._make_query_url( "decision/customer/%s/total" % customer_id, **kwargs ) diff --git a/ns1/rest/tsig.py b/ns1/rest/tsig.py index d68869a..8ff09aa 100644 --- a/ns1/rest/tsig.py +++ b/ns1/rest/tsig.py @@ -3,11 +3,19 @@ # # License under The MIT License (MIT). See LICENSE in project root. # +import warnings + from . import permissions from . import resource class Tsig(resource.BaseResource): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + Use :py:class:`ns1.rest.zones.Zones` zone-level TSIG configuration instead. + """ + ROOT = "tsig" PASSTHRU_FIELDS = [ @@ -27,6 +35,15 @@ def create( errback=None, **kwargs ): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Tsig.create is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) body = {"algorithm": algorithm, "secret": secret} if "permissions" not in kwargs: body["permissions"] = permissions._default_perms @@ -50,6 +67,15 @@ def update( errback=None, **kwargs ): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Tsig.update is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) body = {"algorithm": algorithm, "secret": secret} self._buildStdBody(body, kwargs) @@ -62,6 +88,15 @@ def update( ) def delete(self, tsig_name, callback=None, errback=None): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Tsig.delete is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "DELETE", "%s/%s" % (self.ROOT, tsig_name), @@ -70,11 +105,29 @@ def delete(self, tsig_name, callback=None, errback=None): ) def list(self, callback=None, errback=None): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Tsig.list is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "GET", "%s" % self.ROOT, callback=callback, errback=errback ) def retrieve(self, tsig_name, callback=None, errback=None): + """ + .. deprecated:: + The TSIG key API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Tsig.retrieve is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "GET", "%s/%s" % (self.ROOT, tsig_name), diff --git a/ns1/rest/views.py b/ns1/rest/views.py index 60cbf5a..8414d7a 100644 --- a/ns1/rest/views.py +++ b/ns1/rest/views.py @@ -2,11 +2,17 @@ # Copyright (c) 2014 NSONE, Inc. # # License under The MIT License (MIT). See LICENSE in project root. +import warnings from . import resource class Views(resource.BaseResource): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + ROOT = "views" INT_FIELDS = [ "preference", @@ -25,6 +31,15 @@ def _buildBody(self, view_name, **kwargs): return body def create(self, view_name, callback=None, errback=None, **kwargs): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Views.create is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) body = self._buildBody(view_name, **kwargs) return self.create_raw( @@ -34,6 +49,15 @@ def create(self, view_name, callback=None, errback=None, **kwargs): def create_raw( self, view_name, body, callback=None, errback=None, **kwargs ): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Views.create_raw is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "PUT", "%s/%s" % (self.ROOT, view_name), @@ -43,6 +67,15 @@ def create_raw( ) def update(self, view_name, callback=None, errback=None, **kwargs): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Views.update is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) body = self._buildBody(view_name, **kwargs) return self._make_request( @@ -54,6 +87,15 @@ def update(self, view_name, callback=None, errback=None, **kwargs): ) def delete(self, view_name, callback=None, errback=None): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Views.delete is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "DELETE", "%s/%s" % (self.ROOT, view_name), @@ -62,6 +104,15 @@ def delete(self, view_name, callback=None, errback=None): ) def retrieve(self, view_name, callback=None, errback=None): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + warnings.warn( + "Views.retrieve is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) return self._make_request( "GET", "%s/%s" % (self.ROOT, view_name), diff --git a/ns1/views.py b/ns1/views.py index ebf834c..fdc7098 100644 --- a/ns1/views.py +++ b/ns1/views.py @@ -3,6 +3,8 @@ # # License under The MIT License (MIT). See LICENSE in project root. # +import warnings + from ns1.rest.views import Views @@ -11,7 +13,17 @@ class ViewException(Exception): class View(object): + """ + .. deprecated:: + The Views API is deprecated and will be removed in a future release. + """ + def __init__(self, config, view): + warnings.warn( + "View is deprecated and will be removed in a future release.", + DeprecationWarning, + stacklevel=2, + ) self._rest = Views(config) self.config = config self.view = view diff --git a/tests/unit/test_billing_usage.py b/tests/unit/test_billing_usage.py index cd443d8..a309b98 100644 --- a/tests/unit/test_billing_usage.py +++ b/tests/unit/test_billing_usage.py @@ -106,3 +106,17 @@ def test_rest_get_billing_usage_limits(billing_usage_config, url): errback=None, params={"from": 123, "to": 456}, ) + + +@pytest.mark.parametrize("url", ["billing-usage/redirects"]) +def test_rest_get_billing_usage_for_redirects(billing_usage_config, url): + z = NS1(config=billing_usage_config).billing_usage() + z._make_request = mock.MagicMock() + z.getRedirectsUsage() + z._make_request.assert_called_once_with( + "GET", + url, + callback=None, + errback=None, + params={}, + ) diff --git a/tests/unit/test_pulsar_decisions.py b/tests/unit/test_pulsar_decisions.py index 7381c99..1e8522d 100644 --- a/tests/unit/test_pulsar_decisions.py +++ b/tests/unit/test_pulsar_decisions.py @@ -97,6 +97,48 @@ def pulsar_decisions_config(config): "pulsar/query/decisions/filters/time", {"callback": None, "errback": None}, ), + ( + "get_decisions_records", + None, + "GET", + "pulsar/query/decisions/records", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_results_record", + None, + "GET", + "pulsar/query/decisions/results/record", + {"callback": None, "errback": None}, + ), + ], +) +def test_rest_pulsar_decisions( + pulsar_decisions_config, op, args, method, url, kwargs +): + """Test Pulsar Decisions REST API endpoints.""" + m = ns1.rest.pulsar_decisions.Decisions(pulsar_decisions_config) + m._make_request = mock.MagicMock() + operation = getattr(m, op) + if args is not None: + if ( + isinstance(args, list) + and len(args) == 1 + and isinstance(args[0], dict) + ): + # Handle kwargs case + operation(**args[0]) + else: + # Handle positional args case + operation(*args) + else: + operation() + m._make_request.assert_called_once_with(method, url, **kwargs) + + +@pytest.mark.parametrize( + "op, args, method, url, kwargs", + [ ( "get_decision_customer", ["12345"], @@ -132,42 +174,17 @@ def pulsar_decisions_config(config): "pulsar/query/decision/customer/12345/total", {"callback": None, "errback": None}, ), - ( - "get_decisions_records", - None, - "GET", - "pulsar/query/decisions/records", - {"callback": None, "errback": None}, - ), - ( - "get_decisions_results_record", - None, - "GET", - "pulsar/query/decisions/results/record", - {"callback": None, "errback": None}, - ), ], ) -def test_rest_pulsar_decisions( +def test_rest_pulsar_decisions_deprecated( pulsar_decisions_config, op, args, method, url, kwargs ): - """Test Pulsar Decisions REST API endpoints.""" + """Test deprecated pulsar/query/decision/customer endpoints.""" m = ns1.rest.pulsar_decisions.Decisions(pulsar_decisions_config) m._make_request = mock.MagicMock() operation = getattr(m, op) - if args is not None: - if ( - isinstance(args, list) - and len(args) == 1 - and isinstance(args[0], dict) - ): - # Handle kwargs case - operation(**args[0]) - else: - # Handle positional args case - operation(*args) - else: - operation() + with pytest.warns(DeprecationWarning): + operation(*args) m._make_request.assert_called_once_with(method, url, **kwargs) diff --git a/tests/unit/test_tsig.py b/tests/unit/test_tsig.py index 73c85ea..2751866 100644 --- a/tests/unit/test_tsig.py +++ b/tests/unit/test_tsig.py @@ -29,7 +29,8 @@ def tsig_config(config): def test_rest_tsig_list(tsig_config): t = ns1.rest.tsig.Tsig(tsig_config) t._make_request = mock.MagicMock() - t.list() + with pytest.warns(DeprecationWarning): + t.list() t._make_request.assert_called_once_with( "GET", "tsig", callback=None, errback=None ) @@ -39,7 +40,8 @@ def test_rest_tsig_list(tsig_config): def test_rest_tsig_retrieve(tsig_config, key_name, url): t = ns1.rest.tsig.Tsig(tsig_config) t._make_request = mock.MagicMock() - t.retrieve(key_name) + with pytest.warns(DeprecationWarning): + t.retrieve(key_name) t._make_request.assert_called_once_with( "GET", url, callback=None, errback=None ) @@ -52,7 +54,8 @@ def test_rest_tsig_retrieve(tsig_config, key_name, url): def test_rest_tsig_create(tsig_config, key_name, algorithm, secret, url): t = ns1.rest.tsig.Tsig(tsig_config) t._make_request = mock.MagicMock() - t.create(key_name, algorithm, secret) + with pytest.warns(DeprecationWarning): + t.create(key_name, algorithm, secret) t._make_request.assert_called_once_with( "PUT", url, @@ -80,7 +83,8 @@ def test_rest_tsig_create(tsig_config, key_name, algorithm, secret, url): def test_rest_tsig_update(tsig_config, tsgi_name, algorithm, secret, url): t = ns1.rest.tsig.Tsig(tsig_config) t._make_request = mock.MagicMock() - t.update(tsgi_name, algorithm, secret) + with pytest.warns(DeprecationWarning): + t.update(tsgi_name, algorithm, secret) t._make_request.assert_called_once_with( "POST", url, @@ -97,7 +101,8 @@ def test_rest_tsig_update(tsig_config, tsgi_name, algorithm, secret, url): def test_rest_tsig_delete(tsig_config, tsgi_name, url): t = ns1.rest.tsig.Tsig(tsig_config) t._make_request = mock.MagicMock() - t.delete(tsgi_name) + with pytest.warns(DeprecationWarning): + t.delete(tsgi_name) t._make_request.assert_called_once_with( "DELETE", url, callback=None, errback=None ) diff --git a/tests/unit/test_views.py b/tests/unit/test_views.py index 7f1a679..08d5693 100644 --- a/tests/unit/test_views.py +++ b/tests/unit/test_views.py @@ -29,7 +29,8 @@ def view_config(config): def test_rest_view_retrieve(view_config, view_name, url): z = ns1.rest.views.Views(view_config) z._make_request = mock.MagicMock() - z.retrieve(view_name) + with pytest.warns(DeprecationWarning): + z.retrieve(view_name) z._make_request.assert_called_once_with( "GET", url, callback=None, errback=None ) @@ -47,7 +48,8 @@ def test_rest_view_retrieve(view_config, view_name, url): def test_rest_view_create(view_config, view_name, url): z = ns1.rest.views.Views(view_config) z._make_request = mock.MagicMock() - z.create(view_name=view_name) + with pytest.warns(DeprecationWarning): + z.create(view_name=view_name) z._make_request.assert_called_once_with( "PUT", url, @@ -66,7 +68,8 @@ def test_rest_view_create(view_config, view_name, url): def test_rest_view_update(view_config, view_name, url): z = ns1.rest.views.Views(view_config) z._make_request = mock.MagicMock() - z.update(view_name=view_name) + with pytest.warns(DeprecationWarning): + z.update(view_name=view_name) z._make_request.assert_called_once_with( "POST", url, @@ -80,7 +83,8 @@ def test_rest_view_update(view_config, view_name, url): def test_rest_view_delete(view_config, view_name, url): z = ns1.rest.views.Views(view_config) z._make_request = mock.MagicMock() - z.delete(view_name) + with pytest.warns(DeprecationWarning): + z.delete(view_name) z._make_request.assert_called_once_with( "DELETE", url, callback=None, errback=None )