Merge lp:~elachuni/piston-mini-client/dual-scheme-support into lp:piston-mini-client
- dual-scheme-support
- Merge into trunk
Proposed by
Anthony Lenton
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||||||
Approved revision: | 35 | ||||||||
Merged at revision: | 25 | ||||||||
Proposed branch: | lp:~elachuni/piston-mini-client/dual-scheme-support | ||||||||
Merge into: | lp:piston-mini-client | ||||||||
Diff against target: |
424 lines (+178/-23) 8 files modified
doc/conf.py (+2/-0) doc/index.rst (+1/-0) doc/quickstart.rst (+35/-0) doc/reference.rst (+33/-0) piston_mini_client/__init__.py (+33/-12) piston_mini_client/failhandlers.py (+24/-1) piston_mini_client/serializers.py (+6/-0) piston_mini_client/test/test_resource.py (+44/-10) |
||||||||
To merge this branch: | bzr merge lp:~elachuni/piston-mini-client/dual-scheme-support | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Danny Tamez (community) | Approve | ||
software-store-developers | Pending | ||
Review via email: mp+46914@code.launchpad.net |
Commit message
Description of the change
Overview
========
Provide support for talking to dual http/https apis.
Details
=======
Base work needed to allow certain methods in our api to use http requests, while others need to use https. Detailed docs in the diff itself, and in the related bugs.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'doc/conf.py' | |||
2 | --- doc/conf.py 2011-01-12 17:21:36 +0000 | |||
3 | +++ doc/conf.py 2011-01-20 14:24:08 +0000 | |||
4 | @@ -18,6 +18,8 @@ | |||
5 | 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. | 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. |
6 | 19 | #sys.path.insert(0, os.path.abspath('.')) | 19 | #sys.path.insert(0, os.path.abspath('.')) |
7 | 20 | 20 | ||
8 | 21 | sys.path.insert(0, os.path.abspath('..')) | ||
9 | 22 | |||
10 | 21 | # -- General configuration ----------------------------------------------------- | 23 | # -- General configuration ----------------------------------------------------- |
11 | 22 | 24 | ||
12 | 23 | # If your documentation needs a minimal Sphinx version, state it here. | 25 | # If your documentation needs a minimal Sphinx version, state it here. |
13 | 24 | 26 | ||
14 | === modified file 'doc/index.rst' | |||
15 | --- doc/index.rst 2010-12-09 21:02:11 +0000 | |||
16 | +++ doc/index.rst 2011-01-20 14:24:08 +0000 | |||
17 | @@ -12,6 +12,7 @@ | |||
18 | 12 | :maxdepth: 2 | 12 | :maxdepth: 2 |
19 | 13 | 13 | ||
20 | 14 | quickstart | 14 | quickstart |
21 | 15 | reference | ||
22 | 15 | 16 | ||
23 | 16 | Overview | 17 | Overview |
24 | 17 | ======== | 18 | ======== |
25 | 18 | 19 | ||
26 | === modified file 'doc/quickstart.rst' | |||
27 | --- doc/quickstart.rst 2011-01-12 15:37:40 +0000 | |||
28 | +++ doc/quickstart.rst 2011-01-20 14:24:08 +0000 | |||
29 | @@ -186,3 +186,38 @@ | |||
30 | 186 | * ``MultiExceptionFailHandler``: Raises a different exception according to | 186 | * ``MultiExceptionFailHandler``: Raises a different exception according to |
31 | 187 | what went wrong. | 187 | what went wrong. |
32 | 188 | 188 | ||
33 | 189 | =============================== | ||
34 | 190 | Talking to dual http/https apis | ||
35 | 191 | =============================== | ||
36 | 192 | |||
37 | 193 | Often your API provides a set of public calls, and some other calls that | ||
38 | 194 | are authenticated. | ||
39 | 195 | |||
40 | 196 | Public calls sometimes are heavily used, so we'd like to | ||
41 | 197 | serve them over http. They're public anyway. | ||
42 | 198 | |||
43 | 199 | Authenticated calls involve some sensitive information passing with the user's | ||
44 | 200 | credentials, so we like serving those over https. | ||
45 | 201 | |||
46 | 202 | Once you've got all this set up on the server, you can ask piston_mini_client | ||
47 | 203 | to make each call using the appropriate scheme by using the ``scheme`` | ||
48 | 204 | optional argument when you call ``_get`` or ``_post``:: | ||
49 | 205 | |||
50 | 206 | class DualAPI(PistonAPI): | ||
51 | 207 | default_service_root = 'http://myhostname.com/api/1.0' | ||
52 | 208 | |||
53 | 209 | def public_method(self): | ||
54 | 210 | return self._get('public_method/', scheme='http') | ||
55 | 211 | |||
56 | 212 | def private_method(self): | ||
57 | 213 | return self._post('private_method/', scheme='https') | ||
58 | 214 | |||
59 | 215 | def either(self): | ||
60 | 216 | return self._get('either/') | ||
61 | 217 | |||
62 | 218 | In this case, no matter what scheme the service root uses, calls to | ||
63 | 219 | ``public_method()`` will result in | ||
64 | 220 | an http request, and calls to ``private_method()`` will result in an https | ||
65 | 221 | request. Calls to ``either()`` will leave the scheme alone, so it will | ||
66 | 222 | follow the scheme used to instantiate the api, or fall back to | ||
67 | 223 | ``default_service_root``'s scheme. | ||
68 | 189 | \ No newline at end of file | 224 | \ No newline at end of file |
69 | 190 | 225 | ||
70 | === added file 'doc/reference.rst' | |||
71 | --- doc/reference.rst 1970-01-01 00:00:00 +0000 | |||
72 | +++ doc/reference.rst 2011-01-20 14:24:08 +0000 | |||
73 | @@ -0,0 +1,33 @@ | |||
74 | 1 | Reference | ||
75 | 2 | ========= | ||
76 | 3 | |||
77 | 4 | .. automodule:: piston_mini_client | ||
78 | 5 | :members: | ||
79 | 6 | |||
80 | 7 | ========== | ||
81 | 8 | Validators | ||
82 | 9 | ========== | ||
83 | 10 | |||
84 | 11 | .. automodule:: piston_mini_client.validators | ||
85 | 12 | :members: | ||
86 | 13 | |||
87 | 14 | ============= | ||
88 | 15 | Fail handlers | ||
89 | 16 | ============= | ||
90 | 17 | |||
91 | 18 | .. automodule:: piston_mini_client.failhandlers | ||
92 | 19 | :members: | ||
93 | 20 | |||
94 | 21 | =========== | ||
95 | 22 | Serializers | ||
96 | 23 | =========== | ||
97 | 24 | |||
98 | 25 | .. automodule:: piston_mini_client.serializers | ||
99 | 26 | :members: | ||
100 | 27 | |||
101 | 28 | ============== | ||
102 | 29 | Authentication | ||
103 | 30 | ============== | ||
104 | 31 | |||
105 | 32 | .. automodule:: piston_mini_client.auth | ||
106 | 33 | :members: | ||
107 | 0 | 34 | ||
108 | === modified file 'piston_mini_client/__init__.py' | |||
109 | --- piston_mini_client/__init__.py 2011-01-12 17:21:36 +0000 | |||
110 | +++ piston_mini_client/__init__.py 2011-01-20 14:24:08 +0000 | |||
111 | @@ -6,6 +6,7 @@ | |||
112 | 6 | import simplejson | 6 | import simplejson |
113 | 7 | import urllib | 7 | import urllib |
114 | 8 | from functools import wraps | 8 | from functools import wraps |
115 | 9 | from urlparse import urlparse, urlunparse | ||
116 | 9 | 10 | ||
117 | 10 | from failhandlers import ExceptionFailHandler, APIError | 11 | from failhandlers import ExceptionFailHandler, APIError |
118 | 11 | 12 | ||
119 | @@ -61,8 +62,7 @@ | |||
120 | 61 | 62 | ||
121 | 62 | 63 | ||
122 | 63 | class PistonResponseObject(object): | 64 | class PistonResponseObject(object): |
125 | 64 | """Base class for objects that are returned from api calls. | 65 | """Base class for objects that are returned from api calls.""" |
124 | 65 | """ | ||
126 | 66 | @classmethod | 66 | @classmethod |
127 | 67 | def from_response(cls, body, none_allowed=False): | 67 | def from_response(cls, body, none_allowed=False): |
128 | 68 | data = simplejson.loads(body) | 68 | data = simplejson.loads(body) |
129 | @@ -79,8 +79,7 @@ | |||
130 | 79 | return obj | 79 | return obj |
131 | 80 | 80 | ||
132 | 81 | class PistonSerializable(object): | 81 | class PistonSerializable(object): |
135 | 82 | """Base class for objects that want to be used as api call arguments. | 82 | """Base class for objects that want to be used as api call arguments.""" |
134 | 83 | """ | ||
136 | 84 | _atts = () | 83 | _atts = () |
137 | 85 | 84 | ||
138 | 86 | def __init__(self, **kwargs): | 85 | def __init__(self, **kwargs): |
139 | @@ -119,11 +118,17 @@ | |||
140 | 119 | def __init__(self, service_root=None, cachedir=None, auth=None): | 118 | def __init__(self, service_root=None, cachedir=None, auth=None): |
141 | 120 | if service_root is None: | 119 | if service_root is None: |
142 | 121 | service_root = self.default_service_root | 120 | service_root = self.default_service_root |
143 | 121 | if not service_root: | ||
144 | 122 | raise ValueError("No service_root provided, and no default found") | ||
145 | 123 | parsed_service_root = urlparse(service_root) | ||
146 | 124 | if parsed_service_root.scheme not in ['http', 'https']: | ||
147 | 125 | raise ValueError("service_root's scheme must be http or https") | ||
148 | 122 | self._service_root = service_root | 126 | self._service_root = service_root |
149 | 127 | self._parsed_service_root = list(parsed_service_root) | ||
150 | 123 | self._cachedir = cachedir | 128 | self._cachedir = cachedir |
151 | 124 | self._auth = auth | 129 | self._auth = auth |
152 | 125 | 130 | ||
154 | 126 | def _get(self, path, args=None): | 131 | def _get(self, path, args=None, scheme=None): |
155 | 127 | """Perform an HTTP GET request. | 132 | """Perform an HTTP GET request. |
156 | 128 | 133 | ||
157 | 129 | The provided 'path' is appended to this resource's '_service_root' | 134 | The provided 'path' is appended to this resource's '_service_root' |
158 | @@ -131,6 +136,10 @@ | |||
159 | 131 | 136 | ||
160 | 132 | If provided, 'args' should be a dict specifying additional GET | 137 | If provided, 'args' should be a dict specifying additional GET |
161 | 133 | arguments that will be encoded on to the end of the url. | 138 | arguments that will be encoded on to the end of the url. |
162 | 139 | |||
163 | 140 | 'scheme' must be one of 'http' or 'https', and will determine the | ||
164 | 141 | scheme used for this particular request. If not provided the | ||
165 | 142 | service_root's scheme will be used. | ||
166 | 134 | """ | 143 | """ |
167 | 135 | if args is not None: | 144 | if args is not None: |
168 | 136 | if '?' in path: | 145 | if '?' in path: |
169 | @@ -138,9 +147,9 @@ | |||
170 | 138 | else: | 147 | else: |
171 | 139 | path += '?' | 148 | path += '?' |
172 | 140 | path += urllib.urlencode(args) | 149 | path += urllib.urlencode(args) |
174 | 141 | return self._request(path, method='GET') | 150 | return self._request(path, method='GET', scheme=scheme) |
175 | 142 | 151 | ||
177 | 143 | def _post(self, path, data=None, content_type=None): | 152 | def _post(self, path, data=None, content_type=None, scheme=None): |
178 | 144 | """Perform an HTTP POST request. | 153 | """Perform an HTTP POST request. |
179 | 145 | 154 | ||
180 | 146 | The provided path is appended to this api's '_service_root' attribute | 155 | The provided path is appended to this api's '_service_root' attribute |
181 | @@ -153,6 +162,10 @@ | |||
182 | 153 | serializer from serializers. | 162 | serializer from serializers. |
183 | 154 | 163 | ||
184 | 155 | If content_type is None, self.default_content_type will be used. | 164 | If content_type is None, self.default_content_type will be used. |
185 | 165 | |||
186 | 166 | 'scheme' must be one of 'http' or 'https', and will determine the | ||
187 | 167 | scheme used for this particular request. If not provided the | ||
188 | 168 | service_root's scheme will be used. | ||
189 | 156 | """ | 169 | """ |
190 | 157 | body = data | 170 | body = data |
191 | 158 | if content_type is None: | 171 | if content_type is None: |
192 | @@ -163,9 +176,10 @@ | |||
193 | 163 | serializer = get_serializer(content_type)(data) | 176 | serializer = get_serializer(content_type)(data) |
194 | 164 | body = serializer.serialize() | 177 | body = serializer.serialize() |
195 | 165 | headers = {'Content-type': content_type} | 178 | headers = {'Content-type': content_type} |
197 | 166 | return self._request(path, method='POST', body=body, headers=headers) | 179 | return self._request(path, method='POST', body=body, |
198 | 180 | headers=headers, scheme=scheme) | ||
199 | 167 | 181 | ||
201 | 168 | def _request(self, path, method, body='', headers=None): | 182 | def _request(self, path, method, body='', headers=None, scheme=None): |
202 | 169 | """Perform an HTTP request. | 183 | """Perform an HTTP request. |
203 | 170 | 184 | ||
204 | 171 | You probably want to call one of the _get, _post methods instead. | 185 | You probably want to call one of the _get, _post methods instead. |
205 | @@ -173,7 +187,9 @@ | |||
206 | 173 | http = httplib2.Http(cache=self._cachedir) | 187 | http = httplib2.Http(cache=self._cachedir) |
207 | 174 | if headers is None: | 188 | if headers is None: |
208 | 175 | headers = {} | 189 | headers = {} |
210 | 176 | url = self._path2url(path) | 190 | if scheme not in [None, 'http', 'https']: |
211 | 191 | raise ValueError('Invalid scheme %s' % scheme) | ||
212 | 192 | url = self._path2url(path, scheme=scheme) | ||
213 | 177 | if self._auth: | 193 | if self._auth: |
214 | 178 | self._auth.sign_request(url, method, body, headers) | 194 | self._auth.sign_request(url, method, body, headers) |
215 | 179 | try: | 195 | try: |
216 | @@ -189,6 +205,11 @@ | |||
217 | 189 | body = handler.handle(response, body) | 205 | body = handler.handle(response, body) |
218 | 190 | return body | 206 | return body |
219 | 191 | 207 | ||
222 | 192 | def _path2url(self, path): | 208 | def _path2url(self, path, scheme=None): |
223 | 193 | return self._service_root.rstrip('/') + '/' + path.lstrip('/') | 209 | if scheme is None: |
224 | 210 | service_root = self._service_root | ||
225 | 211 | else: | ||
226 | 212 | parts = [scheme] + self._parsed_service_root[1:] | ||
227 | 213 | service_root = urlunparse(parts) | ||
228 | 214 | return service_root.strip('/') + '/' + path.lstrip('/') | ||
229 | 194 | 215 | ||
230 | 195 | 216 | ||
231 | === modified file 'piston_mini_client/failhandlers.py' | |||
232 | --- piston_mini_client/failhandlers.py 2011-01-18 14:44:10 +0000 | |||
233 | +++ piston_mini_client/failhandlers.py 2011-01-20 14:24:08 +0000 | |||
234 | @@ -8,6 +8,15 @@ | |||
235 | 8 | possibly decorators) to know what to do with these returned objects. | 8 | possibly decorators) to know what to do with these returned objects. |
236 | 9 | """ | 9 | """ |
237 | 10 | 10 | ||
238 | 11 | __all__ = [ | ||
239 | 12 | 'APIError', | ||
240 | 13 | 'BaseFailHandler', | ||
241 | 14 | 'ExceptionFailHandler', | ||
242 | 15 | 'DictFailHandler', | ||
243 | 16 | 'NoneFailHandler', | ||
244 | 17 | 'MultiExceptionFailHandler', | ||
245 | 18 | ] | ||
246 | 19 | |||
247 | 11 | class APIError(Exception): | 20 | class APIError(Exception): |
248 | 12 | def __init__(self, msg, body=None): | 21 | def __init__(self, msg, body=None): |
249 | 13 | self.msg = msg | 22 | self.msg = msg |
250 | @@ -53,7 +62,7 @@ | |||
251 | 53 | went wrong. | 62 | went wrong. |
252 | 54 | """ | 63 | """ |
253 | 55 | def handle(self, response, body): | 64 | def handle(self, response, body): |
255 | 56 | """Raise APIError if a strange status code is found""" | 65 | """Return None if a strange status code is found""" |
256 | 57 | if self.was_error(response): | 66 | if self.was_error(response): |
257 | 58 | return None | 67 | return None |
258 | 59 | return body | 68 | return body |
259 | @@ -63,6 +72,12 @@ | |||
260 | 63 | """A fail handler that returns error information in a dict""" | 72 | """A fail handler that returns error information in a dict""" |
261 | 64 | 73 | ||
262 | 65 | def handle(self, response, body): | 74 | def handle(self, response, body): |
263 | 75 | """Return a dict if a strange status code is found. | ||
264 | 76 | |||
265 | 77 | The returned dict will have two keys: | ||
266 | 78 | * 'response': the httplib2 response header dict | ||
267 | 79 | * 'body': the response body, as a string | ||
268 | 80 | """ | ||
269 | 66 | if self.was_error(response): | 81 | if self.was_error(response): |
270 | 67 | return {'response': response, 'body': body} | 82 | return {'response': response, 'body': body} |
271 | 68 | return body | 83 | return body |
272 | @@ -94,6 +109,14 @@ | |||
273 | 94 | } | 109 | } |
274 | 95 | 110 | ||
275 | 96 | def handle(self, response, body): | 111 | def handle(self, response, body): |
276 | 112 | """Return an exception according to what went wrong. | ||
277 | 113 | |||
278 | 114 | Status codes currently returning their own exception class are: | ||
279 | 115 | * 400: BadRequestError, | ||
280 | 116 | * 401: UnauthorizedError, | ||
281 | 117 | * 404: NotFoundError, and | ||
282 | 118 | * 500: InternalServerErrorError | ||
283 | 119 | """ | ||
284 | 97 | if self.was_error(response): | 120 | if self.was_error(response): |
285 | 98 | status = response.get('status') | 121 | status = response.get('status') |
286 | 99 | exception_class = self.exceptions.get(status, APIError) | 122 | exception_class = self.exceptions.get(status, APIError) |
287 | 100 | 123 | ||
288 | === modified file 'piston_mini_client/serializers.py' | |||
289 | --- piston_mini_client/serializers.py 2011-01-12 17:21:36 +0000 | |||
290 | +++ piston_mini_client/serializers.py 2011-01-20 14:24:08 +0000 | |||
291 | @@ -2,6 +2,12 @@ | |||
292 | 2 | # Copyright 2010-2011 Canonical Ltd. This software is licensed under the | 2 | # Copyright 2010-2011 Canonical Ltd. This software is licensed under the |
293 | 3 | # GNU Lesser General Public License version 3 (see the file LICENSE). | 3 | # GNU Lesser General Public License version 3 (see the file LICENSE). |
294 | 4 | 4 | ||
295 | 5 | __all__ = [ | ||
296 | 6 | 'BaseSerializer', | ||
297 | 7 | 'JSONSerializer', | ||
298 | 8 | 'FormSerializer', | ||
299 | 9 | ] | ||
300 | 10 | |||
301 | 5 | import simplejson | 11 | import simplejson |
302 | 6 | import urllib | 12 | import urllib |
303 | 7 | 13 | ||
304 | 8 | 14 | ||
305 | === modified file 'piston_mini_client/test/test_resource.py' | |||
306 | --- piston_mini_client/test/test_resource.py 2011-01-18 14:44:10 +0000 | |||
307 | +++ piston_mini_client/test/test_resource.py 2011-01-20 14:24:08 +0000 | |||
308 | @@ -13,7 +13,6 @@ | |||
309 | 13 | 13 | ||
310 | 14 | 14 | ||
311 | 15 | class PistonAPITestCase(TestCase): | 15 | class PistonAPITestCase(TestCase): |
312 | 16 | # XXX achuni 2010-11-30: We need to test different return codes | ||
313 | 17 | class CoffeeAPI(PistonAPI): | 16 | class CoffeeAPI(PistonAPI): |
314 | 18 | default_service_root = 'http://localhost:12345' | 17 | default_service_root = 'http://localhost:12345' |
315 | 19 | def brew(self): | 18 | def brew(self): |
316 | @@ -26,6 +25,7 @@ | |||
317 | 26 | 'auth': environ.get('HTTP_AUTHORIZATION'), | 25 | 'auth': environ.get('HTTP_AUTHORIZATION'), |
318 | 27 | 'content_type': environ.get('CONTENT_TYPE'), | 26 | 'content_type': environ.get('CONTENT_TYPE'), |
319 | 28 | 'query': environ.get('QUERY_STRING'), | 27 | 'query': environ.get('QUERY_STRING'), |
320 | 28 | 'scheme': environ.get('wsgi.url_scheme'), | ||
321 | 29 | } | 29 | } |
322 | 30 | return 'hello world' | 30 | return 'hello world' |
323 | 31 | 31 | ||
324 | @@ -97,17 +97,51 @@ | |||
325 | 97 | self.assertEqual('zot=ping&foo=bar', self.called['query']) | 97 | self.assertEqual('zot=ping&foo=bar', self.called['query']) |
326 | 98 | 98 | ||
327 | 99 | def test_path2url_with_no_ending_slash(self): | 99 | def test_path2url_with_no_ending_slash(self): |
330 | 100 | resource = PistonAPI() | 100 | resource = PistonAPI('http://example.com/api') |
329 | 101 | resource._service_root = 'http://example.com/api' | ||
331 | 102 | expected = 'http://example.com/api/frobble' | 101 | expected = 'http://example.com/api/frobble' |
332 | 103 | self.assertEqual(expected, resource._path2url('frobble')) | 102 | self.assertEqual(expected, resource._path2url('frobble')) |
333 | 104 | 103 | ||
334 | 105 | def test_path2url_with_ending_slash(self): | 104 | def test_path2url_with_ending_slash(self): |
337 | 106 | resource = PistonAPI() | 105 | resource = PistonAPI('http://example.com/api/') |
336 | 107 | resource._service_root = 'http://example.com/api/' | ||
338 | 108 | expected = 'http://example.com/api/frobble' | 106 | expected = 'http://example.com/api/frobble' |
339 | 109 | self.assertEqual(expected, resource._path2url('frobble')) | 107 | self.assertEqual(expected, resource._path2url('frobble')) |
340 | 110 | 108 | ||
341 | 109 | def test_instantiation_fails_with_no_service_root(self): | ||
342 | 110 | try: | ||
343 | 111 | self.CoffeeAPI.default_service_root = None | ||
344 | 112 | self.assertRaises(ValueError, self.CoffeeAPI) | ||
345 | 113 | finally: | ||
346 | 114 | self.CoffeeAPI.default_service_root = 'http://localhost:12345' | ||
347 | 115 | |||
348 | 116 | |||
349 | 117 | def test_instantiation_fails_with_invalid_scheme(self): | ||
350 | 118 | self.assertRaises(ValueError, self.CoffeeAPI, 'ftp://foobar.baz') | ||
351 | 119 | |||
352 | 120 | @patch('httplib2.Http.request') | ||
353 | 121 | def test_request_scheme_switch_to_https(self, mock_request): | ||
354 | 122 | mock_request.return_value = ({'status': '200'}, '""') | ||
355 | 123 | api = self.CoffeeAPI() | ||
356 | 124 | api._request('/foo', 'GET', scheme='https') | ||
357 | 125 | mock_request.assert_called_with('https://localhost:12345/foo', | ||
358 | 126 | body='', headers={}, method='GET') | ||
359 | 127 | |||
360 | 128 | @patch('httplib2.Http.request') | ||
361 | 129 | def test_get_scheme_switch_to_https(self, mock_request): | ||
362 | 130 | mock_request.return_value = ({'status': '200'}, '""') | ||
363 | 131 | api = self.CoffeeAPI() | ||
364 | 132 | api._get('/foo', scheme='https') | ||
365 | 133 | mock_request.assert_called_with('https://localhost:12345/foo', | ||
366 | 134 | body='', headers={}, method='GET') | ||
367 | 135 | |||
368 | 136 | @patch('httplib2.Http.request') | ||
369 | 137 | def test_post_scheme_switch_to_https(self, mock_request): | ||
370 | 138 | mock_request.return_value = ({'status': '200'}, '""') | ||
371 | 139 | api = self.CoffeeAPI() | ||
372 | 140 | api._post('/foo', scheme='https') | ||
373 | 141 | mock_request.assert_called_with('https://localhost:12345/foo', | ||
374 | 142 | body='null', headers={'Content-type': 'application/json'}, | ||
375 | 143 | method='POST') | ||
376 | 144 | |||
377 | 111 | 145 | ||
378 | 112 | class PistonResponseObjectTestCase(TestCase): | 146 | class PistonResponseObjectTestCase(TestCase): |
379 | 113 | def test_from_response(self): | 147 | def test_from_response(self): |
380 | @@ -122,7 +156,7 @@ | |||
381 | 122 | class ReturnsJSONTestCase(TestCase): | 156 | class ReturnsJSONTestCase(TestCase): |
382 | 123 | def test_returns_json(self): | 157 | def test_returns_json(self): |
383 | 124 | class MyAPI(PistonAPI): | 158 | class MyAPI(PistonAPI): |
385 | 125 | default_service_root = 'foo' | 159 | default_service_root = 'http://foo' |
386 | 126 | @returns_json | 160 | @returns_json |
387 | 127 | def func(self): | 161 | def func(self): |
388 | 128 | return '{"foo": "bar", "baz": 42}' | 162 | return '{"foo": "bar", "baz": 42}' |
389 | @@ -134,7 +168,7 @@ | |||
390 | 134 | class ReturnsTestCase(TestCase): | 168 | class ReturnsTestCase(TestCase): |
391 | 135 | def test_returns(self): | 169 | def test_returns(self): |
392 | 136 | class MyAPI(PistonAPI): | 170 | class MyAPI(PistonAPI): |
394 | 137 | default_service_root = 'foo' | 171 | default_service_root = 'http://foo' |
395 | 138 | @returns(PistonResponseObject) | 172 | @returns(PistonResponseObject) |
396 | 139 | def func(self): | 173 | def func(self): |
397 | 140 | return '{"foo": "bar", "baz": 42}' | 174 | return '{"foo": "bar", "baz": 42}' |
398 | @@ -144,7 +178,7 @@ | |||
399 | 144 | 178 | ||
400 | 145 | def test_returns_none_allowed(self): | 179 | def test_returns_none_allowed(self): |
401 | 146 | class MyAPI(PistonAPI): | 180 | class MyAPI(PistonAPI): |
403 | 147 | default_service_root = 'foo' | 181 | default_service_root = 'http://foo' |
404 | 148 | @returns(PistonResponseObject, none_allowed=True) | 182 | @returns(PistonResponseObject, none_allowed=True) |
405 | 149 | def func(self): | 183 | def func(self): |
406 | 150 | return 'null' | 184 | return 'null' |
407 | @@ -154,7 +188,7 @@ | |||
408 | 154 | 188 | ||
409 | 155 | def test_returns_none_allowed_normal_response(self): | 189 | def test_returns_none_allowed_normal_response(self): |
410 | 156 | class MyAPI(PistonAPI): | 190 | class MyAPI(PistonAPI): |
412 | 157 | default_service_root = 'foo' | 191 | default_service_root = 'http://foo' |
413 | 158 | @returns(PistonResponseObject, none_allowed=True) | 192 | @returns(PistonResponseObject, none_allowed=True) |
414 | 159 | def func(self): | 193 | def func(self): |
415 | 160 | return '{"foo": "bar", "baz": 42}' | 194 | return '{"foo": "bar", "baz": 42}' |
416 | @@ -166,7 +200,7 @@ | |||
417 | 166 | class ReturnsListOfTestCase(TestCase): | 200 | class ReturnsListOfTestCase(TestCase): |
418 | 167 | def test_returns(self): | 201 | def test_returns(self): |
419 | 168 | class MyAPI(PistonAPI): | 202 | class MyAPI(PistonAPI): |
421 | 169 | default_service_root = 'foo' | 203 | default_service_root = 'http://foo' |
422 | 170 | @returns_list_of(PistonResponseObject) | 204 | @returns_list_of(PistonResponseObject) |
423 | 171 | def func(self): | 205 | def func(self): |
424 | 172 | return '[{"foo": "bar"}, {"baz": 42}]' | 206 | return '[{"foo": "bar"}, {"baz": 42}]' |
Approved. Nice docs!