Merge lp:~jml/piston-mini-client/split-request-base-1041825 into lp:piston-mini-client

Proposed by Jonathan Lange
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
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

To post a comment you must log in.
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.

Revision history for this message
Jonathan Lange (jml) wrote :

On 2012-09-14::

16:48:19 <achuni> jml: I thought PistonAPI was going to use PistonRequester as a client in the end?
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 PistonSerializable._as_serializable... I'm not sure that's the recommended way, but it's what was done last time
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.

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.

Revision history for this message
Jonathan Lange (jml) wrote :

I'm pretty sure that this version has addressed all concerns. I'd be glad to see it land.

Revision history for this message
Anthony Lenton (elachuni) wrote :

Thanks jml!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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',

Subscribers

People subscribed via source and target branches