Skip to content

Commit

Permalink
Add Synthetics support (#433)
Browse files Browse the repository at this point in the history
* Add synthetics resource

* Add integration test

* Fix delete path

* Make the tests pass

* Use set up and tear down methods to clean up

* Add browser test in tests

* Debug pause

* Fix conflict in api_client re: error raised

* Synthetics was missing in the helpers / tests

* [Review feedback] Create separate classes for Synthetics API paths

* Remove synthetics in helpers too
  • Loading branch information
David Bouchare authored Nov 8, 2019
1 parent d3e8be0 commit 3c190f0
Show file tree
Hide file tree
Showing 5 changed files with 491 additions and 6 deletions.
3 changes: 2 additions & 1 deletion datadog/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@
from datadog.api.service_checks import ServiceCheck
from datadog.api.tags import Tag
from datadog.api.users import User
from datadog.api.service_level_objectives import ServiceLevelObjective
from datadog.api.service_level_objectives import ServiceLevelObjective
from datadog.api.synthetics import Synthetics
12 changes: 7 additions & 5 deletions datadog/api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,13 @@ def submit(cls, method, path, api_version=None, body=None, attach_host_name=Fals
except ValueError:
raise ValueError('Invalid JSON response: {0}'.format(content))

if response_obj and 'errors' in response_obj:
# suppress ApiError when specified and just return the response
if not (suppress_response_errors_on_codes and
result.status_code in suppress_response_errors_on_codes):
raise ApiError(response_obj)
# response_obj can be a bool and not a dict
if isinstance(response_obj, dict):
if response_obj and 'errors' in response_obj:
# suppress ApiError when specified and just return the response
if not (suppress_response_errors_on_codes and
result.status_code in suppress_response_errors_on_codes):
raise ApiError(response_obj)
else:
response_obj = None

Expand Down
112 changes: 112 additions & 0 deletions datadog/api/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,115 @@ def _trigger_action(cls, method, name, id=None, **body):
# Do not add body to GET requests, it causes 400 Bad request responses on EU site
body = None
return APIClient.submit(method, path, api_version, body)


class UpdatableAPISyntheticsSubResource(object):
"""
Update Synthetics sub resource
"""

@classmethod
def update_synthetics_items(cls, id, params=None, **body):
"""
Update API sub-resource objects of a resource
:param id: resource id to update sub-resource objects from
:type id: id
:param params: request parameters
:type params: dictionary
:param body: updated sub-resource objects attributes
:type body: dictionary
:returns: Dictionary representing the API's JSON response
"""
if params is None:
params = {}

path = '{resource_name}/tests/{resource_id}/{sub_resource_name}'.format(
resource_name=cls._resource_name,
resource_id=id,
sub_resource_name=cls._sub_resource_name
)
api_version = getattr(cls, '_api_version', None)

return APIClient.submit('PUT', path, api_version, body, **params)


class UpdatableAPISyntheticsResource(object):
"""
Update Synthetics resource
"""

@classmethod
def update_synthetics(cls, id, params=None, **body):
"""
Update an API resource object
:param params: updated resource object source
:type params: dictionary
:param body: updated resource object attributes
:type body: dictionary
:returns: Dictionary representing the API's JSON response
"""
if params is None:
params = {}

path = '{resource_name}/tests/{resource_id}'.format(
resource_name=cls._resource_name,
resource_id=id
)
api_version = getattr(cls, '_api_version', None)

return APIClient.submit('PUT', path, api_version, body, **params)


class ActionAPISyntheticsResource(object):
"""
Actionable Synthetics API Resource
"""
@classmethod
def _trigger_synthetics_class_action(cls, method, name, id=None, params=None, **body):
"""
Trigger an action
:param method: HTTP method to use to contact API endpoint
:type method: HTTP method string
:param name: action name
:type name: string
:param id: trigger the action for the specified resource object
:type id: id
:param params: action parameters
:type params: dictionary
:param body: action body
:type body: dictionary
:returns: Dictionary representing the API's JSON response
"""
if params is None:
params = {}

api_version = getattr(cls, '_api_version', None)

if id is None:
path = '{resource_name}/{action_name}'.format(
resource_name=cls._resource_name,
action_name=name
)
else:
path = '{resource_name}/{action_name}/{resource_id}'.format(
resource_name=cls._resource_name,
resource_id=id,
action_name=name
)
if method == "GET":
# Do not add body to GET requests, it causes 400 Bad request responses on EU site
body = None
return APIClient.submit(method, path, api_version, body, **params)
222 changes: 222 additions & 0 deletions datadog/api/synthetics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
from datadog.api.exceptions import ApiError
from datadog.api.resources import (
CreateableAPIResource,
GetableAPIResource,
ActionAPIResource,
UpdatableAPISyntheticsResource,
UpdatableAPISyntheticsSubResource,
ActionAPISyntheticsResource,
)


class Synthetics(
ActionAPIResource,
ActionAPISyntheticsResource,
CreateableAPIResource,
GetableAPIResource,
UpdatableAPISyntheticsResource,
UpdatableAPISyntheticsSubResource,
):
"""
A wrapper around Sythetics HTTP API.
"""

_resource_name = "synthetics"
_sub_resource_name = "status"

@classmethod
def get_test(cls, id, **params):
"""
Get test's details.
:param id: public id of the test to retrieve
:type id: string
:returns: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests/<public_test_id>

name = "tests"

return super(Synthetics, cls)._trigger_synthetics_class_action(
"GET", id=id, name=name, params=params
)

@classmethod
def get_all_tests(cls, **params):
"""
Get all tests' details.
:returns: Dictionary representing the API's JSON response
"""

for p in ["locations", "tags"]:
if p in params and isinstance(params[p], list):
params[p] = ",".join(params[p])

# API path = "synthetics/tests"

return super(Synthetics, cls).get(id="tests", params=params)

@classmethod
def get_devices(cls, **params):
"""
Get a list of devices for browser checks
:returns: Dictionary representing the API's JSON response
"""

# API path = "synthetics/browser/devices"

name = "browser/devices"

return super(Synthetics, cls)._trigger_synthetics_class_action(
"GET", name=name, params=params
)

@classmethod
def get_locations(cls, **params):
"""
Get a list of all available locations
:return: Dictionary representing the API's JSON response
"""

name = "locations"

# API path = "synthetics/locations

return super(Synthetics, cls)._trigger_synthetics_class_action(
"GET", name=name, params=params
)

@classmethod
def get_results(cls, id, **params):
"""
Get the most recent results for a test
:param id: public id of the test to retrieve results for
:type id: id
:return: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests/<public_test_id>/results

path = "tests/{}/results".format(id)

return super(Synthetics, cls)._trigger_synthetics_class_action("GET", path, params=params)

@classmethod
def get_result(cls, id, result_id, **params):
"""
Get a specific result for a given test.
:param id: public ID of the test to retrieve the most recent result for
:type id: id
:param result_id: result ID of the test to retrieve the most recent result for
:type result_id: id
:returns: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests/results/<result_id>

path = "tests/{}/results/{}".format(id, result_id)

return super(Synthetics, cls)._trigger_synthetics_class_action("GET", path, params=params)

@classmethod
def create_test(cls, **params):
"""
Create a test
:param name: A unique name for the test
:type name: string
:param type: The type of test. Valid values are api and browser
:type type: string
:param subtype: required for SSL test - For a SSL API test, specify ssl as the value.
:Otherwise, you should omit this argument.
:type subtype: string
:param request: The request associated to your API and SSL test
:type request: dict
:param options: List of options to customize the test
:type options: dict
:param message: A description of the test
:type message: string
:param assertions: required for API and SSL test - The assertions associated with the test
:type assertions: list of dict
:param locations: A list of the locations to send the tests from
:type locations: list
:param tags: A list of tags used to filter the test
:type tags: list
:return: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests"

return super(Synthetics, cls).create(id="tests", **params)

@classmethod
def edit_test(cls, id, **params):
"""
Edit a test
:param id: Public id of the test to edit
:type id: string
:return: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests/<public_test_id>"

return super(Synthetics, cls).update_synthetics(id=id, **params)

@classmethod
def start_or_pause_test(cls, id, **body):
"""
Pause a given test
:param id: public id of the test to pause
:type id: string
:param new_status: mew status for the test
:type id: string
:returns: Dictionary representing the API's JSON response
"""

# API path = "synthetics/tests/<public_test_id>/status"

return super(Synthetics, cls).update_synthetics_items(id=id, **body)

@classmethod
def delete_test(cls, **body):
"""
Delete a test
:param ids: list of public IDs to delete corresponding tests
:type ids: list of strings
:return: Dictionary representing the API's JSON response
"""

if not isinstance(body["public_ids"], list):
raise ApiError("Parameter 'ids' must be a list")

# API path = "synthetics/tests/delete

return super(Synthetics, cls)._trigger_action(
"POST", name="synthetics", id="tests/delete", **body
)
Loading

0 comments on commit 3c190f0

Please sign in to comment.