diff --git a/CHANGELOG.md b/CHANGELOG.md index d0008df..2cc2e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.26.0 (August 28th, 2025) + +ENHANCEMENTS: +* Adds support for pulsar decision endpoints + ## 0.25.0 (August 28th, 2025) ENHANCEMENTS: diff --git a/examples/pulsar_decisions.py b/examples/pulsar_decisions.py new file mode 100644 index 0000000..4d2ca3a --- /dev/null +++ b/examples/pulsar_decisions.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# +# Copyright (c) 2014, 2025 NSONE, Inc. +# +# License under The MIT License (MIT). See LICENSE in project root. +# +""" +Example usage of Pulsar Decisions API endpoints. +This example demonstrates how to query Pulsar decision analytics data +using the ns1-python library. +""" +from ns1 import NS1 +import time + +# NS1 will use config in ~/.nsone by default +api = NS1() + +# to specify an apikey here instead, use: + +# from ns1 import Config +# config = Config() +# config.createFromAPIKey('<>') +# api = NS1(config=config) + +# Get current timestamp and 1 hour ago for time-based queries +end_time = int(time.time()) +start_time = end_time - 3600 # 1 hour ago + + +def main(): + """ + Demonstrate various Pulsar Decisions API endpoints. + """ + print("=" * 60) + print("Pulsar Decisions API Examples") + print("=" * 60) + + ############################ + # GET DECISIONS DATA # + ############################ + print("\n1. Getting decisions data...") + try: + decisions = api.pulsardecisions().get_decisions( + start=start_time, end=end_time, period="1h" + ) + print(f" Total decisions: {decisions.get('total', 0)}") + print(f" Number of graphs: {len(decisions.get('graphs', []))}") + except Exception as e: + print(f" Error: {e}") + + ############################ + # GET REGIONAL GRAPH DATA # + ############################ + print("\n2. Getting regional graph data...") + try: + region_data = api.pulsardecisions().get_decisions_graph_region( + start=start_time, end=end_time + ) + print(f" Regions found: {len(region_data.get('data', []))}") + for region in region_data.get("data", [])[:3]: # Show first 3 + print( + f" - {region.get('region')}: {len(region.get('counts', []))} job counts" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET TIME-SERIES GRAPH DATA # + ############################## + print("\n3. Getting time-series graph data...") + try: + time_data = api.pulsardecisions().get_decisions_graph_time( + start=start_time, end=end_time, period="5m" + ) + print(f" Time points: {len(time_data.get('data', []))}") + if time_data.get("data"): + first_point = time_data["data"][0] + print(f" First timestamp: {first_point.get('timestamp')}") + print( + f" Job counts at first point: {len(first_point.get('counts', []))}" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET AREA-BASED DECISIONS # + ############################## + print("\n4. Getting area-based decisions...") + try: + area_data = api.pulsardecisions().get_decisions_area( + start=start_time, end=end_time, area="US" + ) + print(f" Areas found: {len(area_data.get('areas', []))}") + for area in area_data.get("areas", [])[:3]: # Show first 3 + print( + f" - {area.get('area_name')}: {area.get('count')} decisions" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET ASN-BASED DECISIONS # + ############################## + print("\n5. Getting ASN-based decisions...") + try: + asn_data = api.pulsardecisions().get_decisions_asn( + start=start_time, end=end_time + ) + print(f" ASNs found: {len(asn_data.get('data', []))}") + for asn in asn_data.get("data", [])[:3]: # Show first 3 + print( + f" - ASN {asn.get('asn')}: {asn.get('count')} decisions " + f"({asn.get('traffic_distribution', 0)*100:.1f}% of traffic)" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET RESULTS OVER TIME # + ############################## + print("\n6. Getting results over time...") + try: + results_time = api.pulsardecisions().get_decisions_results_time( + start=start_time, + end=end_time, + job="your-job-id", # Replace with actual job ID + ) + print(f" Time points: {len(results_time.get('data', []))}") + if results_time.get("data"): + first_point = results_time["data"][0] + print( + f" Results at first point: {len(first_point.get('results', []))}" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET RESULTS BY AREA # + ############################## + print("\n7. Getting results by area...") + try: + results_area = api.pulsardecisions().get_decisions_results_area( + start=start_time, end=end_time + ) + print(f" Areas found: {len(results_area.get('area', []))}") + for area in results_area.get("area", [])[:3]: # Show first 3 + print( + f" - {area.get('area')}: {area.get('decision_count')} decisions, " + f"{len(area.get('results', []))} unique results" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET FILTER DATA OVER TIME # + ############################## + print("\n8. Getting filter data over time...") + try: + filters_time = api.pulsardecisions().get_filters_time( + start=start_time, end=end_time + ) + print(f" Time points: {len(filters_time.get('filters', []))}") + if filters_time.get("filters"): + first_point = filters_time["filters"][0] + print( + f" Filters at first point: {len(first_point.get('filters', {}))}" + ) + except Exception as e: + print(f" Error: {e}") + + ################################### + # GET CUSTOMER-SPECIFIC DECISIONS # + ################################### + print("\n9. Getting customer-specific decisions...") + try: + customer_id = "your-customer-id" # Replace with actual customer ID + customer_data = api.pulsardecisions().get_decision_customer( + customer_id, start=start_time, end=end_time + ) + print(f" Data points: {len(customer_data.get('data', []))}") + if customer_data.get("data"): + total = sum( + point.get("total", 0) for point in customer_data["data"] + ) + print(f" Total decisions: {total}") + except Exception as e: + print(f" Error: {e}") + + ################################### + # GET RECORD-SPECIFIC DECISIONS # + ################################### + print("\n10. Getting record-specific decisions...") + try: + customer_id = "your-customer-id" # Replace with actual customer ID + domain = "example.com" + rec_type = "A" + record_data = api.pulsardecisions().get_decision_record( + customer_id, domain, rec_type, start=start_time, end=end_time + ) + print(f" Data points: {len(record_data.get('data', []))}") + if record_data.get("data"): + total = sum(point.get("total", 0) for point in record_data["data"]) + print(f" Total decisions for {domain}/{rec_type}: {total}") + except Exception as e: + print(f" Error: {e}") + + #################################### + # GET TOTAL DECISIONS FOR CUSTOMER # + #################################### + print("\n11. Getting total decisions for customer...") + try: + customer_id = "your-customer-id" # Replace with actual customer ID + total_data = api.pulsardecisions().get_decision_total( + customer_id, start=start_time, end=end_time + ) + print(f" Total decisions: {total_data.get('total', 0)}") + except Exception as e: + print(f" Error: {e}") + + ################################ + # GET DECISIONS BY RECORD # + ################################ + print("\n12. Getting decisions by record...") + try: + records_data = api.pulsardecisions().get_decisions_records( + start=start_time, end=end_time + ) + print(f" Total decisions: {records_data.get('total', 0)}") + print(f" Number of records: {len(records_data.get('records', {}))}") + for record_key, record_info in list( + records_data.get("records", {}).items() + )[:3]: + print( + f" - {record_key}: {record_info.get('count')} decisions " + f"({record_info.get('percentage_of_total', 0):.1f}%)" + ) + except Exception as e: + print(f" Error: {e}") + + ############################## + # GET RESULTS BY RECORD # + ############################## + print("\n13. Getting results by record...") + try: + results_record = api.pulsardecisions().get_decisions_results_record( + start=start_time, end=end_time + ) + print(f" Number of records: {len(results_record.get('record', {}))}") + for record_key, record_info in list( + results_record.get("record", {}).items() + )[:3]: + print( + f" - {record_key}: {record_info.get('decision_count')} decisions, " + f"{len(record_info.get('results', {}))} unique results" + ) + except Exception as e: + print(f" Error: {e}") + + ################################## + # QUERYING WITH MULTIPLE FILTERS # + ################################## + print("\n14. Querying with multiple filters...") + try: + filtered_data = api.pulsardecisions().get_decisions( + start=start_time, + end=end_time, + period="1h", + area="US", + job="your-job-id", # Replace with actual job ID + agg="sum", + ) + print( + f" Total decisions (filtered): {filtered_data.get('total', 0)}" + ) + except Exception as e: + print(f" Error: {e}") + + ################################## + # QUERYING WITH MULTIPLE JOBS # + ################################## + print("\n15. Querying with multiple jobs...") + try: + multi_job_data = api.pulsardecisions().get_decisions( + start=start_time, + end=end_time, + jobs=["job1", "job2", "job3"], # Replace with actual job IDs + ) + print( + f" Total decisions (multi-job): {multi_job_data.get('total', 0)}" + ) + except Exception as e: + print(f" Error: {e}") + + print("\n" + "=" * 60) + print("Examples completed!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ns1/__init__.py b/ns1/__init__.py index 46a1726..da2f817 100644 --- a/ns1/__init__.py +++ b/ns1/__init__.py @@ -5,7 +5,7 @@ # from .config import Config -version = "0.25.0" +version = "0.26.0" class NS1: @@ -251,6 +251,16 @@ def billing_usage(self): return ns1.rest.billing_usage.BillingUsage(self.config) + def pulsardecisions(self): + """ + Return a new raw REST interface to Pulsar Decisions resources + + :rtype: :py:class:`ns1.rest.pulsar_decisions.Decisions` + """ + import ns1.rest.pulsar_decisions + + return ns1.rest.pulsar_decisions.Decisions(self.config) + # HIGH LEVEL INTERFACE def loadZone(self, zone, callback=None, errback=None): """ diff --git a/ns1/rest/pulsar_decisions.py b/ns1/rest/pulsar_decisions.py new file mode 100644 index 0000000..def62b9 --- /dev/null +++ b/ns1/rest/pulsar_decisions.py @@ -0,0 +1,233 @@ +# +# Copyright (c) 2014, 2025 NSONE, Inc. +# +# License under The MIT License (MIT). See LICENSE in project root. +# +from . import resource + +try: + from urllib.parse import urlencode +except ImportError: + from urllib import urlencode + + +class Decisions(resource.BaseResource): + ROOT = "pulsar/query" + PASSTHRU_FIELDS = [] + + def _build_query_params(self, **kwargs): + params = {} + + if "start" in kwargs and kwargs["start"]: + params["start"] = int(kwargs["start"]) + if "end" in kwargs and kwargs["end"]: + params["end"] = int(kwargs["end"]) + if "period" in kwargs and kwargs["period"]: + params["period"] = kwargs["period"] + if "area" in kwargs and kwargs["area"]: + params["area"] = kwargs["area"] + if "asn" in kwargs and kwargs["asn"]: + params["asn"] = kwargs["asn"] + if "job" in kwargs and kwargs["job"]: + params["job"] = kwargs["job"] + if "jobs" in kwargs and kwargs["jobs"]: + params["jobs"] = ",".join(kwargs["jobs"]) + if "record" in kwargs and kwargs["record"]: + params["record"] = kwargs["record"] + if "result" in kwargs and kwargs["result"]: + params["result"] = kwargs["result"] + if "agg" in kwargs and kwargs["agg"]: + params["agg"] = kwargs["agg"] + if "geo" in kwargs and kwargs["geo"]: + params["geo"] = kwargs["geo"] + if "zone_id" in kwargs and kwargs["zone_id"]: + params["zone_id"] = kwargs["zone_id"] + if "customer_id" in kwargs and kwargs["customer_id"]: + params["customer_id"] = int(kwargs["customer_id"]) + + return params + + def _make_query_url(self, path, **kwargs): + params = self._build_query_params(**kwargs) + if params: + return "%s?%s" % (path, urlencode(params)) + return path + + def get_decisions(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_graph_region( + self, callback=None, errback=None, **kwargs + ): + path = self._make_query_url("decisions/graph/region", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_graph_time(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions/graph/time", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_area(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions/area", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_asn(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions/asn", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_results_time( + self, callback=None, errback=None, **kwargs + ): + path = self._make_query_url("decisions/results/time", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_results_area( + self, callback=None, errback=None, **kwargs + ): + path = self._make_query_url("decisions/results/area", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_filters_time(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions/filters/time", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decision_customer( + self, customer_id, callback=None, errback=None, **kwargs + ): + path = self._make_query_url( + "decision/customer/%s" % customer_id, **kwargs + ) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decision_customer_undetermined( + self, customer_id, callback=None, errback=None, **kwargs + ): + path = self._make_query_url( + "decision/customer/%s/undetermined" % customer_id, **kwargs + ) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decision_record( + self, + customer_id, + domain, + rec_type, + callback=None, + errback=None, + **kwargs + ): + path = self._make_query_url( + "decision/customer/%s/record/%s/%s" + % (customer_id, domain, rec_type), + **kwargs + ) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decision_record_undetermined( + self, + customer_id, + domain, + rec_type, + callback=None, + errback=None, + **kwargs + ): + path = self._make_query_url( + "decision/customer/%s/record/%s/%s/undetermined" + % (customer_id, domain, rec_type), + **kwargs + ) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decision_total( + self, customer_id, callback=None, errback=None, **kwargs + ): + path = self._make_query_url( + "decision/customer/%s/total" % customer_id, **kwargs + ) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_records(self, callback=None, errback=None, **kwargs): + path = self._make_query_url("decisions/records", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) + + def get_decisions_results_record( + self, callback=None, errback=None, **kwargs + ): + path = self._make_query_url("decisions/results/record", **kwargs) + return self._make_request( + "GET", + "%s/%s" % (self.ROOT, path), + callback=callback, + errback=errback, + ) diff --git a/tests/unit/test_pulsar_decisions.py b/tests/unit/test_pulsar_decisions.py new file mode 100644 index 0000000..7381c99 --- /dev/null +++ b/tests/unit/test_pulsar_decisions.py @@ -0,0 +1,206 @@ +# +# Copyright (c) 2014, 2025 NSONE, Inc. +# +# License under The MIT License (MIT). See LICENSE in project root. +# + +import ns1.rest.pulsar_decisions +import pytest + +try: # Python 3.3 + + import unittest.mock as mock +except ImportError: + import mock + + +@pytest.fixture +def pulsar_decisions_config(config): + config.loadFromDict( + { + "endpoint": "api.nsone.net", + "default_key": "test1", + "keys": { + "test1": { + "key": "key-1", + "desc": "test key number 1", + } + }, + } + ) + + return config + + +@pytest.mark.parametrize( + "op, args, method, url, kwargs", + [ + ( + "get_decisions", + None, + "GET", + "pulsar/query/decisions", + {"callback": None, "errback": None}, + ), + ( + "get_decisions", + [{"start": 1234567890, "end": 1234567900}], + "GET", + "pulsar/query/decisions?start=1234567890&end=1234567900", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_graph_region", + None, + "GET", + "pulsar/query/decisions/graph/region", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_graph_time", + None, + "GET", + "pulsar/query/decisions/graph/time", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_area", + None, + "GET", + "pulsar/query/decisions/area", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_asn", + None, + "GET", + "pulsar/query/decisions/asn", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_results_time", + None, + "GET", + "pulsar/query/decisions/results/time", + {"callback": None, "errback": None}, + ), + ( + "get_decisions_results_area", + None, + "GET", + "pulsar/query/decisions/results/area", + {"callback": None, "errback": None}, + ), + ( + "get_filters_time", + None, + "GET", + "pulsar/query/decisions/filters/time", + {"callback": None, "errback": None}, + ), + ( + "get_decision_customer", + ["12345"], + "GET", + "pulsar/query/decision/customer/12345", + {"callback": None, "errback": None}, + ), + ( + "get_decision_customer_undetermined", + ["12345"], + "GET", + "pulsar/query/decision/customer/12345/undetermined", + {"callback": None, "errback": None}, + ), + ( + "get_decision_record", + ["12345", "example.com", "A"], + "GET", + "pulsar/query/decision/customer/12345/record/example.com/A", + {"callback": None, "errback": None}, + ), + ( + "get_decision_record_undetermined", + ["12345", "example.com", "A"], + "GET", + "pulsar/query/decision/customer/12345/record/example.com/A/undetermined", + {"callback": None, "errback": None}, + ), + ( + "get_decision_total", + ["12345"], + "GET", + "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( + 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) + + +def test_rest_pulsar_decisions_build_query_params(pulsar_decisions_config): + """Test _build_query_params helper method.""" + m = ns1.rest.pulsar_decisions.Decisions(pulsar_decisions_config) + + params = m._build_query_params( + start=1234567890, + end=1234567900, + period="1h", + area="US", + asn="15169", + job="test_job", + jobs=["job1", "job2"], + record="example.com", + result="192.0.2.1", + agg="sum", + geo="country", + zone_id="zone123", + customer_id=12345, + ) + + assert params["start"] == 1234567890 + assert params["end"] == 1234567900 + assert params["period"] == "1h" + assert params["area"] == "US" + assert params["asn"] == "15169" + assert params["job"] == "test_job" + assert params["jobs"] == "job1,job2" + assert params["record"] == "example.com" + assert params["result"] == "192.0.2.1" + assert params["agg"] == "sum" + assert params["geo"] == "country" + assert params["zone_id"] == "zone123" + assert params["customer_id"] == 12345