Codebase list lazr.restfulclient / bfab4a6
Imported Upstream version 0.9.10 SVN-Git Migration 8 years ago
21 changed file(s) with 883 addition(s) and 48 deletion(s). Raw diff Collapse all Expand all
00 Metadata-Version: 1.0
11 Name: lazr.restfulclient
2 Version: 0.9.3
2 Version: 0.9.10
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
5555 NEWS for lazr.restfulclient
5656 ===========================
5757
58 0.9.10 (2009-10-23)
59 ===================
60
61 - lazr.restfulclient now requests the correct WADL media type.
62 - Made HTTPError strings more verbose.
63 - Implemented the equality operator for entry and hosted-file resources.
64 - Resume setting the 'credentials' attribute on ServerRoot to avoid
65 breaking compatibility with launchpadlib.
66
67 0.9.9 (2009-10-07)
68 ==================
69
70 - The WSGI authentication middleware has been moved from lazr.restful
71 to the new lazr.authentication library, and lazr.restfulclient now
72 uses the new library.
73
74 0.9.8 (2009-10-06)
75 ==================
76
77 - Added support for OAuth.
78
79 0.9.7 (2009-09-30)
80 ==================
81
82 - Added support for HTTP Basic Auth.
83
84 0.9.6 (2009-09-16)
85 ==================
86
87 - Made compatible with lazr.restful 0.9.6.
88
89 0.9.5 (2009-08-28)
90 ==================
91
92 - Removed debugging code.
93
94 0.9.4 (2009-08-26)
95 ==================
96
97 - Removed unnecessary build dependencies.
98
99 - Updated tests for newer version of simplejson.
100
101 - Made tests less fragile by cleaning up lazr.restful example filemanager
102 between tests.
103
104 - normalized output of simplejson to unicode.
105
58106 0.9.3 (2009-08-05)
59107 ==================
60108
5454 'src/lazr/restfulclient/NEWS.txt'),
5555 license='LGPL v3',
5656 install_requires=[
57 'httplib2',
58 'lazr.authentication',
59 'lazr.restful',
60 'oauth',
5761 'setuptools',
58 'zope.interface',
59 'lazr.restful>=0.9.2',
60 'wadllib>=1.1.1',
62 'wadllib>=1.1.4',
6163 'wsgi_intercept',
6264 'van.testing',
65 'zope.interface',
6366 ],
6467 url='https://launchpad.net/lazr.restfulclient',
6568 download_url= 'https://launchpad.net/lazr.restfulclient/+download',
7376 docs=['Sphinx',
7477 'z3c.recipe.sphinxdoc']
7578 ),
76 setup_requires=['eggtestinfo', 'setuptools_bzr'],
7779 test_suite='lazr.restfulclient.tests',
7880 )
00 ===========================
11 NEWS for lazr.restfulclient
22 ===========================
3
4 0.9.10 (2009-10-23)
5 ===================
6
7 - lazr.restfulclient now requests the correct WADL media type.
8 - Made HTTPError strings more verbose.
9 - Implemented the equality operator for entry and hosted-file resources.
10 - Resume setting the 'credentials' attribute on ServerRoot to avoid
11 breaking compatibility with launchpadlib.
12
13 0.9.9 (2009-10-07)
14 ==================
15
16 - The WSGI authentication middleware has been moved from lazr.restful
17 to the new lazr.authentication library, and lazr.restfulclient now
18 uses the new library.
19
20 0.9.8 (2009-10-06)
21 ==================
22
23 - Added support for OAuth.
24
25 0.9.7 (2009-09-30)
26 ==================
27
28 - Added support for HTTP Basic Auth.
29
30 0.9.6 (2009-09-16)
31 ==================
32
33 - Made compatible with lazr.restful 0.9.6.
34
35 0.9.5 (2009-08-28)
36 ==================
37
38 - Removed debugging code.
39
40 0.9.4 (2009-08-26)
41 ==================
42
43 - Removed unnecessary build dependencies.
44
45 - Updated tests for newer version of simplejson.
46
47 - Made tests less fragile by cleaning up lazr.restful example filemanager
48 between tests.
49
50 - normalized output of simplejson to unicode.
351
452 0.9.3 (2009-08-05)
553 ==================
7676 react when its cache is a MultipleRepresentationCache.
7777 """
7878
79 def __init__(self, credentials, cache=None, timeout=None,
79 def __init__(self, authorizer=None, cache=None, timeout=None,
8080 proxy_info=None):
8181 super(RestfulHttp, self).__init__(cache, timeout, proxy_info)
82 # The credentials are not used in this class, but you can
83 # use them in a subclass.
84 self.restful_credentials = credentials
85
82 self.authorizer = authorizer
83 if self.authorizer is not None:
84 self.authorizer.authorizeSession(self)
8685
8786 def _request(self, conn, host, absolute_uri, request_uri, method, body,
8887 headers, redirections, cachekey):
102101 if 'accept-encoding' in headers:
103102 headers['te'] = 'deflate, gzip'
104103 del headers['accept-encoding']
104 if headers.has_key('authorization'):
105 # There's an authorization header left over from a
106 # previous request that resulted in a redirect. Remove it
107 # and start again.
108 del headers['authorization']
109 if self.authorizer is not None:
110 self.authorizer.authorizeRequest(
111 absolute_uri, method, body, headers)
105112 return super(RestfulHttp, self)._request(
106113 conn, host, absolute_uri, request_uri, method, body, headers,
107114 redirections, cachekey)
189196 def _request(self, url, data=None, method='GET',
190197 media_type='application/json', extra_headers=None):
191198 """Create an authenticated request object."""
199 # If the user is trying to get data that has been redacted,
200 # give a helpful message.
201 if url == "tag:launchpad.net:2008:redacted":
202 raise ValueError("You tried to access a resource that you "
203 "don't have the server-side permission to see.")
204
192205 # Add extra headers for the request.
193206 headers = {'Accept' : media_type}
194207 if isinstance(self._connection.cache, MultipleRepresentationCache):
217230
218231 def get_wadl_application(self, url):
219232 """GET a WADL representation of the resource at the requested url."""
233 # We're probably talking to an old version of lazr.restful
234 # that misspells the WADL media type. Accept either the correctly
235 # spelled media type or the misspelling.
236 wadl_type = 'application/vnd.sun.wadl+xml'
237 misspelled_wadl_type = 'application/vd.sun.wadl+xml'
238 accept = "%s, %s" % (wadl_type, misspelled_wadl_type)
220239 response, content = self._request(
221 url, media_type='application/vd.sun.wadl+xml')
240 url, media_type=wadl_type, extra_headers={'Accept': accept})
222241 return Application(str(url), content)
223242
224243 def post(self, url, method_name, **kws):
0 # Copyright 2009 Canonical Ltd.
1
2 # This file is part of lazr.restfulclient.
3 #
4 # lazr.restfulclient is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Lesser General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # lazr.restfulclient is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with lazr.restfulclient. If not, see
16 # <http://www.gnu.org/licenses/>.
17
18 """Classes to authorize lazr.restfulclient with various web services.
19
20 This module includes an authorizer classes for HTTP Basic Auth,
21 as well as a base-class authorizer that does nothing.
22
23 A set of classes for authorizing with OAuth is located in the 'oauth'
24 module.
25 """
26
27 __metaclass__ = type
28 __all__ = [
29 'BasicHttpAuthorizer',
30 'HttpAuthorizer',
31 ]
32
33 class HttpAuthorizer:
34 """Handles authentication for HTTP requests.
35
36 There are two ways to authenticate.
37
38 The authorize_session() method is called once when the client is
39 initialized. This works for authentication methods like Basic
40 Auth. The authorize_request is called for every HTTP request,
41 which is useful for authentication methods like Digest and OAuth.
42
43 The base class is a null authorizer which does not perform any
44 authentication at all.
45 """
46 def authorizeSession(self, client):
47 """Set up credentials for the entire session."""
48 pass
49
50 def authorizeRequest(self, absolute_uri, method, body, headers):
51 """Set up credentials for a single request.
52
53 This probably involves setting the Authentication header.
54 """
55 pass
56
57
58 class BasicHttpAuthorizer(HttpAuthorizer):
59 """Handles authentication for services that use HTTP Basic Auth."""
60
61 def __init__(self, username, password):
62 """Constructor.
63
64 :param username: User to send as authorization for all requests.
65 :param password: Password to send as authorization for all requests.
66 """
67 self.username = username
68 self.password = password
69
70 def authorizeSession(self, client):
71 client.add_credentials(self.username, self.password)
72
0 # Copyright 2009 Canonical Ltd.
1
2 # This file is part of lazr.restfulclient.
3 #
4 # lazr.restfulclient is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Lesser General Public License as
6 # published by the Free Software Foundation, either version 3 of the
7 # License, or (at your option) any later version.
8 #
9 # lazr.restfulclient is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with lazr.restfulclient. If not, see
16 # <http://www.gnu.org/licenses/>.
17
18 """OAuth classes for use with lazr.restfulclient."""
19
20
21 from ConfigParser import SafeConfigParser
22 # Work around relative import behavior. The below is equivalent to
23 # from oauth import oauth
24 oauth = __import__('oauth.oauth', {}).oauth
25 (OAuthConsumer, OAuthRequest, OAuthSignatureMethod_PLAINTEXT,
26 OAuthToken) = (oauth.OAuthConsumer, oauth.OAuthRequest,
27 oauth.OAuthSignatureMethod_PLAINTEXT, oauth.OAuthToken)
28
29 from lazr.restfulclient.authorize import HttpAuthorizer
30 from lazr.restfulclient.errors import CredentialsFileError
31
32 __metaclass__ = type
33 __all__ = [
34 'AccessToken',
35 'Consumer',
36 'OAuthAuthorizer',
37 ]
38
39
40 CREDENTIALS_FILE_VERSION = '1'
41
42
43 # These two classes are provided for convenience (so applications don't need
44 # to import from oauth.oauth), and to provide a default argument
45 # for secret.
46 class Consumer(oauth.OAuthConsumer):
47 """An OAuth consumer (application)."""
48
49 def __init__(self, key, secret=''):
50 OAuthConsumer.__init__(self, key, secret)
51
52
53 class AccessToken(oauth.OAuthToken):
54 """An OAuth access token."""
55
56 def __init__(self, key, secret='', context=None):
57 OAuthToken.__init__(self, key, secret)
58 self.context = context
59
60
61 class OAuthAuthorizer(HttpAuthorizer):
62 """A client that signs every outgoing request with OAuth credentials."""
63
64 def __init__(self, consumer_name=None, consumer_secret='',
65 access_token=None, oauth_realm="OAuth"):
66 self.consumer = None
67 if consumer_name is not None:
68 self.consumer = Consumer(consumer_name, consumer_secret)
69 self.access_token = access_token
70 self.oauth_realm = oauth_realm
71
72 def load(self, readable_file):
73 """Load credentials from a file-like object.
74
75 This overrides the consumer and access token given in the constructor
76 and replaces them with the values read from the file.
77
78 :param readable_file: A file-like object to read the credentials from
79 :type readable_file: Any object supporting the file-like `read()`
80 method
81 """
82 # Attempt to load the access token from the file.
83 parser = SafeConfigParser()
84 parser.readfp(readable_file)
85 # Check the version number and extract the access token and
86 # secret. Then convert these to the appropriate instances.
87 if not parser.has_section(CREDENTIALS_FILE_VERSION):
88 raise CredentialsFileError('No configuration for version %s' %
89 CREDENTIALS_FILE_VERSION)
90 consumer_key = parser.get(
91 CREDENTIALS_FILE_VERSION, 'consumer_key')
92 consumer_secret = parser.get(
93 CREDENTIALS_FILE_VERSION, 'consumer_secret')
94 self.consumer = Consumer(consumer_key, consumer_secret)
95 access_token = parser.get(
96 CREDENTIALS_FILE_VERSION, 'access_token')
97 access_secret = parser.get(
98 CREDENTIALS_FILE_VERSION, 'access_secret')
99 self.access_token = AccessToken(access_token, access_secret)
100
101 @classmethod
102 def load_from_path(cls, path):
103 """Convenience method for loading credentials from a file.
104
105 Open the file, create the Credentials and load from the file,
106 and finally close the file and return the newly created
107 Credentials instance.
108
109 :param path: In which file the credential file should be saved.
110 :type path: string
111 :return: The loaded Credentials instance.
112 :rtype: `Credentials`
113 """
114 credentials = cls()
115 credentials_file = open(path, 'r')
116 credentials.load(credentials_file)
117 credentials_file.close()
118 return credentials
119
120 def save(self, writable_file):
121 """Write the credentials to the file-like object.
122
123 :param writable_file: A file-like object to write the credentials to
124 :type writable_file: Any object supporting the file-like `write()`
125 method
126 :raise CredentialsFileError: when there is either no consumer or no
127 access token
128 """
129 if self.consumer is None:
130 raise CredentialsFileError('No consumer')
131 if self.access_token is None:
132 raise CredentialsFileError('No access token')
133
134 parser = SafeConfigParser()
135 parser.add_section(CREDENTIALS_FILE_VERSION)
136 parser.set(CREDENTIALS_FILE_VERSION,
137 'consumer_key', self.consumer.key)
138 parser.set(CREDENTIALS_FILE_VERSION,
139 'consumer_secret', self.consumer.secret)
140 parser.set(CREDENTIALS_FILE_VERSION,
141 'access_token', self.access_token.key)
142 parser.set(CREDENTIALS_FILE_VERSION,
143 'access_secret', self.access_token.secret)
144 parser.write(writable_file)
145
146 def save_to_path(self, path):
147 """Convenience method for saving credentials to a file.
148
149 Create the file, call self.save(), and close the file. Existing
150 files are overwritten.
151
152 :param path: In which file the credential file should be saved.
153 :type path: string
154 """
155 credentials_file = open(path, 'w')
156 self.save(credentials_file)
157 credentials_file.close()
158
159 def authorizeRequest(self, absolute_uri, method, body, headers):
160 """Sign a request with OAuth credentials."""
161 oauth_request = OAuthRequest.from_consumer_and_token(
162 self.consumer, self.access_token, http_url=absolute_uri)
163 oauth_request.sign_request(
164 OAuthSignatureMethod_PLAINTEXT(),
165 self.consumer, self.access_token)
166 headers.update(oauth_request.to_header(self.oauth_realm))
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 >>> def dummy_application(environ, start_response):
18 ... start_response(
19 ... '200', [('Content-type','application/vnd.sun.wadl+xml')])
20 ... return [wadl_string]
21
22
23 The WADL file will be protected with HTTP Basic Auth. To access it,
24 you'll need to provide a username of "user" and a password of
25 "password".
26
27 >>> def authenticate(username, password):
28 ... """Accepts "user/password", rejects everything else.
29 ...
30 ... :return: The username, if the credentials are valid.
31 ... None, otherwise.
32 ... """
33 ... if username == "user" and password == "password":
34 ... return username
35 ... return None
36
37 >>> from lazr.authentication.wsgi import BasicAuthMiddleware
38 >>> def protected_application():
39 ... return BasicAuthMiddleware(
40 ... dummy_application, authenticate_with=authenticate)
41
42 Finally, we'll set up a WSGI intercept so that we can test the web
43 service by making HTTP requests to http://api.launchpad.dev/. (This is
44 the hostname mentioned in the WADL file.)
45
46 >>> import wsgi_intercept
47 >>> from wsgi_intercept.httplib2_intercept import install
48 >>> install()
49 >>> wsgi_intercept.add_wsgi_intercept(
50 ... 'api.launchpad.dev', 80, protected_application)
51
52 With no HttpAuthorizer, a ServiceRoot can't get access to the web service.
53
54 >>> from lazr.restfulclient.resource import ServiceRoot
55 >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
56 Traceback (most recent call last):
57 ...
58 HTTPError: HTTP Error 401: Unauthorized
59 ...
60
61 We can't get access if the authorizer doesn't have the right
62 credentials.
63
64 >>> from lazr.restfulclient.authorize import BasicHttpAuthorizer
65
66 >>> bad_authorizer = BasicHttpAuthorizer("baduser", "badpassword")
67 >>> client = ServiceRoot(bad_authorizer, "http://api.launchpad.dev/")
68 Traceback (most recent call last):
69 ...
70 HTTPError: HTTP Error 401: Unauthorized
71 ...
72
73 If we provide the right credentials, we can retrieve the WADL. We'll
74 still get an exception, because our fake web service is too fake for
75 ServiceRoot--it doesn't serve any JSON resources--but we're able to
76 make HTTP requests without getting 401 errors.
77
78 >>> authorizer = BasicHttpAuthorizer("user", "password")
79 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
80 Traceback (most recent call last):
81 ...
82 ValueError: No JSON object could be decoded
83
84 Teardown.
85
86 >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
87
88
89 The OAuthAuthorizer
90 -------------------
91
92 This authorizer handles OAuth authorization. To test it, we'll protect
93 the dummy application with a piece of OAuth middleware. The middleware
94 will accept only one consumer/token combination.
95
96 >>> from oauth.oauth import OAuthConsumer, OAuthToken
97 >>> valid_consumer = OAuthConsumer("consumer", '')
98 >>> valid_token = OAuthToken("token", "secret")
99
100 Our authenticate() implementation checks against the one valid
101 consumer and token.
102
103 >>> def authenticate(consumer, token, parameters):
104 ... """Accepts the valid consumer and token, rejects everything else.
105 ...
106 ... :return: The consumer, if the credentials are valid.
107 ... None, otherwise.
108 ... """
109 ... if consumer == valid_consumer and token == valid_token:
110 ... return consumer
111 ... return None
112
113 Our data store helps the middleware look up consumer and token objects
114 from the information provided in a signed OAuth request.
115
116 >>> from lazr.authentication.testing.oauth import SimpleOAuthDataStore
117 >>> data_store = SimpleOAuthDataStore(
118 ... {valid_consumer.key : valid_consumer},
119 ... {valid_token.key : valid_token})
120
121 Now we're ready to protect the dummy_application with OAuthMiddleware,
122 using our authenticate() implementation and our data store.
123
124 >>> from lazr.authentication.wsgi import OAuthMiddleware
125 >>> def protected_application():
126 ... return OAuthMiddleware(
127 ... dummy_application, realm="OAuth test",
128 ... authenticate_with=authenticate, data_store=data_store)
129 >>> wsgi_intercept.add_wsgi_intercept(
130 ... 'api.launchpad.dev', 80, protected_application)
131
132 Let's try out some clients. As you'd expect, you can't get through the
133 middleware with no HTTPAuthorizer at all.
134
135 >>> from lazr.restfulclient.authorize.oauth import OAuthAuthorizer
136 >>> client = ServiceRoot(None, "http://api.launchpad.dev/")
137 Traceback (most recent call last):
138 ...
139 HTTPError: HTTP Error 401: Unauthorized
140 ...
141
142 Invalid credentials are also no help.
143
144 >>> authorizer = OAuthAuthorizer(
145 ... valid_consumer.key, access_token=OAuthToken("invalid", "token"))
146 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
147 Traceback (most recent call last):
148 ...
149 HTTPError: HTTP Error 401: Unauthorized
150 ...
151
152 But valid credentials work fine (again, up to the point at which
153 lazr.restfulclient runs against the limits of this simple web service).
154
155 >>> authorizer = OAuthAuthorizer(
156 ... valid_consumer.key, access_token=valid_token)
157 >>> client = ServiceRoot(authorizer, "http://api.launchpad.dev/")
158 Traceback (most recent call last):
159 ...
160 ValueError: No JSON object could be decoded
161
162 Teardown.
163
164 >>> wsgi_intercept.remove_wsgi_intercept("api.launchpad.dev", 80)
165
166 Accessing the authorizer object after the fact
167 ----------------------------------------------
168
169 A ServiceRoot object has a 'credentials' attribute which contains the
170 Authorizer used to authorize outgoing requests.
171
172 >>> from lazr.restfulclient.resource import ServiceRoot
173 >>> root = ServiceRoot(authorizer, "http://cookbooks.dev/1.0/")
174 >>> root.credentials
175 <lazr.restfulclient.authorize.oauth.OAuthAuthorizer object...>
176
177 Server-side permissions
178 -----------------------
179
180 The server may hide some data from you because you lack the permission
181 to see it. To avoid objects that are mysteriously missing fields, the
182 server will serve a special "redacted" value that lets you know you
183 don't have permission to see the data.
184
185 >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
186 >>> service = CookbookWebServiceClient()
187
188 >>> cookbook = service.recipes[1].cookbook
189 >>> print cookbook.confirmed
190 tag:launchpad.net:2008:redacted
191
192 If you try to make an HTTP request for the "redacted" value (usually
193 by following a link that you don't know is redacted), you'll get a
194 helpful error.
195
196 >>> service.load("tag:launchpad.net:2008:redacted")
197 Traceback (most recent call last):
198 ...
199 ValueError: You tried to access a resource that you don't have the
200 server-side permission to see.
1414 ...
1515 header: Transfer-Encoding: deflate
1616 ...
17 header: Content-Type: application/vd.sun.wadl+xml
17 header: Content-Type: application/vnd.sun.wadl+xml
1818 send: 'GET /1.0/ ...
1919 reply: ...200...
2020 ...
1313 >>> len(names)
1414 5
1515 >>> names
16 ['Baked beans', ..., 'Roast chicken']
16 [u'Baked beans', ..., u'Roast chicken']
1717
1818 But it's almost always better to slice them.
1919
2020 >>> sorted([recipe.dish.name for recipe in service.recipes[:2]])
21 ['Roast chicken', 'Roast chicken']
21 [u'Roast chicken', u'Roast chicken']
2222
2323 You can get a slice of any collection, so long as you provide start
2424 and end points keyed to the beginning of the list. You can't key a
9090 >>> cookbook.cuisine
9191 u'Fran\xe7aise'
9292 >>> cookbook.description
93 ''
93 u''
9494
9595 >>> cookbook.cuisine = 'Dessert'
9696 >>> cookbook.description = "A new description"
161161 Traceback (most recent call last):
162162 ...
163163 HTTPError: HTTP Error 404: Not Found
164 ...
164165
165166 You can't bookmark the return value of a named operation. This is not
166167 really desirable, but that's how things work right now.
201202 Traceback (most recent call last):
202203 ...
203204 HTTPError: HTTP Error 404: Not Found
205 ...
204206
205207 >>> print service.load(new_link).name
206208 Another Name
228230 Traceback (most recent call last):
229231 ...
230232 HTTPError: HTTP Error 404: Not Found
231
233 ...
232234
233235 Validation
234236 ----------
279281 >>> cookbook.description = " Some extraneous whitespace "
280282 >>> cookbook.lp_save()
281283 >>> cookbook.description
282 'Some extraneous whitespace'
284 u'Some extraneous whitespace'
283285
284286 Data types
285287 ----------
329331 Traceback (most recent call last):
330332 ...
331333 HTTPError: HTTP Error 412: Precondition Failed
334 ...
332335
333336 Now the second client has a chance to look at the changes that were
334337 made, before making their own changes.
356359 Traceback (most recent call last):
357360 ...
358361 HTTPError: HTTP Error 412: Precondition Failed
362 ...
359363
360364 >>> second_cookbook.lp_refresh()
361365 >>> print second_cookbook.description
367371 >>> first_cookbook.lp_refresh()
368372 >>> print first_cookbook.description
369373 A conflicting description
374
375
376 Comparing entries
377 -----------------
378
379 Two entries are equal if they represent the same state of the same
380 server-side resource.
381
382 >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
383 >>> service = CookbookWebServiceClient()
384
385 What does this mean? Well, two distinct objects that represent the
386 same resource are equal.
387
388 >>> recipe = service.recipes[1]
389 >>> recipe_2 = service.load(recipe.self_link)
390 >>> recipe is recipe_2
391 False
392
393 >>> recipe == recipe_2
394 True
395 >>> recipe != recipe_2
396 False
397
398 Two totally different entries are not equal.
399
400 >>> another_recipe = service.recipes[2]
401 >>> recipe == another_recipe
402 False
403
404 If one entry represents the current state of the server, and the other
405 is out of date or has client-side modifications, they will not be
406 considered equal.
407
408 Here, 'recipe' has been modified and 'recipe_2' represents the current
409 state of the server.
410
411 >>> recipe.instructions = "Modified for equality testing."
412 >>> recipe == recipe_2
413 False
414
415 After a save, 'recipe' is up to date, and 'recipe_2' is out of date.
416
417 >>> recipe.lp_save()
418 >>> recipe == recipe_2
419 False
420
421 Refreshing 'recipe_2' brings it up to date, and equality succeeds again.
422
423 >>> recipe_2.lp_refresh()
424 >>> recipe == recipe_2
425 True
426
427 If you make the _exact same_ client-side modifications to two objects
428 representing the same resource, the objects will be considered equal.
429
430 >>> recipe.instructions = "Modified again."
431 >>> recipe_2.instructions = recipe.instructions
432 >>> recipe == recipe_2
433 True
434
435 If you then save one of the objects, they will stop being equal,
436 because the saved object has a new ETag.
437
438 >>> recipe.lp_save()
439 >>> recipe == recipe_2
440 False
0 ************
1 Hosted files
2 ************
3
04 Some resources published by lazr.restful are externally hosted files
15 that can have binary representations. lazr.restfulclient gives you
26 access to these resources.
1620 Traceback (most recent call last):
1721 ...
1822 HTTPError: HTTP Error 404: Not Found
23 ...
1924
2025 You can open a hosted file for write access and write to it as though
2126 it were a file on disk.
6873 >>> last_modified == last_modified_2
6974 False
7075
71 Once it exists, a file can be deleted.
76 Once a file exists, it can be deleted.
7277
7378 >>> cover.delete()
7479 >>> cover.open()
7580 Traceback (most recent call last):
7681 ...
7782 HTTPError: HTTP Error 404: Not Found
83 ...
84
85 Comparing hosted files
86 ----------------------
87
88 Two hosted file objects are the same if they point to the same
89 server-side resource.
7890
7991
80 == Error handling ==
92 >>> cover = service.cookbooks['Everyday Greens'].cover
93 >>> cover_2 = service.cookbooks['Everyday Greens'].cover
94 >>> cover == cover_2
95 True
96
97 >>> other_cover = service.cookbooks['The Joy of Cooking'].cover
98 >>> cover == other_cover
99 False
100
101 Error handling
102 --------------
81103
82104 The only access modes supported are 'r' and 'w'.
83105
113135 ValueError: Files opened for read access can't specify filename.
114136
115137
116 == Caching ==
138 Caching
139 -------
117140
118141 Hosted file resources implement the normal server-side caching
119142 mechanism.
145168 reply: '...304 Not Modified...
146169 25
147170
171
148172 Finally, some cleanup code that deletes the cover.
149173
150174 >>> cover.delete()
7878 ... name="null", cuisine="General",
7979 ... copyright_date=date, price=1.23, last_printing=date)
8080 >>> cookbook.name
81 'null'
81 u'null'
8282
8383 >>> cookbook = service.cookbooks.create(
8484 ... name="4.56", cuisine="General",
8585 ... copyright_date=date, price=1.23, last_printing=date)
8686 >>> cookbook.name
87 '4.56'
87 u'4.56'
8888
8989 >>> cookbook = service.cookbooks.create(
9090 ... name='"foo"', cuisine="General",
9191 ... copyright_date=date, price=1.23, last_printing=date)
9292 >>> cookbook.name
93 '"foo"'
93 u'"foo"'
9494
9595 A named operation that takes a non-string object (such as a float)
9696 will not accept a string that's the JSON representation of the
3939
4040 >>> print service.recipes[1].dish.name
4141 Roast chicken
42
43 Error reporting
44 ===============
45
46 If there's an error communicating with the server, lazr.restfulclient
47 raises an HTTPError. The error might be a client-side error (maybe you
48 tried to access something that doesn't exist) or a server-side error
49 (maybe the server crashed due to a bug). The string representation of
50 the error should have enough information to help you figure out what
51 happened.
52
53 >>> service.load("http://cookbooks.dev/")
54 Traceback (most recent call last):
55 ...
56 HTTPError: HTTP Error 404: Not Found
57 Response headers:
58 ---
59 ...
60 content-type: text/plain
61 ...
62 ---
63 Response body:
64 ---
65 ...
66 ---
6060 """An HTTP non-2xx response code was received."""
6161
6262 def __str__(self):
63 return 'HTTP Error %s: %s' % (
64 self.response.status, self.response.reason)
63 """Show the error code, response headers, and response body."""
64 headers = "\n".join(["%s: %s" % pair
65 for pair in sorted(self.response.items())])
66 return ("HTTP Error %s: %s\n"
67 "Response headers:\n---\n%s\n---\n"
68 "Response body:\n---\n%s\n---\n") % (
69 self.response.status, self.response.reason, headers, self.content)
4040 from _json import DatetimeJSONEncoder
4141 from errors import HTTPError
4242
43 missing = object()
4344
4445 class HeaderDictionary:
4546 """A dictionary that bridges httplib2's and wadllib's expectations.
6162
6263 def __getitem__(self, key):
6364 """Retrieve a value, converting the key to lowercase."""
64 missing = object()
6565 value = self.get(key, missing)
6666 if value is missing:
6767 raise KeyError(key)
332332 self.__dict__['_wadl_resource'] = self._wadl_resource.bind(
333333 representation, self.JSON_MEDIA_TYPE)
334334
335 def __ne__(self, other):
336 """Inequality operator."""
337 return not self == other
338
335339
336340 class HostedFile(Resource):
337341 """A resource representing a file managed by a lazr.restful service."""
351355 """HostedFile objects define no web service parameters."""
352356 return []
353357
358 def __eq__(self, other):
359 """Equality comparison.
360
361 Two hosted files are the same if they have the same URL.
362
363 There is no need to check the contents because the only way to
364 retrieve or modify the hosted file contents is to open a
365 filehandle, which goes direct to the server.
366 """
367 return self._wadl_resource.url == other._wadl_resource.url
368
354369
355370 class ServiceRoot(Resource):
356371 """Entry point to the service. Subclass this for a service-specific client.
362377 # instantiating resources of a certain WADL type.
363378 RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile}
364379
365 def __init__(self, credentials, service_root, cache=None,
380 def __init__(self, authorizer, service_root, cache=None,
366381 timeout=None, proxy_info=None):
367382 """Root access to a lazr.restful API.
368383
371386 :type service_root: string
372387 """
373388 self._root_uri = URI(service_root)
374 self.credentials = credentials
375389 # Get the WADL definition.
376390 self._browser = Browser(
377 self, self.credentials, cache, timeout, proxy_info)
391 self, authorizer, cache, timeout, proxy_info)
378392 self._wadl = self._browser.get_wadl_application(self._root_uri)
379393
380394 # Get the root resource.
382396 bound_root = root_resource.bind(
383397 self._browser.get(root_resource), 'application/json')
384398 super(ServiceRoot, self).__init__(None, bound_root)
385
386 def httpFactory(self, credentials, cache, timeout, proxy_info):
387 return RestfulHttp(credentials, cache, timeout, proxy_info)
399 self.credentials = authorizer
400
401 def httpFactory(self, authorizer, cache, timeout, proxy_info):
402 return RestfulHttp(authorizer, cache, timeout, proxy_info)
388403
389404 def load(self, url):
390405 """Load a resource given its URL."""
391406 document = self._browser.get(url)
392407 try:
393 representation = simplejson.loads(document)
408 representation = simplejson.loads(unicode(document))
394409 except ValueError:
395410 raise ValueError("%s doesn't serve a JSON document." % url)
396411 type_link = representation.get("resource_type_link")
494509 # The operation returned a document with nothing
495510 # special about it.
496511 if content_type == self.JSON_MEDIA_TYPE:
497 return simplejson.loads(content)
512 return simplejson.loads(unicode(content))
498513 # We don't know how to process the content.
499514 return content
500515
501516 # The operation returned a representation of some
502517 # resource. Instantiate a Resource object for it.
503 document = simplejson.loads(content)
518 document = simplejson.loads(unicode(content))
504519 if document is None:
505520 # The operation returned a null value.
506521 return document
576591 (self.__class__.__name__, name))
577592 self._dirty_attributes[name] = value
578593
594 def __eq__(self, other):
595 """Equality operator.
596
597 Two entries are the same if their self_link and http_etag
598 attributes are the same, and if their dirty attribute dicts
599 contain the same values.
600 """
601 return (
602 self.self_link == other.self_link and
603 self.http_etag == other.http_etag and
604 self._dirty_attributes == other._dirty_attributes)
605
579606 def lp_refresh(self, new_url=None):
580607 """Update this resource's representation."""
581608 etag = getattr(self, 'http_etag', None)
612639 if response.status == 209 and content_type == self.JSON_MEDIA_TYPE:
613640 # The server sent back a new representation of the object.
614641 # Use it in preference to the existing representation.
615 new_representation = simplejson.loads(content)
642 new_representation = simplejson.loads(unicode(content))
616643 self._wadl_resource.representation = new_representation
617644 self._wadl_resource.media_type = content_type
618645
651678 if next_link is None:
652679 break
653680 current_page = simplejson.loads(
654 self._root._browser.get(URI(next_link)))
681 unicode(self._root._browser.get(URI(next_link))))
655682
656683 def __getitem__(self, key):
657684 """Look up a slice, or a subordinate resource by index.
724751 # Iterate over pages until we have the correct number of entries.
725752 while more_needed > 0 and page_url is not None:
726753 representation = simplejson.loads(
727 self._root._browser.get(page_url))
754 unicode(self._root._browser.get(page_url)))
728755 current_page_entries = representation['entries']
729756 entry_dicts += current_page_entries[:more_needed]
730757 more_needed = desired_size - len(entry_dicts)
816843 # is to retrieve a representation of the resource and see how
817844 # the resource describes itself.
818845 try:
819 representation = simplejson.loads(self._root._browser.get(url))
846 representation = simplejson.loads(
847 unicode(self._root._browser.get(url)))
820848 except HTTPError, error:
821849 # There's no resource corresponding to the given ID.
822850 if error.response.status == 404:
2424 import atexit
2525 import doctest
2626 import os
27 import wsgi_intercept
28 from wsgi_intercept.httplib2_intercept import install, uninstall
2927 from pkg_resources import (
3028 resource_filename, resource_exists, resource_listdir, cleanup_resources)
3129 import unittest
32 from lazr.restful.example.tests.test_integration import WSGILayer
30 import wsgi_intercept
31 from wsgi_intercept.httplib2_intercept import install, uninstall
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
3337 from lazr.restful.testing.webservice import WebServiceApplication
38
3439
3540 DOCTEST_FLAGS = (
3641 doctest.ELLIPSIS |
4651
4752 def tearDown(test):
4853 uninstall()
54 file_manager = getUtility(IFileManager)
55 file_manager.files = {}
56 file_manager.counter = 0
4957
5058
5159 def additional_tests():
0 # Copyright 2009 Canonical Ltd.
1
2 # This file is part of lazr.restfulclient.
3 #
4 # lazr.restfulclient is free software: you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public License
6 # as published by the Free Software Foundation, version 3 of the
7 # License.
8 #
9 # lazr.restfulclient is distributed in the hope that it will be
10 # useful, but WITHOUT ANY WARRANTY; without even the implied warranty
11 # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with lazr.restfulclient. If not, see <http://www.gnu.org/licenses/>.
16
17 """Tests for the Credentials class."""
18
19 __metaclass__ = type
20
21
22 import os
23 import os.path
24 import shutil
25 import tempfile
26 import unittest
27
28 from lazr.restfulclient.authorize.oauth import AccessToken, OAuthAuthorizer
29
30
31 class TestCredentialsSaveAndLoad(unittest.TestCase):
32 """Test for saving and loading credentials into an Authorizer."""
33
34 def setUp(self):
35 self.temp_dir = tempfile.mkdtemp()
36
37 def tearDown(self):
38 shutil.rmtree(self.temp_dir)
39
40 def test_save_to_and_load_from__path(self):
41 # Credentials can be saved to and loaded from a file using
42 # save_to_path() and load_from_path().
43 credentials_path = os.path.join(self.temp_dir, 'credentials')
44 credentials = OAuthAuthorizer(
45 'consumer.key', consumer_secret='consumer.secret',
46 access_token=AccessToken('access.key', 'access.secret'))
47 credentials.save_to_path(credentials_path)
48 self.assertTrue(os.path.exists(credentials_path))
49
50 loaded_credentials = OAuthAuthorizer.load_from_path(credentials_path)
51 self.assertEqual(loaded_credentials.consumer.key, 'consumer.key')
52 self.assertEqual(
53 loaded_credentials.consumer.secret, 'consumer.secret')
54 self.assertEqual(
55 loaded_credentials.access_token.key, 'access.key')
56 self.assertEqual(
57 loaded_credentials.access_token.secret, 'access.secret')
58
59 def test_suite():
60 return unittest.TestLoader().loadTestsFromName(__name__)
00 Metadata-Version: 1.0
11 Name: lazr.restfulclient
2 Version: 0.9.3
2 Version: 0.9.10
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
5555 NEWS for lazr.restfulclient
5656 ===========================
5757
58 0.9.10 (2009-10-23)
59 ===================
60
61 - lazr.restfulclient now requests the correct WADL media type.
62 - Made HTTPError strings more verbose.
63 - Implemented the equality operator for entry and hosted-file resources.
64 - Resume setting the 'credentials' attribute on ServerRoot to avoid
65 breaking compatibility with launchpadlib.
66
67 0.9.9 (2009-10-07)
68 ==================
69
70 - The WSGI authentication middleware has been moved from lazr.restful
71 to the new lazr.authentication library, and lazr.restfulclient now
72 uses the new library.
73
74 0.9.8 (2009-10-06)
75 ==================
76
77 - Added support for OAuth.
78
79 0.9.7 (2009-09-30)
80 ==================
81
82 - Added support for HTTP Basic Auth.
83
84 0.9.6 (2009-09-16)
85 ==================
86
87 - Made compatible with lazr.restful 0.9.6.
88
89 0.9.5 (2009-08-28)
90 ==================
91
92 - Removed debugging code.
93
94 0.9.4 (2009-08-26)
95 ==================
96
97 - Removed unnecessary build dependencies.
98
99 - Updated tests for newer version of simplejson.
100
101 - Made tests less fragile by cleaning up lazr.restful example filemanager
102 between tests.
103
104 - normalized output of simplejson to unicode.
105
58106 0.9.3 (2009-08-05)
59107 ==================
60108
1919 src/lazr/restfulclient/errors.py
2020 src/lazr/restfulclient/resource.py
2121 src/lazr/restfulclient/version.txt
22 src/lazr/restfulclient/authorize/__init__.py
23 src/lazr/restfulclient/authorize/oauth.py
2224 src/lazr/restfulclient/docs/__init__.py
25 src/lazr/restfulclient/docs/authorizer.txt
2326 src/lazr/restfulclient/docs/caching.txt
2427 src/lazr/restfulclient/docs/collections.txt
2528 src/lazr/restfulclient/docs/entries.txt
2831 src/lazr/restfulclient/docs/toplevel.txt
2932 src/lazr/restfulclient/tests/__init__.py
3033 src/lazr/restfulclient/tests/example.py
31 src/lazr/restfulclient/tests/test_docs.py
34 src/lazr/restfulclient/tests/test_docs.py
35 src/lazr/restfulclient/tests/test_oauth_credentials.py
0 httplib2
1 lazr.authentication
2 lazr.restful
3 oauth
04 setuptools
1 zope.interface
2 lazr.restful>=0.9.2
3 wadllib>=1.1.1
5 wadllib>=1.1.4
46 wsgi_intercept
57 van.testing
8 zope.interface
69
710 [docs]
811 Sphinx