Skip to content

Commit d4f4402

Browse files
author
Ben Picolo
committed
Merge pull request #69 from bpicolo/immutable_async_client
Don't mutate clients.
2 parents 8d6e536 + 0138977 commit d4f4402

File tree

10 files changed

+186
-67
lines changed

10 files changed

+186
-67
lines changed

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name="swaggerpy",
13-
version="0.7.0",
13+
version="0.7.1",
1414
license="BSD 3-Clause License",
1515
description="Library for accessing Swagger-enabled API's",
1616
long_description=open(os.path.join(os.path.dirname(__file__),
@@ -32,7 +32,8 @@
3232
"tissue",
3333
"coverage",
3434
"ordereddict",
35-
"httpretty"
35+
"httpretty",
36+
"bottle"
3637
],
3738
install_requires=[
3839
"requests",

swaggerpy/async_http_client.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@ class AsynchronousHttpClient(http_client.HttpClient):
3333
"""Asynchronous HTTP client implementation.
3434
"""
3535

36-
def setup(self, request_params):
36+
def start_request(self, request_params):
3737
"""Sets up the request params as per Twisted Agent needs.
3838
Sets up crochet and triggers the API request in background
3939
4040
:param request_params: request parameters for API call
4141
:type request_params: dict
42+
43+
:return: crochet EventualResult
4244
"""
4345
# request_params has mandatory: method, url, params, headers
44-
self.request_params = {
46+
request_params = {
4547
'method': str(request_params['method']),
4648
'bodyProducer': stringify_body(request_params),
4749
'headers': listify_headers(request_params['headers']),
@@ -50,42 +52,46 @@ def setup(self, request_params):
5052
}
5153

5254
crochet.setup()
53-
self.eventual = self.fetch_deferred()
55+
return self.fetch_deferred(request_params)
5456

55-
def cancel(self):
57+
def cancel(self, eventual):
5658
"""Try to cancel the API call using crochet's cancel() API
59+
60+
:param eventual: Crochet EventualResult
5761
"""
58-
self.eventual.cancel()
62+
eventual.cancel()
5963

60-
def wait(self, timeout):
64+
def wait(self, timeout, eventual):
6165
"""Requests based implemention with timeout
6266
6367
:param timeout: time in seconds to wait for response
68+
:param eventual: Crochet EventualResult
69+
6470
:return: Requests response
6571
:rtype: requests.Response
6672
"""
67-
log.info(u"%s %s", self.request_params.get('method'),
68-
self.request_params.get('uri'))
6973
# finished_resp is returned here
7074
# TODO(#44): catch known exceptions and raise common exceptions
71-
return self.eventual.wait(timeout)
75+
return eventual.wait(timeout)
7276

7377
@crochet.run_in_reactor
74-
def fetch_deferred(self):
78+
def fetch_deferred(self, request_params):
7579
"""The main core to start the reacter and run the API
7680
in the background. Also the callbacks are registered here
81+
82+
:return: crochet EventualResult
7783
"""
7884
finished_resp = Deferred()
7985
agent = Agent(reactor)
80-
deferred = agent.request(**self.request_params)
86+
deferred = agent.request(**request_params)
8187

8288
def response_callback(response):
8389
"""Callback for response received from server, even 4XX, 5XX possible
8490
response param stores the headers and status code.
8591
It needs a callback method to be registered to store the response
8692
body which is provided using deliverBody
8793
"""
88-
response.deliverBody(_HTTPBodyFetcher(self.request_params,
94+
response.deliverBody(_HTTPBodyFetcher(request_params,
8995
response, finished_resp))
9096
deferred.addCallback(response_callback)
9197

@@ -101,10 +107,8 @@ def response_errback(reason):
101107
return finished_resp
102108

103109

104-
class AsyncResponse():
105-
"""AsyncResponse inherits from requests.Response
106-
to inherit methods like json(), raise_for_status()
107-
110+
class AsyncResponse(object):
111+
"""
108112
Remove the property text and content and make them as overridable attrs
109113
"""
110114
def __init__(self, req, resp, data):

swaggerpy/http_client.py

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,27 +71,35 @@ def set_api_key(self, host, api_key, param_name=u'api_key'):
7171
raise NotImplementedError(
7272
u"%s: Method not implemented", self.__class__.__name__)
7373

74-
def setup(self, request_params):
75-
"""Store the request params for calling later.
76-
74+
def start_request(self, request_params):
75+
"""
7776
:param request_params: Complete request data.
7877
:type request_params: dict
78+
79+
:returns: The client's request object
7980
"""
8081
raise NotImplementedError(
8182
u"%s: Method not implemented", self.__class__.__name__)
8283

83-
def wait(self, timeout):
84+
def wait(self, timeout, request):
8485
"""Calls the API with request_params and waits till timeout.
8586
8687
:param timeout: time in seconds to wait for response.
8788
:type timeout: float
89+
:param request: request object from the client
90+
In the Sync client this is a requests.Request
91+
In the Async client this is a crochet.EventualResult
8892
:return: Implementation specific response
8993
"""
9094
raise NotImplementedError(
9195
u"%s: Method not implemented", self.__class__.__name__)
9296

93-
def cancel(self):
97+
def cancel(self, request):
9498
"""Cancels the API call
99+
100+
:param request: request object from the client
101+
In the Sync client this is a requests.Request
102+
In the Async client this is a crochet.EventualResult
95103
"""
96104
raise NotImplementedError(
97105
u"%s: Method not implemented", self.__class__.__name__)
@@ -143,6 +151,8 @@ def __init__(self, host, username, password):
143151
def apply(self, request):
144152
request.auth = self.auth
145153

154+
return request
155+
146156

147157
# noinspection PyDocstring
148158
class ApiKeyAuthenticator(Authenticator):
@@ -162,6 +172,7 @@ def __init__(self, host, api_key, param_name=u'api_key'):
162172

163173
def apply(self, request):
164174
request.params[self.param_name] = self.api_key
175+
return request
165176

166177

167178
class SynchronousHttpClient(HttpClient):
@@ -175,14 +186,21 @@ def __init__(self):
175186
def close(self):
176187
self.session.close()
177188

178-
def setup(self, request_params):
189+
def start_request(self, request_params):
190+
"""
191+
:return: request
192+
:rtype: requests.Request
193+
"""
179194
# if files in request_params OR
180195
# if content-type is x-www-form-urlencoded, no need to stringify
181196
if ('files' not in request_params and
182197
request_params['headers'].get('content-type') != APP_FORM):
183198
stringify_body(request_params)
184-
self.request_params = request_params
185-
self.purge_content_types_if_file_present()
199+
request_params = self.purge_content_types_if_file_present(
200+
request_params,
201+
)
202+
203+
return self.authenticated_request(request_params)
186204

187205
def set_basic_auth(self, host, username, password):
188206
self.authenticator = BasicAuthenticator(
@@ -192,31 +210,35 @@ def set_api_key(self, host, api_key, param_name=u'api_key'):
192210
self.authenticator = ApiKeyAuthenticator(
193211
host=host, api_key=api_key, param_name=param_name)
194212

195-
def wait(self, timeout):
213+
def wait(self, timeout, request):
196214
"""Requests based implemention with timeout.
197215
198216
:param timeout: time in seconds to wait for response
217+
:param request: requests.Request
218+
199219
:return: Requests response
200220
:rtype: requests.Response
201221
"""
202-
log.info(u"%s %s(%r)", self.request_params['method'],
203-
self.request_params['url'],
204-
self.request_params['params'])
205-
req = requests.Request(**self.request_params)
206-
self.apply_authentication(req)
207-
return self.session.send(self.session.prepare_request(req),
208-
timeout=timeout)
222+
log.info(u"%s %s(%r)", request.method, request.url, request.params)
223+
return self.session.send(
224+
self.session.prepare_request(request),
225+
timeout=timeout,
226+
)
209227

210-
def purge_content_types_if_file_present(self):
228+
def purge_content_types_if_file_present(self, request_params):
211229
"""'Requests' adds 'multipart/form-data' to content-type if
212230
files are in the request. Hence, any existing content-type
213231
like application/x-www-form... should be removed
214232
"""
215-
if 'files' in self.request_params:
216-
self.request_params['headers'].pop('content-type', '')
233+
if 'files' in request_params:
234+
request_params['headers'].pop('content-type', '')
217235

218-
def cancel(self):
236+
return request_params
237+
238+
def cancel(self, request):
219239
"""Nothing to be done for Synchronous client
240+
241+
:param request: requests.Request
220242
"""
221243

222244
def request(self, method, url, params=None, data=None, headers=None):
@@ -227,16 +249,20 @@ def request(self, method, url, params=None, data=None, headers=None):
227249
"""
228250
if not headers:
229251
headers = {}
230-
kwargs = {}
252+
request_params = {}
231253
for i in ('method', 'url', 'params', 'data', 'headers'):
232-
kwargs[i] = locals()[i]
233-
req = requests.Request(**kwargs)
234-
self.apply_authentication(req)
235-
return self.session.send(self.session.prepare_request(req))
236-
237-
def apply_authentication(self, req):
238-
if self.authenticator and self.authenticator.matches(req.url):
239-
self.authenticator.apply(req)
254+
request_params[i] = locals()[i]
255+
request = self.authenticated_request(request_params)
256+
return self.session.send(self.session.prepare_request(request))
257+
258+
def authenticated_request(self, request_params):
259+
return self.apply_authentication(requests.Request(**request_params))
260+
261+
def apply_authentication(self, request):
262+
if self.authenticator and self.authenticator.matches(request.url):
263+
return self.authenticator.apply(request)
264+
265+
return request
240266

241267

242268
def stringify_body(request_params):

swaggerpy/response.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ def handle_response_errors(e, CustomError):
2929

3030

3131
class HTTPFuture(object):
32+
3233
"""A future which inputs HTTP params"""
3334
def __init__(self, http_client, request_params, postHTTP_callback):
3435
"""Kicks API call for Asynchronous client
3536
36-
:param http_client: instance with public methods: setup(), wait()
37+
:param http_client: instance with public methods:
38+
start_request(), wait(), cancel()
3739
:param request_params: dict containing API request parameters
3840
:param postHTTP_callback: function to callback on finish
3941
"""
4042
self._http_client = http_client
4143
self._postHTTP_callback = postHTTP_callback
42-
self._http_client.setup(request_params)
44+
# A request is an EventualResult in the async client
45+
self._request = self._http_client.start_request(request_params)
4346
self._cancelled = False
4447

4548
def cancelled(self):
@@ -52,7 +55,7 @@ def cancel(self):
5255
"""Try to cancel the API (meaningful for Asynchronous client)
5356
"""
5457
self._cancelled = True
55-
self._http_client.cancel()
58+
self._http_client.cancel(self._request)
5659

5760
def result(self, **kwargs):
5861
"""Blocking call to wait for API response
@@ -70,7 +73,7 @@ def result(self, **kwargs):
7073

7174
if self.cancelled():
7275
raise CancelledError()
73-
response = self._http_client.wait(timeout)
76+
response = self._http_client.wait(timeout, self._request)
7477
try:
7578
response.raise_for_status()
7679
except Exception as e:

tests/async_http_client_test.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from mock import patch, Mock
1717
from ordereddict import OrderedDict
1818

19+
from crochet._eventloop import EventualResult
20+
from twisted.internet.defer import Deferred
21+
1922
import swaggerpy.async_http_client
2023
import swaggerpy.exception
2124
import swaggerpy.http_client
@@ -107,21 +110,26 @@ def test_listify_headers(self):
107110
def test_success_AsyncHTTP_response(self):
108111
Response = namedtuple("MyResponse",
109112
"version code phrase headers length deliverBody")
110-
with patch.object(swaggerpy.async_http_client.AsynchronousHttpClient,
111-
'fetch_deferred') as mock_Async:
113+
with patch.object(
114+
swaggerpy.async_http_client.AsynchronousHttpClient,
115+
'fetch_deferred',
116+
return_value=Mock(
117+
autospec=EventualResult,
118+
_deferred=Mock(autospec=Deferred),
119+
),
120+
) as mock_Async:
112121
req = {
113122
'method': 'GET',
114123
'url': 'foo',
115124
'data': None,
116125
'headers': {'foo': 'bar'},
117-
'params': ''}
126+
'params': ''
127+
}
118128
mock_Async.return_value.wait.return_value = Response(
119129
1, 2, 3, 4, 5, 6)
120130
async_client = swaggerpy.async_http_client.AsynchronousHttpClient()
121-
async_client.setup(req)
122-
resp = async_client.wait(5)
123-
headers = async_client.request_params['headers']
124-
self.assertTrue(headers.hasHeader('foo'))
131+
eventual = async_client.start_request(req)
132+
resp = async_client.wait(5, eventual)
125133
self.assertEqual(2, resp.code)
126134

127135

tests/integration/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)