Imported Debian patch 0.9.14-1
Luca Falavigna
14 years ago
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: lazr.restfulclient |
2 | Version: 0.9.13 | |
2 | Version: 0.9.14 | |
3 | 3 | Summary: This is a template for your lazr package. To start your own lazr package, |
4 | 4 | Home-page: https://launchpad.net/lazr.restfulclient |
5 | 5 | Author: LAZR Developers |
7 | 7 | License: LGPL v3 |
8 | 8 | Download-URL: https://launchpad.net/lazr.restfulclient/+download |
9 | 9 | Description: .. |
10 | This file is part of lazr.restfulclient. | |
10 | This file is part of lazr.restfulclient. | |
11 | 11 | |
12 | lazr.restfulclient is free software: you can redistribute it and/or modify it | |
13 | under the terms of the GNU Lesser General Public License as published by | |
14 | the Free Software Foundation, version 3 of the License. | |
12 | lazr.restfulclient is free software: you can redistribute it and/or modify it | |
13 | under the terms of the GNU Lesser General Public License as published by | |
14 | the Free Software Foundation, version 3 of the License. | |
15 | 15 | |
16 | lazr.restfulclient is distributed in the hope that it will be useful, but | |
17 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | |
18 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | |
19 | License for more details. | |
16 | lazr.restfulclient is distributed in the hope that it will be useful, but | |
17 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | |
18 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | |
19 | License for more details. | |
20 | 20 | |
21 | You should have received a copy of the GNU Lesser General Public License | |
22 | along with lazr.restfulclient. If not, see <http://www.gnu.org/licenses/>. | |
21 | You should have received a copy of the GNU Lesser General Public License | |
22 | along with lazr.restfulclient. If not, see <http://www.gnu.org/licenses/>. | |
23 | 23 | |
24 | 24 | LAZR restfulclient |
25 | 25 | ************ |
34 | 34 | directory, you should probably improve it. |
35 | 35 | |
36 | 36 | .. toctree:: |
37 | :glob: | |
37 | :glob: | |
38 | 38 | |
39 | * | |
40 | docs/* | |
39 | * | |
40 | docs/* | |
41 | 41 | |
42 | 42 | .. _Sphinx: http://sphinx.pocoo.org/ |
43 | 43 | .. _Table of contents: http://sphinx.pocoo.org/concepts.html#the-toc-tree |
47 | 47 | |
48 | 48 | The lazr.restfulclient package is importable, and has a version number. |
49 | 49 | |
50 | >>> import lazr.restfulclient | |
51 | >>> print 'VERSION:', lazr.restfulclient.__version__ | |
52 | VERSION: ... | |
50 | >>> import lazr.restfulclient | |
51 | >>> print 'VERSION:', lazr.restfulclient.__version__ | |
52 | VERSION: ... | |
53 | 53 | |
54 | 54 | =========================== |
55 | 55 | NEWS for lazr.restfulclient |
56 | 56 | =========================== |
57 | 57 | |
58 | 0.9.14 (2010-04-15) | |
59 | =================== | |
60 | ||
61 | - Clients now send a useful and somewhat customizable User-Agent | |
62 | string. | |
63 | ||
64 | - Added a workaround for a bug in httplib2. | |
65 | ||
66 | - Removed the software dependency on lazr.restful except when running | |
67 | the full test suite. (The standalone_test test suite tests basic | |
68 | functionality of lazr.restfulclient to make sure the code base | |
69 | doesn't fundamentally depend on lazr.restful.) | |
70 | ||
58 | 71 | 0.9.13 (2010-03-24) |
59 | 72 | =================== |
60 | 73 | |
61 | - Removed of some no-longer-needed compatibility code for buggy | |
62 | servers, and fixed the tests to work with the new release of simplejson. | |
74 | - Removed some no-longer-needed compatibility code for buggy | |
75 | servers, and fixed the tests to work with the new release of simplejson. | |
63 | 76 | |
64 | 77 | - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't |
65 | strict enough. The maximum filename length is now 143 characters. | |
78 | strict enough. The maximum filename length is now 143 characters. | |
66 | 79 | |
67 | 80 | 0.9.12 (2010-03-09) |
68 | 81 | =================== |
69 | 82 | |
70 | 83 | - Fixed a bug that prevented a unicode string from being used as a |
71 | cache filename. | |
84 | cache filename. | |
72 | 85 | |
73 | 86 | 0.9.11 (2010-02-11) |
74 | 87 | =================== |
75 | 88 | |
76 | 89 | - If a lazr.restful web service publishes multiple versions, you can |
77 | now specify which version to use in a separate constructor argument, | |
78 | rather than sticking it on to the end of the service root. | |
90 | now specify which version to use in a separate constructor argument, | |
91 | rather than sticking it on to the end of the service root. | |
79 | 92 | - Filenames in the cache will never be longer than 150 characters, |
80 | to avoid errors on eCryptfs filesystems. | |
93 | to avoid errors on eCryptfs filesystems. | |
81 | 94 | - Added a proof-of-concept test for OAuth-signed anonymous access. |
82 | 95 | - Fixed comparisons of entries and hosted files with None. |
83 | 96 | |
88 | 101 | - Made HTTPError strings more verbose. |
89 | 102 | - Implemented the equality operator for entry and hosted-file resources. |
90 | 103 | - Resume setting the 'credentials' attribute on ServerRoot to avoid |
91 | breaking compatibility with launchpadlib. | |
104 | breaking compatibility with launchpadlib. | |
92 | 105 | |
93 | 106 | 0.9.9 (2009-10-07) |
94 | 107 | ================== |
95 | 108 | |
96 | 109 | - The WSGI authentication middleware has been moved from lazr.restful |
97 | to the new lazr.authentication library, and lazr.restfulclient now | |
98 | uses the new library. | |
110 | to the new lazr.authentication library, and lazr.restfulclient now | |
111 | uses the new library. | |
99 | 112 | |
100 | 113 | 0.9.8 (2009-10-06) |
101 | 114 | ================== |
125 | 138 | - Updated tests for newer version of simplejson. |
126 | 139 | |
127 | 140 | - Made tests less fragile by cleaning up lazr.restful example filemanager |
128 | between tests. | |
141 | between tests. | |
129 | 142 | |
130 | 143 | - normalized output of simplejson to unicode. |
131 | 144 | |
138 | 151 | ================== |
139 | 152 | |
140 | 153 | - Fields that can contain binary data are no longer run through |
141 | simplejson.dumps(). | |
154 | simplejson.dumps(). | |
142 | 155 | |
143 | 156 | - For fields that can take on a limited set of values, you can now get |
144 | a list of possible values. | |
157 | a list of possible values. | |
145 | 158 | |
146 | 159 | 0.9.1 (2009-07-13) |
147 | 160 | ================== |
148 | 161 | |
149 | 162 | - The client now knows to look for multipart/form-data representations |
150 | and will create them as appropriate. The upshot of this is that you | |
151 | can now send binary data when invoking named operations that will | |
152 | accept binary data. | |
163 | and will create them as appropriate. The upshot of this is that you | |
164 | can now send binary data when invoking named operations that will | |
165 | accept binary data. | |
153 | 166 | |
154 | 167 | |
155 | 168 | 0.9 (2009-04-29) |
0 | lazr.restfulclient (0.9.14-1) unstable; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | * debian/control: | |
4 | - Remove Conflicts/Replaces with python-lazr-restfulclient, they were | |
5 | useful for Ubuntu Lucid only. | |
6 | ||
7 | -- Luca Falavigna <dktrkranz@debian.org> Thu, 29 Apr 2010 21:42:36 +0200 | |
8 | ||
0 | 9 | lazr.restfulclient (0.9.13+ds-1) unstable; urgency=low |
1 | 10 | |
2 | 11 | * New upstream release. |
12 | 12 | Package: python-lazr.restfulclient |
13 | 13 | Architecture: all |
14 | 14 | Depends: ${python:Depends}, ${misc:Depends}, python-zope.interface, python-wadllib (>= 1.1.4), python-pkg-resources, python-simplejson, python-httplib2 |
15 | Conflicts: python-lazr-restfulclient (<< 0.9.9-1) | |
16 | Replaces: python-lazr-restfulclient (<< 0.9.9-1) | |
17 | 15 | Description: client for lazr.restful-based web services |
18 | 16 | A programmable client library that takes advantage of the commonalities |
19 | 17 | among lazr.rest web services to provide added functionality on top |
0 | 0 | version=3 |
1 | opts=dversionmangle=s/\+ds// \ | |
2 | 1 | https://launchpad.net/lazr.restfulclient/+download http://launchpad.net/lazr.restfulclient/.*/lazr.restfulclient-(.+).tar.gz |
56 | 56 | install_requires=[ |
57 | 57 | 'httplib2', |
58 | 58 | 'lazr.authentication', |
59 | 'lazr.restful>=0.9.18', | |
60 | 59 | 'oauth', |
61 | 60 | 'setuptools', |
62 | 61 | 'wadllib>=1.1.4', |
63 | 62 | 'wsgi_intercept', |
64 | 'van.testing', | |
65 | 'zope.interface', | |
66 | 63 | ], |
67 | 64 | url='https://launchpad.net/lazr.restfulclient', |
68 | 65 | download_url= 'https://launchpad.net/lazr.restfulclient/+download', |
74 | 71 | "Programming Language :: Python"], |
75 | 72 | extras_require=dict( |
76 | 73 | docs=['Sphinx', |
77 | 'z3c.recipe.sphinxdoc'] | |
74 | 'z3c.recipe.sphinxdoc'], | |
75 | test=['lazr.restful>=0.9.25', | |
76 | 'van.testing'], | |
78 | 77 | ), |
79 | 78 | test_suite='lazr.restfulclient.tests', |
80 | 79 | ) |
1 | 1 | NEWS for lazr.restfulclient |
2 | 2 | =========================== |
3 | 3 | |
4 | 0.9.14 (2010-04-15) | |
5 | =================== | |
6 | ||
7 | - Clients now send a useful and somewhat customizable User-Agent | |
8 | string. | |
9 | ||
10 | - Added a workaround for a bug in httplib2. | |
11 | ||
12 | - Removed the software dependency on lazr.restful except when running | |
13 | the full test suite. (The standalone_test test suite tests basic | |
14 | functionality of lazr.restfulclient to make sure the code base | |
15 | doesn't fundamentally depend on lazr.restful.) | |
16 | ||
4 | 17 | 0.9.13 (2010-03-24) |
5 | 18 | =================== |
6 | 19 | |
7 | - Removed of some no-longer-needed compatibility code for buggy | |
20 | - Removed some no-longer-needed compatibility code for buggy | |
8 | 21 | servers, and fixed the tests to work with the new release of simplejson. |
9 | 22 | |
10 | 23 | - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't |
223 | 223 | class Browser: |
224 | 224 | """A class for making calls to lazr.restful web services.""" |
225 | 225 | |
226 | NOT_MODIFIED = object() | |
227 | ||
226 | 228 | def __init__(self, service_root, credentials, cache=None, timeout=None, |
227 | proxy_info=None): | |
229 | proxy_info=None, user_agent=None): | |
228 | 230 | """Initialize, possibly creating a cache. |
229 | 231 | |
230 | 232 | If no cache is provided, a temporary directory will be used as |
238 | 240 | cache = MultipleRepresentationCache(cache) |
239 | 241 | self._connection = service_root.httpFactory( |
240 | 242 | credentials, cache, timeout, proxy_info) |
243 | self.user_agent = user_agent | |
241 | 244 | |
242 | 245 | def _request(self, url, data=None, method='GET', |
243 | 246 | media_type='application/json', extra_headers=None): |
250 | 253 | |
251 | 254 | # Add extra headers for the request. |
252 | 255 | headers = {'Accept' : media_type} |
256 | if self.user_agent is not None: | |
257 | headers['User-Agent'] = self.user_agent | |
253 | 258 | if isinstance(self._connection.cache, MultipleRepresentationCache): |
254 | 259 | self._connection.cache.request_media_type = media_type |
255 | 260 | if extra_headers is not None: |
257 | 262 | # Make the request. |
258 | 263 | response, content = self._connection.request( |
259 | 264 | str(url), method=method, body=data, headers=headers) |
265 | if response.status == 304: | |
266 | # The resource didn't change. | |
267 | if content == '': | |
268 | if ('If-None-Match' in headers | |
269 | or 'If-Modified-Since' in headers): | |
270 | # The caller made a conditional request, and the | |
271 | # condition failed. Rather than send an empty | |
272 | # representation, which might be misinterpreted, | |
273 | # send a special object that will let the calling code know | |
274 | # that the resource was not modified. | |
275 | return response, self.NOT_MODIFIED | |
276 | else: | |
277 | # The caller didn't make a conditional request, | |
278 | # but the response code is 304 and there's no | |
279 | # content. The only way to handle this is to raise | |
280 | # an error. | |
281 | raise HTTPError(response, content) | |
282 | else: | |
283 | # XXX leonardr 2010/04/12 bug=httplib2#97 | |
284 | # | |
285 | # Why is this check here? Why would there ever be any | |
286 | # content when the response code is 304? It's because of | |
287 | # an httplib2 bug that sometimes sets a 304 response | |
288 | # code when caching retrieved documents. When the | |
289 | # cached document is retrieved, we get a 304 response | |
290 | # code and a full representation. | |
291 | # | |
292 | # Since the cache lookup succeeded, the 'real' | |
293 | # response code is 200. This code undoes the bad | |
294 | # behavior in httplib2. | |
295 | response.status = 200 | |
296 | return response, content | |
260 | 297 | # Turn non-2xx responses into exceptions. |
261 | 298 | if response.status // 100 != 2: |
262 | 299 | raise HTTPError(response, content) |
54 | 54 | """ |
55 | 55 | pass |
56 | 56 | |
57 | @property | |
58 | def user_agent_params(self): | |
59 | """Any parameters necessary to identify this user agent. | |
60 | ||
61 | By default this is an empty dict (because authentication | |
62 | details don't contain any information about the application | |
63 | making the request), but when a resource is protected by | |
64 | OAuth, the OAuth consumer name is part of the user agent. | |
65 | """ | |
66 | return {} | |
67 | ||
57 | 68 | |
58 | 69 | class BasicHttpAuthorizer(HttpAuthorizer): |
59 | 70 | """Handles authentication for services that use HTTP Basic Auth.""" |
68 | 68 | self.consumer = Consumer(consumer_name, consumer_secret) |
69 | 69 | self.access_token = access_token |
70 | 70 | self.oauth_realm = oauth_realm |
71 | ||
72 | @property | |
73 | def user_agent_params(self): | |
74 | """Any information necessary to identify this user agent. | |
75 | ||
76 | In this case, the OAuth consumer name. | |
77 | """ | |
78 | if self.consumer is None: | |
79 | return {} | |
80 | return {'oauth_consumer' : self.consumer.key} | |
71 | 81 | |
72 | 82 | def load(self, readable_file): |
73 | 83 | """Load credentials from a file-like object. |
0 | Authorizers | |
1 | =========== | |
2 | ||
3 | Authorizers are objects that encapsulate knowledge about a particular | |
4 | web service's authentication scheme. lazr.restfulclient includes | |
5 | authorizers for common HTTP authentication schemes. | |
6 | ||
7 | The BasicHttpAuthorizer | |
8 | ----------------------- | |
9 | ||
10 | This authorizer handles HTTP Basic Auth. To test it, we'll create a | |
11 | fake web service that serves some dummy WADL. | |
12 | ||
13 | >>> import pkg_resources | |
14 | >>> wadl_string = pkg_resources.resource_string( | |
15 | ... 'wadllib.tests.data', 'launchpad-wadl.xml') | |
16 | ||
17 | >>> responses = { 'application/vnd.sun.wadl+xml' : wadl_string, | |
18 | ... 'application/json' : '{}' } | |
19 | ||
20 | >>> def dummy_application(environ, start_response): | |
21 | ... media_type = environ['HTTP_ACCEPT'] | |
22 | ... content = responses[media_type] | |
23 | ... start_response( | |
24 | ... '200', [('Content-type', media_type)]) | |
25 | ... return [content] | |
26 | ||
27 | ||
28 | The WADL file will be protected with HTTP Basic Auth. To access it, | |
29 | you'll need to provide a username of "user" and a password of | |
30 | "password". | |
31 | ||
32 | >>> def authenticate(username, password): | |
33 | ... """Accepts "user/password", rejects everything else. | |
34 | ... | |
35 | ... :return: The username, if the credentials are valid. | |
36 | ... None, otherwise. | |
37 | ... """ | |
38 | ... if username == "user" and password == "password": | |
39 | ... return username | |
40 | ... return None | |
41 | ||
42 | >>> from lazr.authentication.wsgi import BasicAuthMiddleware | |
43 | >>> def protected_application(): | |
44 | ... return BasicAuthMiddleware( | |
45 | ... dummy_application, authenticate_with=authenticate) | |
46 | ||
47 | Finally, we'll set up a WSGI intercept so that we can test the web | |
48 | service by making HTTP requests to http://api.launchpad.dev/. (This is | |
49 | the hostname mentioned in the WADL file.) | |
50 | ||
51 | >>> import wsgi_intercept | |
52 | >>> from wsgi_intercept.httplib2_intercept import install | |
53 | >>> install() | |
54 | >>> wsgi_intercept.add_wsgi_intercept( | |
55 | ... 'api.launchpad.dev', 80, protected_application) | |
56 | ||
57 | With no HttpAuthorizer, a ServiceRoot can't get access to the web service. | |
58 | ||
59 | >>> from lazr.restfulclient.resource import ServiceRoot | |
60 | >>> client = ServiceRoot(None, "http://api.launchpad.dev/") | |
61 | Traceback (most recent call last): | |
62 | ... | |
63 | HTTPError: HTTP Error 401: Unauthorized | |
64 | ... | |
65 | ||
66 | We can't get access if the authorizer doesn't have the right | |
67 | credentials. | |
68 | ||
69 | >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer | |
70 | ||
71 | >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword") | |
72 | >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/") | |
73 | Traceback (most recent call last): | |
74 | ... | |
75 | HTTPError: HTTP Error 401: Unauthorized | |
76 | ... | |
77 | ||
78 | If we provide the right credentials, we can retrieve the WADL. We'll | |
79 | still get an exception, because our fake web service is too fake for | |
80 | ServiceRoot--its 'service root' resource doesn't match the WADL--but | |
81 | we're able to make HTTP requests without getting 401 errors. | |
82 | ||
83 | Note that the HTTP request includes the User-Agent header, but that | |
84 | that header contains no special information about the authorization | |
85 | method. This will change when the authorization method is OAuth. | |
86 | ||
87 | >>> import httplib2 | |
88 | >>> httplib2.debuglevel = 1 | |
89 | ||
90 | >>> authorizer = BasicHttpAuthorizer("user", "password") | |
91 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
92 | send: 'GET / ...user-agent: lazr.restfulclient ...' | |
93 | ... | |
94 | ||
95 | Teardown. | |
96 | ||
97 | >>> httplib2.debuglevel = 0 | |
98 | >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) | |
99 | ||
100 | ||
101 | The OAuthAuthorizer | |
102 | ------------------- | |
103 | ||
104 | This authorizer handles OAuth authorization. To test it, we'll protect | |
105 | the dummy application with a piece of OAuth middleware. The middleware | |
106 | will accept only one consumer/token combination, though it will also | |
107 | allow anonymous access: if you pass in an empty token and secret, | |
108 | you'll get a lower level of access. | |
109 | ||
110 | >>> from oauth.oauth import OAuthConsumer, OAuthToken | |
111 | >>> valid_consumer = OAuthConsumer("consumer", '') | |
112 | >>> valid_token = OAuthToken("token", "secret") | |
113 | >>> empty_token = OAuthToken("", "") | |
114 | ||
115 | Our authenticate() implementation checks against the one valid | |
116 | consumer and token. | |
117 | ||
118 | >>> def authenticate(consumer, token, parameters): | |
119 | ... """Accepts the valid consumer and token, rejects everything else. | |
120 | ... | |
121 | ... :return: The consumer, if the credentials are valid. | |
122 | ... None, otherwise. | |
123 | ... """ | |
124 | ... if token.key == '' and token.secret == '': | |
125 | ... # Anonymous access. | |
126 | ... return consumer | |
127 | ... if consumer == valid_consumer and token == valid_token: | |
128 | ... return consumer | |
129 | ... return None | |
130 | ||
131 | Our data store helps the middleware look up consumer and token objects | |
132 | from the information provided in a signed OAuth request. | |
133 | ||
134 | >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore | |
135 | ||
136 | >>> class AnonymousAccessDataStore(SimpleOAuthDataStore): | |
137 | ... """A data store that will accept any consumer.""" | |
138 | ... def lookup_consumer(self, consumer): | |
139 | ... """If there's no matching consumer, just create one. | |
140 | ... | |
141 | ... This will let anonymous requests succeed with any | |
142 | ... consumer key.""" | |
143 | ... consumer = super( | |
144 | ... AnonymousAccessDataStore, self).lookup_consumer( | |
145 | ... consumer) | |
146 | ... if consumer is None: | |
147 | ... consumer = OAuthConsumer(consumer, '') | |
148 | ... return consumer | |
149 | ||
150 | >>> data_store = AnonymousAccessDataStore( | |
151 | ... {valid_consumer.key : valid_consumer}, | |
152 | ... {valid_token.key : valid_token, | |
153 | ... empty_token.key : empty_token}) | |
154 | ||
155 | Now we're ready to protect the dummy_application with OAuthMiddleware, | |
156 | using our authenticate() implementation and our data store. | |
157 | ||
158 | >>> from lazr.authentication.wsgi import OAuthMiddleware | |
159 | >>> def protected_application(): | |
160 | ... return OAuthMiddleware( | |
161 | ... dummy_application, realm="OAuth test", | |
162 | ... authenticate_with=authenticate, data_store=data_store) | |
163 | >>> wsgi_intercept.add_wsgi_intercept( | |
164 | ... 'api.launchpad.dev', 80, protected_application) | |
165 | ||
166 | Let's try out some clients. As you'd expect, you can't get through the | |
167 | middleware with no HTTPAuthorizer at all. | |
168 | ||
169 | >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer | |
170 | >>> client = ServiceRoot(None, "http://api.launchpad.dev/") | |
171 | Traceback (most recent call last): | |
172 | ... | |
173 | HTTPError: HTTP Error 401: Unauthorized | |
174 | ... | |
175 | ||
176 | Invalid credentials are also no help. | |
177 | ||
178 | >>> authorizer = OAuthAuthorizer( | |
179 | ... valid_consumer.key, access_token=OAuthToken("invalid", "token")) | |
180 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
181 | Traceback (most recent call last): | |
182 | ... | |
183 | HTTPError: HTTP Error 401: Unauthorized | |
184 | ... | |
185 | ||
186 | But valid credentials work fine (again, up to the point at which | |
187 | lazr.restfulclient runs against the limits of this simple web | |
188 | service). Note that the User-Agent header mentions the name of the | |
189 | OAuth consumer. | |
190 | ||
191 | >>> httplib2.debuglevel = 1 | |
192 | >>> authorizer = OAuthAuthorizer( | |
193 | ... valid_consumer.key, access_token=valid_token) | |
194 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
195 | send: 'GET /...user-agent: lazr.restfulclient... oauth_consumer="consumer"...' | |
196 | ... | |
197 | >>> httplib2.debuglevel = 0 | |
198 | ||
199 | It's even possible to get anonymous access by providing an empty | |
200 | access token. | |
201 | ||
202 | >>> authorizer = OAuthAuthorizer( | |
203 | ... valid_consumer.key, access_token=empty_token) | |
204 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
205 | ||
206 | Because of the way the AnonymousAccessDataStore (defined | |
207 | earlier in the test) works, you can even get anonymous access by | |
208 | specifying an OAuth consumer that's not in the server-side list of | |
209 | valid consumers. | |
210 | ||
211 | >>> authorizer = OAuthAuthorizer( | |
212 | ... "random consumer", access_token=empty_token) | |
213 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
214 | ||
215 | A ServiceRoot object has a 'credentials' attribute which contains the | |
216 | Authorizer used to authorize outgoing requests. | |
217 | ||
218 | >>> from lazr.restfulclient.resource import ServiceRoot | |
219 | >>> root = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
220 | >>> root.credentials | |
221 | <lazr.restfulclient.authorize.oauth.OAuthAuthorizer object...> | |
222 | ||
223 | If you try to provide credentials with an unrecognized OAuth consumer, | |
224 | you'll get an error--even if the credentials are valid. The data store | |
225 | used in this test only lets unrecognized OAuth consumers through when | |
226 | they request anonymous access. | |
227 | ||
228 | >>> authorizer = OAuthAuthorizer( | |
229 | ... 'random consumer', access_token=valid_token) | |
230 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
231 | Traceback (most recent call last): | |
232 | ... | |
233 | HTTPError: HTTP Error 401: Unauthorized | |
234 | ... | |
235 | ||
236 | >>> authorizer = OAuthAuthorizer( | |
237 | ... 'random consumer', access_token=OAuthToken("invalid", "token")) | |
238 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
239 | Traceback (most recent call last): | |
240 | ... | |
241 | HTTPError: HTTP Error 401: Unauthorized | |
242 | ... | |
243 | ||
244 | Teardown. | |
245 | ||
246 | >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) | |
247 |
0 | Authorizers | |
1 | =========== | |
2 | ||
3 | Authorizers are objects that encapsulate knowledge about a particular | |
4 | web service's authentication scheme. lazr.restfulclient includes | |
5 | authorizers for common HTTP authentication schemes. | |
6 | ||
7 | The BasicHttpAuthorizer | |
8 | ----------------------- | |
9 | ||
10 | This authorizer handles HTTP Basic Auth. To test it, we'll create a | |
11 | fake web service that serves some dummy WADL. | |
12 | ||
13 | >>> import pkg_resources | |
14 | >>> wadl_string = pkg_resources.resource_string( | |
15 | ... 'wadllib.tests.data', 'launchpad-wadl.xml') | |
16 | ||
17 | >>> responses = { 'application/vnd.sun.wadl+xml' : wadl_string, | |
18 | ... 'application/json' : '{}' } | |
19 | ||
20 | >>> def dummy_application(environ, start_response): | |
21 | ... media_type = environ['HTTP_ACCEPT'] | |
22 | ... content = responses[media_type] | |
23 | ... start_response( | |
24 | ... '200', [('Content-type', media_type)]) | |
25 | ... return [content] | |
26 | ||
27 | ||
28 | The WADL file will be protected with HTTP Basic Auth. To access it, | |
29 | you'll need to provide a username of "user" and a password of | |
30 | "password". | |
31 | ||
32 | >>> def authenticate(username, password): | |
33 | ... """Accepts "user/password", rejects everything else. | |
34 | ... | |
35 | ... :return: The username, if the credentials are valid. | |
36 | ... None, otherwise. | |
37 | ... """ | |
38 | ... if username == "user" and password == "password": | |
39 | ... return username | |
40 | ... return None | |
41 | ||
42 | >>> from lazr.authentication.wsgi import BasicAuthMiddleware | |
43 | >>> def protected_application(): | |
44 | ... return BasicAuthMiddleware( | |
45 | ... dummy_application, authenticate_with=authenticate) | |
46 | ||
47 | Finally, we'll set up a WSGI intercept so that we can test the web | |
48 | service by making HTTP requests to http://api.launchpad.dev/. (This is | |
49 | the hostname mentioned in the WADL file.) | |
50 | ||
51 | >>> import wsgi_intercept | |
52 | >>> from wsgi_intercept.httplib2_intercept import install | |
53 | >>> install() | |
54 | >>> wsgi_intercept.add_wsgi_intercept( | |
55 | ... 'api.launchpad.dev', 80, protected_application) | |
56 | ||
57 | With no HttpAuthorizer, a ServiceRoot can't get access to the web service. | |
58 | ||
59 | >>> from lazr.restfulclient.resource import ServiceRoot | |
60 | >>> client = ServiceRoot(None, "http://api.launchpad.dev/") | |
61 | Traceback (most recent call last): | |
62 | ... | |
63 | HTTPError: HTTP Error 401: Unauthorized | |
64 | ... | |
65 | ||
66 | We can't get access if the authorizer doesn't have the right | |
67 | credentials. | |
68 | ||
69 | >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer | |
70 | ||
71 | >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword") | |
72 | >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/") | |
73 | Traceback (most recent call last): | |
74 | ... | |
75 | HTTPError: HTTP Error 401: Unauthorized | |
76 | ... | |
77 | ||
78 | If we provide the right credentials, we can retrieve the WADL. We'll | |
79 | still get an exception, because our fake web service is too fake for | |
80 | ServiceRoot--its 'service root' resource doesn't match the WADL--but | |
81 | we're able to make HTTP requests without getting 401 errors. | |
82 | ||
83 | >>> authorizer = BasicHttpAuthorizer("user", "password") | |
84 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
85 | ||
86 | Teardown. | |
87 | ||
88 | >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) | |
89 | ||
90 | ||
91 | The OAuthAuthorizer | |
92 | ------------------- | |
93 | ||
94 | This authorizer handles OAuth authorization. To test it, we'll protect | |
95 | the dummy application with a piece of OAuth middleware. The middleware | |
96 | will accept only one consumer/token combination, though it will also | |
97 | allow anonymous access: if you pass in an empty token and secret, | |
98 | you'll get a lower level of access. | |
99 | ||
100 | >>> from oauth.oauth import OAuthConsumer, OAuthToken | |
101 | >>> valid_consumer = OAuthConsumer("consumer", '') | |
102 | >>> valid_token = OAuthToken("token", "secret") | |
103 | >>> empty_token = OAuthToken("", "") | |
104 | ||
105 | Our authenticate() implementation checks against the one valid | |
106 | consumer and token. | |
107 | ||
108 | >>> def authenticate(consumer, token, parameters): | |
109 | ... """Accepts the valid consumer and token, rejects everything else. | |
110 | ... | |
111 | ... :return: The consumer, if the credentials are valid. | |
112 | ... None, otherwise. | |
113 | ... """ | |
114 | ... if token.key == '' and token.secret == '': | |
115 | ... # Anonymous access. | |
116 | ... return consumer | |
117 | ... if consumer == valid_consumer and token == valid_token: | |
118 | ... return consumer | |
119 | ... return None | |
120 | ||
121 | Our data store helps the middleware look up consumer and token objects | |
122 | from the information provided in a signed OAuth request. | |
123 | ||
124 | >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore | |
125 | ||
126 | >>> class AnonymousAccessDataStore(SimpleOAuthDataStore): | |
127 | ... """A data store that will accept any consumer.""" | |
128 | ... def lookup_consumer(self, consumer): | |
129 | ... """If there's no matching consumer, just create one. | |
130 | ... | |
131 | ... This will let anonymous requests succeed with any | |
132 | ... consumer key.""" | |
133 | ... consumer = super( | |
134 | ... AnonymousAccessDataStore, self).lookup_consumer( | |
135 | ... consumer) | |
136 | ... if consumer is None: | |
137 | ... consumer = OAuthConsumer(consumer, '') | |
138 | ... return consumer | |
139 | ||
140 | >>> data_store = AnonymousAccessDataStore( | |
141 | ... {valid_consumer.key : valid_consumer}, | |
142 | ... {valid_token.key : valid_token, | |
143 | ... empty_token.key : empty_token}) | |
144 | ||
145 | Now we're ready to protect the dummy_application with OAuthMiddleware, | |
146 | using our authenticate() implementation and our data store. | |
147 | ||
148 | >>> from lazr.authentication.wsgi import OAuthMiddleware | |
149 | >>> def protected_application(): | |
150 | ... return OAuthMiddleware( | |
151 | ... dummy_application, realm="OAuth test", | |
152 | ... authenticate_with=authenticate, data_store=data_store) | |
153 | >>> wsgi_intercept.add_wsgi_intercept( | |
154 | ... 'api.launchpad.dev', 80, protected_application) | |
155 | ||
156 | Let's try out some clients. As you'd expect, you can't get through the | |
157 | middleware with no HTTPAuthorizer at all. | |
158 | ||
159 | >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer | |
160 | >>> client = ServiceRoot(None, "http://api.launchpad.dev/") | |
161 | Traceback (most recent call last): | |
162 | ... | |
163 | HTTPError: HTTP Error 401: Unauthorized | |
164 | ... | |
165 | ||
166 | Invalid credentials are also no help. | |
167 | ||
168 | >>> authorizer = OAuthAuthorizer( | |
169 | ... valid_consumer.key, access_token=OAuthToken("invalid", "token")) | |
170 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
171 | Traceback (most recent call last): | |
172 | ... | |
173 | HTTPError: HTTP Error 401: Unauthorized | |
174 | ... | |
175 | ||
176 | But valid credentials work fine (again, up to the point at which | |
177 | lazr.restfulclient runs against the limits of this simple web service). | |
178 | ||
179 | >>> authorizer = OAuthAuthorizer( | |
180 | ... valid_consumer.key, access_token=valid_token) | |
181 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
182 | ||
183 | It's even possible to get anonymous access by providing an empty | |
184 | access token. | |
185 | ||
186 | >>> authorizer = OAuthAuthorizer( | |
187 | ... valid_consumer, access_token=empty_token) | |
188 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
189 | ||
190 | Because of the way the AnonymousAccessDataStore (defined | |
191 | earlier in the test) works, you can even get anonymous access by | |
192 | specifying an OAuth consumer that's not in the server-side list of | |
193 | valid consumers. | |
194 | ||
195 | >>> authorizer = OAuthAuthorizer( | |
196 | ... "random consumer", access_token=empty_token) | |
197 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
198 | ||
199 | If you try to provide credentials with an unrecognized OAuth consumer, | |
200 | you'll get an error--even if the credentials are valid. The data store | |
201 | used in this test only lets unrecognized OAuth consumers through when | |
202 | they request anonymous access. | |
203 | ||
204 | >>> authorizer = OAuthAuthorizer( | |
205 | ... 'random consumer', access_token=valid_token) | |
206 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
207 | Traceback (most recent call last): | |
208 | ... | |
209 | HTTPError: HTTP Error 401: Unauthorized | |
210 | ... | |
211 | ||
212 | >>> authorizer = OAuthAuthorizer( | |
213 | ... 'random consumer', access_token=OAuthToken("invalid", "token")) | |
214 | >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/") | |
215 | Traceback (most recent call last): | |
216 | ... | |
217 | HTTPError: HTTP Error 401: Unauthorized | |
218 | ... | |
219 | ||
220 | Teardown. | |
221 | ||
222 | >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80) | |
223 | ||
224 | Accessing the authorizer object after the fact | |
225 | ---------------------------------------------- | |
226 | ||
227 | A ServiceRoot object has a 'credentials' attribute which contains the | |
228 | Authorizer used to authorize outgoing requests. | |
229 | ||
230 | >>> from lazr.restfulclient.resource import ServiceRoot | |
231 | >>> root = ServiceRoot(authorizer, "http://cookbooks.dev/1.0/") | |
232 | >>> root.credentials | |
233 | <lazr.restfulclient.authorize.oauth.OAuthAuthorizer object...> | |
234 | ||
235 | Server-side permissions | |
236 | ----------------------- | |
237 | ||
238 | The server may hide some data from you because you lack the permission | |
239 | to see it. To avoid objects that are mysteriously missing fields, the | |
240 | server will serve a special "redacted" value that lets you know you | |
241 | don't have permission to see the data. | |
242 | ||
243 | >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient | |
244 | >>> service = CookbookWebServiceClient() | |
245 | ||
246 | >>> cookbook = service.recipes[1].cookbook | |
247 | >>> print cookbook.confirmed | |
248 | tag:launchpad.net:2008:redacted | |
249 | ||
250 | If you try to make an HTTP request for the "redacted" value (usually | |
251 | by following a link that you don't know is redacted), you'll get a | |
252 | helpful error. | |
253 | ||
254 | >>> service.load("tag:launchpad.net:2008:redacted") | |
255 | Traceback (most recent call last): | |
256 | ... | |
257 | ValueError: You tried to access a resource that you don't have the | |
258 | server-side permission to see. |
89 | 89 | |
90 | 90 | This will save you a *lot* of time in subsequent sessions, because |
91 | 91 | you'll be able to use cached versions of the initial (very expensive) |
92 | documents. | |
92 | documents. A new client will not re-request the service root at all. | |
93 | 93 | |
94 | 94 | >>> second_service = CookbookWebServiceClient(cache=unicode(tempdir)) |
95 | send: 'GET /1.0/ ... | |
96 | reply: ...304... | |
97 | ... | |
98 | send: 'GET /1.0/ ... | |
99 | reply: ...304... | |
100 | ... | |
95 | ||
96 | You'll also be able to make conditional requests for many resources | |
97 | and avoid transferring their full representations. | |
101 | 98 | |
102 | 99 | >>> print second_service.recipes[4].instructions |
103 | 100 | send: 'GET /1.0/recipes/4 ... |
108 | 105 | Of course, if you ever need to clear the cache directory, you'll have |
109 | 106 | to do it yourself. |
110 | 107 | |
108 | Cleanup. | |
109 | ||
110 | >>> import shutil | |
111 | >>> shutil.rmtree(tempdir) | |
112 | ||
113 | Cache expiration | |
114 | ---------------- | |
115 | ||
116 | The '1.0' version of the example web service, which we've been using up til | |
117 | now, sets a long cache expiry time for the service root. That's why we | |
118 | were able to create a second client that didn't request the service | |
119 | root at all--just fetched the representations from its cache. | |
120 | ||
121 | The 'devel' version of the example web service sets a cache expiry | |
122 | time of two seconds. Let's see what that looks like on the client side. | |
123 | ||
124 | >>> tempdir = tempfile.mkdtemp() | |
125 | >>> first_service = CookbookWebServiceClient( | |
126 | ... cache=tempdir, version='devel') | |
127 | send: 'GET /devel/ ... | |
128 | reply: ...200... | |
129 | ... | |
130 | send: 'GET /devel/ ... | |
131 | reply: ...200... | |
132 | ... | |
133 | ||
134 | Now let's wait for three seconds to make sure the representations become | |
135 | stale. | |
136 | ||
137 | >>> from time import sleep | |
138 | >>> sleep(3) | |
139 | ||
140 | When the representations are stale, a new client makes *conditional* | |
141 | requests for the representations. If the conditions fail (as they do | |
142 | here), the cached representations are considered to have been | |
143 | refreshed, just as if the server had sent them again. | |
144 | ||
145 | >>> second_service = CookbookWebServiceClient( | |
146 | ... cache=tempdir, version='devel') | |
147 | send: 'GET /devel/ ... | |
148 | reply: ...304... | |
149 | ... | |
150 | send: 'GET /devel/ ... | |
151 | reply: ...304... | |
152 | ... | |
153 | ||
154 | Let's quickly create another client before the representation grows | |
155 | stale again. | |
156 | ||
157 | >>> second_service = CookbookWebServiceClient( | |
158 | ... cache=tempdir, version='devel') | |
159 | ||
160 | When the representations are not stale, a new client does not make any | |
161 | HTTP requests at all--it fetches representations direct from the | |
162 | cache. | |
163 | ||
164 | Cleanup. | |
165 | ||
111 | 166 | >>> httplib2.debuglevel = 0 |
112 | >>> import shutil | |
113 | 167 | >>> shutil.rmtree(tempdir) |
114 | 168 | |
115 | 169 | Cache filenames |
443 | 443 | >>> recipe.lp_save() |
444 | 444 | >>> recipe == recipe_2 |
445 | 445 | False |
446 | ||
447 | Server-side permissions | |
448 | ----------------------- | |
449 | ||
450 | The server may hide some data from you because you lack the permission | |
451 | to see it. To avoid objects that are mysteriously missing fields, the | |
452 | server will serve a special "redacted" value that lets you know you | |
453 | don't have permission to see the data. | |
454 | ||
455 | >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient | |
456 | >>> service = CookbookWebServiceClient() | |
457 | ||
458 | >>> cookbook = service.recipes[1].cookbook | |
459 | >>> print cookbook.confirmed | |
460 | tag:launchpad.net:2008:redacted | |
461 | ||
462 | If you try to make an HTTP request for the "redacted" value (usually | |
463 | by following a link that you don't know is redacted), you'll get a | |
464 | helpful error. | |
465 | ||
466 | >>> service.load("tag:launchpad.net:2008:redacted") | |
467 | Traceback (most recent call last): | |
468 | ... | |
469 | ValueError: You tried to access a resource that you don't have the | |
470 | server-side permission to see. |
29 | 29 | |
30 | 30 | |
31 | 31 | import cgi |
32 | from email.message import Message | |
32 | 33 | import simplejson |
33 | 34 | from StringIO import StringIO |
34 | 35 | import urllib |
39 | 40 | from _browser import Browser, RestfulHttp |
40 | 41 | from _json import DatetimeJSONEncoder |
41 | 42 | from errors import HTTPError |
43 | ||
44 | from lazr.restfulclient import __version__ | |
42 | 45 | |
43 | 46 | missing = object() |
44 | 47 | |
281 | 284 | headers = {} |
282 | 285 | if etag is not None: |
283 | 286 | headers['If-None-Match'] = etag |
284 | try: | |
285 | representation = self._root._browser.get( | |
286 | self._wadl_resource, headers=headers) | |
287 | except HTTPError, e: | |
288 | if e.response['status'] == '304': | |
289 | # The entry wasn't modified. No need to do anything. | |
290 | return | |
291 | else: | |
292 | raise e | |
287 | representation = self._root._browser.get( | |
288 | self._wadl_resource, headers=headers) | |
289 | if representation == self._root._browser.NOT_MODIFIED: | |
290 | # The entry wasn't modified. No need to do anything. | |
291 | return | |
293 | 292 | # __setattr__ assumes we're setting an attribute of the resource, |
294 | 293 | # so we manipulate __dict__ directly. |
295 | 294 | self.__dict__['_wadl_resource'] = self._wadl_resource.bind( |
379 | 378 | RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile} |
380 | 379 | |
381 | 380 | def __init__(self, authorizer, service_root, cache=None, |
382 | timeout=None, proxy_info=None, version=None): | |
381 | timeout=None, proxy_info=None, version=None, | |
382 | base_client_name=''): | |
383 | 383 | """Root access to a lazr.restful API. |
384 | 384 | |
385 | 385 | :param credentials: The credentials used to access the service. |
393 | 393 | if service_root[-1] != '/': |
394 | 394 | service_root += '/' |
395 | 395 | self._root_uri = URI(service_root) |
396 | ||
397 | # Set up data necessary to calculate the User-Agent header. | |
398 | self._base_client_name = base_client_name | |
399 | ||
396 | 400 | # Get the WADL definition. |
401 | self.credentials = authorizer | |
397 | 402 | self._browser = Browser( |
398 | self, authorizer, cache, timeout, proxy_info) | |
403 | self, authorizer, cache, timeout, proxy_info, self._user_agent) | |
399 | 404 | self._wadl = self._browser.get_wadl_application(self._root_uri) |
400 | 405 | |
401 | 406 | # Get the root resource. |
403 | 408 | bound_root = root_resource.bind( |
404 | 409 | self._browser.get(root_resource), 'application/json') |
405 | 410 | super(ServiceRoot, self).__init__(None, bound_root) |
406 | self.credentials = authorizer | |
411 | ||
412 | @property | |
413 | def _user_agent(self): | |
414 | """The value for the User-Agent header. | |
415 | ||
416 | This will be something like: | |
417 | launchpadlib 1.6.1, lazr.restfulclient 1.0.0; oauth_consumer=apport | |
418 | ||
419 | That is, a string describing lazr.restfulclient and an | |
420 | optional custom client built on top, and parameters | |
421 | containing any authorization-specific information that | |
422 | identifies the user agent (such as the OAuth consumer key). | |
423 | """ | |
424 | base_portion = "lazr.restfulclient %s" % __version__ | |
425 | if self._base_client_name != '': | |
426 | base_portion = self._base_client_name + ' (' + base_portion + ')' | |
427 | ||
428 | message = Message() | |
429 | message['User-Agent'] = base_portion | |
430 | if self.credentials is not None: | |
431 | for key, value in self.credentials.user_agent_params.items(): | |
432 | message.set_param(key, value, 'User-Agent') | |
433 | return message['User-Agent'] | |
407 | 434 | |
408 | 435 | def httpFactory(self, authorizer, cache, timeout, proxy_info): |
409 | 436 | return RestfulHttp(authorizer, cache, timeout, proxy_info) |
48 | 48 | RESOURCE_TYPE_CLASSES['recipes'] = RecipeSet |
49 | 49 | RESOURCE_TYPE_CLASSES['cookbooks'] = CookbookSet |
50 | 50 | |
51 | def __init__(self, service_root="http://cookbooks.dev/", version='1.0', | |
52 | cache=None): | |
51 | DEFAULT_SERVICE_ROOT = "http://cookbooks.dev/" | |
52 | DEFAULT_VERSION = "1.0" | |
53 | ||
54 | def __init__(self, service_root=DEFAULT_SERVICE_ROOT, | |
55 | version=DEFAULT_VERSION, cache=None): | |
53 | 56 | super(CookbookWebServiceClient, self).__init__( |
54 | 57 | None, service_root, cache=cache, version=version) |
30 | 30 | import wsgi_intercept |
31 | 31 | from wsgi_intercept.httplib2_intercept import install, uninstall |
32 | 32 | |
33 | from zope.component import getUtility | |
34 | ||
35 | from lazr.restful.example.base.interfaces import IFileManager | |
36 | from lazr.restful.example.base.tests.test_integration import WSGILayer | |
37 | from lazr.restful.testing.webservice import WebServiceApplication | |
38 | ||
33 | # We avoid importing anything from lazr.restful into the module level, | |
34 | # so that standalone_tests() can run without any support from | |
35 | # lazr.restful. | |
39 | 36 | |
40 | 37 | DOCTEST_FLAGS = ( |
41 | 38 | doctest.ELLIPSIS | |
44 | 41 | |
45 | 42 | |
46 | 43 | def setUp(test): |
44 | from lazr.restful.example.base.tests.test_integration import WSGILayer | |
47 | 45 | install() |
48 | 46 | wsgi_intercept.add_wsgi_intercept( |
49 | 47 | 'cookbooks.dev', 80, WSGILayer.make_application) |
50 | 48 | |
51 | 49 | |
52 | 50 | def tearDown(test): |
51 | from lazr.restful.example.base.interfaces import IFileManager | |
52 | from zope.component import getUtility | |
53 | 53 | uninstall() |
54 | 54 | file_manager = getUtility(IFileManager) |
55 | 55 | file_manager.files = {} |
56 | 56 | file_manager.counter = 0 |
57 | 57 | |
58 | 58 | |
59 | def find_doctests(suffix): | |
60 | """Find doctests matching a certain suffix.""" | |
61 | # Always include README.txt. | |
62 | doctest_files = [ | |
63 | os.path.abspath(resource_filename('lazr.restfulclient', 'README.txt'))] | |
64 | # Match other doctests against the suffix. | |
65 | if resource_exists('lazr.restfulclient', 'docs'): | |
66 | for name in resource_listdir('lazr.restfulclient', 'docs'): | |
67 | if name.endswith(suffix): | |
68 | doctest_files.append( | |
69 | os.path.abspath( | |
70 | resource_filename( | |
71 | 'lazr.restfulclient', 'docs/%s' % name))) | |
72 | return doctest_files | |
73 | ||
74 | ||
59 | 75 | def additional_tests(): |
60 | 76 | "Run the doc tests (README.txt and docs/*, if any exist)" |
61 | doctest_files = [ | |
62 | os.path.abspath(resource_filename('lazr.restfulclient', 'README.txt'))] | |
63 | if resource_exists('lazr.restfulclient', 'docs'): | |
64 | for name in resource_listdir('lazr.restfulclient', 'docs'): | |
65 | if name.endswith('.txt'): | |
66 | doctest_files.append( | |
67 | os.path.abspath( | |
68 | resource_filename('lazr.restfulclient', 'docs/%s' % name))) | |
77 | from lazr.restful.example.base.tests.test_integration import WSGILayer | |
69 | 78 | kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS, |
70 | 79 | setUp=setUp, tearDown=tearDown) |
71 | 80 | atexit.register(cleanup_resources) |
72 | suite = doctest.DocFileSuite(*doctest_files, **kwargs) | |
81 | suite = doctest.DocFileSuite(*find_doctests('.txt'), **kwargs) | |
73 | 82 | suite.layer = WSGILayer |
74 | 83 | return suite |
84 | ||
85 | ||
86 | def standalone_tests(): | |
87 | "Run the tests that don't need lazr.restful." | |
88 | kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS) | |
89 | atexit.register(cleanup_resources) | |
90 | suite = doctest.DocFileSuite(*find_doctests('.standalone.txt'), **kwargs) | |
91 | return suite |
0 | Metadata-Version: 1.0 | |
1 | Name: lazr.restfulclient | |
2 | Version: 0.9.13 | |
3 | Summary: This is a template for your lazr package. To start your own lazr package, | |
4 | Home-page: https://launchpad.net/lazr.restfulclient | |
5 | Author: LAZR Developers | |
6 | Author-email: lazr-developers@lists.launchpad.net | |
7 | License: LGPL v3 | |
8 | Download-URL: https://launchpad.net/lazr.restfulclient/+download | |
9 | Description: .. | |
10 | This file is part of lazr.restfulclient. | |
11 | ||
12 | lazr.restfulclient is free software: you can redistribute it and/or modify it | |
13 | under the terms of the GNU Lesser General Public License as published by | |
14 | the Free Software Foundation, version 3 of the License. | |
15 | ||
16 | lazr.restfulclient is distributed in the hope that it will be useful, but | |
17 | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | |
18 | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public | |
19 | License for more details. | |
20 | ||
21 | You should have received a copy of the GNU Lesser General Public License | |
22 | along with lazr.restfulclient. If not, see <http://www.gnu.org/licenses/>. | |
23 | ||
24 | LAZR restfulclient | |
25 | ************ | |
26 | ||
27 | This is a pure template for new lazr namespace packages. | |
28 | ||
29 | Please see https://dev.launchpad.net/LazrStyleGuide and | |
30 | https://dev.launchpad.net/Hacking for how to develop in this | |
31 | package. | |
32 | ||
33 | This is an example Sphinx_ `Table of contents`_. If you add files to the docs | |
34 | directory, you should probably improve it. | |
35 | ||
36 | .. toctree:: | |
37 | :glob: | |
38 | ||
39 | * | |
40 | docs/* | |
41 | ||
42 | .. _Sphinx: http://sphinx.pocoo.org/ | |
43 | .. _Table of contents: http://sphinx.pocoo.org/concepts.html#the-toc-tree | |
44 | ||
45 | Importable | |
46 | ========== | |
47 | ||
48 | The lazr.restfulclient package is importable, and has a version number. | |
49 | ||
50 | >>> import lazr.restfulclient | |
51 | >>> print 'VERSION:', lazr.restfulclient.__version__ | |
52 | VERSION: ... | |
53 | ||
54 | =========================== | |
55 | NEWS for lazr.restfulclient | |
56 | =========================== | |
57 | ||
58 | 0.9.13 (2010-03-24) | |
59 | =================== | |
60 | ||
61 | - Removed of some no-longer-needed compatibility code for buggy | |
62 | servers, and fixed the tests to work with the new release of simplejson. | |
63 | ||
64 | - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't | |
65 | strict enough. The maximum filename length is now 143 characters. | |
66 | ||
67 | 0.9.12 (2010-03-09) | |
68 | =================== | |
69 | ||
70 | - Fixed a bug that prevented a unicode string from being used as a | |
71 | cache filename. | |
72 | ||
73 | 0.9.11 (2010-02-11) | |
74 | =================== | |
75 | ||
76 | - If a lazr.restful web service publishes multiple versions, you can | |
77 | now specify which version to use in a separate constructor argument, | |
78 | rather than sticking it on to the end of the service root. | |
79 | - Filenames in the cache will never be longer than 150 characters, | |
80 | to avoid errors on eCryptfs filesystems. | |
81 | - Added a proof-of-concept test for OAuth-signed anonymous access. | |
82 | - Fixed comparisons of entries and hosted files with None. | |
83 | ||
84 | 0.9.10 (2009-10-23) | |
85 | =================== | |
86 | ||
87 | - lazr.restfulclient now requests the correct WADL media type. | |
88 | - Made HTTPError strings more verbose. | |
89 | - Implemented the equality operator for entry and hosted-file resources. | |
90 | - Resume setting the 'credentials' attribute on ServerRoot to avoid | |
91 | breaking compatibility with launchpadlib. | |
92 | ||
93 | 0.9.9 (2009-10-07) | |
94 | ================== | |
95 | ||
96 | - The WSGI authentication middleware has been moved from lazr.restful | |
97 | to the new lazr.authentication library, and lazr.restfulclient now | |
98 | uses the new library. | |
99 | ||
100 | 0.9.8 (2009-10-06) | |
101 | ================== | |
102 | ||
103 | - Added support for OAuth. | |
104 | ||
105 | 0.9.7 (2009-09-30) | |
106 | ================== | |
107 | ||
108 | - Added support for HTTP Basic Auth. | |
109 | ||
110 | 0.9.6 (2009-09-16) | |
111 | ================== | |
112 | ||
113 | - Made compatible with lazr.restful 0.9.6. | |
114 | ||
115 | 0.9.5 (2009-08-28) | |
116 | ================== | |
117 | ||
118 | - Removed debugging code. | |
119 | ||
120 | 0.9.4 (2009-08-26) | |
121 | ================== | |
122 | ||
123 | - Removed unnecessary build dependencies. | |
124 | ||
125 | - Updated tests for newer version of simplejson. | |
126 | ||
127 | - Made tests less fragile by cleaning up lazr.restful example filemanager | |
128 | between tests. | |
129 | ||
130 | - normalized output of simplejson to unicode. | |
131 | ||
132 | 0.9.3 (2009-08-05) | |
133 | ================== | |
134 | ||
135 | Removed a sys.path hack from setup.py. | |
136 | ||
137 | 0.9.2 (2009-07-16) | |
138 | ================== | |
139 | ||
140 | - Fields that can contain binary data are no longer run through | |
141 | simplejson.dumps(). | |
142 | ||
143 | - For fields that can take on a limited set of values, you can now get | |
144 | a list of possible values. | |
145 | ||
146 | 0.9.1 (2009-07-13) | |
147 | ================== | |
148 | ||
149 | - The client now knows to look for multipart/form-data representations | |
150 | and will create them as appropriate. The upshot of this is that you | |
151 | can now send binary data when invoking named operations that will | |
152 | accept binary data. | |
153 | ||
154 | ||
155 | 0.9 (2009-04-29) | |
156 | ================ | |
157 | ||
158 | - Initial public release | |
159 | ||
160 | Platform: UNKNOWN | |
161 | Classifier: Development Status :: 5 - Production/Stable | |
162 | Classifier: Intended Audience :: Developers | |
163 | Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) | |
164 | Classifier: Operating System :: OS Independent | |
165 | Classifier: Programming Language :: Python |
9 | 9 | src/lazr.restfulclient.egg-info/namespace_packages.txt |
10 | 10 | src/lazr.restfulclient.egg-info/not-zip-safe |
11 | 11 | src/lazr.restfulclient.egg-info/requires.txt |
12 | src/lazr.restfulclient.egg-info/test_info.txt | |
12 | 13 | src/lazr.restfulclient.egg-info/top_level.txt |
13 | 14 | src/lazr/restfulclient/NEWS.txt |
14 | 15 | src/lazr/restfulclient/README.txt |