From 6a9bd5edf3dc8927b179011f4ca91c0e8144c3be Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 12:04:34 -0500 Subject: [PATCH 1/6] added dangling cname check and health check to client --- src/ultra_rest_client/ultra_rest_client.py | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/ultra_rest_client/ultra_rest_client.py b/src/ultra_rest_client/ultra_rest_client.py index 63234bf..c02ec6f 100644 --- a/src/ultra_rest_client/ultra_rest_client.py +++ b/src/ultra_rest_client/ultra_rest_client.py @@ -954,6 +954,57 @@ def export_zone(self, zone_name): self.clear_task(task_id) return result + # Health Checks + def create_health_check(self, zone_name): + """Initiates a health check for a zone. + + Arguments: + zone_name -- The name of the zone to perform a health check on. + + Returns: + A dictionary containing the location header from the response, which includes + the timestamp identifier needed to retrieve the health check results. + """ + return self.rest_api_connection.post(f"/v1/zones/{zone_name}/healthchecks", json.dumps({})) + + def get_health_check(self, zone_name, timestamp): + """Retrieves the results of a previously initiated health check. + + Arguments: + zone_name -- The name of the zone that was checked. + timestamp -- The timestamp identifier returned from create_health_check. + + Returns: + A dictionary containing detailed health check results, including version, + state, and a list of check results with nested validation details. + """ + return self.rest_api_connection.get(f"/v1/zones/{zone_name}/healthchecks/{timestamp}") + + def create_dangling_cname_check(self, zone_name): + """Initiates a dangling CNAME (DCNAME) check for a zone. + + Arguments: + zone_name -- The name of the zone to perform a dangling CNAME check on. + + Returns: + A dictionary containing the response from the API. Note that while a location + header is returned, it is not used for retrieving results as only one set of + DCNAME results is kept per zone. + """ + return self.rest_api_connection.post(f"/v1/zones/{zone_name}/healthchecks/dangling", json.dumps({})) + + def get_dangling_cname_check(self, zone_name): + """Retrieves the results of a dangling CNAME check. + + Arguments: + zone_name -- The name of the zone to retrieve dangling CNAME check results for. + + Returns: + A dictionary containing detailed dangling CNAME check results, including version, + zone, status, resultInfo, and a list of dangling records. + """ + return self.rest_api_connection.get(f"/v1/zones/{zone_name}/healthchecks/dangling") + def build_params(q, args): params = args.copy() if q: From 69c7516eedf9cc0919aaf1f52cccb5b0aff9c7fc Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 16:12:55 -0500 Subject: [PATCH 2/6] added nxdomain report and method to fetch report results --- src/ultra_rest_client/ultra_rest_client.py | 89 ++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/ultra_rest_client/ultra_rest_client.py b/src/ultra_rest_client/ultra_rest_client.py index c02ec6f..c2096a0 100644 --- a/src/ultra_rest_client/ultra_rest_client.py +++ b/src/ultra_rest_client/ultra_rest_client.py @@ -1005,6 +1005,95 @@ def get_dangling_cname_check(self, zone_name): """ return self.rest_api_connection.get(f"/v1/zones/{zone_name}/healthchecks/dangling") + def create_advanced_nxdomain_report(self, start_date, end_date, zone_names, limit=100): + """Initiates the creation of an Advanced NX Domain report. + + This method sends a POST request to generate a report that identifies NX domain queries + (DNS queries for non-existent domains) for the specified zones within the given date range. + + Arguments: + start_date -- Start date of the report in 'yyyy-MM-dd' format. Must not be more than 30 days prior to end_date. + end_date -- End date of the report in 'yyyy-MM-dd' format. + zone_names -- A single zone name (string) or a list of zone names to include in the report. + limit -- Optional. Number of records to return (default: 100, maximum: 100000). + + Returns: + A dictionary containing the response from the API, including the requestId which can be used + with get_report_results() to retrieve the report data once processing is complete. + + Example response: + { + "requestId": "HQV_NXD-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx" + } + """ + # Ensure zone_names is a list + if isinstance(zone_names, str): + zone_names = [zone_names] + + # Construct the payload + payload = { + "hostQueryVolume": { + "startDate": start_date, + "endDate": end_date, + "zoneNames": zone_names + }, + "sortFields": { + "nxdomainCount": "DESC" + } + } + + # Construct the URL with query parameters + endpoint = f"/v1/reports/dns_resolution/query_volume/host?advance=true&reportType=ADVANCED_NXDOMAINS&limit={limit}" + + # Send the request + return self.rest_api_connection.post(endpoint, json.dumps(payload)) + + def get_report_results(self, report_id): + """Retrieves the results of any report using the report ID. + + This method sends a GET request to fetch the results of a previously initiated report. + The report may still be processing, in which case the response will indicate this status. + + Arguments: + report_id -- The report ID returned from a report creation method (e.g., create_advanced_nxdomain_report). + + Returns: + A dictionary or list containing the report results if the report is complete, or an error + message indicating the report is still processing. + + Example processing response: + { + "errors": [ + { + "message": "Report is in process. Please try again later.", + "code": "410005", + "param": "", + "traceId": "BA756737632ACCAB" + } + ], + "message": "The information for this request is not available." + } + + Example completed report response (for Advanced NX Domain report): + [ + { + "zoneName": "example.me.", + "hostName": "test.example.me.", + "accountName": "myaccount", + "startDate": "2025-02-01", + "endDate": "2025-03-03", + "rspTotal": 302, + "nxdomainCount": 302 + }, + ... + ] + """ + # Construct the URL with the report ID + endpoint = f"/v1/requests/{report_id}" + + # Send the request + return self.rest_api_connection.get(endpoint) + def build_params(q, args): params = args.copy() if q: From 29ca30084610ed73dec54d1487f9d76ba90a7298 Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 16:39:50 -0500 Subject: [PATCH 3/6] added snapshot endpoints --- src/ultra_rest_client/ultra_rest_client.py | 84 ++++++++++++---------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/ultra_rest_client/ultra_rest_client.py b/src/ultra_rest_client/ultra_rest_client.py index c2096a0..3b19525 100644 --- a/src/ultra_rest_client/ultra_rest_client.py +++ b/src/ultra_rest_client/ultra_rest_client.py @@ -1030,7 +1030,6 @@ def create_advanced_nxdomain_report(self, start_date, end_date, zone_names, limi if isinstance(zone_names, str): zone_names = [zone_names] - # Construct the payload payload = { "hostQueryVolume": { "startDate": start_date, @@ -1042,10 +1041,7 @@ def create_advanced_nxdomain_report(self, start_date, end_date, zone_names, limi } } - # Construct the URL with query parameters endpoint = f"/v1/reports/dns_resolution/query_volume/host?advance=true&reportType=ADVANCED_NXDOMAINS&limit={limit}" - - # Send the request return self.rest_api_connection.post(endpoint, json.dumps(payload)) def get_report_results(self, report_id): @@ -1060,39 +1056,55 @@ def get_report_results(self, report_id): Returns: A dictionary or list containing the report results if the report is complete, or an error message indicating the report is still processing. - - Example processing response: - { - "errors": [ - { - "message": "Report is in process. Please try again later.", - "code": "410005", - "param": "", - "traceId": "BA756737632ACCAB" - } - ], - "message": "The information for this request is not available." - } - - Example completed report response (for Advanced NX Domain report): - [ - { - "zoneName": "example.me.", - "hostName": "test.example.me.", - "accountName": "myaccount", - "startDate": "2025-02-01", - "endDate": "2025-03-03", - "rspTotal": 302, - "nxdomainCount": 302 - }, - ... - ] """ - # Construct the URL with the report ID - endpoint = f"/v1/requests/{report_id}" - - # Send the request - return self.rest_api_connection.get(endpoint) + return self.rest_api_connection.get(f"/v1/requests/{report_id}") + + # Zone Snapshots + def create_snapshot(self, zone_name): + """Creates a snapshot of a zone. + + This method sends a POST request to create a snapshot of the specified zone, + capturing its current state. A zone can only have one snapshot at a time. + + Arguments: + zone_name -- The name of the zone to create a snapshot for. + + Returns: + A dictionary containing the response from the API, including a task_id + that identifies the snapshot creation task. + """ + return self.rest_api_connection.post(f"/v1/zones/{zone_name}/snapshot", json.dumps({})) + + def get_snapshot(self, zone_name): + """Retrieves the current snapshot for a zone. + + This method sends a GET request to fetch the current snapshot for the specified zone, + returning all details of the snapshot in a structured JSON object. + + Arguments: + zone_name -- The name of the zone to retrieve the snapshot for. + + Returns: + A dictionary containing detailed snapshot information, including the zone name + and a list of resource record sets (rrSets) with their properties. + """ + return self.rest_api_connection.get(f"/v1/zones/{zone_name}/snapshot") + + def restore_snapshot(self, zone_name): + """Restores a zone to its snapshot. + + This method sends a POST request to restore the specified zone to the state + captured in its snapshot. This operation should be used with caution as it + will revert all changes made since the snapshot was created. + + Arguments: + zone_name -- The name of the zone to restore from its snapshot. + + Returns: + A dictionary containing the response from the API, including a task_id + that identifies the restore operation task. + """ + return self.rest_api_connection.post(f"/v1/zones/{zone_name}/restore", json.dumps({})) def build_params(q, args): params = args.copy() From 7d2d1446a31688ee35c089811fd1f2172a789042 Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 19:02:19 -0500 Subject: [PATCH 4/6] added a couple more reporting endpoints --- src/ultra_rest_client/ultra_rest_client.py | 73 ++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/ultra_rest_client/ultra_rest_client.py b/src/ultra_rest_client/ultra_rest_client.py index 3b19525..8dc38f4 100644 --- a/src/ultra_rest_client/ultra_rest_client.py +++ b/src/ultra_rest_client/ultra_rest_client.py @@ -1059,6 +1059,79 @@ def get_report_results(self, report_id): """ return self.rest_api_connection.get(f"/v1/requests/{report_id}") + def create_projected_query_volume_report(self, accountName, sortFields=None): + """Initiates the creation of a Projected Query Volume Report. + + This method sends a POST request to generate a report that provides projected query volume + data for the specified account. + + Arguments: + accountName -- The name of the account for which the report is being run. + sortFields -- Optional. A dictionary defining sortable columns and their sort directions. + Valid sortable columns include: 'month', 'currentDay', 'rspMtd', 'rspMtd7dAvg', + 'rspMtd30dAvg', 'ttlAvg', and 'rspDaily' (each with values 'ASC' or 'DESC'). + If not provided, a default sort will be applied (rspMtd: DESC). + + Returns: + A dictionary containing the response from the API, including the requestId which can be used + with get_report_results() to retrieve the report data once processing is complete. + """ + payload = { + "projectedQueryVolume": { + "accountName": accountName + } + } + + if sortFields: + payload["sortFields"] = sortFields + else: + payload["sortFields"] = { + "rspMtd": "DESC" + } + + return self.rest_api_connection.post("/v1/reports/dns_resolution/projected_query_volume", json.dumps(payload)) + + def create_zone_query_volume_report(self, startDate, endDate, zoneQueryVolume=None, sortFields=None, offset=0, limit=1000): + """Initiates the creation of a Zone Query Volume Report. + + This method sends a POST request to generate a report that aggregates query volumes for multiple zones + over a specified period (up to 13 months). + + Arguments: + startDate -- Start date of the report in 'YYYY-MM-DD' format. + endDate -- End date of the report in 'YYYY-MM-DD' format. + zoneQueryVolume -- Optional. A dictionary with additional fields (e.g., 'zoneName', 'accountName', 'ultra2'). + sortFields -- Optional. A dictionary mapping sortable column names to sort directions ('ASC' or 'DESC'). + Valid sortable columns include: 'zoneName', 'startDate', 'endDate', 'rspTotal', etc. + If not provided, default sort criteria will be applied. + offset -- Optional. Pagination offset (default: 0). + limit -- Optional. Pagination limit (default: 1000). + + Returns: + A dictionary containing the response from the API, including the requestId which can be used + with get_report_results() to retrieve the report data once processing is complete. + """ + if zoneQueryVolume is None: + zoneQueryVolume = {} + + zoneQueryVolume["startDate"] = startDate + zoneQueryVolume["endDate"] = endDate + + payload = { + "zoneQueryVolume": zoneQueryVolume + } + + if sortFields: + payload["sortFields"] = sortFields + else: + payload["sortFields"] = { + "zoneName": "ASC", + "endDate": "ASC" + } + + endpoint = f"/v1/reports/dns_resolution/query_volume/zone?offset={offset}&limit={limit}" + return self.rest_api_connection.post(endpoint, json.dumps(payload)) + # Zone Snapshots def create_snapshot(self, zone_name): """Creates a snapshot of a zone. From 40d187fed2917a83b570f6966753c12b4c5fa9ce Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 20:18:14 -0500 Subject: [PATCH 5/6] added TaskHandler and ReportHandler utilities --- .plugin-version | 2 +- README.md | 4 + examples/report_handler_example.py | 39 ++++++ sample.py => examples/sample.py | 2 +- examples/task_handler_example.py | 47 +++++++ src/ultra_rest_client/__init__.py | 4 +- src/ultra_rest_client/utils/README.md | 80 ++++++++++++ src/ultra_rest_client/utils/__init__.py | 1 + src/ultra_rest_client/utils/reports.py | 124 ++++++++++++++++++ src/ultra_rest_client/utils/tasks.py | 163 ++++++++++++++++++++++++ 10 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 examples/report_handler_example.py rename sample.py => examples/sample.py (99%) create mode 100644 examples/task_handler_example.py create mode 100644 src/ultra_rest_client/utils/README.md create mode 100644 src/ultra_rest_client/utils/__init__.py create mode 100644 src/ultra_rest_client/utils/reports.py create mode 100644 src/ultra_rest_client/utils/tasks.py diff --git a/.plugin-version b/.plugin-version index fbc97b0..b1d18bc 100644 --- a/.plugin-version +++ b/.plugin-version @@ -1 +1 @@ -v2.2.5 +v2.3.0 diff --git a/README.md b/README.md index 12eac1d..e6a7bdb 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,10 @@ export USERNAME='your_username' export PASSWORD='your_password' ``` +### Background Tasks + +Utilities for handling long running tasks that process in the background, such as reports or exports, are available and documented [here](./src/ultra_rest_client/utils/README.md) + ## Functionality The sample code does not attempt to implement a client for all available UltraDNS REST API functionality. It provides access to basic functionality. Adding additional functionality should be relatively straightforward, and any contributions from the UltraDNS community would be greatly appreciated. See [sample.py](sample.py) for an example of how to use this library in your own code. diff --git a/examples/report_handler_example.py b/examples/report_handler_example.py new file mode 100644 index 0000000..af14dc3 --- /dev/null +++ b/examples/report_handler_example.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +""" +Example script demonstrating the use of the ReportHandler utility class. + +This script shows how to use the ReportHandler to handle report generation API responses +from the UltraDNS API. +""" + +from ultra_rest_client import RestApiClient, ReportHandler + +# Initialize the client +client = RestApiClient('your_username', 'your_password') + +# Example 1: Creating an advanced NXDOMAIN report +# -------------------------------------------- +# The endpoint returns a requestId that needs to be polled until the report is complete +print("Example 1: Creating an advanced NXDOMAIN report") +response = client.create_advanced_nxdomain_report( + startDate='2023-01-01', + endDate='2023-01-31', + zoneNames=['example.com'] +) +print("Initial response:", response) + +# The ReportHandler will automatically poll until the report is complete +report_result = ReportHandler(response, client) +print("Final result after polling:") +print(report_result) # This will print the final result + +# Example 2: Creating a projected query volume report +# -------------------------------------------- +print("\nExample 2: Creating a projected query volume report") +response = client.create_projected_query_volume_report('your_account_name') +print("Initial response:", response) + +# You can set a maximum number of retries to avoid indefinite polling +report_result = ReportHandler(response, client, max_retries=30) +print("Final result after polling (or max retries):") +print(report_result) \ No newline at end of file diff --git a/sample.py b/examples/sample.py similarity index 99% rename from sample.py rename to examples/sample.py index f6e3406..e58370b 100644 --- a/sample.py +++ b/examples/sample.py @@ -86,7 +86,7 @@ print('get first 5 primary zones with j: %s' % client.get_zones(offset=0, limit=5, sort="NAME", reverse=False, q={"name":"j", "zone_type":"PRIMARY"})) #creating a zone with upload -result = client.create_primary_zone_by_upload(account_name, 'sample.client.me.', './zone.txt') +result = client.create_primary_zone_by_upload(account_name, 'sample.client.me.', '../zone.txt') print('create zone via upload: %s' % result) # check the task status diff --git a/examples/task_handler_example.py b/examples/task_handler_example.py new file mode 100644 index 0000000..9c928d1 --- /dev/null +++ b/examples/task_handler_example.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +""" +Example script demonstrating the use of the TaskHandler utility class. + +This script shows how to use the TaskHandler to handle asynchronous task responses +from the UltraDNS API. +""" + +from ultra_rest_client import RestApiClient, TaskHandler + +# Initialize the client +client = RestApiClient('your_username', 'your_password') + +# Example 1: Handling a response with a task_id +# -------------------------------------------- +# Some API endpoints return a task_id indicating a background task +print("Example 1: Creating a snapshot (returns a task_id)") +response = client.create_snapshot('example.com') +print("Initial response:", response) + +# The TaskHandler will automatically poll the task endpoint until completion +task_result = TaskHandler(response, client) +print("Final result after polling:") +print(task_result) # This will print the final result + +# Example 2: Handling a response with a location +# -------------------------------------------- +# Some API endpoints return a location that needs to be polled until completion +print("\nExample 2: Creating a health check (returns a location)") +response = client.create_health_check('example.com') +print("Initial response:", response) + +# The TaskHandler will automatically poll the location until completion +location_result = TaskHandler(response, client, poll_interval=2) # Poll every 2 seconds +print("Final result after polling:") +print(location_result) + +# Example 3: Handling a regular response (no task_id or location) +# -------------------------------------------- +# If no task_id or location is present, the TaskHandler will return the original response +print("\nExample 3: Getting zone information (returns a regular response)") +response = client.get_zone('example.com') +print("Initial response:", response) + +regular_result = TaskHandler(response, client) +print("Result from TaskHandler:") +print(regular_result) # This will print the original response \ No newline at end of file diff --git a/src/ultra_rest_client/__init__.py b/src/ultra_rest_client/__init__.py index 475613c..aa52323 100644 --- a/src/ultra_rest_client/__init__.py +++ b/src/ultra_rest_client/__init__.py @@ -1,2 +1,4 @@ from .ultra_rest_client import RestApiClient -from .connection import RestApiConnection \ No newline at end of file +from .connection import RestApiConnection +from .utils.tasks import TaskHandler +from .utils.reports import ReportHandler \ No newline at end of file diff --git a/src/ultra_rest_client/utils/README.md b/src/ultra_rest_client/utils/README.md new file mode 100644 index 0000000..349af62 --- /dev/null +++ b/src/ultra_rest_client/utils/README.md @@ -0,0 +1,80 @@ +# Ultra REST Client Utilities + +This directory contains utility classes for the Ultra REST Client. + +## TaskHandler + +The `TaskHandler` class is a utility for handling API responses that return a `task_id` or a `location`. It automatically polls the appropriate endpoint until the task is complete or an error occurs. + +### How it works + +1. When you create a `TaskHandler` instance with an API response, it inspects the response to determine if it contains a `task_id` or a `location`. +2. If a `task_id` is found, it polls the `/tasks/{task_id}` endpoint until the task is complete or an error occurs. +3. If a `location` is found, it polls the location URL until a final status is reached. +4. If neither a `task_id` nor a `location` is found, it returns the original response. + +### Usage + +```python +from ultra_rest_client import RestApiClient, TaskHandler + +# Create a client +client = RestApiClient('username', 'password') + +# Make an API call that returns a task_id +response = client.create_snapshot('example.com') + +# Create a TaskHandler to handle the response +task_result = TaskHandler(response, client) + +# The TaskHandler will poll until the task is complete +# The result is exactly what the API returns +print(task_result) +``` + +### Parameters + +- `response`: The API response to process. +- `client` (RestApiClient): The RestApiClient instance to use for API calls. +- `poll_interval` (int, optional): The interval in seconds between polling attempts. Defaults to 1. + +## ReportHandler + +The `ReportHandler` class is a utility for handling API responses from reporting API endpoints that return a `requestId`. It automatically polls the appropriate endpoint until the report is complete or an error occurs. + +### How it works + +1. When you create a `ReportHandler` instance with an API response, it inspects the response to determine if it contains a `requestId`. +2. If a `requestId` is found, it polls the appropriate endpoint until the report is complete or an error occurs. +3. If no `requestId` is found, it returns the original response. + +### Usage + +```python +from ultra_rest_client import RestApiClient, ReportHandler + +# Create a client +client = RestApiClient('username', 'password') + +# Make an API call that returns a requestId +response = client.create_advanced_nxdomain_report( + startDate='2023-01-01', + endDate='2023-01-31', + zoneNames=['example.com'] +) + +# Create a ReportHandler to handle the response +# You can specify a maximum number of retries to avoid indefinite polling +report_result = ReportHandler(response, client, max_retries=30) + +# The ReportHandler will poll until the report is complete +# The result is exactly what the API returns +print(report_result) +``` + +### Parameters + +- `response`: The API response to process. +- `client` (RestApiClient): The RestApiClient instance to use for API calls. +- `poll_interval` (int, optional): The interval in seconds between polling attempts. Defaults to 1. +- `max_retries` (int, optional): The maximum number of polling attempts. Defaults to None (unlimited). \ No newline at end of file diff --git a/src/ultra_rest_client/utils/__init__.py b/src/ultra_rest_client/utils/__init__.py new file mode 100644 index 0000000..91c5400 --- /dev/null +++ b/src/ultra_rest_client/utils/__init__.py @@ -0,0 +1 @@ +"""Utility modules for the Ultra REST Client.""" \ No newline at end of file diff --git a/src/ultra_rest_client/utils/reports.py b/src/ultra_rest_client/utils/reports.py new file mode 100644 index 0000000..59a781f --- /dev/null +++ b/src/ultra_rest_client/utils/reports.py @@ -0,0 +1,124 @@ +""" +Report handling utilities for the Ultra REST Client. + +This module provides utilities for handling report responses from the UltraDNS API. +""" +import time + + +class ReportHandler: + """ + A utility class for handling API responses from reporting API endpoints. + + This class inspects an API response and, if it contains a requestId, polls + the appropriate endpoint until the report is complete. + """ + + def __init__(self, response, client, poll_interval=1, max_retries=None): + """ + Initialize the ReportHandler with an API response. + + Args: + response: The API response to process. + client (RestApiClient): The RestApiClient instance to use for API calls. + poll_interval (int, optional): The interval in seconds between polling attempts. + Defaults to 1. + max_retries (int, optional): The maximum number of polling attempts. + If None, will poll indefinitely until the report is complete. + Defaults to None. + + Returns: + The final result of the report polling, or the original response + if no requestId is present. + """ + self.client = client + self.poll_interval = poll_interval + self.max_retries = max_retries + + # Process the response + self.result = self._process_response(response) + + def _process_response(self, response): + """ + Process the API response and determine the appropriate action. + + Args: + response: The API response to process. + + Returns: + The final result after processing. + """ + if isinstance(response, dict) and 'requestId' in response: + return self._handle_report(response['requestId']) + + return response + + def _handle_report(self, request_id): + """ + Handle a response containing a requestId. + + This method polls the report endpoint until the report is complete. + + Args: + request_id (str): The ID of the report to poll. + + Returns: + The final report result. + """ + retry_count = 0 + + while True: + if self.max_retries is not None and retry_count >= self.max_retries: + return { + 'error': 'Maximum retry limit reached', + 'requestId': request_id + } + + report_response = self.client.get_report_results(request_id) + + if isinstance(report_response, dict): + if 'errors' in report_response and isinstance(report_response['errors'], list): + for error in report_response['errors']: + if 'code' in error and str(error['code']) in ['410005', '410004']: + retry_count += 1 + time.sleep(self.poll_interval) + break + else: + return report_response + + continue + + elif 'errorCode' in report_response: + error_code = str(report_response['errorCode']) + if error_code in ['410005', '410004']: + retry_count += 1 + time.sleep(self.poll_interval) + continue + + return report_response + + def __repr__(self): + """Return a string representation of the result.""" + return repr(self.result) + + def __str__(self): + """Return a string representation of the result.""" + return str(self.result) + + def __getitem__(self, key): + """Allow dictionary-like access to the result.""" + return self.result[key] + + def __iter__(self): + """Allow iteration over the result.""" + return iter(self.result) + + def __len__(self): + """Return the length of the result.""" + return len(self.result) + + def get(self, key, default=None): + """Get a value from the result with a default if the key is not found.""" + if isinstance(self.result, dict): + return self.result.get(key, default) + return default \ No newline at end of file diff --git a/src/ultra_rest_client/utils/tasks.py b/src/ultra_rest_client/utils/tasks.py new file mode 100644 index 0000000..05d7fc9 --- /dev/null +++ b/src/ultra_rest_client/utils/tasks.py @@ -0,0 +1,163 @@ +""" +Task handling utilities for the Ultra REST Client. + +This module provides utilities for handling asynchronous tasks and location-based responses +from the UltraDNS API. +""" +import time + + +class TaskHandler: + """ + A utility class for handling API responses that return a task_id or location. + + This class inspects an API response and, based on its contents, either polls a task endpoint + or a location endpoint until a final result is reached. + + Example usage: + from ultra_rest_client import RestApiClient, TaskHandler + client = RestApiClient('username', 'password') + + # An endpoint that returns a task_id + response = client.create_snapshot('example.me') + task_data = TaskHandler(response, client) + print(task_data) # Prints the result data + + # An endpoint that returns a location + response = client.create_health_check('example.me') + location_data = TaskHandler(response, client) + print(location_data) # Prints the result data + """ + + def __init__(self, response, client, poll_interval=1): + """ + Initialize the TaskHandler with an API response. + + Args: + response: The API response to process. + client (RestApiClient): The RestApiClient instance to use for API calls. + poll_interval (int, optional): The interval in seconds between polling attempts. + Defaults to 1. + + Returns: + The final result of the task or location polling, or the original response + if neither a task_id nor a location is present. + """ + self.poll_interval = poll_interval + self.client = client + + # Process the response + self.result = self._process_response(response) + + def _process_response(self, response): + """ + Process the API response and determine the appropriate action. + + Args: + response: The API response to process. + + Returns: + The final result after processing. + """ + # Check if the response contains a task_id + if isinstance(response, dict) and 'task_id' in response: + return self._handle_task(response['task_id']) + + # Check if the response contains a location + if isinstance(response, dict) and 'location' in response: + return self._handle_location(response['location']) + + # If neither a task_id nor a location is present, return the original response + return response + + def _handle_task(self, task_id): + """ + Handle a response containing a task_id. + + This method polls the /tasks/{task_id} endpoint until the task is complete + or an error occurs. + + Args: + task_id (str): The ID of the task to poll. + + Returns: + The final result of the task. + """ + while True: + # Poll the task endpoint + task_response = self.client.get_task(task_id) + + # Check the task status + if task_response.get('code') in ['PENDING', 'IN_PROCESS']: + # Task is still processing, wait and try again + time.sleep(self.poll_interval) + continue + + if task_response.get('code') == 'ERROR': + # Task encountered an error, return the error response + return task_response + + if task_response.get('code') == 'COMPLETE': + # Task is complete + if task_response.get('hasData', False): + # If the task has data, fetch it from the resultUri + result_uri = task_response.get('resultUri') + if result_uri: + return self.client.rest_api_connection.get(result_uri) + + # If no data or no resultUri, return the task response + return task_response + + # If we reach here, the task has an unknown status, return the response + return task_response + + def _handle_location(self, location): + """ + Handle a response containing a location. + + This method polls the location URL until a final status is reached. + + Args: + location (str): The location URL to poll. + + Returns: + The final result from the location. + """ + while True: + location_response = self.client.rest_api_connection.get(location) + + # Check for completion status in either 'state' or 'status' field + state = location_response.get('state', '').upper() + status = location_response.get('status', '').upper() + + # If either field indicates completion, return the result + if state in ['COMPLETED', 'ERROR'] or status in ['COMPLETED', 'ERROR']: + return location_response + + time.sleep(self.poll_interval) + + def __repr__(self): + """Return a string representation of the result.""" + return repr(self.result) + + def __str__(self): + """Return a string representation of the result.""" + return str(self.result) + + def __getitem__(self, key): + """Allow dictionary-like access to the result.""" + return self.result[key] + + def __iter__(self): + """Allow iteration over the result.""" + return iter(self.result) + + def __len__(self): + """Return the length of the result.""" + return len(self.result) + + def get(self, key, default=None): + """Get a value from the result with a default if the key is not found.""" + if isinstance(self.result, dict): + return self.result.get(key, default) + return default \ No newline at end of file From 7bbb8184611897b36c1a713b8af65c7991ae1374 Mon Sep 17 00:00:00 2001 From: sbarbett Date: Tue, 4 Mar 2025 22:31:26 -0500 Subject: [PATCH 6/6] forgot to commit a couple files --- src/ultra_rest_client/utils/README.md | 2 -- src/ultra_rest_client/utils/tasks.py | 28 --------------------------- 2 files changed, 30 deletions(-) diff --git a/src/ultra_rest_client/utils/README.md b/src/ultra_rest_client/utils/README.md index 349af62..8747630 100644 --- a/src/ultra_rest_client/utils/README.md +++ b/src/ultra_rest_client/utils/README.md @@ -28,7 +28,6 @@ response = client.create_snapshot('example.com') task_result = TaskHandler(response, client) # The TaskHandler will poll until the task is complete -# The result is exactly what the API returns print(task_result) ``` @@ -68,7 +67,6 @@ response = client.create_advanced_nxdomain_report( report_result = ReportHandler(response, client, max_retries=30) # The ReportHandler will poll until the report is complete -# The result is exactly what the API returns print(report_result) ``` diff --git a/src/ultra_rest_client/utils/tasks.py b/src/ultra_rest_client/utils/tasks.py index 05d7fc9..51d940b 100644 --- a/src/ultra_rest_client/utils/tasks.py +++ b/src/ultra_rest_client/utils/tasks.py @@ -13,20 +13,6 @@ class TaskHandler: This class inspects an API response and, based on its contents, either polls a task endpoint or a location endpoint until a final result is reached. - - Example usage: - from ultra_rest_client import RestApiClient, TaskHandler - client = RestApiClient('username', 'password') - - # An endpoint that returns a task_id - response = client.create_snapshot('example.me') - task_data = TaskHandler(response, client) - print(task_data) # Prints the result data - - # An endpoint that returns a location - response = client.create_health_check('example.me') - location_data = TaskHandler(response, client) - print(location_data) # Prints the result data """ def __init__(self, response, client, poll_interval=1): @@ -46,7 +32,6 @@ def __init__(self, response, client, poll_interval=1): self.poll_interval = poll_interval self.client = client - # Process the response self.result = self._process_response(response) def _process_response(self, response): @@ -59,15 +44,12 @@ def _process_response(self, response): Returns: The final result after processing. """ - # Check if the response contains a task_id if isinstance(response, dict) and 'task_id' in response: return self._handle_task(response['task_id']) - # Check if the response contains a location if isinstance(response, dict) and 'location' in response: return self._handle_location(response['location']) - # If neither a task_id nor a location is present, return the original response return response def _handle_task(self, task_id): @@ -84,31 +66,23 @@ def _handle_task(self, task_id): The final result of the task. """ while True: - # Poll the task endpoint task_response = self.client.get_task(task_id) - # Check the task status if task_response.get('code') in ['PENDING', 'IN_PROCESS']: - # Task is still processing, wait and try again time.sleep(self.poll_interval) continue if task_response.get('code') == 'ERROR': - # Task encountered an error, return the error response return task_response if task_response.get('code') == 'COMPLETE': - # Task is complete if task_response.get('hasData', False): - # If the task has data, fetch it from the resultUri result_uri = task_response.get('resultUri') if result_uri: return self.client.rest_api_connection.get(result_uri) - # If no data or no resultUri, return the task response return task_response - # If we reach here, the task has an unknown status, return the response return task_response def _handle_location(self, location): @@ -126,11 +100,9 @@ def _handle_location(self, location): while True: location_response = self.client.rest_api_connection.get(location) - # Check for completion status in either 'state' or 'status' field state = location_response.get('state', '').upper() status = location_response.get('status', '').upper() - # If either field indicates completion, return the result if state in ['COMPLETED', 'ERROR'] or status in ['COMPLETED', 'ERROR']: return location_response