Skip to content

Commit

Permalink
Added timeouts to API requests.
Browse files Browse the repository at this point in the history
  • Loading branch information
RidleyLarsen committed May 22, 2020
1 parent ebec50a commit a538611
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 36 deletions.
59 changes: 37 additions & 22 deletions CmixAPIClient/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
}

DEFAULT_API_TIMEOUT = 16

# - it seems like this class would work better as a singleton - and
# maybe the method above (default_cmix_api) could create the singleton,
Expand All @@ -43,6 +44,8 @@
# - default_cmix_api could also check_auth_headers before returning the
# singleton - if it's not authenticated it could try authenticating or
# creating a new instance THEN authenticating


class CmixAPI(object):
# valid survey statuses
SURVEY_STATUS_DESIGN = 'DESIGN'
Expand All @@ -52,7 +55,9 @@ class CmixAPI(object):
# valid extra survey url params
SURVEY_PARAMS_STATUS_AFTER = 'statusAfter'

def __init__(self, username=None, password=None, client_id=None, client_secret=None, test=False, *args, **kwargs):
def __init__(
self, username=None, password=None, client_id=None, client_secret=None, test=False, timeout=None, *args, **kwargs
):
if None in [username, password, client_id, client_secret]:
raise CmixError("All authentication data is required.")
self.username = username
Expand All @@ -62,6 +67,7 @@ def __init__(self, username=None, password=None, client_id=None, client_secret=N
self.url_type = 'BASE_URL'
if test is True:
self.url_type = 'TEST_URL'
self.timeout = timeout if timeout is not None else DEFAULT_API_TIMEOUT

def check_auth_headers(self):
if self._authentication_headers is None:
Expand All @@ -78,7 +84,12 @@ def authenticate(self, *args, **kwargs):

auth_url = '{}/access-token'.format(CMIX_SERVICES['auth'][self.url_type])
try:
auth_response = requests.post(auth_url, json=auth_payload, headers={"Content-Type": "application/json"})
auth_response = requests.post(
auth_url,
json=auth_payload,
headers={"Content-Type": "application/json"},
timeout=self.timeout
)
if auth_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code: {} and error {}'.format(
Expand Down Expand Up @@ -118,7 +129,7 @@ def fetch_banner_filter(self, survey_id, question_a, question_b, response_id):
'responseId': response_id
}]
}
response = requests.post(url, headers=self._authentication_headers, json=payload)
response = requests.post(url, headers=self._authentication_headers, json=payload, timeout=self.timeout)
return response.json()

def fetch_raw_results(self, survey_id, payload):
Expand All @@ -136,13 +147,13 @@ def fetch_raw_results(self, survey_id, payload):
log.debug('Requesting raw results for CMIX survey {}'.format(survey_id))
base_url = CMIX_SERVICES['reporting'][self.url_type]
url = '{}/surveys/{}/response-counts'.format(base_url, survey_id)
response = requests.post(url, headers=self._authentication_headers, json=payload)
response = requests.post(url, headers=self._authentication_headers, json=payload, timeout=self.timeout)
return response.json()

def api_get(self, endpoint, error=''):
self.check_auth_headers()
url = '{}/{}'.format(CMIX_SERVICES['survey'][self.url_type], endpoint)
response = requests.get(url, headers=self._authentication_headers)
response = requests.get(url, headers=self._authentication_headers, timeout=self.timeout)
if response.status_code != 200:
if '' == error:
error = 'CMIX returned a non-200 response code'
Expand All @@ -158,7 +169,7 @@ def api_get(self, endpoint, error=''):
def api_delete(self, endpoint, error=''):
self.check_auth_headers()
url = '{}/{}'.format(CMIX_SERVICES['survey'][self.url_type], endpoint)
response = requests.delete(url, headers=self._authentication_headers)
response = requests.delete(url, headers=self._authentication_headers, timeout=self.timeout)
if response.status_code != 200:
if '' == error:
error = 'CMIX returned a non-200 response code'
Expand Down Expand Up @@ -186,7 +197,7 @@ def get_surveys(self, status, *args, **kwargs):
extra_params = kwargs.get('extra_params')
if extra_params is not None:
surveys_url = self.add_extra_url_params(surveys_url, extra_params)
surveys_response = requests.get(surveys_url, headers=self._authentication_headers)
surveys_response = requests.get(surveys_url, headers=self._authentication_headers, timeout=self.timeout)
return surveys_response.json()

def add_extra_url_params(self, url, params):
Expand All @@ -198,7 +209,7 @@ def add_extra_url_params(self, url, params):
def get_survey_data_layouts(self, survey_id):
self.check_auth_headers()
data_layouts_url = '{}/surveys/{}/data-layouts'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
data_layouts_response = requests.get(data_layouts_url, headers=self._authentication_headers)
data_layouts_response = requests.get(data_layouts_url, headers=self._authentication_headers, timeout=self.timeout)
if data_layouts_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting data_layouts: {} and error {}'.format(
Expand All @@ -211,19 +222,19 @@ def get_survey_data_layouts(self, survey_id):
def get_survey_definition(self, survey_id):
self.check_auth_headers()
definition_url = '{}/surveys/{}/definition'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
definition_response = requests.get(definition_url, headers=self._authentication_headers)
definition_response = requests.get(definition_url, headers=self._authentication_headers, timeout=self.timeout)
return definition_response.json()

def get_survey_xml(self, survey_id):
self.check_auth_headers()
xml_url = '{}/surveys/{}'.format(CMIX_SERVICES['file'][self.url_type], survey_id)
xml_response = requests.get(xml_url, headers=self._authentication_headers)
xml_response = requests.get(xml_url, headers=self._authentication_headers, timeout=self.timeout)
return xml_response.content

def get_survey_test_url(self, survey_id):
self.check_auth_headers()
survey_url = '{}/surveys/{}'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
survey_response = requests.get(survey_url, headers=self._authentication_headers)
survey_response = requests.get(survey_url, headers=self._authentication_headers, timeout=self.timeout)
test_token = survey_response.json().get('testToken', None)
if test_token is None:
raise CmixError('Survey endpoint for CMIX ID {} did not return a test token.'.format(survey_id))
Expand All @@ -242,13 +253,13 @@ def get_survey_respondents(self, survey_id, respondent_type, live):
"LIVE" if live else "TEST",
respondent_type,
)
respondents_response = requests.get(respondents_url, headers=self._authentication_headers)
respondents_response = requests.get(respondents_url, headers=self._authentication_headers, timeout=self.timeout)
return respondents_response.json()

def get_survey_locales(self, survey_id):
self.check_auth_headers()
locales_url = '{}/surveys/{}/locales'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
locales_response = requests.get(locales_url, headers=self._authentication_headers)
locales_response = requests.get(locales_url, headers=self._authentication_headers, timeout=self.timeout)
if locales_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting locales: {} and error {}'.format(
Expand All @@ -261,7 +272,7 @@ def get_survey_locales(self, survey_id):
def get_survey_status(self, survey_id):
self.check_auth_headers()
status_url = '{}/surveys/{}'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
status_response = requests.get(status_url, headers=self._authentication_headers)
status_response = requests.get(status_url, headers=self._authentication_headers, timeout=self.timeout)
status = status_response.json().get('status', None)
if status is None:
raise CmixError('Get Survey Status returned without a status. Response: {}'.format(status_response.json()))
Expand All @@ -270,7 +281,7 @@ def get_survey_status(self, survey_id):
def get_survey_sections(self, survey_id):
self.check_auth_headers()
sections_url = '{}/surveys/{}/sections'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
sections_response = requests.get(sections_url, headers=self._authentication_headers)
sections_response = requests.get(sections_url, headers=self._authentication_headers, timeout=self.timeout)
if sections_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting sections: {} and error {}'.format(
Expand All @@ -283,7 +294,7 @@ def get_survey_sections(self, survey_id):
def get_survey_sources(self, survey_id):
self.check_auth_headers()
sources_url = '{}/surveys/{}/sources'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
sources_response = requests.get(sources_url, headers=self._authentication_headers)
sources_response = requests.get(sources_url, headers=self._authentication_headers, timeout=self.timeout)
if sources_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting sources: {} and error {}'.format(
Expand All @@ -299,7 +310,11 @@ def get_survey_completes(self, survey_id):
def get_survey_termination_codes(self, survey_id):
self.check_auth_headers()
termination_codes_url = '{}/surveys/{}/termination-codes'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
termination_codes_response = requests.get(termination_codes_url, headers=self._authentication_headers)
termination_codes_response = requests.get(
termination_codes_url,
headers=self._authentication_headers,
timeout=self.timeout
)
if termination_codes_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting termination_codes: {} and error {}'.format(
Expand All @@ -322,7 +337,7 @@ def create_export_archive(self, survey_id, export_type):
"terminates": False
}

archive_response = requests.post(archive_url, json=payload, headers=headers)
archive_response = requests.post(archive_url, json=payload, headers=headers, timeout=self.timeout)
if archive_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code: {} and error {}'.format(
Expand Down Expand Up @@ -366,7 +381,7 @@ def get_archive_status(self, survey_id, archive_id, layout_id):
layout_id,
archive_id # The archive ID on CMIX.
)
archive_response = requests.get(archive_url, headers=self._authentication_headers)
archive_response = requests.get(archive_url, headers=self._authentication_headers, timeout=self.timeout)
if archive_response.status_code > 299:
raise CmixError(
'CMIX returned an invalid response code getting archive status: HTTP {} and error {}'.format(
Expand All @@ -390,7 +405,7 @@ def update_project(self, project_id, status=None):
raise CmixError("No update data was provided for CMIX Project {}".format(project_id))

url = '{}/projects/{}'.format(CMIX_SERVICES['survey'][self.url_type], project_id)
response = requests.patch(url, json=payload_json, headers=self._authentication_headers)
response = requests.patch(url, json=payload_json, headers=self._authentication_headers, timeout=self.timeout)
if response.status_code > 299:
raise CmixError(
'CMIX returned an invalid response code during project update: HTTP {} and error {}'.format(
Expand All @@ -408,7 +423,7 @@ def create_survey(self, xml_string):

url = '{}/surveys/data'.format(CMIX_SERVICES['file'][self.url_type])
payload = {"data": xml_string}
response = requests.post(url, payload, headers=self._authentication_headers)
response = requests.post(url, payload, headers=self._authentication_headers, timeout=self.timeout)
if response.status_code > 299:
raise CmixError(
'Error while creating survey. CMIX responded with status' +
Expand All @@ -425,7 +440,7 @@ def create_survey(self, xml_string):
def get_survey_simulations(self, survey_id):
self.check_auth_headers()
simulations_url = '{}/surveys/{}/simulations'.format(CMIX_SERVICES['survey'][self.url_type], survey_id)
simulations_response = requests.get(simulations_url, headers=self._authentication_headers)
simulations_response = requests.get(simulations_url, headers=self._authentication_headers, timeout=self.timeout)
if simulations_response.status_code != 200:
raise CmixError(
'CMIX returned a non-200 response code while getting simulations: {} and error {}'.format(
Expand Down
29 changes: 17 additions & 12 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def default_cmix_api():
username="test_username",
password="test_password",
client_id="test_client_id",
client_secret="test_client_secret"
client_secret="test_client_secret",
timeout=5
)


Expand All @@ -38,7 +39,7 @@ def helper_get(self, function_name, endpoint):

base_url = CMIX_SERVICES['survey']['BASE_URL']
project_url = '{}/{}'.format(base_url, endpoint)
mock_request.get.assert_any_call(project_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(project_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand Down Expand Up @@ -157,7 +158,7 @@ def test_get_survey_data_layouts(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/data-layouts'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand All @@ -182,7 +183,7 @@ def test_get_survey_status(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

def test_get_survey_status_error_handled(self):
self.cmix_api._authentication_headers = {'Authentication': 'Bearer test'}
Expand Down Expand Up @@ -210,7 +211,7 @@ def test_get_survey_sections(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/sections'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand All @@ -235,7 +236,7 @@ def test_get_survey_locales(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/locales'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand All @@ -260,7 +261,7 @@ def test_get_survey_sources(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/sources'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand Down Expand Up @@ -326,7 +327,7 @@ def test_get_survey_termination_codes(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/termination-codes'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand Down Expand Up @@ -355,12 +356,16 @@ def test_get_surveys(self):
]
self.cmix_api.get_surveys('LIVE')
expected_url = '{}/surveys?status={}'.format(CMIX_SERVICES['survey']['BASE_URL'], 'LIVE')
mock_request.get.assert_any_call(expected_url, headers=mock.ANY)
mock_request.get.assert_any_call(expected_url, headers=mock.ANY, timeout=5)

expected_url_with_params = '{}/surveys?status={}&hello=world&test=params'.format(
CMIX_SERVICES['survey']['BASE_URL'], 'LIVE')
self.cmix_api.get_surveys('LIVE', extra_params=["hello=world", "test=params"])
mock_request.get.assert_any_call(expected_url_with_params, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(
expected_url_with_params,
headers=self.cmix_api._authentication_headers,
timeout=5
)

def test_fetch_banner_filter(self):
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand Down Expand Up @@ -394,7 +399,7 @@ def test_fetch_banner_filter(self):
'responseId': response_id
}]
}
mock_request.post.assert_any_call(expected_url, json=expected_payload, headers=mock.ANY)
mock_request.post.assert_any_call(expected_url, json=expected_payload, headers=mock.ANY, timeout=5)

def test_get_archive_status(self):
survey_id = 1337
Expand Down Expand Up @@ -440,7 +445,7 @@ def test_get_survey_simulations(self):

base_url = CMIX_SERVICES['survey']['BASE_URL']
surveys_url = '{}/surveys/{}/simulations'.format(base_url, self.survey_id)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers)
mock_request.get.assert_any_call(surveys_url, headers=self.cmix_api._authentication_headers, timeout=5)

# error case (survey not found)
with mock.patch('CmixAPIClient.api.requests') as mock_request:
Expand Down
Loading

0 comments on commit a538611

Please sign in to comment.