Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 7ff5149

Browse files
authored
Merge pull request #274 from cloudant/257-default-timeout
257 timeout argument
2 parents 31ef287 + 7164920 commit 7ff5149

File tree

7 files changed

+119
-18
lines changed

7 files changed

+119
-18
lines changed

CHANGES.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
2.4.0 (Unreleased)
22
==================
3-
3+
- [NEW] Added ``timeout`` option to the client constructor for setting a timeout on a HTTP connection or a response.
44

55
2.3.1 (2016-11-30)
66
==================

docs/getting_started.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Note: If you require retrying requests after an HTTP 429 error, the
2727
``Replay429Adapter`` can be added when constructing a ``Cloudant``
2828
client and configured with an initial back off and retry count.
2929

30+
Note: Currently, the connect and read timeout will wait forever for
31+
a HTTP connection or a response on all requests. A timeout can be
32+
set using the ``timeout`` argument when constructing a client.
33+
3034
Connecting with a client
3135
^^^^^^^^^^^^^^^^^^^^^^^^
3236

@@ -46,6 +50,10 @@ Connecting with a client
4650
# client = Cloudant(USERNAME, PASSWORD, account=ACCOUNT_NAME,
4751
# adapter=Replay429Adapter(retries=10, initialBackoff=0.01))
4852
53+
# or with a connect and read timeout of 5 minutes
54+
# client = Cloudant(USERNAME, PASSWORD, account=ACCOUNT_NAME,
55+
# timeout=300)
56+
4957
# Perform client tasks...
5058
session = client.session()
5159
print 'Username: {0}'.format(session['userCtx']['name'])

src/cloudant/_common_util.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2015, 2016 IBM. All rights reserved.
2+
# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -292,19 +292,21 @@ class InfiniteSession(Session):
292292
information in the event of expired session authentication.
293293
"""
294294

295-
def __init__(self, username, password, server_url):
295+
def __init__(self, username, password, server_url, **kwargs):
296296
super(InfiniteSession, self).__init__()
297297
self._username = username
298298
self._password = password
299299
self._server_url = server_url
300+
self._timeout = kwargs.get('timeout', None)
300301

301302
def request(self, method, url, **kwargs):
302303
"""
303304
Overrides ``requests.Session.request`` to perform a POST to the
304305
_session endpoint to renew Session cookie authentication settings and
305306
then retry the original request, if necessary.
306307
"""
307-
resp = super(InfiniteSession, self).request(method, url, **kwargs)
308+
resp = super(InfiniteSession, self).request(
309+
method, url, timeout=self._timeout, **kwargs)
308310
path = url_parse(url).path.lower()
309311
post_to_session = method.upper() == 'POST' and path == '/_session'
310312
is_expired = any((
@@ -319,10 +321,30 @@ def request(self, method, url, **kwargs):
319321
data={'name': self._username, 'password': self._password},
320322
headers={'Content-Type': 'application/x-www-form-urlencoded'}
321323
)
322-
resp = super(InfiniteSession, self).request(method, url, **kwargs)
324+
resp = super(InfiniteSession, self).request(
325+
method, url, timeout=self._timeout, **kwargs)
323326

324327
return resp
325328

329+
class ClientSession(Session):
330+
"""
331+
This class extends Session and provides a default timeout.
332+
"""
333+
334+
def __init__(self, username, password, server_url, **kwargs):
335+
super(ClientSession, self).__init__()
336+
self._username = username
337+
self._password = password
338+
self._server_url = server_url
339+
self._timeout = kwargs.get('timeout', None)
340+
341+
def request(self, method, url, **kwargs):
342+
"""
343+
Overrides ``requests.Session.request`` to set the timeout.
344+
"""
345+
resp = super(ClientSession, self).request(
346+
method, url, timeout=self._timeout, **kwargs)
347+
return resp
326348

327349
class CloudFoundryService(object):
328350
""" Manages Cloud Foundry service configuration. """

src/cloudant/client.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (C) 2015, 2016 IBM Corp. All rights reserved.
2+
# Copyright (C) 2015, 2016, 2017 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -19,7 +19,6 @@
1919
import base64
2020
import json
2121
import posixpath
22-
import requests
2322

2423
from ._2to3 import bytes_, unicode_
2524
from .database import CloudantDatabase, CouchDatabase
@@ -28,8 +27,8 @@
2827
from ._common_util import (
2928
USER_AGENT,
3029
append_response_error_content,
31-
InfiniteSession
32-
)
30+
InfiniteSession,
31+
ClientSession)
3332

3433

3534
class CouchDB(dict):
@@ -56,6 +55,15 @@ class CouchDB(dict):
5655
:param bool auto_renew: Keyword argument, if set to True performs
5756
automatic renewal of expired session authentication settings.
5857
Default is False.
58+
:param float timeout: Timeout in seconds (use float for milliseconds, for
59+
example 0.1 for 100 ms) for connecting to and reading bytes from the
60+
server. If a single value is provided it will be applied to both the
61+
connect and read timeouts. To specify different values for each timeout
62+
use a tuple. For example, a 10 second connect timeout and a 1 minute
63+
read timeout would be (10, 60). This follows the same behaviour as the
64+
`Requests library timeout argument
65+
<http://docs.python-requests.org/en/master/user/quickstart/#timeouts>`_.
66+
but will apply to every request made using this client.
5967
"""
6068
_DATABASE_CLASS = CouchDatabase
6169

@@ -69,6 +77,7 @@ def __init__(self, user, auth_token, admin_party=False, **kwargs):
6977
self.admin_party = admin_party
7078
self.encoder = kwargs.get('encoder') or json.JSONEncoder
7179
self.adapter = kwargs.get('adapter')
80+
self._timeout = kwargs.get('timeout', None)
7281
self.r_session = None
7382
self._auto_renew = kwargs.get('auto_renew', False)
7483
connect_to_couch = kwargs.get('connect', False)
@@ -87,10 +96,16 @@ def connect(self):
8796
self.r_session = InfiniteSession(
8897
self._user,
8998
self._auth_token,
90-
self.server_url
99+
self.server_url,
100+
timeout=self._timeout
91101
)
92102
else:
93-
self.r_session = requests.Session()
103+
self.r_session = ClientSession(
104+
self._user,
105+
self._auth_token,
106+
self.server_url,
107+
timeout=self._timeout
108+
)
94109
# If a Transport Adapter was supplied add it to the session
95110
if self.adapter is not None:
96111
self.r_session.mount(self.server_url, self.adapter)

tests/unit/client_tests.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2015, 2016 IBM. All rights reserved.
2+
# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -28,6 +28,8 @@
2828
import os
2929
import datetime
3030

31+
from requests import ConnectTimeout
32+
3133
from cloudant import cloudant, couchdb, couchdb_admin_party
3234
from cloudant.client import Cloudant, CouchDB
3335
from cloudant.error import CloudantArgumentError, CloudantClientException
@@ -536,6 +538,15 @@ def test_connect_headers(self):
536538
finally:
537539
self.client.disconnect()
538540

541+
def test_connect_timeout(self):
542+
"""
543+
Test that a connect timeout occurs when instantiating
544+
a client object with a timeout of 10 ms.
545+
"""
546+
with self.assertRaises(ConnectTimeout) as cm:
547+
self.set_up_client(auto_connect=True, timeout=.01)
548+
self.assertTrue(str(cm.exception).find('timed out.'))
549+
539550
def test_db_updates_infinite_feed_call(self):
540551
"""
541552
Test that infinite_db_updates() method call constructs and returns an

tests/unit/replicator_tests.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2015 IBM. All rights reserved.
2+
# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -25,9 +25,10 @@
2525
import unittest
2626
import uuid
2727
import time
28-
import requests
2928

3029
from flaky import flaky
30+
import requests
31+
from requests import ConnectionError
3132

3233
from cloudant.replicator import Replicator
3334
from cloudant.document import Document
@@ -210,6 +211,46 @@ def test_create_replication(self):
210211
])
211212
)
212213

214+
def test_timeout_in_create_replication(self):
215+
"""
216+
Test that a read timeout exception is thrown when creating a
217+
replicator with a timeout value of 500 ms.
218+
"""
219+
# Setup client with a timeout
220+
self.set_up_client(auto_connect=True, timeout=.5)
221+
self.db = self.client[self.test_target_dbname]
222+
self.target_db = self.client[self.test_dbname]
223+
# Construct a replicator with the updated client
224+
self.replicator = Replicator(self.client)
225+
226+
repl_id = 'test-repl-{}'.format(unicode_(uuid.uuid4()))
227+
repl_doc = self.replicator.create_replication(
228+
self.db,
229+
self.target_db,
230+
repl_id
231+
)
232+
self.replication_ids.append(repl_id)
233+
# Test that the replication document was created
234+
expected_keys = ['_id', '_rev', 'source', 'target', 'user_ctx']
235+
# If Admin Party mode then user_ctx will not be in the key list
236+
if self.client.admin_party:
237+
expected_keys.pop()
238+
self.assertTrue(all(x in list(repl_doc.keys()) for x in expected_keys))
239+
self.assertEqual(repl_doc['_id'], repl_id)
240+
self.assertTrue(repl_doc['_rev'].startswith('1-'))
241+
# Now that we know that the replication document was created,
242+
# check that the replication timed out.
243+
repl_doc = Document(self.replicator.database, repl_id)
244+
repl_doc.fetch()
245+
if repl_doc.get('_replication_state') not in ('completed', 'error'):
246+
# assert that a connection error is thrown because the read timed out
247+
with self.assertRaises(ConnectionError) as cm:
248+
changes = self.replicator.database.changes(
249+
feed='continuous')
250+
for change in changes:
251+
continue
252+
self.assertTrue(str(cm.exception).endswith('Read timed out.'))
253+
213254
def test_create_replication_without_a_source(self):
214255
"""
215256
Test that the replication document is not created and fails as expected
@@ -357,5 +398,6 @@ def test_follow_replication(self):
357398
self.assertEqual(repl_states[-1], 'completed')
358399
self.assertNotIn('error', repl_states)
359400

401+
360402
if __name__ == '__main__':
361403
unittest.main()

tests/unit/unit_t_db_base.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
# Copyright (c) 2015 IBM. All rights reserved.
2+
# Copyright (c) 2015, 2016, 2017 IBM Corp. All rights reserved.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -131,7 +131,8 @@ def setUp(self):
131131
"""
132132
self.set_up_client()
133133

134-
def set_up_client(self, auto_connect=False, auto_renew=False, encoder=None):
134+
def set_up_client(self, auto_connect=False, auto_renew=False, encoder=None,
135+
timeout=(30,300)):
135136
if os.environ.get('RUN_CLOUDANT_TESTS') is None:
136137
admin_party = False
137138
if (os.environ.get('ADMIN_PARTY') and
@@ -147,7 +148,8 @@ def set_up_client(self, auto_connect=False, auto_renew=False, encoder=None):
147148
url=self.url,
148149
connect=auto_connect,
149150
auto_renew=auto_renew,
150-
encoder=encoder
151+
encoder=encoder,
152+
timeout=timeout
151153
)
152154
else:
153155
self.account = os.environ.get('CLOUDANT_ACCOUNT')
@@ -163,7 +165,8 @@ def set_up_client(self, auto_connect=False, auto_renew=False, encoder=None):
163165
x_cloudant_user=self.account,
164166
connect=auto_connect,
165167
auto_renew=auto_renew,
166-
encoder=encoder
168+
encoder=encoder,
169+
timeout=timeout
167170
)
168171

169172

0 commit comments

Comments
 (0)