Skip to content

Commit

Permalink
Merge branch 'master' into fix/catalog-attribute-access
Browse files Browse the repository at this point in the history
  • Loading branch information
haleemur authored Nov 29, 2022
2 parents 7fee88f + f6c3f8d commit f553404
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 5 deletions.
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ jobs:
name: 'Unit Tests'
command: |
source ~/.virtualenvs/tap-marketo/bin/activate
python -m unittest
pip install nose coverage
nosetests --with-coverage --cover-erase --cover-package=tap_marketo --cover-html-dir=htmlcov tests
coverage html
- store_test_results:
path: test_output/report.xml
- store_artifacts:
path: htmlcov
# - add_ssh_keys
# - run:
# name: 'Integration Tests'
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 2.4.4
* Implement Request TimeOut [#78](https://github.com/singer-io/tap-marketo/pull/78)

## 2.4.3
* Remove CR characters as CSV chunks are being written [#73](https://github.com/singer-io/tap-marketo/pull/73)

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from setuptools import setup

setup(name='tap-marketo',
version='2.4.3',
version='2.4.4',
description='Singer.io tap for extracting data from the Marketo API',
author='Stitch',
url='http://singer.io',
Expand Down
20 changes: 17 additions & 3 deletions tap_marketo/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
RATE_LIMIT_CALLS = 100
RATE_LIMIT_SECONDS = 20

# timeout request after 300 seconds
REQUEST_TIMEOUT = 300

DEFAULT_USER_AGENT = "Singer.io/tap-marketo"
DOMAIN_RE = r"([\d]{3}-[\w]{3}-[\d]{3})"

Expand Down Expand Up @@ -80,7 +83,8 @@ def __init__(self, endpoint, client_id, client_secret,
max_daily_calls=MAX_DAILY_CALLS,
user_agent=DEFAULT_USER_AGENT,
job_timeout=JOB_TIMEOUT,
poll_interval=POLL_INTERVAL, **kwargs):
poll_interval=POLL_INTERVAL,
request_timeout=REQUEST_TIMEOUT, **kwargs):

self.domain = extract_domain(endpoint)
self.client_id = client_id
Expand All @@ -90,6 +94,12 @@ def __init__(self, endpoint, client_id, client_secret,
self.job_timeout = job_timeout
self.poll_interval = poll_interval

# if request_timeout is other than 0,"0" or "" then use request_timeout
if request_timeout and float(request_timeout):
self.request_timeout = float(request_timeout)
else: # If value is 0,"0" or "" then set default to 300 seconds.
self.request_timeout = REQUEST_TIMEOUT

self.token_expires = None
self.access_token = None
self.calls_today = 0
Expand Down Expand Up @@ -124,6 +134,8 @@ def get_bulk_endpoint(self, stream_name, action, export_id=None):
endpoint += "{}.json".format(action)
return endpoint

# backoff for Timeout error is already included in "requests.exceptions.RequestException"
# as it is a parent class of "Timeout" error
@singer.utils.backoff((requests.exceptions.RequestException), singer.utils.exception_is_4xx)
def refresh_token(self):
# http://developers.marketo.com/rest-api/authentication/#creating_an_access_token
Expand All @@ -136,7 +148,7 @@ def refresh_token(self):

try:
url = self.get_url("identity/oauth/token")
resp = requests.get(url, params=params)
resp = requests.get(url, params=params, timeout=self.request_timeout)
resp_time = pendulum.utcnow()
except requests.exceptions.ConnectionError as e:
raise ApiException("Connection error while refreshing token at {}.".format(url)) from e
Expand All @@ -158,6 +170,8 @@ def refresh_token(self):
self.token_expires = resp_time.add(seconds=data["expires_in"] - 15)
singer.log_info("Token valid until %s", self.token_expires)

# backoff for Timeout error is already included in "requests.exceptions.RequestException"
# as it is the parent class of "Timeout" error
@singer.utils.ratelimit(RATE_LIMIT_CALLS, RATE_LIMIT_SECONDS)
@singer.utils.backoff((requests.exceptions.RequestException), singer.utils.exception_is_4xx)
def _request(self, method, url, endpoint_name=None, stream=False, **kwargs):
Expand All @@ -168,7 +182,7 @@ def _request(self, method, url, endpoint_name=None, stream=False, **kwargs):
req = requests.Request(method, url, headers=headers, **kwargs).prepare()
singer.log_info("%s: %s", method, req.url)
with singer.metrics.http_request_timer(endpoint_name):
resp = self._session.send(req, stream=stream)
resp = self._session.send(req, stream=stream, timeout=self.request_timeout)

resp.raise_for_status()
return resp
Expand Down
280 changes: 280 additions & 0 deletions tests/test_request_timeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import unittest
import requests
import pendulum
from unittest import mock

from tap_marketo.client import Client

# Mock response object
def get_mock_http_response(*args, **kwargs):
contents = '{"access_token": "test", "expires_in":100}'
response = requests.Response()
response.status_code = 200
response._content = contents.encode()
return response

# Mock request object
class MockRequest:
def __init__(self):
self.url = "test"
mock_request_object = MockRequest()

@mock.patch('requests.Session.send')
@mock.patch("requests.Request.prepare")
@mock.patch("requests.get", side_effect = get_mock_http_response)
class TestRequestTimeoutValue(unittest.TestCase):

def test_no_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is not provided in config then default value is used
"""
config = { # No request_timeout in config
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test"
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 300.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=300.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=300.0)

def test_integer_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config(integer value) then it should be use
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": 100 # integer timeout in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 100.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=100.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=100.0)

def test_float_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config(float value) then it should be use
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": 100.5 # float timeout in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 100.5)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=100.5)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=100.5)

def test_string_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config(string value) then it should be use
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": '100' # string format timeout in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 100.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=100.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=100.0)

def test_empty_string_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config with empty string then default value is used
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": '' # empty string in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 300.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=300.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=300.0)

def test_zero_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config with zero value then default value is used
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": 0.0 # zero value in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 300.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=300.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=300.0)

def test_zero_string_request_timeout_in_config(self, mocked_get, mocked_prepare, mocked_send):
"""
Verify that if request_timeout is provided in config with zero in string format then default value is used
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test",
"request_timeout": '0.0' # zero value in config
}
mocked_prepare.return_value = mock_request_object

# Initialize Client object which set value for self.request_timeout
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)
# Verify request_timeout is set with expected value
self.assertEqual(client.request_timeout, 300.0)

# Call _request method which call session.send with timeout
client._request("test", "test")
# Verify session.send is called with expected timeout
mocked_send.assert_called_with(mock_request_object, stream=False, timeout=300.0)

# Call refresh_token method which call requests.get with timeout
client.refresh_token()
# Verify requests.get is called with expected timeout
mocked_get.assert_called_with('https://123-ABC-789.mktorest.com/identity/oauth/token',
params={'grant_type': 'client_credentials', 'client_id': 'test', 'client_secret': 'test'},
timeout=300.0)


@mock.patch("time.sleep")
class TestRequestTimeoutBackoff(unittest.TestCase):

@mock.patch("requests.get", side_effect = requests.exceptions.Timeout)
def test_request_timeout_backoff_in_refresh_token(self, mocked_request, mocked_sleep):
"""
Verify refresh_token function is backoff for 5 times on Timeout exceeption
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test"
}
# Initialize Client object
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)

try:
client.refresh_token()
except requests.exceptions.Timeout:
pass
# Verify that requests.get is called 5 times
self.assertEqual(mocked_request.call_count, 5)

@mock.patch('requests.Session.send', side_effect = requests.exceptions.Timeout)
def test_request_timeout_backoff_in__request_function(self, mocked_send, mocked_sleep):
"""
Verify _request function is backoff for 5 times on Timeout exceeption
"""
config = {
"endpoint": "123-ABC-789",
"client_id": "test",
"client_secret": "test"
}
# Initialize Client object
client = Client(**config)
client.token_expires = pendulum.utcnow().add(days=1)

try:
client._request("test", "test")
except requests.exceptions.Timeout:
pass
# Verify that session.send is called 5 times
self.assertEqual(mocked_send.call_count, 5)

0 comments on commit f553404

Please sign in to comment.