Merge lp:~jml/piston-mini-client/split-request-base-1041825 into lp:piston-mini-client
- split-request-base-1041825
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||
Approved revision: | 81 | ||||
Merged at revision: | 64 | ||||
Proposed branch: | lp:~jml/piston-mini-client/split-request-base-1041825 | ||||
Merge into: | lp:piston-mini-client | ||||
Diff against target: |
778 lines (+444/-184) 4 files modified
piston_mini_client/__init__.py (+427/-182) piston_mini_client/tests/test_failhandlers.py (+15/-0) piston_mini_client/tests/test_log_to_file.py (+1/-1) setup.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~jml/piston-mini-client/split-request-base-1041825 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Anthony Lenton | Approve | ||
Review via email: mp+124464@code.launchpad.net |
Commit message
Add PistonRequester, enabling access to piston_mini_client functionality on a URL-by-URL basis
Description of the change
PistonAPI does two really useful things:
1. It makes it really easy to define a Python front-end for an HTTP API
2. It has a bunch of stuff to make doing HTTP requests easier
Sometimes, especially with callback services like pkgme-service, it would be really really useful to do 2. without doing 1. If you take a look at pkgme-service, you'll see code written by the original author (hello achuni!) that duplicates stuff going on in piston-mini-client.
This branch extracts that core, requesting logic into a base class and make PistonAPI subclass it. The base class is called PistonRequester (better names welcome).
Backwards compatibility has been preserved. Everything that subclasses PistonAPI today should still work after this change lands.
I've also changed setup.py to use setuptools, so I could run tox in a virtualenv.
Thanks,
jml
- 69. By Jonathan Lange
-
Update docstring.
- 70. By Jonathan Lange
-
Scheme not needed in PistonRequester methods.
- 71. By Jonathan Lange
-
Give the full URL in errors, not just the service root.
- 72. By Jonathan Lange
-
Don't have special timeout handling logic. Just take parameters.
- 73. By Jonathan Lange
-
As few class variables as we can get away with.
- 74. By Jonathan Lange
-
Don't do special log_filename consulting in PistonRequester.
- 75. By Jonathan Lange
-
Use PistonRequester, rather than subclass it.
- 76. By Jonathan Lange
-
Make all the rest of the attributes private.
- 77. By Jonathan Lange
-
Issue deprecation warnings.
Jonathan Lange (jml) wrote : | # |
- 78. By Jonathan Lange
-
Rename to DEPRECATED_
ATTRIBUTES - 79. By Jonathan Lange
-
Update deprecation message.
- 80. By Jonathan Lange
-
Don't emit deprecation warnings on the public attributes
- 81. By Jonathan Lange
-
Fix a forwarding bug.
Jonathan Lange (jml) wrote : | # |
I'm pretty sure that this version has addressed all concerns. I'd be glad to see it land.
Preview Diff
1 | === modified file 'piston_mini_client/__init__.py' |
2 | --- piston_mini_client/__init__.py 2012-07-30 17:09:26 +0000 |
3 | +++ piston_mini_client/__init__.py 2012-09-20 17:51:19 +0000 |
4 | @@ -225,36 +225,28 @@ |
5 | return self.as_serializable() |
6 | |
7 | |
8 | -class PistonAPI(object): |
9 | +class PistonRequester(object): |
10 | """This class provides methods to make http requests slightly easier. |
11 | |
12 | - It's a wrapper around ``httplib2`` to allow for a bit of state to |
13 | - be stored (like the service root) so that you don't need to repeat |
14 | - yourself as much. |
15 | - |
16 | - It's not intended to be used directly. Children classes should implement |
17 | - methods that actually call out to the api methods. |
18 | - |
19 | - When you define your API's methods you'll |
20 | - want to just call out to the ``_get``, ``_post``, ``_put`` or ``_delete`` |
21 | - methods provided by this class. |
22 | + It's a wrapper around ``httplib2`` that takes care of a few common tasks |
23 | + around making HTTP requests: authentication, proxies, serialization, |
24 | + timeouts, etc. |
25 | + |
26 | + To use it, just instantiate it and call ``get``, ``post``, ``put`` or |
27 | + ``delete`` to do various requests. |
28 | + |
29 | + If you want to wrap an HTTP API, you probably want to make a subclass of |
30 | + ``PistonAPI``, which uses this class. |
31 | """ |
32 | + |
33 | SUPPORTED_SCHEMAS = ("http", "https") |
34 | - default_service_root = '' |
35 | - default_content_type = 'application/json' |
36 | - default_timeout = None |
37 | - fail_handler = ExceptionFailHandler |
38 | - extra_headers = None |
39 | - serializers = None |
40 | |
41 | - def __init__(self, service_root=None, cachedir=None, auth=None, |
42 | + def __init__(self, cachedir=None, auth=None, |
43 | offline_mode=False, disable_ssl_validation=False, |
44 | - log_filename=None, timeout=None): |
45 | - """Initialize a ``PistonAPI``. |
46 | - |
47 | - ``service_root`` is the url to the server's service root. |
48 | - Children classes can provide a ``default_service_root`` class |
49 | - attribute that will be used if ``service_root`` is ``None``. |
50 | + log_filename=None, timeout=None, fail_handler=None, |
51 | + extra_headers=None, serializers=None, |
52 | + default_content_type='application/json'): |
53 | + """Initialize a ``PistonRequester``. |
54 | |
55 | ``cachedir`` will be used as ``httplib2``'s cache directory if |
56 | provided. |
57 | @@ -274,26 +266,7 @@ |
58 | |
59 | If you pass in a ``log_filename``, all requests and responses |
60 | including headers will be logged to this file. |
61 | - |
62 | - ``timeout`` will be used as a socket timeout for all calls this |
63 | - instance makes. To explicitly set no timeout, set timeout=0. The |
64 | - default timeout=None will first check for an environment variable |
65 | - ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT`` and try to use that. If this |
66 | - environment variable is not found or it is an invalid float, the |
67 | - class's ``default_timeout`` will be used. Finally, if the class's |
68 | - default is also None, Python's default timeout for sockets will be |
69 | - used. All these should be in seconds. |
70 | """ |
71 | - if service_root is None: |
72 | - service_root = self.default_service_root |
73 | - if not service_root: |
74 | - raise ValueError("No service_root provided, and no default found") |
75 | - parsed_service_root = urlparse(service_root) |
76 | - scheme = parsed_service_root.scheme |
77 | - if scheme not in self.SUPPORTED_SCHEMAS: |
78 | - raise ValueError("service_root's scheme must be http or https") |
79 | - self._service_root = service_root |
80 | - self._parsed_service_root = list(parsed_service_root) |
81 | if cachedir: |
82 | self._create_dir_if_needed(cachedir) |
83 | self._httplib2_cache = httplib2.FileCache(cachedir, safe=safename) |
84 | @@ -302,11 +275,6 @@ |
85 | self._auth = auth |
86 | self._offline_mode = offline_mode |
87 | self._disable_ssl_validation = disable_ssl_validation |
88 | - if timeout is None: |
89 | - try: |
90 | - timeout = float(os.environ.get(TIMEOUT_ENVVAR)) |
91 | - except (TypeError, ValueError): |
92 | - timeout = self.default_timeout |
93 | self._timeout = timeout |
94 | # create one httplib2.Http object per scheme so that we can |
95 | # have per-scheme proxy settings (see also Issue 26 |
96 | @@ -314,11 +282,15 @@ |
97 | self._http = {} |
98 | for scheme in self.SUPPORTED_SCHEMAS: |
99 | self._http[scheme] = self._get_http_obj_for_scheme(scheme) |
100 | - if self.serializers is None: |
101 | - self.serializers = {} |
102 | - if log_filename is None: |
103 | - log_filename = os.environ.get(LOG_FILENAME_ENVVAR) |
104 | - self.log_filename = log_filename |
105 | + if serializers is None: |
106 | + serializers = {} |
107 | + self._serializers = {} |
108 | + self._log_filename = log_filename |
109 | + self._default_content_type = default_content_type |
110 | + if fail_handler is None: |
111 | + fail_handler = ExceptionFailHandler |
112 | + self._fail_handler = fail_handler |
113 | + self._extra_headers = extra_headers |
114 | |
115 | def _create_dir_if_needed(self, path): |
116 | """ helper that checks/creates path if it does not exists |
117 | @@ -372,120 +344,17 @@ |
118 | return proxy_info |
119 | return None |
120 | |
121 | - def _get(self, path, args=None, scheme=None, extra_headers=None): |
122 | - """Perform an HTTP GET request. |
123 | - |
124 | - The provided ``path`` is appended to this resource's ``_service_root`` |
125 | - attribute to obtain the absolute URL that will be requested. |
126 | - |
127 | - If provided, ``args`` should be a dict specifying additional GET |
128 | - arguments that will be encoded on to the end of the url. |
129 | - |
130 | - ``scheme`` must be one of *http* or *https*, and will determine the |
131 | - scheme used for this particular request. If not provided the |
132 | - service_root's scheme will be used. |
133 | - |
134 | - ``extra_headers`` is an optional dictionary of header key/values that |
135 | - will be added to the http request. |
136 | - """ |
137 | - if args is not None: |
138 | - if '?' in path: |
139 | - path += '&' |
140 | - else: |
141 | - path += '?' |
142 | - path += urlencode(args) |
143 | - headers = self._prepare_headers(extra_headers=extra_headers) |
144 | - return self._request(path, method='GET', scheme=scheme, |
145 | - headers=headers) |
146 | - |
147 | - def _post(self, path, data=None, content_type=None, scheme=None, |
148 | - extra_headers=None): |
149 | - """Perform an HTTP POST request. |
150 | - |
151 | - The provided ``path`` is appended to this api's ``_service_root`` |
152 | - attribute to obtain the absolute URL that will be requested. ``data`` |
153 | - should be: |
154 | - |
155 | - - A string, in which case it will be used directly as the request's |
156 | - body, or |
157 | - - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
158 | - (something with an ``as_serializable`` method) or even ``None``, |
159 | - in which case it will be serialized into a string according to |
160 | - ``content_type``. |
161 | - |
162 | - If ``content_type`` is ``None``, ``self.default_content_type`` will |
163 | - be used. |
164 | - |
165 | - ``scheme`` must be one of *http* or *https*, and will determine the |
166 | - scheme used for this particular request. If not provided the |
167 | - service_root's scheme will be used. |
168 | - |
169 | - ``extra_headers`` is an optional dictionary of header key/values that |
170 | - will be added to the http request. |
171 | - """ |
172 | - body, headers = self._prepare_request(data, content_type, |
173 | - extra_headers=extra_headers) |
174 | - return self._request(path, method='POST', body=body, |
175 | - headers=headers, scheme=scheme) |
176 | - |
177 | - def _put(self, path, data=None, content_type=None, scheme=None, |
178 | - extra_headers=None): |
179 | - """Perform an HTTP PUT request. |
180 | - |
181 | - The provided ``path`` is appended to this api's ``_service_root`` |
182 | - attribute to obtain the absolute URL that will be requested. ``data`` |
183 | - should be: |
184 | - |
185 | - - A string, in which case it will be used directly as the request's |
186 | - body, or |
187 | - - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
188 | - (something with an ``as_serializable`` method) or even ``None``, |
189 | - in which case it will be serialized into a string according to |
190 | - ``content_type``. |
191 | - |
192 | - If ``content_type`` is ``None``, ``self.default_content_type`` will be |
193 | - used. |
194 | - |
195 | - ``scheme`` must be one of *http* or *https*, and will determine the |
196 | - scheme used for this particular request. If not provided the |
197 | - service_root's scheme will be used. |
198 | - |
199 | - ``extra_headers`` is an optional dictionary of header key/values that |
200 | - will be added to the http request. |
201 | - """ |
202 | - body, headers = self._prepare_request(data, content_type, |
203 | - extra_headers=extra_headers) |
204 | - return self._request(path, method='PUT', body=body, |
205 | - headers=headers, scheme=scheme) |
206 | - |
207 | - def _delete(self, path, scheme=None, extra_headers=None): |
208 | - """Perform an HTTP DELETE request. |
209 | - |
210 | - The provided ``path`` is appended to this resource's ``_service_root`` |
211 | - attribute to obtain the absolute URL that will be requested. |
212 | - |
213 | - ``scheme`` must be one of *http* or *https*, and will determine the |
214 | - scheme used for this particular request. If not provided the |
215 | - service_root's scheme will be used. |
216 | - |
217 | - ``extra_headers`` is an optional dictionary of header key/values that |
218 | - will be added to the http request. |
219 | - """ |
220 | - headers = self._prepare_headers(extra_headers=extra_headers) |
221 | - return self._request(path, method='DELETE', scheme=scheme, |
222 | - headers=headers) |
223 | - |
224 | def _prepare_request(self, data=None, content_type=None, |
225 | extra_headers=None): |
226 | """Put together a set of headers and a body for a request. |
227 | |
228 | - If ``content_type`` is not provided, ``self.default_content_type`` |
229 | + If ``content_type`` is not provided, ``self._default_content_type`` |
230 | will be assumed. |
231 | |
232 | You probably never need to call this method directly. |
233 | """ |
234 | if content_type is None: |
235 | - content_type = self.default_content_type |
236 | + content_type = self._default_content_type |
237 | body = self._prepare_body(data, content_type) |
238 | headers = self._prepare_headers(content_type, extra_headers) |
239 | return body, headers |
240 | @@ -503,8 +372,8 @@ |
241 | headers = {} |
242 | if content_type: |
243 | headers['Content-type'] = content_type |
244 | - if self.extra_headers is not None: |
245 | - headers.update(self.extra_headers) |
246 | + if self._extra_headers is not None: |
247 | + headers.update(self._extra_headers) |
248 | if extra_headers is not None: |
249 | headers.update(extra_headers) |
250 | return headers |
251 | @@ -523,17 +392,14 @@ |
252 | body = serializer.serialize(data) |
253 | return body |
254 | |
255 | - def _request(self, path, method, body='', headers=None, scheme=None): |
256 | + def request_url(self, url, method, body='', headers=None): |
257 | """Perform an HTTP request. |
258 | |
259 | - You probably want to call one of the ``_get``, ``_post``, ``_put`` |
260 | + You probably want to call one of the ``get``, ``post``, ``put`` |
261 | methods instead. |
262 | """ |
263 | if headers is None: |
264 | headers = {} |
265 | - if scheme not in [None, 'http', 'https']: |
266 | - raise ValueError('Invalid scheme %s' % scheme) |
267 | - url = self._path2url(path, scheme=scheme) |
268 | |
269 | # in offline mode either get it from the cache or return None |
270 | if self._offline_mode: |
271 | @@ -542,12 +408,11 @@ |
272 | raise OfflineModeException(err) |
273 | return self._get_from_cache(url) |
274 | |
275 | - if scheme is None: |
276 | - scheme = urlparse(url).scheme |
277 | + scheme = urlparse(url).scheme |
278 | |
279 | if self._auth: |
280 | self._auth.sign_request(url, method, body, headers) |
281 | - if self.log_filename: |
282 | + if self._log_filename: |
283 | self._dump_request(url, method, body, headers) |
284 | try: |
285 | response, response_body = self._http[scheme].request( |
286 | @@ -555,24 +420,24 @@ |
287 | except AttributeError as e: |
288 | # Special case out httplib2's way of telling us unable to connect |
289 | if e.args[0] == "'NoneType' object has no attribute 'makefile'": |
290 | - raise APIError('Unable to connect to %s' % self._service_root) |
291 | + raise APIError('Unable to connect to %s' % (url,)) |
292 | else: |
293 | raise |
294 | except socket.timeout as e: |
295 | raise TimeoutError('Timed out attempting to connect to %s' % |
296 | - self._service_root) |
297 | + (url,)) |
298 | except (socket.gaierror, socket.error) as e: |
299 | - msg = 'connecting to %s: %s' % (self._service_root, e.message) |
300 | + msg = 'connecting to %s: %s' % (url, e.message) |
301 | raise SocketError(msg) |
302 | - if self.log_filename: |
303 | + if self._log_filename: |
304 | self._dump_response(response, response_body) |
305 | - handler = self.fail_handler(url, method, body, headers) |
306 | + handler = self._fail_handler(url, method, body, headers) |
307 | body = handler.handle(response, response_body) |
308 | return body |
309 | |
310 | def _dump_request(self, url, method, body, headers): |
311 | try: |
312 | - with open(self.log_filename, 'a') as f: |
313 | + with open(self._log_filename, 'a') as f: |
314 | formatted = format_request(url, method, body, headers) |
315 | f.write("{0}: {1}".format(datetime.now(), formatted)) |
316 | except IOError: |
317 | @@ -580,7 +445,7 @@ |
318 | |
319 | def _dump_response(self, response, body): |
320 | try: |
321 | - with open(self.log_filename, 'a') as f: |
322 | + with open(self._log_filename, 'a') as f: |
323 | formatted = format_response(response, body) |
324 | f.write("{0}: {1}".format(datetime.now(), formatted)) |
325 | except IOError: |
326 | @@ -599,6 +464,300 @@ |
327 | '\r\n\r\n', 1) |
328 | return content |
329 | |
330 | + def _get_serializer(self, content_type=None): |
331 | + # Import here to avoid a circular import |
332 | + from piston_mini_client.serializers import get_serializer |
333 | + if content_type is None: |
334 | + content_type = self._default_content_type |
335 | + default_serializer = get_serializer(content_type) |
336 | + return self._serializers.get(content_type, default_serializer) |
337 | + |
338 | + def get(self, url, args=None, extra_headers=None): |
339 | + """Perform an HTTP GET request from ``url``. |
340 | + |
341 | + If provided, ``args`` should be a dict specifying additional GET |
342 | + arguments that will be encoded on to the end of the url. |
343 | + |
344 | + ``extra_headers`` is an optional dictionary of header key/values that |
345 | + will be added to the http request. |
346 | + """ |
347 | + if args is not None: |
348 | + if '?' in url: |
349 | + url += '&' |
350 | + else: |
351 | + url += '?' |
352 | + url += urlencode(args) |
353 | + headers = self._prepare_headers(extra_headers=extra_headers) |
354 | + return self.request_url(url, method='GET', headers=headers) |
355 | + |
356 | + def post(self, url, data=None, content_type=None, extra_headers=None): |
357 | + """Perform an HTTP POST request to ``url``. |
358 | + |
359 | + ``data`` should be: |
360 | + |
361 | + - A string, in which case it will be used directly as the request's |
362 | + body, or |
363 | + - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
364 | + (something with an ``as_serializable`` method) or even ``None``, |
365 | + in which case it will be serialized into a string according to |
366 | + ``content_type``. |
367 | + |
368 | + If ``content_type`` is ``None``, ``self._default_content_type`` will |
369 | + be used. |
370 | + |
371 | + ``extra_headers`` is an optional dictionary of header key/values that |
372 | + will be added to the http request. |
373 | + """ |
374 | + body, headers = self._prepare_request( |
375 | + data, content_type, extra_headers=extra_headers) |
376 | + return self.request_url( |
377 | + url, method='POST', body=body, headers=headers) |
378 | + |
379 | + def put(self, url, data=None, content_type=None, extra_headers=None): |
380 | + """Perform an HTTP PUT request to ``url``. |
381 | + |
382 | + ``data`` should be: |
383 | + |
384 | + - A string, in which case it will be used directly as the request's |
385 | + body, or |
386 | + - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
387 | + (something with an ``as_serializable`` method) or even ``None``, |
388 | + in which case it will be serialized into a string according to |
389 | + ``content_type``. |
390 | + |
391 | + If ``content_type`` is ``None``, ``self._default_content_type`` will |
392 | + be used. |
393 | + |
394 | + ``extra_headers`` is an optional dictionary of header key/values that |
395 | + will be added to the http request. |
396 | + """ |
397 | + body, headers = self._prepare_request( |
398 | + data, content_type, extra_headers=extra_headers) |
399 | + return self.request_url( |
400 | + url, method='PUT', body=body, headers=headers) |
401 | + |
402 | + def delete(self, url, extra_headers=None): |
403 | + """Perform an HTTP DELETE request on ``url``. |
404 | + |
405 | + ``extra_headers`` is an optional dictionary of header key/values that |
406 | + will be added to the http request. |
407 | + """ |
408 | + headers = self._prepare_headers(extra_headers=extra_headers) |
409 | + return self.request_url(url, method='DELETE', headers=headers) |
410 | + |
411 | + |
412 | +class _DeprecatedRequesterDecorator(object): |
413 | + |
414 | + __DEPRECATED_ATTRIBUTES = { |
415 | + '_httplib2_cache': '_httplib2_cache', |
416 | + '_auth': '_auth', |
417 | + '_offline_mode': '_offline_mode', |
418 | + '_disable_ssl_validation': '_disable_ssl_validation', |
419 | + '_timeout': '_timeout', |
420 | + '_http': '_http', |
421 | + } |
422 | + |
423 | + def __warn(self, name): |
424 | + import warnings |
425 | + warnings.warn( |
426 | + "PistonAPI.%s is deprecated; Use PistonAPI._requester.%s " |
427 | + "instead. Both are likely to break in the future. Please " |
428 | + "file a bug if you'd like them not to" % (name, name), |
429 | + DeprecationWarning, |
430 | + stacklevel=4) |
431 | + |
432 | + def __getattr__(self, name): |
433 | + if name in self.__DEPRECATED_ATTRIBUTES: |
434 | + self.__warn(name) |
435 | + return getattr(self._requester, self.__DEPRECATED_ATTRIBUTES[name]) |
436 | + raise AttributeError( |
437 | + '%r object has no attribute %r' |
438 | + % (self.__class__.__name__, name)) |
439 | + |
440 | + def __setattr__(self, name, value): |
441 | + if name in self.__DEPRECATED_ATTRIBUTES: |
442 | + self.__warn(name) |
443 | + setattr(self._requester, self.__DEPRECATED_ATTRIBUTES[name], value) |
444 | + else: |
445 | + super(_DeprecatedRequesterDecorator, self).__setattr__(name, value) |
446 | + |
447 | + def __deprecated_call(self, method_name, *args, **kwargs): |
448 | + self.__warn(method_name) |
449 | + method = getattr(self._requester, method_name) |
450 | + return method(*args, **kwargs) |
451 | + |
452 | + def _create_dir_if_needed(self, path): |
453 | + """ helper that checks/creates path if it does not exists |
454 | + """ |
455 | + return self.__deprecated_call('_create_dir_if_needed', path) |
456 | + |
457 | + def _get_http_obj_for_scheme(self, scheme): |
458 | + return self.__deprecated_call('_get_http_obj_for_scheme', scheme) |
459 | + |
460 | + def _get_proxy_info(self, scheme): |
461 | + return self.__deprecated_call('_get_proxy_info', scheme) |
462 | + |
463 | + def _prepare_request(self, data=None, content_type=None, |
464 | + extra_headers=None): |
465 | + """Put together a set of headers and a body for a request. |
466 | + |
467 | + If ``content_type`` is not provided, ``self.default_content_type`` |
468 | + will be assumed. |
469 | + |
470 | + You probably never need to call this method directly. |
471 | + """ |
472 | + return self.__deprecated_call( |
473 | + '_prepare_request', data=data, content_type=content_type, |
474 | + extra_headers=extra_headers) |
475 | + |
476 | + def _prepare_headers(self, content_type=None, extra_headers=None): |
477 | + """Put together and return a complete set of headers. |
478 | + |
479 | + If ``content_type`` is provided, it will be added as |
480 | + the Content-type header. |
481 | + |
482 | + Any provided ``extra_headers`` will be added last. |
483 | + |
484 | + You probably never need to call this method directly. |
485 | + """ |
486 | + return self.__deprecated_call( |
487 | + '_prepare_headers', content_type=content_type, |
488 | + extra_headers=extra_headers) |
489 | + |
490 | + def _prepare_body(self, data=None, content_type=None): |
491 | + """Serialize data into a request body. |
492 | + |
493 | + ``data`` will be serialized into a string, according to |
494 | + ``content_type``. |
495 | + |
496 | + You probably never need to call this method directly. |
497 | + """ |
498 | + return self.__deprecated_call( |
499 | + '_prepare_body', data=data, content_type=content_type) |
500 | + |
501 | + def _dump_request(self, url, method, body, headers): |
502 | + return self.__deprecated_call( |
503 | + '_dump_request', url, method, body, headers) |
504 | + |
505 | + def _dump_response(self, response, body): |
506 | + return self.__deprecated_call('_dump_response', response, body) |
507 | + |
508 | + def _get_from_cache(self, url): |
509 | + """ get a given url from the cachedir even if its expired |
510 | + or return None if no data is available |
511 | + """ |
512 | + return self.__deprecated_call('_get_from_cache', url) |
513 | + |
514 | + def _get_serializer(self, content_type=None): |
515 | + return self.__deprecated_call( |
516 | + '_get_serializer', content_type=content_type) |
517 | + |
518 | + |
519 | +class PistonAPI(_DeprecatedRequesterDecorator): |
520 | + """This class provides methods to make http requests slightly easier. |
521 | + |
522 | + It's a wrapper around ``httplib2`` to allow for a bit of state to |
523 | + be stored (like the service root) so that you don't need to repeat |
524 | + yourself as much. |
525 | + |
526 | + It's not intended to be used directly. Children classes should implement |
527 | + methods that actually call out to the api methods. |
528 | + |
529 | + When you define your API's methods you'll |
530 | + want to just call out to the ``_get``, ``_post``, ``_put`` or ``_delete`` |
531 | + methods provided by this class. |
532 | + """ |
533 | + |
534 | + SUPPORTED_SCHEMAS = PistonRequester.SUPPORTED_SCHEMAS |
535 | + default_service_root = '' |
536 | + default_timeout = None |
537 | + fail_handler = ExceptionFailHandler |
538 | + extra_headers = None |
539 | + serializers = None |
540 | + default_content_type = 'application/json' |
541 | + |
542 | + # Attributes that are forwarded to PistonRequester for backwards |
543 | + # compatibility reasons, but are not deprecated. |
544 | + # |
545 | + # Should only have attributes that users are expected to set on |
546 | + # constructed PistonAPI instances. |
547 | + __FORWARDED_ATTRIBUTES = { |
548 | + 'serializers': '_serializers', |
549 | + 'log_filename': '_log_filename', |
550 | + 'default_content_type': '_default_content_type', |
551 | + 'fail_handler': '_fail_handler', |
552 | + 'extra_headers': '_extra_headers', |
553 | + } |
554 | + |
555 | + def __init__(self, service_root=None, cachedir=None, auth=None, |
556 | + offline_mode=False, disable_ssl_validation=False, |
557 | + log_filename=None, timeout=None): |
558 | + """Initialize a ``PistonAPI``. |
559 | + |
560 | + ``service_root`` is the url to the server's service root. |
561 | + Children classes can provide a ``default_service_root`` class |
562 | + attribute that will be used if ``service_root`` is ``None``. |
563 | + |
564 | + ``timeout`` will be used as a socket timeout for all calls this |
565 | + instance makes. To explicitly set no timeout, set timeout=0. The |
566 | + default timeout=None will first check for an environment variable |
567 | + ``PISTON_MINI_CLIENT_DEFAULT_TIMEOUT`` and try to use that. If this |
568 | + environment variable is not found or it is an invalid float, the |
569 | + class's ``default_timeout`` will be used. Finally, if the class's |
570 | + default is also None, Python's default timeout for sockets will be |
571 | + used. All these should be in seconds. |
572 | + |
573 | + For all other arguments, see ``PistonRequester``. |
574 | + """ |
575 | + if timeout is None: |
576 | + try: |
577 | + timeout = float(os.environ.get(TIMEOUT_ENVVAR)) |
578 | + except (TypeError, ValueError): |
579 | + timeout = self.default_timeout |
580 | + if log_filename is None: |
581 | + log_filename = os.environ.get(LOG_FILENAME_ENVVAR) |
582 | + self._requester = PistonRequester( |
583 | + cachedir=cachedir, |
584 | + auth=auth, |
585 | + offline_mode=offline_mode, |
586 | + disable_ssl_validation=disable_ssl_validation, |
587 | + log_filename=log_filename, |
588 | + timeout=timeout, |
589 | + fail_handler=self.fail_handler, |
590 | + extra_headers=self.extra_headers, |
591 | + default_content_type=self.default_content_type, |
592 | + serializers=self.serializers) |
593 | + if service_root is None: |
594 | + service_root = self.default_service_root |
595 | + if not service_root: |
596 | + raise ValueError("No service_root provided, and no default found") |
597 | + parsed_service_root = urlparse(service_root) |
598 | + scheme = parsed_service_root.scheme |
599 | + if scheme not in self.SUPPORTED_SCHEMAS: |
600 | + raise ValueError("service_root's scheme must be http or https") |
601 | + self._service_root = service_root |
602 | + self._parsed_service_root = list(parsed_service_root) |
603 | + |
604 | + def __getattr__(self, name): |
605 | + if name in self.__FORWARDED_ATTRIBUTES: |
606 | + return getattr(self._requester, self.__FORWARDED_ATTRIBUTES[name]) |
607 | + return super(PistonAPI, self).__getattr__(name) |
608 | + |
609 | + def __setattr__(self, name, value): |
610 | + if name in self.__FORWARDED_ATTRIBUTES: |
611 | + setattr(self._requester, self.__FORWARDED_ATTRIBUTES[name], value) |
612 | + super(PistonAPI, self).__setattr__(name, value) |
613 | + |
614 | + def _request(self, path, method, body='', headers=None, scheme=None): |
615 | + """Perform an HTTP request. |
616 | + |
617 | + You probably want to call one of the ``_get``, ``_post``, ``_put`` |
618 | + methods instead. |
619 | + """ |
620 | + url = self._path2url(path, scheme) |
621 | + return self._requester.request_url( |
622 | + url, method, body=body, headers=headers) |
623 | + |
624 | def _path2url(self, path, scheme=None): |
625 | if scheme is None: |
626 | service_root = self._service_root |
627 | @@ -607,10 +766,96 @@ |
628 | service_root = urlunparse(parts) |
629 | return (service_root.strip('/') + '/' + path.lstrip('/')) |
630 | |
631 | - def _get_serializer(self, content_type=None): |
632 | - # Import here to avoid a circular import |
633 | - from piston_mini_client.serializers import get_serializer |
634 | - if content_type is None: |
635 | - content_type = self.default_content_type |
636 | - default_serializer = get_serializer(content_type) |
637 | - return self.serializers.get(content_type, default_serializer) |
638 | + def _get(self, path, args=None, scheme=None, extra_headers=None): |
639 | + """Perform an HTTP GET request. |
640 | + |
641 | + The provided ``path`` is appended to this resource's ``_service_root`` |
642 | + attribute to obtain the absolute URL that will be requested. |
643 | + |
644 | + If provided, ``args`` should be a dict specifying additional GET |
645 | + arguments that will be encoded on to the end of the url. |
646 | + |
647 | + ``scheme`` must be one of *http* or *https*, and will determine the |
648 | + scheme used for this particular request. If not provided the |
649 | + service_root's scheme will be used. |
650 | + |
651 | + ``extra_headers`` is an optional dictionary of header key/values that |
652 | + will be added to the http request. |
653 | + """ |
654 | + return self._requester.get( |
655 | + self._path2url(path, scheme), args=args, |
656 | + extra_headers=extra_headers) |
657 | + |
658 | + def _post(self, path, data=None, content_type=None, scheme=None, |
659 | + extra_headers=None): |
660 | + """Perform an HTTP POST request. |
661 | + |
662 | + The provided ``path`` is appended to this api's ``_service_root`` |
663 | + attribute to obtain the absolute URL that will be requested. ``data`` |
664 | + should be: |
665 | + |
666 | + - A string, in which case it will be used directly as the request's |
667 | + body, or |
668 | + - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
669 | + (something with an ``as_serializable`` method) or even ``None``, |
670 | + in which case it will be serialized into a string according to |
671 | + ``content_type``. |
672 | + |
673 | + If ``content_type`` is ``None``, ``self.default_content_type`` will |
674 | + be used. |
675 | + |
676 | + ``scheme`` must be one of *http* or *https*, and will determine the |
677 | + scheme used for this particular request. If not provided the |
678 | + service_root's scheme will be used. |
679 | + |
680 | + ``extra_headers`` is an optional dictionary of header key/values that |
681 | + will be added to the http request. |
682 | + """ |
683 | + return self._requester.post( |
684 | + self._path2url(path, scheme), data=data, |
685 | + content_type=content_type, extra_headers=extra_headers) |
686 | + |
687 | + def _put(self, path, data=None, content_type=None, scheme=None, |
688 | + extra_headers=None): |
689 | + """Perform an HTTP PUT request. |
690 | + |
691 | + The provided ``path`` is appended to this api's ``_service_root`` |
692 | + attribute to obtain the absolute URL that will be requested. ``data`` |
693 | + should be: |
694 | + |
695 | + - A string, in which case it will be used directly as the request's |
696 | + body, or |
697 | + - A ``list``, ``dict``, ``int``, ``bool`` or ``PistonSerializable`` |
698 | + (something with an ``as_serializable`` method) or even ``None``, |
699 | + in which case it will be serialized into a string according to |
700 | + ``content_type``. |
701 | + |
702 | + If ``content_type`` is ``None``, ``self.default_content_type`` will be |
703 | + used. |
704 | + |
705 | + ``scheme`` must be one of *http* or *https*, and will determine the |
706 | + scheme used for this particular request. If not provided the |
707 | + service_root's scheme will be used. |
708 | + |
709 | + ``extra_headers`` is an optional dictionary of header key/values that |
710 | + will be added to the http request. |
711 | + """ |
712 | + return self._requester.put( |
713 | + self._path2url(path, scheme), data=data, |
714 | + content_type=content_type, extra_headers=extra_headers) |
715 | + |
716 | + def _delete(self, path, scheme=None, extra_headers=None): |
717 | + """Perform an HTTP DELETE request. |
718 | + |
719 | + The provided ``path`` is appended to this resource's ``_service_root`` |
720 | + attribute to obtain the absolute URL that will be requested. |
721 | + |
722 | + ``scheme`` must be one of *http* or *https*, and will determine the |
723 | + scheme used for this particular request. If not provided the |
724 | + service_root's scheme will be used. |
725 | + |
726 | + ``extra_headers`` is an optional dictionary of header key/values that |
727 | + will be added to the http request. |
728 | + """ |
729 | + return self._requester.delete( |
730 | + self._path2url(path, scheme), extra_headers=extra_headers) |
731 | |
732 | === modified file 'piston_mini_client/tests/test_failhandlers.py' |
733 | --- piston_mini_client/tests/test_failhandlers.py 2012-07-30 17:09:26 +0000 |
734 | +++ piston_mini_client/tests/test_failhandlers.py 2012-09-20 17:51:19 +0000 |
735 | @@ -143,6 +143,21 @@ |
736 | |
737 | self.assertEqual(None, api.grow()) |
738 | |
739 | + def test_set_via_class_variable(self): |
740 | + """fail_handler can be overridden by specifying it as a class variable. |
741 | + """ |
742 | + api = GardeningAPI() |
743 | + self.assertEqual(NoneFailHandler, api.fail_handler) |
744 | + |
745 | + def test_forwarding(self): |
746 | + """Setting fail_handler on a PistonAPI instance actually works.""" |
747 | + sentinel = object() |
748 | + api = GardeningAPI() |
749 | + api.fail_handler = sentinel |
750 | + self.assertEqual(sentinel, api.fail_handler) |
751 | + # Not a public API, so OK if future changes break this. |
752 | + self.assertEqual(sentinel, api._requester._fail_handler) |
753 | + |
754 | @patch('httplib2.Http.request') |
755 | def test_interacts_well_with_returns_on_fail(self, mock_request): |
756 | """Check that NoneFailHandler interacts well with returns""" |
757 | |
758 | === modified file 'piston_mini_client/tests/test_log_to_file.py' |
759 | --- piston_mini_client/tests/test_log_to_file.py 2012-07-30 17:09:26 +0000 |
760 | +++ piston_mini_client/tests/test_log_to_file.py 2012-09-20 17:51:19 +0000 |
761 | @@ -76,4 +76,4 @@ |
762 | api = AnnoyAPI() |
763 | |
764 | self.assertEqual(api.log_filename, sentinel) |
765 | - mock_get.assert_called_with(LOG_FILENAME_ENVVAR) |
766 | + mock_get.assert_any_call(LOG_FILENAME_ENVVAR) |
767 | |
768 | === modified file 'setup.py' |
769 | --- setup.py 2012-06-13 13:25:54 +0000 |
770 | +++ setup.py 2012-09-20 17:51:19 +0000 |
771 | @@ -1,6 +1,6 @@ |
772 | #!/usr/bin/env python |
773 | |
774 | -from distutils.core import setup |
775 | +from setuptools import setup |
776 | |
777 | setup(name='piston-mini-client', |
778 | version='0.7.2', |
On 2012-09-14::
16:48:19 <achuni> jml: I thought PistonAPI was going to use PistonRequester as a client in the end? ble._as_ serializable. .. I'm not sure that's the recommended way, but it's what was done last time
16:48:33 <achuni> (or did we agree to go the other way around in the end?)
16:48:59 <jml> achuni: you mean, use composition instead of inheritance?
16:49:04 <achuni> yep
16:49:51 <jml> achuni: I'd be very happy to do that. It'll mean potentially breaking backwards compat w/ existing PistonAPI users
16:50:21 <jml> achuni: or alternatively wrapping every existing method (e.g. _prepare_headers, _get_proxy_info, ...)
16:50:25 <achuni> jml: even if you leave the current _get / _post / ... methods there?
16:50:35 <achuni> ah, right
16:50:36 <achuni> hm
16:50:39 <jml> achuni: your call.
16:51:38 <achuni> jml: yep, we'd need to wrap all methods, I think... I'd say the wrappers can be deprecated immediately
16:52:23 <jml> achuni: want warnings for the deprecations, or are docstrings enough?
16:52:52 <achuni> jml: like PistonSerializa
16:53:11 <jml> achuni: cool. I'll do that then.
I've now done this. I haven't cleared up the deprecation warnings in the tests.