Merge lp:~leonardr/launchpadlib/oauth-in-restfulclient into lp:launchpadlib
- oauth-in-restfulclient
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | 61 |
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/launchpadlib/oauth-in-restfulclient |
Merge into: | lp:launchpadlib |
Diff against target: |
333 lines 6 files modified
setup.py (+1/-1) src/launchpadlib/NEWS.txt (+6/-0) src/launchpadlib/__init__.py (+1/-1) src/launchpadlib/credentials.py (+9/-126) src/launchpadlib/launchpad.py (+0/-28) src/launchpadlib/tests/test_credentials.py (+0/-60) |
To merge this branch: | bzr merge lp:~leonardr/launchpadlib/oauth-in-restfulclient |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella | Approve | ||
Review via email: mp+13790@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
- 61. By Leonard Richardson
-
Prep for release.
Revision history for this message
Gavin Panella (allenap) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'setup.py' | |||
2 | --- setup.py 2009-07-08 18:48:28 +0000 | |||
3 | +++ setup.py 2009-10-22 16:05:21 +0000 | |||
4 | @@ -60,7 +60,7 @@ | |||
5 | 60 | license='LGPL v3', | 60 | license='LGPL v3', |
6 | 61 | install_requires=[ | 61 | install_requires=[ |
7 | 62 | 'httplib2', | 62 | 'httplib2', |
9 | 63 | 'lazr.restfulclient', | 63 | 'lazr.restfulclient>=0.9.9', |
10 | 64 | 'lazr.uri', | 64 | 'lazr.uri', |
11 | 65 | 'oauth', | 65 | 'oauth', |
12 | 66 | 'setuptools', | 66 | 'setuptools', |
13 | 67 | 67 | ||
14 | === modified file 'src/launchpadlib/NEWS.txt' | |||
15 | --- src/launchpadlib/NEWS.txt 2009-10-01 19:15:41 +0000 | |||
16 | +++ src/launchpadlib/NEWS.txt 2009-10-22 16:05:21 +0000 | |||
17 | @@ -2,6 +2,12 @@ | |||
18 | 2 | NEWS for launchpadlib | 2 | NEWS for launchpadlib |
19 | 3 | ===================== | 3 | ===================== |
20 | 4 | 4 | ||
21 | 5 | 1.5.3 (2009-10-22) | ||
22 | 6 | ================== | ||
23 | 7 | |||
24 | 8 | - Moved some more code from launchpadlib into the more generic | ||
25 | 9 | lazr.restfulclient. | ||
26 | 10 | |||
27 | 5 | 1.5.2 (2009-10-01) | 11 | 1.5.2 (2009-10-01) |
28 | 6 | ================== | 12 | ================== |
29 | 7 | 13 | ||
30 | 8 | 14 | ||
31 | === modified file 'src/launchpadlib/__init__.py' | |||
32 | --- src/launchpadlib/__init__.py 2009-10-01 19:15:41 +0000 | |||
33 | +++ src/launchpadlib/__init__.py 2009-10-22 16:05:21 +0000 | |||
34 | @@ -14,4 +14,4 @@ | |||
35 | 14 | # You should have received a copy of the GNU Lesser General Public License | 14 | # You should have received a copy of the GNU Lesser General Public License |
36 | 15 | # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
37 | 16 | 16 | ||
39 | 17 | __version__ = '1.5.2' | 17 | __version__ = '1.5.3' |
40 | 18 | 18 | ||
41 | === modified file 'src/launchpadlib/credentials.py' | |||
42 | --- src/launchpadlib/credentials.py 2009-03-26 21:07:35 +0000 | |||
43 | +++ src/launchpadlib/credentials.py 2009-10-22 16:05:21 +0000 | |||
44 | @@ -23,23 +23,22 @@ | |||
45 | 23 | 'Credentials', | 23 | 'Credentials', |
46 | 24 | ] | 24 | ] |
47 | 25 | 25 | ||
48 | 26 | from ConfigParser import SafeConfigParser | ||
49 | 27 | import cgi | 26 | import cgi |
50 | 28 | import httplib2 | 27 | import httplib2 |
51 | 29 | from oauth.oauth import OAuthConsumer, OAuthToken | ||
52 | 30 | from urllib import urlencode | 28 | from urllib import urlencode |
53 | 31 | 29 | ||
58 | 32 | from lazr.restfulclient.errors import CredentialsFileError, HTTPError | 30 | from lazr.restfulclient.errors import HTTPError |
59 | 33 | 31 | from lazr.restfulclient.authorize.oauth import ( | |
60 | 34 | 32 | AccessToken as _AccessToken, Consumer, OAuthAuthorizer) | |
61 | 35 | CREDENTIALS_FILE_VERSION = '1' | 33 | |
62 | 34 | |||
63 | 36 | STAGING_WEB_ROOT = 'https://staging.launchpad.net/' | 35 | STAGING_WEB_ROOT = 'https://staging.launchpad.net/' |
64 | 37 | request_token_page = '+request-token' | 36 | request_token_page = '+request-token' |
65 | 38 | access_token_page = '+access-token' | 37 | access_token_page = '+access-token' |
66 | 39 | authorize_token_page = '+authorize-token' | 38 | authorize_token_page = '+authorize-token' |
67 | 40 | 39 | ||
68 | 41 | 40 | ||
70 | 42 | class Credentials: | 41 | class Credentials(OAuthAuthorizer): |
71 | 43 | """Standard credentials storage and usage class. | 42 | """Standard credentials storage and usage class. |
72 | 44 | 43 | ||
73 | 45 | :ivar consumer: The consumer (application) | 44 | :ivar consumer: The consumer (application) |
74 | @@ -49,107 +48,6 @@ | |||
75 | 49 | """ | 48 | """ |
76 | 50 | _request_token = None | 49 | _request_token = None |
77 | 51 | 50 | ||
78 | 52 | def __init__(self, consumer_name=None, consumer_secret='', | ||
79 | 53 | access_token=None): | ||
80 | 54 | """The user's Launchpad API credentials. | ||
81 | 55 | |||
82 | 56 | :param consumer_name: The name of the consumer (application) | ||
83 | 57 | :param consumer_secret: The secret of the consumer | ||
84 | 58 | :param access_token: The authenticated user access token | ||
85 | 59 | :type access_token: `AccessToken` | ||
86 | 60 | """ | ||
87 | 61 | self.consumer = None | ||
88 | 62 | if consumer_name is not None: | ||
89 | 63 | self.consumer = Consumer(consumer_name, consumer_secret) | ||
90 | 64 | self.access_token = access_token | ||
91 | 65 | |||
92 | 66 | def load(self, readable_file): | ||
93 | 67 | """Load credentials from a file-like object. | ||
94 | 68 | |||
95 | 69 | This overrides the consumer and access token given in the constructor | ||
96 | 70 | and replaces them with the values read from the file. | ||
97 | 71 | |||
98 | 72 | :param readable_file: A file-like object to read the credentials from | ||
99 | 73 | :type readable_file: Any object supporting the file-like `read()` | ||
100 | 74 | method | ||
101 | 75 | """ | ||
102 | 76 | # Attempt to load the access token from the file. | ||
103 | 77 | parser = SafeConfigParser() | ||
104 | 78 | parser.readfp(readable_file) | ||
105 | 79 | # Check the version number and extract the access token and | ||
106 | 80 | # secret. Then convert these to the appropriate instances. | ||
107 | 81 | if not parser.has_section(CREDENTIALS_FILE_VERSION): | ||
108 | 82 | raise CredentialsFileError('No configuration for version %s' % | ||
109 | 83 | CREDENTIALS_FILE_VERSION) | ||
110 | 84 | consumer_key = parser.get( | ||
111 | 85 | CREDENTIALS_FILE_VERSION, 'consumer_key') | ||
112 | 86 | consumer_secret = parser.get( | ||
113 | 87 | CREDENTIALS_FILE_VERSION, 'consumer_secret') | ||
114 | 88 | self.consumer = Consumer(consumer_key, consumer_secret) | ||
115 | 89 | access_token = parser.get( | ||
116 | 90 | CREDENTIALS_FILE_VERSION, 'access_token') | ||
117 | 91 | access_secret = parser.get( | ||
118 | 92 | CREDENTIALS_FILE_VERSION, 'access_secret') | ||
119 | 93 | self.access_token = AccessToken(access_token, access_secret) | ||
120 | 94 | |||
121 | 95 | @classmethod | ||
122 | 96 | def load_from_path(cls, path): | ||
123 | 97 | """Convenience method for loading credentials from a file. | ||
124 | 98 | |||
125 | 99 | Open the file, create the Credentials and load from the file, | ||
126 | 100 | and finally close the file and return the newly created | ||
127 | 101 | Credentials instance. | ||
128 | 102 | |||
129 | 103 | :param path: In which file the credential file should be saved. | ||
130 | 104 | :type path: string | ||
131 | 105 | :return: The loaded Credentials instance. | ||
132 | 106 | :rtype: `Credentials` | ||
133 | 107 | """ | ||
134 | 108 | credentials = cls() | ||
135 | 109 | credentials_file = open(path, 'r') | ||
136 | 110 | credentials.load(credentials_file) | ||
137 | 111 | credentials_file.close() | ||
138 | 112 | return credentials | ||
139 | 113 | |||
140 | 114 | def save(self, writable_file): | ||
141 | 115 | """Write the credentials to the file-like object. | ||
142 | 116 | |||
143 | 117 | :param writable_file: A file-like object to write the credentials to | ||
144 | 118 | :type writable_file: Any object supporting the file-like `write()` | ||
145 | 119 | method | ||
146 | 120 | :raise CredentialsFileError: when there is either no consumer or no | ||
147 | 121 | access token | ||
148 | 122 | """ | ||
149 | 123 | if self.consumer is None: | ||
150 | 124 | raise CredentialsFileError('No consumer') | ||
151 | 125 | if self.access_token is None: | ||
152 | 126 | raise CredentialsFileError('No access token') | ||
153 | 127 | |||
154 | 128 | parser = SafeConfigParser() | ||
155 | 129 | parser.add_section(CREDENTIALS_FILE_VERSION) | ||
156 | 130 | parser.set(CREDENTIALS_FILE_VERSION, | ||
157 | 131 | 'consumer_key', self.consumer.key) | ||
158 | 132 | parser.set(CREDENTIALS_FILE_VERSION, | ||
159 | 133 | 'consumer_secret', self.consumer.secret) | ||
160 | 134 | parser.set(CREDENTIALS_FILE_VERSION, | ||
161 | 135 | 'access_token', self.access_token.key) | ||
162 | 136 | parser.set(CREDENTIALS_FILE_VERSION, | ||
163 | 137 | 'access_secret', self.access_token.secret) | ||
164 | 138 | parser.write(writable_file) | ||
165 | 139 | |||
166 | 140 | def save_to_path(self, path): | ||
167 | 141 | """Convenience method for saving credentials to a file. | ||
168 | 142 | |||
169 | 143 | Create the file, call self.save(), and close the file. Existing | ||
170 | 144 | files are overwritten. | ||
171 | 145 | |||
172 | 146 | :param path: In which file the credential file should be saved. | ||
173 | 147 | :type path: string | ||
174 | 148 | """ | ||
175 | 149 | credentials_file = open(path, 'w') | ||
176 | 150 | self.save(credentials_file) | ||
177 | 151 | credentials_file.close() | ||
178 | 152 | |||
179 | 153 | def get_request_token(self, context=None, web_root=STAGING_WEB_ROOT): | 51 | def get_request_token(self, context=None, web_root=STAGING_WEB_ROOT): |
180 | 154 | """Request an OAuth token to Launchpad. | 52 | """Request an OAuth token to Launchpad. |
181 | 155 | 53 | ||
182 | @@ -162,7 +60,7 @@ | |||
183 | 162 | validity within Launchpad. | 60 | validity within Launchpad. |
184 | 163 | :param web_root: The URL of the website on which the token | 61 | :param web_root: The URL of the website on which the token |
185 | 164 | should be requested. | 62 | should be requested. |
187 | 165 | :return: The URL for the user to authorize the `OAuthToken` provided | 63 | :return: The URL for the user to authorize the `AccessToken` provided |
188 | 166 | by Launchpad. | 64 | by Launchpad. |
189 | 167 | """ | 65 | """ |
190 | 168 | assert self.consumer is not None, "Consumer not specified." | 66 | assert self.consumer is not None, "Consumer not specified." |
191 | @@ -176,7 +74,7 @@ | |||
192 | 176 | url, method='POST', body=urlencode(params)) | 74 | url, method='POST', body=urlencode(params)) |
193 | 177 | if response.status != 200: | 75 | if response.status != 200: |
194 | 178 | raise HTTPError(response, content) | 76 | raise HTTPError(response, content) |
196 | 179 | self._request_token = OAuthToken.from_string(content) | 77 | self._request_token = AccessToken.from_string(content) |
197 | 180 | url = '%s%s?oauth_token=%s' % (web_root, authorize_token_page, | 78 | url = '%s%s?oauth_token=%s' % (web_root, authorize_token_page, |
198 | 181 | self._request_token.key) | 79 | self._request_token.key) |
199 | 182 | if context is not None: | 80 | if context is not None: |
200 | @@ -210,24 +108,9 @@ | |||
201 | 210 | self.access_token = AccessToken.from_string(content) | 108 | self.access_token = AccessToken.from_string(content) |
202 | 211 | 109 | ||
203 | 212 | 110 | ||
216 | 213 | # These two classes are provided for convenience (so applications don't need | 111 | class AccessToken(_AccessToken): |
205 | 214 | # to import from launchpadlib._oauth.oauth), and to provide a default argument | ||
206 | 215 | # for secret. | ||
207 | 216 | |||
208 | 217 | class Consumer(OAuthConsumer): | ||
209 | 218 | """An OAuth consumer (application).""" | ||
210 | 219 | |||
211 | 220 | def __init__(self, key, secret=''): | ||
212 | 221 | super(Consumer, self).__init__(key, secret) | ||
213 | 222 | |||
214 | 223 | |||
215 | 224 | class AccessToken(OAuthToken): | ||
217 | 225 | """An OAuth access token.""" | 112 | """An OAuth access token.""" |
218 | 226 | 113 | ||
219 | 227 | def __init__(self, key, secret='', context=None): | ||
220 | 228 | super(AccessToken, self).__init__(key, secret) | ||
221 | 229 | self.context = context | ||
222 | 230 | |||
223 | 231 | @classmethod | 114 | @classmethod |
224 | 232 | def from_string(cls, query_string): | 115 | def from_string(cls, query_string): |
225 | 233 | """Create and return a new `AccessToken` from the given string.""" | 116 | """Create and return a new `AccessToken` from the given string.""" |
226 | 234 | 117 | ||
227 | === modified file 'src/launchpadlib/launchpad.py' | |||
228 | --- src/launchpadlib/launchpad.py 2009-09-29 14:54:55 +0000 | |||
229 | +++ src/launchpadlib/launchpad.py 2009-10-22 16:05:21 +0000 | |||
230 | @@ -107,9 +107,6 @@ | |||
231 | 107 | super(Launchpad, self).__init__( | 107 | super(Launchpad, self).__init__( |
232 | 108 | credentials, service_root, cache, timeout, proxy_info) | 108 | credentials, service_root, cache, timeout, proxy_info) |
233 | 109 | 109 | ||
234 | 110 | def httpFactory(self, credentials, cache, timeout, proxy_info): | ||
235 | 111 | return OAuthSigningHttp(credentials, cache, timeout, proxy_info) | ||
236 | 112 | |||
237 | 113 | @classmethod | 110 | @classmethod |
238 | 114 | def login(cls, consumer_name, token_string, access_secret, | 111 | def login(cls, consumer_name, token_string, access_secret, |
239 | 115 | service_root=STAGING_SERVICE_ROOT, | 112 | service_root=STAGING_SERVICE_ROOT, |
240 | @@ -252,28 +249,3 @@ | |||
241 | 252 | os.path.join(credentials_path, consumer_name), | 249 | os.path.join(credentials_path, consumer_name), |
242 | 253 | stat.S_IREAD | stat.S_IWRITE) | 250 | stat.S_IREAD | stat.S_IWRITE) |
243 | 254 | return launchpad | 251 | return launchpad |
244 | 255 | |||
245 | 256 | |||
246 | 257 | class OAuthSigningHttp(RestfulHttp): | ||
247 | 258 | """A client that signs every outgoing request with OAuth credentials.""" | ||
248 | 259 | |||
249 | 260 | def _request(self, conn, host, absolute_uri, request_uri, method, body, | ||
250 | 261 | headers, redirections, cachekey): | ||
251 | 262 | """Sign a request with OAuth credentials before sending it.""" | ||
252 | 263 | oauth_request = OAuthRequest.from_consumer_and_token( | ||
253 | 264 | self.restful_credentials.consumer, | ||
254 | 265 | self.restful_credentials.access_token, | ||
255 | 266 | http_url=absolute_uri) | ||
256 | 267 | oauth_request.sign_request( | ||
257 | 268 | OAuthSignatureMethod_PLAINTEXT(), | ||
258 | 269 | self.restful_credentials.consumer, | ||
259 | 270 | self.restful_credentials.access_token) | ||
260 | 271 | if headers.has_key('authorization'): | ||
261 | 272 | # There's an authorization header left over from a | ||
262 | 273 | # previous request that resulted in a redirect. Remove it | ||
263 | 274 | # and start again. | ||
264 | 275 | del headers['authorization'] | ||
265 | 276 | headers.update(oauth_request.to_header(OAUTH_REALM)) | ||
266 | 277 | return super(OAuthSigningHttp, self)._request( | ||
267 | 278 | conn, host, absolute_uri, request_uri, method, body, headers, | ||
268 | 279 | redirections, cachekey) | ||
269 | 280 | 252 | ||
270 | === removed file 'src/launchpadlib/tests/test_credentials.py' | |||
271 | --- src/launchpadlib/tests/test_credentials.py 2009-07-09 12:54:19 +0000 | |||
272 | +++ src/launchpadlib/tests/test_credentials.py 1970-01-01 00:00:00 +0000 | |||
273 | @@ -1,60 +0,0 @@ | |||
274 | 1 | # Copyright 2009 Canonical Ltd. | ||
275 | 2 | |||
276 | 3 | # This file is part of launchpadlib. | ||
277 | 4 | # | ||
278 | 5 | # launchpadlib is free software: you can redistribute it and/or modify it | ||
279 | 6 | # under the terms of the GNU Lesser General Public License as published by the | ||
280 | 7 | # Free Software Foundation, version 3 of the License. | ||
281 | 8 | # | ||
282 | 9 | # launchpadlib is distributed in the hope that it will be useful, but WITHOUT | ||
283 | 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
284 | 11 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License | ||
285 | 12 | # for more details. | ||
286 | 13 | # | ||
287 | 14 | # You should have received a copy of the GNU Lesser General Public License | ||
288 | 15 | # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. | ||
289 | 16 | |||
290 | 17 | """Tests for the Credentials class.""" | ||
291 | 18 | |||
292 | 19 | __metaclass__ = type | ||
293 | 20 | |||
294 | 21 | |||
295 | 22 | import os | ||
296 | 23 | import os.path | ||
297 | 24 | import shutil | ||
298 | 25 | import tempfile | ||
299 | 26 | import unittest | ||
300 | 27 | |||
301 | 28 | from launchpadlib.credentials import AccessToken, Credentials | ||
302 | 29 | |||
303 | 30 | |||
304 | 31 | class TestCredentialsSaveAndLoad(unittest.TestCase): | ||
305 | 32 | """Test for saving and loading credentials.""" | ||
306 | 33 | |||
307 | 34 | def setUp(self): | ||
308 | 35 | self.temp_dir = tempfile.mkdtemp() | ||
309 | 36 | |||
310 | 37 | def tearDown(self): | ||
311 | 38 | shutil.rmtree(self.temp_dir) | ||
312 | 39 | |||
313 | 40 | def test_save_to_and_load_from__path(self): | ||
314 | 41 | # Credentials can be saved to and loaded from a file using | ||
315 | 42 | # save_to_path() and load_from_path(). | ||
316 | 43 | credentials_path = os.path.join(self.temp_dir, 'credentials') | ||
317 | 44 | credentials = Credentials( | ||
318 | 45 | 'consumer.key', consumer_secret='consumer.secret', | ||
319 | 46 | access_token=AccessToken('access.key', 'access.secret')) | ||
320 | 47 | credentials.save_to_path(credentials_path) | ||
321 | 48 | self.assertTrue(os.path.exists(credentials_path)) | ||
322 | 49 | |||
323 | 50 | loaded_credentials = Credentials.load_from_path(credentials_path) | ||
324 | 51 | self.assertEqual(loaded_credentials.consumer.key, 'consumer.key') | ||
325 | 52 | self.assertEqual( | ||
326 | 53 | loaded_credentials.consumer.secret, 'consumer.secret') | ||
327 | 54 | self.assertEqual( | ||
328 | 55 | loaded_credentials.access_token.key, 'access.key') | ||
329 | 56 | self.assertEqual( | ||
330 | 57 | loaded_credentials.access_token.secret, 'access.secret') | ||
331 | 58 | |||
332 | 59 | def test_suite(): | ||
333 | 60 | return unittest.TestLoader().loadTestsFromName(__name__) |
This branch removes a lot of code from launchpadlib that now lives in lazr.restfulclient. There should be no changes visible from outside. I've run the launchpadlib tests under a Launchpad installation and test-driven launchpadlib using bin/py to go through the token request process.