Codebase list lazr.restfulclient / debian/0.9.14-1
Imported Debian patch 0.9.14-1 Luca Falavigna 14 years ago
22 changed file(s) with 548 addition(s) and 511 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 1.0
11 Name: lazr.restfulclient
2 Version: 0.9.13
2 Version: 0.9.14
33 Summary: This is a template for your lazr package. To start your own lazr package,
44 Home-page: https://launchpad.net/lazr.restfulclient
55 Author: LAZR Developers
77 License: LGPL v3
88 Download-URL: https://launchpad.net/lazr.restfulclient/+download
99 Description: ..
10 This file is part of lazr.restfulclient.
10 This file is part of lazr.restfulclient.
1111
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.
1515
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.
2020
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/>.
2323
2424 LAZR restfulclient
2525 ************
3434 directory, you should probably improve it.
3535
3636 .. toctree::
37 :glob:
37 :glob:
3838
39 *
40 docs/*
39 *
40 docs/*
4141
4242 .. _Sphinx: http://sphinx.pocoo.org/
4343 .. _Table of contents: http://sphinx.pocoo.org/concepts.html#the-toc-tree
4747
4848 The lazr.restfulclient package is importable, and has a version number.
4949
50 >>> import lazr.restfulclient
51 >>> print 'VERSION:', lazr.restfulclient.__version__
52 VERSION: ...
50 >>> import lazr.restfulclient
51 >>> print 'VERSION:', lazr.restfulclient.__version__
52 VERSION: ...
5353
5454 ===========================
5555 NEWS for lazr.restfulclient
5656 ===========================
5757
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
5871 0.9.13 (2010-03-24)
5972 ===================
6073
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.
6376
6477 - 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.
6679
6780 0.9.12 (2010-03-09)
6881 ===================
6982
7083 - Fixed a bug that prevented a unicode string from being used as a
71 cache filename.
84 cache filename.
7285
7386 0.9.11 (2010-02-11)
7487 ===================
7588
7689 - 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.
7992 - 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.
8194 - Added a proof-of-concept test for OAuth-signed anonymous access.
8295 - Fixed comparisons of entries and hosted files with None.
8396
88101 - Made HTTPError strings more verbose.
89102 - Implemented the equality operator for entry and hosted-file resources.
90103 - Resume setting the 'credentials' attribute on ServerRoot to avoid
91 breaking compatibility with launchpadlib.
104 breaking compatibility with launchpadlib.
92105
93106 0.9.9 (2009-10-07)
94107 ==================
95108
96109 - 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.
99112
100113 0.9.8 (2009-10-06)
101114 ==================
125138 - Updated tests for newer version of simplejson.
126139
127140 - Made tests less fragile by cleaning up lazr.restful example filemanager
128 between tests.
141 between tests.
129142
130143 - normalized output of simplejson to unicode.
131144
138151 ==================
139152
140153 - Fields that can contain binary data are no longer run through
141 simplejson.dumps().
154 simplejson.dumps().
142155
143156 - 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.
145158
146159 0.9.1 (2009-07-13)
147160 ==================
148161
149162 - 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.
153166
154167
155168 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
09 lazr.restfulclient (0.9.13+ds-1) unstable; urgency=low
110
211 * New upstream release.
1212 Package: python-lazr.restfulclient
1313 Architecture: all
1414 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)
1715 Description: client for lazr.restful-based web services
1816 A programmable client library that takes advantage of the commonalities
1917 among lazr.rest web services to provide added functionality on top
00 version=3
1 opts=dversionmangle=s/\+ds// \
21 https://launchpad.net/lazr.restfulclient/+download http://launchpad.net/lazr.restfulclient/.*/lazr.restfulclient-(.+).tar.gz
+0
-5
setup.cfg less more
0 [egg_info]
1 tag_build =
2 tag_date = 0
3 tag_svn_revision = 0
4
5656 install_requires=[
5757 'httplib2',
5858 'lazr.authentication',
59 'lazr.restful>=0.9.18',
6059 'oauth',
6160 'setuptools',
6261 'wadllib>=1.1.4',
6362 'wsgi_intercept',
64 'van.testing',
65 'zope.interface',
6663 ],
6764 url='https://launchpad.net/lazr.restfulclient',
6865 download_url= 'https://launchpad.net/lazr.restfulclient/+download',
7471 "Programming Language :: Python"],
7572 extras_require=dict(
7673 docs=['Sphinx',
77 'z3c.recipe.sphinxdoc']
74 'z3c.recipe.sphinxdoc'],
75 test=['lazr.restful>=0.9.25',
76 'van.testing'],
7877 ),
7978 test_suite='lazr.restfulclient.tests',
8079 )
11 NEWS for lazr.restfulclient
22 ===========================
33
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
417 0.9.13 (2010-03-24)
518 ===================
619
7 - Removed of some no-longer-needed compatibility code for buggy
20 - Removed some no-longer-needed compatibility code for buggy
821 servers, and fixed the tests to work with the new release of simplejson.
922
1023 - The fix in 0.9.11 to avoid errors on eCryptfs filesystems wasn't
223223 class Browser:
224224 """A class for making calls to lazr.restful web services."""
225225
226 NOT_MODIFIED = object()
227
226228 def __init__(self, service_root, credentials, cache=None, timeout=None,
227 proxy_info=None):
229 proxy_info=None, user_agent=None):
228230 """Initialize, possibly creating a cache.
229231
230232 If no cache is provided, a temporary directory will be used as
238240 cache = MultipleRepresentationCache(cache)
239241 self._connection = service_root.httpFactory(
240242 credentials, cache, timeout, proxy_info)
243 self.user_agent = user_agent
241244
242245 def _request(self, url, data=None, method='GET',
243246 media_type='application/json', extra_headers=None):
250253
251254 # Add extra headers for the request.
252255 headers = {'Accept' : media_type}
256 if self.user_agent is not None:
257 headers['User-Agent'] = self.user_agent
253258 if isinstance(self._connection.cache, MultipleRepresentationCache):
254259 self._connection.cache.request_media_type = media_type
255260 if extra_headers is not None:
257262 # Make the request.
258263 response, content = self._connection.request(
259264 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
260297 # Turn non-2xx responses into exceptions.
261298 if response.status // 100 != 2:
262299 raise HTTPError(response, content)
5454 """
5555 pass
5656
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
5768
5869 class BasicHttpAuthorizer(HttpAuthorizer):
5970 """Handles authentication for services that use HTTP Basic Auth."""
6868 self.consumer = Consumer(consumer_name, consumer_secret)
6969 self.access_token = access_token
7070 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}
7181
7282 def load(self, readable_file):
7383 """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
-259
src/lazr/restfulclient/docs/authorizer.txt less more
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.
8989
9090 This will save you a *lot* of time in subsequent sessions, because
9191 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.
9393
9494 >>> 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.
10198
10299 >>> print second_service.recipes[4].instructions
103100 send: 'GET /1.0/recipes/4 ...
108105 Of course, if you ever need to clear the cache directory, you'll have
109106 to do it yourself.
110107
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
111166 >>> httplib2.debuglevel = 0
112 >>> import shutil
113167 >>> shutil.rmtree(tempdir)
114168
115169 Cache filenames
443443 >>> recipe.lp_save()
444444 >>> recipe == recipe_2
445445 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.
2929
3030
3131 import cgi
32 from email.message import Message
3233 import simplejson
3334 from StringIO import StringIO
3435 import urllib
3940 from _browser import Browser, RestfulHttp
4041 from _json import DatetimeJSONEncoder
4142 from errors import HTTPError
43
44 from lazr.restfulclient import __version__
4245
4346 missing = object()
4447
281284 headers = {}
282285 if etag is not None:
283286 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
293292 # __setattr__ assumes we're setting an attribute of the resource,
294293 # so we manipulate __dict__ directly.
295294 self.__dict__['_wadl_resource'] = self._wadl_resource.bind(
379378 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
380379
381380 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=''):
383383 """Root access to a lazr.restful API.
384384
385385 :param credentials: The credentials used to access the service.
393393 if service_root[-1] != '/':
394394 service_root += '/'
395395 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
396400 # Get the WADL definition.
401 self.credentials = authorizer
397402 self._browser = Browser(
398 self, authorizer, cache, timeout, proxy_info)
403 self, authorizer, cache, timeout, proxy_info, self._user_agent)
399404 self._wadl = self._browser.get_wadl_application(self._root_uri)
400405
401406 # Get the root resource.
403408 bound_root = root_resource.bind(
404409 self._browser.get(root_resource), 'application/json')
405410 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']
407434
408435 def httpFactory(self, authorizer, cache, timeout, proxy_info):
409436 return RestfulHttp(authorizer, cache, timeout, proxy_info)
4848 RESOURCE_TYPE_CLASSES['recipes'] = RecipeSet
4949 RESOURCE_TYPE_CLASSES['cookbooks'] = CookbookSet
5050
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):
5356 super(CookbookWebServiceClient, self).__init__(
5457 None, service_root, cache=cache, version=version)
3030 import wsgi_intercept
3131 from wsgi_intercept.httplib2_intercept import install, uninstall
3232
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.
3936
4037 DOCTEST_FLAGS = (
4138 doctest.ELLIPSIS |
4441
4542
4643 def setUp(test):
44 from lazr.restful.example.base.tests.test_integration import WSGILayer
4745 install()
4846 wsgi_intercept.add_wsgi_intercept(
4947 'cookbooks.dev', 80, WSGILayer.make_application)
5048
5149
5250 def tearDown(test):
51 from lazr.restful.example.base.interfaces import IFileManager
52 from zope.component import getUtility
5353 uninstall()
5454 file_manager = getUtility(IFileManager)
5555 file_manager.files = {}
5656 file_manager.counter = 0
5757
5858
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
5975 def additional_tests():
6076 "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
6978 kwargs = dict(module_relative=False, optionflags=DOCTEST_FLAGS,
7079 setUp=setUp, tearDown=tearDown)
7180 atexit.register(cleanup_resources)
72 suite = doctest.DocFileSuite(*doctest_files, **kwargs)
81 suite = doctest.DocFileSuite(*find_doctests('.txt'), **kwargs)
7382 suite.layer = WSGILayer
7483 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
-166
src/lazr.restfulclient.egg-info/PKG-INFO less more
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
99 src/lazr.restfulclient.egg-info/namespace_packages.txt
1010 src/lazr.restfulclient.egg-info/not-zip-safe
1111 src/lazr.restfulclient.egg-info/requires.txt
12 src/lazr.restfulclient.egg-info/test_info.txt
1213 src/lazr.restfulclient.egg-info/top_level.txt
1314 src/lazr/restfulclient/NEWS.txt
1415 src/lazr/restfulclient/README.txt
+0
-1
src/lazr.restfulclient.egg-info/not-zip-safe less more
0
0 test_module = None
1 test_suite = lazr.restfulclient.tests
2 test_loader = None
3 tests_require =