Imported Upstream version 0.9.10
SVN-Git Migration
8 years ago
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: lazr.restfulclient |
2 | Version: 0.9.3 | |
2 | Version: 0.9.10 | |
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 |
55 | 55 | NEWS for lazr.restfulclient |
56 | 56 | =========================== |
57 | 57 | |
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 | ||
58 | 106 | 0.9.3 (2009-08-05) |
59 | 107 | ================== |
60 | 108 |
54 | 54 | 'src/lazr/restfulclient/NEWS.txt'), |
55 | 55 | license='LGPL v3', |
56 | 56 | install_requires=[ |
57 | 'httplib2', | |
58 | 'lazr.authentication', | |
59 | 'lazr.restful', | |
60 | 'oauth', | |
57 | 61 | 'setuptools', |
58 | 'zope.interface', | |
59 | 'lazr.restful>=0.9.2', | |
60 | 'wadllib>=1.1.1', | |
62 | 'wadllib>=1.1.4', | |
61 | 63 | 'wsgi_intercept', |
62 | 64 | 'van.testing', |
65 | 'zope.interface', | |
63 | 66 | ], |
64 | 67 | url='https://launchpad.net/lazr.restfulclient', |
65 | 68 | download_url= 'https://launchpad.net/lazr.restfulclient/+download', |
73 | 76 | docs=['Sphinx', |
74 | 77 | 'z3c.recipe.sphinxdoc'] |
75 | 78 | ), |
76 | setup_requires=['eggtestinfo', 'setuptools_bzr'], | |
77 | 79 | test_suite='lazr.restfulclient.tests', |
78 | 80 | ) |
0 | 0 | =========================== |
1 | 1 | NEWS for lazr.restfulclient |
2 | 2 | =========================== |
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. | |
3 | 51 | |
4 | 52 | 0.9.3 (2009-08-05) |
5 | 53 | ================== |
76 | 76 | react when its cache is a MultipleRepresentationCache. |
77 | 77 | """ |
78 | 78 | |
79 | def __init__(self, credentials, cache=None, timeout=None, | |
79 | def __init__(self, authorizer=None, cache=None, timeout=None, | |
80 | 80 | proxy_info=None): |
81 | 81 | 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) | |
86 | 85 | |
87 | 86 | def _request(self, conn, host, absolute_uri, request_uri, method, body, |
88 | 87 | headers, redirections, cachekey): |
102 | 101 | if 'accept-encoding' in headers: |
103 | 102 | headers['te'] = 'deflate, gzip' |
104 | 103 | 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) | |
105 | 112 | return super(RestfulHttp, self)._request( |
106 | 113 | conn, host, absolute_uri, request_uri, method, body, headers, |
107 | 114 | redirections, cachekey) |
189 | 196 | def _request(self, url, data=None, method='GET', |
190 | 197 | media_type='application/json', extra_headers=None): |
191 | 198 | """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 | ||
192 | 205 | # Add extra headers for the request. |
193 | 206 | headers = {'Accept' : media_type} |
194 | 207 | if isinstance(self._connection.cache, MultipleRepresentationCache): |
217 | 230 | |
218 | 231 | def get_wadl_application(self, url): |
219 | 232 | """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) | |
220 | 239 | response, content = self._request( |
221 | url, media_type='application/vd.sun.wadl+xml') | |
240 | url, media_type=wadl_type, extra_headers={'Accept': accept}) | |
222 | 241 | return Application(str(url), content) |
223 | 242 | |
224 | 243 | 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. |
14 | 14 | ... |
15 | 15 | header: Transfer-Encoding: deflate |
16 | 16 | ... |
17 | header: Content-Type: application/vd.sun.wadl+xml | |
17 | header: Content-Type: application/vnd.sun.wadl+xml | |
18 | 18 | send: 'GET /1.0/ ... |
19 | 19 | reply: ...200... |
20 | 20 | ... |
13 | 13 | >>> len(names) |
14 | 14 | 5 |
15 | 15 | >>> names |
16 | ['Baked beans', ..., 'Roast chicken'] | |
16 | [u'Baked beans', ..., u'Roast chicken'] | |
17 | 17 | |
18 | 18 | But it's almost always better to slice them. |
19 | 19 | |
20 | 20 | >>> sorted([recipe.dish.name for recipe in service.recipes[:2]]) |
21 | ['Roast chicken', 'Roast chicken'] | |
21 | [u'Roast chicken', u'Roast chicken'] | |
22 | 22 | |
23 | 23 | You can get a slice of any collection, so long as you provide start |
24 | 24 | and end points keyed to the beginning of the list. You can't key a |
90 | 90 | >>> cookbook.cuisine |
91 | 91 | u'Fran\xe7aise' |
92 | 92 | >>> cookbook.description |
93 | '' | |
93 | u'' | |
94 | 94 | |
95 | 95 | >>> cookbook.cuisine = 'Dessert' |
96 | 96 | >>> cookbook.description = "A new description" |
161 | 161 | Traceback (most recent call last): |
162 | 162 | ... |
163 | 163 | HTTPError: HTTP Error 404: Not Found |
164 | ... | |
164 | 165 | |
165 | 166 | You can't bookmark the return value of a named operation. This is not |
166 | 167 | really desirable, but that's how things work right now. |
201 | 202 | Traceback (most recent call last): |
202 | 203 | ... |
203 | 204 | HTTPError: HTTP Error 404: Not Found |
205 | ... | |
204 | 206 | |
205 | 207 | >>> print service.load(new_link).name |
206 | 208 | Another Name |
228 | 230 | Traceback (most recent call last): |
229 | 231 | ... |
230 | 232 | HTTPError: HTTP Error 404: Not Found |
231 | ||
233 | ... | |
232 | 234 | |
233 | 235 | Validation |
234 | 236 | ---------- |
279 | 281 | >>> cookbook.description = " Some extraneous whitespace " |
280 | 282 | >>> cookbook.lp_save() |
281 | 283 | >>> cookbook.description |
282 | 'Some extraneous whitespace' | |
284 | u'Some extraneous whitespace' | |
283 | 285 | |
284 | 286 | Data types |
285 | 287 | ---------- |
329 | 331 | Traceback (most recent call last): |
330 | 332 | ... |
331 | 333 | HTTPError: HTTP Error 412: Precondition Failed |
334 | ... | |
332 | 335 | |
333 | 336 | Now the second client has a chance to look at the changes that were |
334 | 337 | made, before making their own changes. |
356 | 359 | Traceback (most recent call last): |
357 | 360 | ... |
358 | 361 | HTTPError: HTTP Error 412: Precondition Failed |
362 | ... | |
359 | 363 | |
360 | 364 | >>> second_cookbook.lp_refresh() |
361 | 365 | >>> print second_cookbook.description |
367 | 371 | >>> first_cookbook.lp_refresh() |
368 | 372 | >>> print first_cookbook.description |
369 | 373 | 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 | ||
0 | 4 | Some resources published by lazr.restful are externally hosted files |
1 | 5 | that can have binary representations. lazr.restfulclient gives you |
2 | 6 | access to these resources. |
16 | 20 | Traceback (most recent call last): |
17 | 21 | ... |
18 | 22 | HTTPError: HTTP Error 404: Not Found |
23 | ... | |
19 | 24 | |
20 | 25 | You can open a hosted file for write access and write to it as though |
21 | 26 | it were a file on disk. |
68 | 73 | >>> last_modified == last_modified_2 |
69 | 74 | False |
70 | 75 | |
71 | Once it exists, a file can be deleted. | |
76 | Once a file exists, it can be deleted. | |
72 | 77 | |
73 | 78 | >>> cover.delete() |
74 | 79 | >>> cover.open() |
75 | 80 | Traceback (most recent call last): |
76 | 81 | ... |
77 | 82 | 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. | |
78 | 90 | |
79 | 91 | |
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 | -------------- | |
81 | 103 | |
82 | 104 | The only access modes supported are 'r' and 'w'. |
83 | 105 | |
113 | 135 | ValueError: Files opened for read access can't specify filename. |
114 | 136 | |
115 | 137 | |
116 | == Caching == | |
138 | Caching | |
139 | ------- | |
117 | 140 | |
118 | 141 | Hosted file resources implement the normal server-side caching |
119 | 142 | mechanism. |
145 | 168 | reply: '...304 Not Modified... |
146 | 169 | 25 |
147 | 170 | |
171 | ||
148 | 172 | Finally, some cleanup code that deletes the cover. |
149 | 173 | |
150 | 174 | >>> cover.delete() |
78 | 78 | ... name="null", cuisine="General", |
79 | 79 | ... copyright_date=date, price=1.23, last_printing=date) |
80 | 80 | >>> cookbook.name |
81 | 'null' | |
81 | u'null' | |
82 | 82 | |
83 | 83 | >>> cookbook = service.cookbooks.create( |
84 | 84 | ... name="4.56", cuisine="General", |
85 | 85 | ... copyright_date=date, price=1.23, last_printing=date) |
86 | 86 | >>> cookbook.name |
87 | '4.56' | |
87 | u'4.56' | |
88 | 88 | |
89 | 89 | >>> cookbook = service.cookbooks.create( |
90 | 90 | ... name='"foo"', cuisine="General", |
91 | 91 | ... copyright_date=date, price=1.23, last_printing=date) |
92 | 92 | >>> cookbook.name |
93 | '"foo"' | |
93 | u'"foo"' | |
94 | 94 | |
95 | 95 | A named operation that takes a non-string object (such as a float) |
96 | 96 | will not accept a string that's the JSON representation of the |
39 | 39 | |
40 | 40 | >>> print service.recipes[1].dish.name |
41 | 41 | 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 | --- |
60 | 60 | """An HTTP non-2xx response code was received.""" |
61 | 61 | |
62 | 62 | 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) |
40 | 40 | from _json import DatetimeJSONEncoder |
41 | 41 | from errors import HTTPError |
42 | 42 | |
43 | missing = object() | |
43 | 44 | |
44 | 45 | class HeaderDictionary: |
45 | 46 | """A dictionary that bridges httplib2's and wadllib's expectations. |
61 | 62 | |
62 | 63 | def __getitem__(self, key): |
63 | 64 | """Retrieve a value, converting the key to lowercase.""" |
64 | missing = object() | |
65 | 65 | value = self.get(key, missing) |
66 | 66 | if value is missing: |
67 | 67 | raise KeyError(key) |
332 | 332 | self.__dict__['_wadl_resource'] = self._wadl_resource.bind( |
333 | 333 | representation, self.JSON_MEDIA_TYPE) |
334 | 334 | |
335 | def __ne__(self, other): | |
336 | """Inequality operator.""" | |
337 | return not self == other | |
338 | ||
335 | 339 | |
336 | 340 | class HostedFile(Resource): |
337 | 341 | """A resource representing a file managed by a lazr.restful service.""" |
351 | 355 | """HostedFile objects define no web service parameters.""" |
352 | 356 | return [] |
353 | 357 | |
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 | ||
354 | 369 | |
355 | 370 | class ServiceRoot(Resource): |
356 | 371 | """Entry point to the service. Subclass this for a service-specific client. |
362 | 377 | # instantiating resources of a certain WADL type. |
363 | 378 | RESOURCE_TYPE_CLASSES = {'HostedFile': HostedFile} |
364 | 379 | |
365 | def __init__(self, credentials, service_root, cache=None, | |
380 | def __init__(self, authorizer, service_root, cache=None, | |
366 | 381 | timeout=None, proxy_info=None): |
367 | 382 | """Root access to a lazr.restful API. |
368 | 383 | |
371 | 386 | :type service_root: string |
372 | 387 | """ |
373 | 388 | self._root_uri = URI(service_root) |
374 | self.credentials = credentials | |
375 | 389 | # Get the WADL definition. |
376 | 390 | self._browser = Browser( |
377 | self, self.credentials, cache, timeout, proxy_info) | |
391 | self, authorizer, cache, timeout, proxy_info) | |
378 | 392 | self._wadl = self._browser.get_wadl_application(self._root_uri) |
379 | 393 | |
380 | 394 | # Get the root resource. |
382 | 396 | bound_root = root_resource.bind( |
383 | 397 | self._browser.get(root_resource), 'application/json') |
384 | 398 | 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) | |
388 | 403 | |
389 | 404 | def load(self, url): |
390 | 405 | """Load a resource given its URL.""" |
391 | 406 | document = self._browser.get(url) |
392 | 407 | try: |
393 | representation = simplejson.loads(document) | |
408 | representation = simplejson.loads(unicode(document)) | |
394 | 409 | except ValueError: |
395 | 410 | raise ValueError("%s doesn't serve a JSON document." % url) |
396 | 411 | type_link = representation.get("resource_type_link") |
494 | 509 | # The operation returned a document with nothing |
495 | 510 | # special about it. |
496 | 511 | if content_type == self.JSON_MEDIA_TYPE: |
497 | return simplejson.loads(content) | |
512 | return simplejson.loads(unicode(content)) | |
498 | 513 | # We don't know how to process the content. |
499 | 514 | return content |
500 | 515 | |
501 | 516 | # The operation returned a representation of some |
502 | 517 | # resource. Instantiate a Resource object for it. |
503 | document = simplejson.loads(content) | |
518 | document = simplejson.loads(unicode(content)) | |
504 | 519 | if document is None: |
505 | 520 | # The operation returned a null value. |
506 | 521 | return document |
576 | 591 | (self.__class__.__name__, name)) |
577 | 592 | self._dirty_attributes[name] = value |
578 | 593 | |
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 | ||
579 | 606 | def lp_refresh(self, new_url=None): |
580 | 607 | """Update this resource's representation.""" |
581 | 608 | etag = getattr(self, 'http_etag', None) |
612 | 639 | if response.status == 209 and content_type == self.JSON_MEDIA_TYPE: |
613 | 640 | # The server sent back a new representation of the object. |
614 | 641 | # Use it in preference to the existing representation. |
615 | new_representation = simplejson.loads(content) | |
642 | new_representation = simplejson.loads(unicode(content)) | |
616 | 643 | self._wadl_resource.representation = new_representation |
617 | 644 | self._wadl_resource.media_type = content_type |
618 | 645 | |
651 | 678 | if next_link is None: |
652 | 679 | break |
653 | 680 | current_page = simplejson.loads( |
654 | self._root._browser.get(URI(next_link))) | |
681 | unicode(self._root._browser.get(URI(next_link)))) | |
655 | 682 | |
656 | 683 | def __getitem__(self, key): |
657 | 684 | """Look up a slice, or a subordinate resource by index. |
724 | 751 | # Iterate over pages until we have the correct number of entries. |
725 | 752 | while more_needed > 0 and page_url is not None: |
726 | 753 | representation = simplejson.loads( |
727 | self._root._browser.get(page_url)) | |
754 | unicode(self._root._browser.get(page_url))) | |
728 | 755 | current_page_entries = representation['entries'] |
729 | 756 | entry_dicts += current_page_entries[:more_needed] |
730 | 757 | more_needed = desired_size - len(entry_dicts) |
816 | 843 | # is to retrieve a representation of the resource and see how |
817 | 844 | # the resource describes itself. |
818 | 845 | try: |
819 | representation = simplejson.loads(self._root._browser.get(url)) | |
846 | representation = simplejson.loads( | |
847 | unicode(self._root._browser.get(url))) | |
820 | 848 | except HTTPError, error: |
821 | 849 | # There's no resource corresponding to the given ID. |
822 | 850 | if error.response.status == 404: |
24 | 24 | import atexit |
25 | 25 | import doctest |
26 | 26 | import os |
27 | import wsgi_intercept | |
28 | from wsgi_intercept.httplib2_intercept import install, uninstall | |
29 | 27 | from pkg_resources import ( |
30 | 28 | resource_filename, resource_exists, resource_listdir, cleanup_resources) |
31 | 29 | 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 | |
33 | 37 | from lazr.restful.testing.webservice import WebServiceApplication |
38 | ||
34 | 39 | |
35 | 40 | DOCTEST_FLAGS = ( |
36 | 41 | doctest.ELLIPSIS | |
46 | 51 | |
47 | 52 | def tearDown(test): |
48 | 53 | uninstall() |
54 | file_manager = getUtility(IFileManager) | |
55 | file_manager.files = {} | |
56 | file_manager.counter = 0 | |
49 | 57 | |
50 | 58 | |
51 | 59 | 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__) |
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: lazr.restfulclient |
2 | Version: 0.9.3 | |
2 | Version: 0.9.10 | |
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 |
55 | 55 | NEWS for lazr.restfulclient |
56 | 56 | =========================== |
57 | 57 | |
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 | ||
58 | 106 | 0.9.3 (2009-08-05) |
59 | 107 | ================== |
60 | 108 |
19 | 19 | src/lazr/restfulclient/errors.py |
20 | 20 | src/lazr/restfulclient/resource.py |
21 | 21 | src/lazr/restfulclient/version.txt |
22 | src/lazr/restfulclient/authorize/__init__.py | |
23 | src/lazr/restfulclient/authorize/oauth.py | |
22 | 24 | src/lazr/restfulclient/docs/__init__.py |
25 | src/lazr/restfulclient/docs/authorizer.txt | |
23 | 26 | src/lazr/restfulclient/docs/caching.txt |
24 | 27 | src/lazr/restfulclient/docs/collections.txt |
25 | 28 | src/lazr/restfulclient/docs/entries.txt |
28 | 31 | src/lazr/restfulclient/docs/toplevel.txt |
29 | 32 | src/lazr/restfulclient/tests/__init__.py |
30 | 33 | 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⏎ |