Skip to content

Commit 99c3465

Browse files
author
Shariq Torres
authored
Merge pull request #178 from Kilo59/timeouts
Allow user to set a request `timeout`
2 parents 26e9fea + a1f7e1d commit 99c3465

File tree

7 files changed

+290
-159
lines changed

7 files changed

+290
-159
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ setuptools = "*"
1515
twine = "*"
1616
pytest = ">=6.2.1"
1717
pytest-cov = ">=2.11.1"
18+
requests-mock = ">=1.9.3"
1819

1920
[scripts]
2021
# these commands can be invoked with `pipenv run <script_name>`

Pipfile.lock

Lines changed: 141 additions & 123 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lob/api_requestor.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import lob
77
from lob import error
88
from lob.version import VERSION
9+
from lob.constants import TIMEOUT_DEFAULT
910

1011

1112
def _is_file_like(obj):
@@ -39,7 +40,7 @@ def parse_response(self, resp):
3940
else: # pragma: no cover
4041
raise error.APIError(payload['error']['message'], resp.content, resp.status_code, resp)
4142

42-
def request(self, method, url, params=None):
43+
def request(self, method, url, params=None, timeout=TIMEOUT_DEFAULT):
4344
headers = {
4445
'User-Agent': 'Lob/v1 PythonBindings/%s' % VERSION
4546
}
@@ -55,13 +56,19 @@ def request(self, method, url, params=None):
5556
del params['headers']
5657

5758
if method == 'get':
58-
return self.parse_response(
59-
requests.get(lob.api_base + url, auth=(self.api_key, ''), params=params, headers=headers)
60-
)
59+
resp = requests.get(
60+
lob.api_base + url,
61+
auth=(self.api_key, ''),
62+
params=params,
63+
headers=headers,
64+
timeout=timeout
65+
)
66+
return self.parse_response(resp)
67+
6168
elif method == 'delete':
62-
return self.parse_response(
63-
requests.delete(lob.api_base + url, auth=(self.api_key, ''), headers=headers)
64-
)
69+
resp = requests.delete(lob.api_base + url, auth=(self.api_key, ''), headers=headers, timeout=timeout)
70+
return self.parse_response(resp)
71+
6572
elif method == 'post':
6673
query = {}
6774
data = {}
@@ -93,7 +100,5 @@ def request(self, method, url, params=None):
93100
data[k + '[' + k2 + ']'] = v2
94101
else:
95102
data[k] = v
96-
97-
return self.parse_response(
98-
requests.post(lob.api_base + url, auth=(self.api_key, ''), params=query, data=data, files=files, headers=headers)
99-
)
103+
resp = requests.post(lob.api_base + url, auth=(self.api_key, ''), params=query, data=data, files=files, headers=headers, timeout=timeout)
104+
return self.parse_response(resp)

lob/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
TIMEOUT_DEFAULT = 30

lob/error.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ class AuthenticationError(LobError):
2424

2525

2626
class InvalidRequestError(LobError):
27-
pass
27+
pass

lob/resource.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from lob import api_requestor
66
from lob.compat import string_type
7+
from lob.constants import TIMEOUT_DEFAULT
78

89

910
def lob_format(resp):
@@ -86,16 +87,16 @@ def __str__(self):
8687

8788
class APIResource(LobObject):
8889
@classmethod
89-
def retrieve(cls, id, **params):
90+
def retrieve(cls, id, timeout=TIMEOUT_DEFAULT, **params):
9091
requestor = api_requestor.APIRequestor()
91-
response = requestor.request('get', '%s/%s' % (cls.endpoint, id), params)
92+
response = requestor.request('get', '%s/%s' % (cls.endpoint, id), params, timeout=timeout)
9293
return lob_format(response)
9394

9495

9596
# API Operations
9697
class ListableAPIResource(APIResource):
9798
@classmethod
98-
def list(cls, **params):
99+
def list(cls, timeout=TIMEOUT_DEFAULT, **params):
99100
for key, value in params.copy().items():
100101
if isinstance(params[key], dict):
101102
for subKey in value:
@@ -105,31 +106,31 @@ def list(cls, **params):
105106
params[str(key) + '[]'] = params[key]
106107
del params[key]
107108
requestor = api_requestor.APIRequestor()
108-
response = requestor.request('get', cls.endpoint, params)
109+
response = requestor.request('get', cls.endpoint, params, timeout=timeout)
109110
return lob_format(response)
110111

111112

112113
class DeleteableAPIResource(APIResource):
113114
@classmethod
114-
def delete(cls, id):
115+
def delete(cls, id, timeout=TIMEOUT_DEFAULT):
115116
requestor = api_requestor.APIRequestor()
116-
response = requestor.request('delete', '%s/%s' % (cls.endpoint, id))
117+
response = requestor.request('delete', '%s/%s' % (cls.endpoint, id), timeout=timeout)
117118
return lob_format(response)
118119

119120

120121
class CreateableAPIResource(APIResource):
121122
@classmethod
122-
def create(cls, **params):
123+
def create(cls, timeout=TIMEOUT_DEFAULT, **params):
123124
requestor = api_requestor.APIRequestor()
124-
response = requestor.request('post', cls.endpoint, params)
125+
response = requestor.request('post', cls.endpoint, params, timeout=timeout)
125126
return lob_format(response)
126127

127128

128129
class VerifiableAPIResource(APIResource):
129130
@classmethod
130-
def verify(cls, id, **params):
131+
def verify(cls, id, timeout=TIMEOUT_DEFAULT, **params):
131132
requestor = api_requestor.APIRequestor()
132-
response = requestor.request('post', '%s/%s/verify' % (cls.endpoint, id), params)
133+
response = requestor.request('post', '%s/%s/verify' % (cls.endpoint, id), params, timeout=timeout)
133134
return lob_format(response)
134135

135136

@@ -150,23 +151,23 @@ class Card(ListableAPIResource, DeleteableAPIResource, CreateableAPIResource):
150151
endpoint = '/cards'
151152

152153
@classmethod
153-
def update(cls, card_id, **params):
154+
def update(cls, card_id, timeout=TIMEOUT_DEFAULT, **params):
154155
requestor = api_requestor.APIRequestor()
155-
response = requestor.request('post', '%s/%s' % (cls.endpoint, card_id), params)
156+
response = requestor.request('post', '%s/%s' % (cls.endpoint, card_id), params, timeout=timeout)
156157
return lob_format(response)
157158

158159

159160
class CardOrder(ListableAPIResource, CreateableAPIResource):
160161
endpoint = '/cards/%s/orders'
161162

162163
@classmethod
163-
def create(cls, card_id, **params):
164+
def create(cls, card_id, timeout=TIMEOUT_DEFAULT, **params):
164165
requestor = api_requestor.APIRequestor()
165-
response = requestor.request('post', cls.endpoint % card_id, params)
166+
response = requestor.request('post', cls.endpoint % card_id, params, timeout=timeout)
166167
return lob_format(response)
167168

168169
@classmethod
169-
def list(cls, card_id, **params):
170+
def list(cls, card_id, timeout=TIMEOUT_DEFAULT, **params):
170171
for key, value in params.copy().items():
171172
if isinstance(params[key], dict):
172173
for subKey in value:
@@ -176,67 +177,67 @@ def list(cls, card_id, **params):
176177
params[str(key) + '[]'] = params[key]
177178
del params[key]
178179
requestor = api_requestor.APIRequestor()
179-
response = requestor.request('get', cls.endpoint % card_id, params)
180+
response = requestor.request('get', cls.endpoint % card_id, params, timeout=timeout)
180181
return lob_format(response)
181182

182183

183184
class Check(ListableAPIResource, CreateableAPIResource, DeleteableAPIResource):
184185
endpoint = '/checks'
185186

186187
@classmethod
187-
def create(cls, **params):
188+
def create(cls, timeout=TIMEOUT_DEFAULT, **params):
188189
if isinstance(params, dict):
189190
if 'to_address' in params:
190191
params['to'] = params['to_address']
191192
params.pop('to_address')
192193
if 'from_address' in params:
193194
params['from'] = params['from_address']
194195
params.pop('from_address')
195-
return super(Check, cls).create(**params)
196+
return super(Check, cls).create(timeout=timeout, **params)
196197

197198

198199
class Letter(ListableAPIResource, CreateableAPIResource, DeleteableAPIResource):
199200
endpoint = '/letters'
200201

201202
@classmethod
202-
def create(cls, **params):
203+
def create(cls, timeout=TIMEOUT_DEFAULT, **params):
203204
if isinstance(params, dict):
204205
if 'from_address' in params:
205206
params['from'] = params['from_address']
206207
params.pop('from_address')
207208
if 'to_address' in params:
208209
params['to'] = params['to_address']
209210
params.pop('to_address')
210-
return super(Letter, cls).create(**params)
211+
return super(Letter, cls).create(timeout=timeout, **params)
211212

212213

213214
class Postcard(ListableAPIResource, CreateableAPIResource, DeleteableAPIResource):
214215
endpoint = '/postcards'
215216

216217
@classmethod
217-
def create(cls, **params):
218+
def create(cls, timeout=TIMEOUT_DEFAULT, **params):
218219
if isinstance(params, dict):
219220
if 'from_address' in params:
220221
params['from'] = params['from_address']
221222
params.pop('from_address')
222223
if 'to_address' in params:
223224
params['to'] = params['to_address']
224225
params.pop('to_address')
225-
return super(Postcard, cls).create(**params)
226+
return super(Postcard, cls).create(timeout=timeout, **params)
226227

227228
class SelfMailer(ListableAPIResource, CreateableAPIResource, DeleteableAPIResource):
228229
endpoint = '/self_mailers'
229230

230231
@classmethod
231-
def create(cls, **params):
232+
def create(cls, timeout=TIMEOUT_DEFAULT, **params):
232233
if isinstance(params, dict):
233234
if 'from_address' in params:
234235
params['from'] = params['from_address']
235236
params.pop('from_address')
236237
if 'to_address' in params:
237238
params['to'] = params['to_address']
238239
params.pop('to_address')
239-
return super(SelfMailer, cls).create(**params)
240+
return super(SelfMailer, cls).create(timeout=timeout, **params)
240241

241242

242243
class USAutocompletion(CreateableAPIResource):

tests/test_timeout.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import unittest
2+
import lob
3+
import pytest
4+
import requests_mock
5+
class TimeoutTests(unittest.TestCase):
6+
def setUp(self):
7+
lob.api_key = 'mockAPIkey123'
8+
9+
def return_list(self, request, context):
10+
return {
11+
"data": [
12+
{
13+
"id": "adr_e68217bd744d65c8",
14+
"description": "Harry - Office",
15+
"name": "HARRY ZHANG",
16+
"company": "LOB",
17+
"phone": "5555555555",
18+
"email": "[email protected]",
19+
"address_line1": "210 KING ST STE 6100",
20+
"address_line2": None,
21+
"address_city": "SAN FRANCISCO",
22+
"address_state": "CA",
23+
"address_zip": "94107-1741",
24+
"address_country": "UNITED STATES",
25+
"metadata": {},
26+
"date_created": "2019-08-12T00:16:00.361Z",
27+
"date_modified": "2019-08-12T00:16:00.361Z",
28+
"object": "address"
29+
},
30+
{
31+
"id": "adr_asdi2y3riuasasoi",
32+
"description": "Harry - Office",
33+
"name": "Harry Zhang",
34+
"company": "Lob",
35+
"phone": "5555555555",
36+
"email": "[email protected]",
37+
"metadata": {},
38+
"address_line1": "370 WATER ST",
39+
"address_line2": "",
40+
"address_city": "SUMMERSIDE",
41+
"address_state": "PRINCE EDWARD ISLAND",
42+
"address_zip": "C1N 1C4",
43+
"address_country": "CANADA",
44+
"date_created": "2019-09-20T00:14:00.361Z",
45+
"date_modified": "2019-09-20T00:14:00.361Z",
46+
"object": "address"
47+
}
48+
],
49+
"object": "list",
50+
"next_url": "https://api.lob.com/v1/addresses?limit=2&after=eyJkYXRlT2Zmc2V0IjoiMjAxOS0wOC0wN1QyMTo1OTo0Ni43NjRaIiwiaWRPZmZzZXQiOiJhZHJfODMwYmYwZWFiZGFhYTQwOSJ9",
51+
"previous_url": None,
52+
"count": 2
53+
}
54+
55+
def return_status(self, request, context):
56+
return {
57+
"id": "adr_123456789",
58+
"deleted": True
59+
}
60+
61+
def return_single(self, request, context):
62+
return {
63+
"id": "adr_d3489cd64c791ab5",
64+
"description": "Harry - Office",
65+
"name": "HARRY ZHANG",
66+
"company": "LOB",
67+
"phone": "5555555555",
68+
"email": "[email protected]",
69+
"address_line1": "210 KING ST STE 6100",
70+
"address_city": "SAN FRANCISCO",
71+
"address_state": "CA",
72+
"address_zip": "94107",
73+
"address_country": "UNITED STATES",
74+
"metadata": {},
75+
"date_created": "2017-09-05T17:47:53.767Z",
76+
"date_modified": "2017-09-05T17:47:53.767Z",
77+
"object": "address"
78+
}
79+
@requests_mock.Mocker()
80+
def test_connection_timeout_on_get_requests(self, adapter):
81+
adapter.register_uri('GET', 'https://api.lob.com/v1/addresses', json=self.return_list)
82+
lob.Address.list(timeout=3)
83+
self.assertEqual(3, adapter.last_request.timeout)
84+
85+
@requests_mock.Mocker()
86+
def test_connection_timeout_triggers_on_delete_requests(self, adapter):
87+
adapter.register_uri('DELETE', 'https://api.lob.com/v1/addresses/adr_12345', json=self.return_single)
88+
lob.Address.delete(id='adr_12345', timeout=22)
89+
self.assertEqual(22, adapter.last_request.timeout)
90+
91+
92+
@requests_mock.Mocker()
93+
def test_connection_timeout_triggers_on_post_requests(self, adapter):
94+
adapter.register_uri('POST', 'https://api.lob.com/v1/addresses', json=self.return_status)
95+
lob.Address.create(
96+
name='Lob',
97+
address_line1='185 Berry Street',
98+
address_line2='Suite 1510',
99+
address_city='San Francisco',
100+
address_zip='94017',
101+
address_state='CA',
102+
address_country='US',
103+
timeout=33
104+
)
105+
self.assertEqual(33, adapter.last_request.timeout)

0 commit comments

Comments
 (0)