Merge lp:~leonardr/launchpadlib/anonymous-access into lp:launchpadlib
- anonymous-access
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Guilherme Salgado |
Approved revision: | 76 |
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/launchpadlib/anonymous-access |
Merge into: | lp:launchpadlib |
Diff against target: |
269 lines (+130/-36) 5 files modified
src/launchpadlib/NEWS.txt (+4/-2) src/launchpadlib/__init__.py (+1/-1) src/launchpadlib/credentials.py (+10/-0) src/launchpadlib/docs/introduction.txt (+69/-15) src/launchpadlib/launchpad.py (+46/-18) |
To merge this branch: | bzr merge lp:~leonardr/launchpadlib/anonymous-access |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Guilherme Salgado (community) | Approve | ||
Review via email: mp+16213@code.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
Guilherme Salgado (salgado) wrote : | # |
Hi Leonard,
Just a couple nitpicks, but it looks good to go.
status approved
review approve
On Tue, 2009-12-15 at 18:33 +0000, Leonard Richardson wrote:
> Leonard Richardson has proposed merging
> === modified file 'src/launchpadl
> --- src/launchpadli
> +++ src/launchpadli
> @@ -19,6 +19,7 @@
> __metaclass__ = type
> __all__ = [
> 'AccessToken',
> + 'AnonymousAcces
> 'RequestTokenAu
> 'Consumer',
> 'Credentials',
> @@ -169,6 +170,14 @@
> return cls(key, secret, context)
>
>
> +class AnonymousAccess
> + """An OAuth access token that doesn't authenticate anybody.
> +
> + This token can be used for anonymous access."""
PEP-8 says the closing triple quotes should be on a line by itself for
multi-line docstrings.
> + def __init__(self):
> + super(Anonymous
> +
> +
> class SimulatedLaunch
> """A programmable substitute for a human-operated web browser.
>
> === modified file 'src/launchpadl
> --- src/launchpadli
> +++ src/launchpadli
> @@ -32,7 +32,8 @@
> from lazr.restfulcli
> CollectionWithK
> from launchpadlib.
> - AccessToken, Credentials, AuthorizeReques
> + AccessToken, AnonymousAccess
> + AuthorizeReques
> from oauth.oauth import OAuthRequest, OAuthSignatureM
> from launchpadlib import uris
>
> @@ -170,6 +171,19 @@
> return cls(credentials, service_root, cache, timeout, proxy_info)
>
> @classmethod
> + def login_anonymously(
> + cls, consumer_name, service_
> + launchpadlib_
> + """Get access to Launchpad without providing any credentials.
> +
> + """
You could fit that whole docstring on a single line, no?
> + service_root_dir, cache_path = cls._get_paths(
> + service_root, launchpadlib_dir)
> + token = AnonymousAccess
> + credentials = Credentials(
> + return cls(credentials, service_root, cache_path, timeout, proxy_info)
> +
> + @classmethod
> def login_with(cls, consumer_name,
> service_
> launchpadlib_
--
Guilherme Salgado <email address hidden>
Leonard Richardson (leonardr) wrote : | # |
Here's an incremental diff. I noticed that one of the tests was failing because it _is_ now possible to access Launchpad without any credentials. I replaced it with a more specific test, but I'm not convinced the new test adds any new test coverage. Let me know if you think I should keep it.
- 77. By Leonard Richardson
-
Response to feedback, and fixed a broken test.
- 78. By Leonard Richardson
-
Prep for release.
Preview Diff
1 | === modified file 'src/launchpadlib/NEWS.txt' | |||
2 | --- src/launchpadlib/NEWS.txt 2009-11-03 14:45:05 +0000 | |||
3 | +++ src/launchpadlib/NEWS.txt 2009-12-17 13:18:16 +0000 | |||
4 | @@ -2,8 +2,10 @@ | |||
5 | 2 | NEWS for launchpadlib | 2 | NEWS for launchpadlib |
6 | 3 | ===================== | 3 | ===================== |
7 | 4 | 4 | ||
10 | 5 | Development | 5 | 1.5.4 (2009-12-17) |
11 | 6 | =========== | 6 | ================== |
12 | 7 | |||
13 | 8 | - Made it easy to get anonymous access to a Launchpad instance. | ||
14 | 7 | 9 | ||
15 | 8 | - Made it easy to plug in different clients that take the user's | 10 | - Made it easy to plug in different clients that take the user's |
16 | 9 | Launchpad login and password for purposes of authorizing a request | 11 | Launchpad login and password for purposes of authorizing a request |
17 | 10 | 12 | ||
18 | === modified file 'src/launchpadlib/__init__.py' | |||
19 | --- src/launchpadlib/__init__.py 2009-10-22 16:02:25 +0000 | |||
20 | +++ src/launchpadlib/__init__.py 2009-12-17 13:18:16 +0000 | |||
21 | @@ -14,4 +14,4 @@ | |||
22 | 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 |
23 | 15 | # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. | 15 | # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>. |
24 | 16 | 16 | ||
26 | 17 | __version__ = '1.5.3' | 17 | __version__ = '1.5.4' |
27 | 18 | 18 | ||
28 | === modified file 'src/launchpadlib/credentials.py' | |||
29 | --- src/launchpadlib/credentials.py 2009-11-02 21:03:53 +0000 | |||
30 | +++ src/launchpadlib/credentials.py 2009-12-17 13:18:16 +0000 | |||
31 | @@ -19,6 +19,7 @@ | |||
32 | 19 | __metaclass__ = type | 19 | __metaclass__ = type |
33 | 20 | __all__ = [ | 20 | __all__ = [ |
34 | 21 | 'AccessToken', | 21 | 'AccessToken', |
35 | 22 | 'AnonymousAccessToken', | ||
36 | 22 | 'RequestTokenAuthorizationEngine', | 23 | 'RequestTokenAuthorizationEngine', |
37 | 23 | 'Consumer', | 24 | 'Consumer', |
38 | 24 | 'Credentials', | 25 | 'Credentials', |
39 | @@ -169,6 +170,15 @@ | |||
40 | 169 | return cls(key, secret, context) | 170 | return cls(key, secret, context) |
41 | 170 | 171 | ||
42 | 171 | 172 | ||
43 | 173 | class AnonymousAccessToken(_AccessToken): | ||
44 | 174 | """An OAuth access token that doesn't authenticate anybody. | ||
45 | 175 | |||
46 | 176 | This token can be used for anonymous access. | ||
47 | 177 | """ | ||
48 | 178 | def __init__(self): | ||
49 | 179 | super(AnonymousAccessToken, self).__init__('','') | ||
50 | 180 | |||
51 | 181 | |||
52 | 172 | class SimulatedLaunchpadBrowser(object): | 182 | class SimulatedLaunchpadBrowser(object): |
53 | 173 | """A programmable substitute for a human-operated web browser. | 183 | """A programmable substitute for a human-operated web browser. |
54 | 174 | 184 | ||
55 | 175 | 185 | ||
56 | === modified file 'src/launchpadlib/docs/introduction.txt' | |||
57 | --- src/launchpadlib/docs/introduction.txt 2009-11-03 13:50:27 +0000 | |||
58 | +++ src/launchpadlib/docs/introduction.txt 2009-12-17 13:18:16 +0000 | |||
59 | @@ -147,20 +147,71 @@ | |||
60 | 147 | 'test' | 147 | 'test' |
61 | 148 | 148 | ||
62 | 149 | 149 | ||
63 | 150 | Anonymous access | ||
64 | 151 | ================ | ||
65 | 152 | |||
66 | 153 | An anonymous access token doesn't authenticate any particular | ||
67 | 154 | user. Using it will give a client read-only access to the public parts | ||
68 | 155 | of the Launchpad dataset. | ||
69 | 156 | |||
70 | 157 | >>> from launchpadlib.credentials import AnonymousAccessToken | ||
71 | 158 | >>> anonymous_token = AnonymousAccessToken() | ||
72 | 159 | |||
73 | 160 | >>> from launchpadlib.credentials import Credentials | ||
74 | 161 | >>> credentials = Credentials( | ||
75 | 162 | ... consumer_name="a consumer", access_token=anonymous_token) | ||
76 | 163 | >>> launchpad = Launchpad(credentials=credentials) | ||
77 | 164 | |||
78 | 165 | >>> salgado = launchpad.people['salgado'] | ||
79 | 166 | >>> print salgado.display_name | ||
80 | 167 | Guilherme Salgado | ||
81 | 168 | |||
82 | 169 | An anonymous client can't modify the dataset, or read any data that's | ||
83 | 170 | permission-controlled or scoped to a particular user. | ||
84 | 171 | |||
85 | 172 | >>> launchpad.me | ||
86 | 173 | Traceback (most recent call last): | ||
87 | 174 | ... | ||
88 | 175 | HTTPError: HTTP Error 401: Unauthorized | ||
89 | 176 | ... | ||
90 | 177 | |||
91 | 178 | >>> salgado.display_name = "This won't work." | ||
92 | 179 | >>> salgado.lp_save() | ||
93 | 180 | Traceback (most recent call last): | ||
94 | 181 | ... | ||
95 | 182 | HTTPError: HTTP Error 401: Unauthorized | ||
96 | 183 | ... | ||
97 | 184 | |||
98 | 150 | Convenience | 185 | Convenience |
99 | 151 | =========== | 186 | =========== |
100 | 152 | 187 | ||
104 | 153 | When the consumer name, access token and access secret are all known up-front, | 188 | When you want anonymous access, a convenience method is available for |
105 | 154 | a convenience method is available for logging into the web service in one | 189 | setting up a web service connection in one function call. All you have |
106 | 155 | function call. | 190 | to provide is the consumer name. |
107 | 191 | |||
108 | 192 | >>> launchpad = Launchpad.login_anonymously('launchpad-library') | ||
109 | 193 | >>> sorted(launchpad.people) | ||
110 | 194 | [...] | ||
111 | 195 | |||
112 | 196 | >>> launchpad.me | ||
113 | 197 | Traceback (most recent call last): | ||
114 | 198 | ... | ||
115 | 199 | HTTPError: HTTP Error 401: Unauthorized | ||
116 | 200 | ... | ||
117 | 201 | |||
118 | 202 | Another function call is useful when the consumer name, access token | ||
119 | 203 | and access secret are all known up-front. | ||
120 | 156 | 204 | ||
121 | 157 | >>> launchpad = Launchpad.login( | 205 | >>> launchpad = Launchpad.login( |
122 | 158 | ... 'launchpad-library', 'salgado-change-anything', 'test') | 206 | ... 'launchpad-library', 'salgado-change-anything', 'test') |
123 | 159 | >>> sorted(launchpad.people) | 207 | >>> sorted(launchpad.people) |
124 | 160 | [...] | 208 | [...] |
125 | 161 | 209 | ||
128 | 162 | If that is not the case the application should obtain authorization from | 210 | >>> print launchpad.me.name |
129 | 163 | the user and get the credentials directly from Launchpad. | 211 | salgado |
130 | 212 | |||
131 | 213 | Otherwise, the application should obtain authorization from the user | ||
132 | 214 | and get a new set of credentials directly from Launchpad. | ||
133 | 164 | 215 | ||
134 | 165 | First we must get a request token. | 216 | First we must get a request token. |
135 | 166 | 217 | ||
136 | @@ -402,16 +453,6 @@ | |||
137 | 402 | Bad credentials | 453 | Bad credentials |
138 | 403 | =============== | 454 | =============== |
139 | 404 | 455 | ||
140 | 405 | The application is not allowed to access Launchpad if there are no | ||
141 | 406 | credentials. | ||
142 | 407 | |||
143 | 408 | >>> credentials = Credentials(consumer) | ||
144 | 409 | >>> launchpad = Launchpad(credentials=credentials) | ||
145 | 410 | Traceback (most recent call last): | ||
146 | 411 | ... | ||
147 | 412 | HTTPError: HTTP Error 401: Unauthorized | ||
148 | 413 | ... | ||
149 | 414 | |||
150 | 415 | The application is not allowed to access Launchpad with a bad access token. | 456 | The application is not allowed to access Launchpad with a bad access token. |
151 | 416 | 457 | ||
152 | 417 | >>> access_token = AccessToken('bad', 'no-secret') | 458 | >>> access_token = AccessToken('bad', 'no-secret') |
153 | @@ -424,6 +465,19 @@ | |||
154 | 424 | HTTPError: HTTP Error 401: Unauthorized | 465 | HTTPError: HTTP Error 401: Unauthorized |
155 | 425 | ... | 466 | ... |
156 | 426 | 467 | ||
157 | 468 | The application is not allowed to access Launchpad with a consumer | ||
158 | 469 | name that doesn't match the credentials. | ||
159 | 470 | |||
160 | 471 | >>> access_token = AccessToken('salgado-change-anything', 'test') | ||
161 | 472 | >>> credentials = Credentials( | ||
162 | 473 | ... consumer_name='not-the-launchpad-library', | ||
163 | 474 | ... access_token=access_token) | ||
164 | 475 | >>> launchpad = Launchpad(credentials=credentials) | ||
165 | 476 | Traceback (most recent call last): | ||
166 | 477 | ... | ||
167 | 478 | HTTPError: HTTP Error 401: Unauthorized | ||
168 | 479 | ... | ||
169 | 480 | |||
170 | 427 | The application is not allowed to access Launchpad with a bad access secret. | 481 | The application is not allowed to access Launchpad with a bad access secret. |
171 | 428 | 482 | ||
172 | 429 | >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret') | 483 | >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret') |
173 | 430 | 484 | ||
174 | === modified file 'src/launchpadlib/launchpad.py' | |||
175 | --- src/launchpadlib/launchpad.py 2009-12-03 14:38:29 +0000 | |||
176 | +++ src/launchpadlib/launchpad.py 2009-12-17 13:18:16 +0000 | |||
177 | @@ -32,7 +32,8 @@ | |||
178 | 32 | from lazr.restfulclient.resource import ( | 32 | from lazr.restfulclient.resource import ( |
179 | 33 | CollectionWithKeyBasedLookup, HostedFile, ServiceRoot) | 33 | CollectionWithKeyBasedLookup, HostedFile, ServiceRoot) |
180 | 34 | from launchpadlib.credentials import ( | 34 | from launchpadlib.credentials import ( |
182 | 35 | AccessToken, Credentials, AuthorizeRequestTokenWithBrowser) | 35 | AccessToken, AnonymousAccessToken, Credentials, |
183 | 36 | AuthorizeRequestTokenWithBrowser) | ||
184 | 36 | from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT | 37 | from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT |
185 | 37 | from launchpadlib import uris | 38 | from launchpadlib import uris |
186 | 38 | 39 | ||
187 | @@ -170,6 +171,17 @@ | |||
188 | 170 | return cls(credentials, service_root, cache, timeout, proxy_info) | 171 | return cls(credentials, service_root, cache, timeout, proxy_info) |
189 | 171 | 172 | ||
190 | 172 | @classmethod | 173 | @classmethod |
191 | 174 | def login_anonymously( | ||
192 | 175 | cls, consumer_name, service_root=uris.STAGING_SERVICE_ROOT, | ||
193 | 176 | launchpadlib_dir=None, timeout=None, proxy_info=None): | ||
194 | 177 | """Get access to Launchpad without providing any credentials.""" | ||
195 | 178 | service_root_dir, cache_path = cls._get_paths( | ||
196 | 179 | service_root, launchpadlib_dir) | ||
197 | 180 | token = AnonymousAccessToken() | ||
198 | 181 | credentials = Credentials(consumer_name, access_token=token) | ||
199 | 182 | return cls(credentials, service_root, cache_path, timeout, proxy_info) | ||
200 | 183 | |||
201 | 184 | @classmethod | ||
202 | 173 | def login_with(cls, consumer_name, | 185 | def login_with(cls, consumer_name, |
203 | 174 | service_root=uris.STAGING_SERVICE_ROOT, | 186 | service_root=uris.STAGING_SERVICE_ROOT, |
204 | 175 | launchpadlib_dir=None, timeout=None, proxy_info=None, | 187 | launchpadlib_dir=None, timeout=None, proxy_info=None, |
205 | @@ -210,27 +222,13 @@ | |||
206 | 210 | :rtype: `Launchpad` | 222 | :rtype: `Launchpad` |
207 | 211 | 223 | ||
208 | 212 | """ | 224 | """ |
225 | 213 | if launchpadlib_dir is None: | 225 | service_root_dir, cache_path = cls._get_paths( |
226 | 214 | home_dir = os.environ['HOME'] | 226 | service_root, launchpadlib_dir) |
211 | 215 | launchpadlib_dir = os.path.join(home_dir, '.launchpadlib') | ||
212 | 216 | launchpadlib_dir = os.path.expanduser(launchpadlib_dir) | ||
213 | 217 | if not os.path.exists(launchpadlib_dir): | ||
214 | 218 | os.makedirs(launchpadlib_dir,0700) | ||
215 | 219 | os.chmod(launchpadlib_dir,0700) | ||
216 | 220 | # Determine the real service root. | ||
217 | 221 | service_root = uris.lookup_service_root(service_root) | ||
218 | 222 | # Each service root has its own cache and credential dirs. | ||
219 | 223 | scheme, host_name, path, query, fragment = urlparse.urlsplit( | ||
220 | 224 | service_root) | ||
221 | 225 | service_root_dir = os.path.join(launchpadlib_dir, host_name) | ||
222 | 226 | cache_path = os.path.join(service_root_dir, 'cache') | ||
223 | 227 | if not os.path.exists(cache_path): | ||
224 | 228 | os.makedirs(cache_path) | ||
227 | 229 | credentials_path = os.path.join(service_root_dir, 'credentials') | 227 | credentials_path = os.path.join(service_root_dir, 'credentials') |
228 | 230 | if not os.path.exists(credentials_path): | 228 | if not os.path.exists(credentials_path): |
229 | 231 | os.makedirs(credentials_path) | 229 | os.makedirs(credentials_path) |
230 | 232 | if credentials_file is None: | 230 | if credentials_file is None: |
232 | 233 | consumer_credentials_path = os.path.join(credentials_path, | 231 | consumer_credentials_path = os.path.join(credentials_path, |
233 | 234 | consumer_name) | 232 | consumer_name) |
234 | 235 | else: | 233 | else: |
235 | 236 | consumer_credentials_path = credentials_file | 234 | consumer_credentials_path = credentials_file |
236 | @@ -250,3 +248,33 @@ | |||
237 | 250 | launchpad.credentials.save_to_path(consumer_credentials_path) | 248 | launchpad.credentials.save_to_path(consumer_credentials_path) |
238 | 251 | os.chmod(consumer_credentials_path, stat.S_IREAD | stat.S_IWRITE) | 249 | os.chmod(consumer_credentials_path, stat.S_IREAD | stat.S_IWRITE) |
239 | 252 | return launchpad | 250 | return launchpad |
240 | 251 | |||
241 | 252 | @classmethod | ||
242 | 253 | def _get_paths(cls, service_root, launchpadlib_dir=None): | ||
243 | 254 | """Locate launchpadlib-related user paths and ensure they exist. | ||
244 | 255 | |||
245 | 256 | This is a helper function used by login_with() and | ||
246 | 257 | login_anonymously(). | ||
247 | 258 | |||
248 | 259 | :param service_root: The service root the user wants to connect to. | ||
249 | 260 | :param launchpadlib_dir: The user's base launchpadlib directory, | ||
250 | 261 | if known. | ||
251 | 262 | :return: A 2-tuple: (cache_dir, service_root_dir) | ||
252 | 263 | """ | ||
253 | 264 | if launchpadlib_dir is None: | ||
254 | 265 | home_dir = os.environ['HOME'] | ||
255 | 266 | launchpadlib_dir = os.path.join(home_dir, '.launchpadlib') | ||
256 | 267 | launchpadlib_dir = os.path.expanduser(launchpadlib_dir) | ||
257 | 268 | if not os.path.exists(launchpadlib_dir): | ||
258 | 269 | os.makedirs(launchpadlib_dir,0700) | ||
259 | 270 | os.chmod(launchpadlib_dir,0700) | ||
260 | 271 | # Determine the real service root. | ||
261 | 272 | service_root = uris.lookup_service_root(service_root) | ||
262 | 273 | # Each service root has its own cache and credential dirs. | ||
263 | 274 | scheme, host_name, path, query, fragment = urlparse.urlsplit( | ||
264 | 275 | service_root) | ||
265 | 276 | service_root_dir = os.path.join(launchpadlib_dir, host_name) | ||
266 | 277 | cache_path = os.path.join(service_root_dir, 'cache') | ||
267 | 278 | if not os.path.exists(cache_path): | ||
268 | 279 | os.makedirs(cache_path) | ||
269 | 280 | return (cache_path, service_root_dir) |
This branch introduces a new _AccessToken subclass, AnonymousAccess Token. You can use it to get read-only public access to the Launchpad web service, without authenticating as any particular user.
I also added a login_anonymously() convenience method to Launchpad. Because anonymous access doesn't go through the normal credential- obtaining process, I couldn't have this method defer to login(). I refactored some login() code into _get_paths() so that it could be used by both login() and login_anonymous ly().
Anonymous credentials are not stored on disk.