Merge lp:~leonardr/launchpadlib/anonymous-access into lp:launchpadlib

Proposed by Leonard Richardson
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
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) Approve
Review via email: mp+16213@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch introduces a new _AccessToken subclass, AnonymousAccessToken. 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_anonymously().

Anonymous credentials are not stored on disk.

Revision history for this message
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/launchpadlib/credentials.py'
> --- src/launchpadlib/credentials.py 2009-11-02 21:03:53 +0000
> +++ src/launchpadlib/credentials.py 2009-12-15 18:33:17 +0000
> @@ -19,6 +19,7 @@
> __metaclass__ = type
> __all__ = [
> 'AccessToken',
> + 'AnonymousAccessToken',
> 'RequestTokenAuthorizationEngine',
> 'Consumer',
> 'Credentials',
> @@ -169,6 +170,14 @@
> return cls(key, secret, context)
>
>
> +class AnonymousAccessToken(_AccessToken):
> + """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(AnonymousAccessToken, self).__init__('','')
> +
> +
> class SimulatedLaunchpadBrowser(object):
> """A programmable substitute for a human-operated web browser.
>

> === modified file 'src/launchpadlib/launchpad.py'
> --- src/launchpadlib/launchpad.py 2009-12-03 14:38:29 +0000
> +++ src/launchpadlib/launchpad.py 2009-12-15 18:33:17 +0000
> @@ -32,7 +32,8 @@
> from lazr.restfulclient.resource import (
> CollectionWithKeyBasedLookup, HostedFile, ServiceRoot)
> from launchpadlib.credentials import (
> - AccessToken, Credentials, AuthorizeRequestTokenWithBrowser)
> + AccessToken, AnonymousAccessToken, Credentials,
> + AuthorizeRequestTokenWithBrowser)
> from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
> 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_root=uris.STAGING_SERVICE_ROOT,
> + launchpadlib_dir=None, timeout=None, proxy_info=None):
> + """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 = AnonymousAccessToken()
> + credentials = Credentials(consumer_name, access_token=token)
> + return cls(credentials, service_root, cache_path, timeout, proxy_info)
> +
> + @classmethod
> def login_with(cls, consumer_name,
> service_root=uris.STAGING_SERVICE_ROOT,
> launchpadlib_dir=None, timeout=None, proxy_info=None,

--
Guilherme Salgado <email address hidden>

review: Approve
Revision history for this message
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.

http://pastebin.ubuntu.com/342874/

77. By Leonard Richardson

Response to feedback, and fixed a broken test.

78. By Leonard Richardson

Prep for release.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/launchpadlib/NEWS.txt'
--- src/launchpadlib/NEWS.txt 2009-11-03 14:45:05 +0000
+++ src/launchpadlib/NEWS.txt 2009-12-17 13:18:16 +0000
@@ -2,8 +2,10 @@
2NEWS for launchpadlib2NEWS for launchpadlib
3=====================3=====================
44
5Development51.5.4 (2009-12-17)
6===========6==================
7
8- Made it easy to get anonymous access to a Launchpad instance.
79
8- Made it easy to plug in different clients that take the user's10- Made it easy to plug in different clients that take the user's
9 Launchpad login and password for purposes of authorizing a request11 Launchpad login and password for purposes of authorizing a request
1012
=== modified file 'src/launchpadlib/__init__.py'
--- src/launchpadlib/__init__.py 2009-10-22 16:02:25 +0000
+++ src/launchpadlib/__init__.py 2009-12-17 13:18:16 +0000
@@ -14,4 +14,4 @@
14# You should have received a copy of the GNU Lesser General Public License14# You should have received a copy of the GNU Lesser General Public License
15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.15# along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
1616
17__version__ = '1.5.3'17__version__ = '1.5.4'
1818
=== modified file 'src/launchpadlib/credentials.py'
--- src/launchpadlib/credentials.py 2009-11-02 21:03:53 +0000
+++ src/launchpadlib/credentials.py 2009-12-17 13:18:16 +0000
@@ -19,6 +19,7 @@
19__metaclass__ = type19__metaclass__ = type
20__all__ = [20__all__ = [
21 'AccessToken',21 'AccessToken',
22 'AnonymousAccessToken',
22 'RequestTokenAuthorizationEngine',23 'RequestTokenAuthorizationEngine',
23 'Consumer',24 'Consumer',
24 'Credentials',25 'Credentials',
@@ -169,6 +170,15 @@
169 return cls(key, secret, context)170 return cls(key, secret, context)
170171
171172
173class AnonymousAccessToken(_AccessToken):
174 """An OAuth access token that doesn't authenticate anybody.
175
176 This token can be used for anonymous access.
177 """
178 def __init__(self):
179 super(AnonymousAccessToken, self).__init__('','')
180
181
172class SimulatedLaunchpadBrowser(object):182class SimulatedLaunchpadBrowser(object):
173 """A programmable substitute for a human-operated web browser.183 """A programmable substitute for a human-operated web browser.
174184
175185
=== modified file 'src/launchpadlib/docs/introduction.txt'
--- src/launchpadlib/docs/introduction.txt 2009-11-03 13:50:27 +0000
+++ src/launchpadlib/docs/introduction.txt 2009-12-17 13:18:16 +0000
@@ -147,20 +147,71 @@
147 'test'147 'test'
148148
149149
150Anonymous access
151================
152
153An anonymous access token doesn't authenticate any particular
154user. Using it will give a client read-only access to the public parts
155of the Launchpad dataset.
156
157 >>> from launchpadlib.credentials import AnonymousAccessToken
158 >>> anonymous_token = AnonymousAccessToken()
159
160 >>> from launchpadlib.credentials import Credentials
161 >>> credentials = Credentials(
162 ... consumer_name="a consumer", access_token=anonymous_token)
163 >>> launchpad = Launchpad(credentials=credentials)
164
165 >>> salgado = launchpad.people['salgado']
166 >>> print salgado.display_name
167 Guilherme Salgado
168
169An anonymous client can't modify the dataset, or read any data that's
170permission-controlled or scoped to a particular user.
171
172 >>> launchpad.me
173 Traceback (most recent call last):
174 ...
175 HTTPError: HTTP Error 401: Unauthorized
176 ...
177
178 >>> salgado.display_name = "This won't work."
179 >>> salgado.lp_save()
180 Traceback (most recent call last):
181 ...
182 HTTPError: HTTP Error 401: Unauthorized
183 ...
184
150Convenience185Convenience
151===========186===========
152187
153When the consumer name, access token and access secret are all known up-front,188When you want anonymous access, a convenience method is available for
154a convenience method is available for logging into the web service in one189setting up a web service connection in one function call. All you have
155function call.190to provide is the consumer name.
191
192 >>> launchpad = Launchpad.login_anonymously('launchpad-library')
193 >>> sorted(launchpad.people)
194 [...]
195
196 >>> launchpad.me
197 Traceback (most recent call last):
198 ...
199 HTTPError: HTTP Error 401: Unauthorized
200 ...
201
202Another function call is useful when the consumer name, access token
203and access secret are all known up-front.
156204
157 >>> launchpad = Launchpad.login(205 >>> launchpad = Launchpad.login(
158 ... 'launchpad-library', 'salgado-change-anything', 'test')206 ... 'launchpad-library', 'salgado-change-anything', 'test')
159 >>> sorted(launchpad.people)207 >>> sorted(launchpad.people)
160 [...]208 [...]
161209
162If that is not the case the application should obtain authorization from210 >>> print launchpad.me.name
163the user and get the credentials directly from Launchpad.211 salgado
212
213Otherwise, the application should obtain authorization from the user
214and get a new set of credentials directly from Launchpad.
164215
165First we must get a request token.216First we must get a request token.
166217
@@ -402,16 +453,6 @@
402Bad credentials453Bad credentials
403===============454===============
404455
405The application is not allowed to access Launchpad if there are no
406credentials.
407
408 >>> credentials = Credentials(consumer)
409 >>> launchpad = Launchpad(credentials=credentials)
410 Traceback (most recent call last):
411 ...
412 HTTPError: HTTP Error 401: Unauthorized
413 ...
414
415The application is not allowed to access Launchpad with a bad access token.456The application is not allowed to access Launchpad with a bad access token.
416457
417 >>> access_token = AccessToken('bad', 'no-secret')458 >>> access_token = AccessToken('bad', 'no-secret')
@@ -424,6 +465,19 @@
424 HTTPError: HTTP Error 401: Unauthorized465 HTTPError: HTTP Error 401: Unauthorized
425 ...466 ...
426467
468The application is not allowed to access Launchpad with a consumer
469name that doesn't match the credentials.
470
471 >>> access_token = AccessToken('salgado-change-anything', 'test')
472 >>> credentials = Credentials(
473 ... consumer_name='not-the-launchpad-library',
474 ... access_token=access_token)
475 >>> launchpad = Launchpad(credentials=credentials)
476 Traceback (most recent call last):
477 ...
478 HTTPError: HTTP Error 401: Unauthorized
479 ...
480
427The application is not allowed to access Launchpad with a bad access secret.481The application is not allowed to access Launchpad with a bad access secret.
428482
429 >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')483 >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')
430484
=== modified file 'src/launchpadlib/launchpad.py'
--- src/launchpadlib/launchpad.py 2009-12-03 14:38:29 +0000
+++ src/launchpadlib/launchpad.py 2009-12-17 13:18:16 +0000
@@ -32,7 +32,8 @@
32from lazr.restfulclient.resource import (32from lazr.restfulclient.resource import (
33 CollectionWithKeyBasedLookup, HostedFile, ServiceRoot)33 CollectionWithKeyBasedLookup, HostedFile, ServiceRoot)
34from launchpadlib.credentials import (34from launchpadlib.credentials import (
35 AccessToken, Credentials, AuthorizeRequestTokenWithBrowser)35 AccessToken, AnonymousAccessToken, Credentials,
36 AuthorizeRequestTokenWithBrowser)
36from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT37from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
37from launchpadlib import uris38from launchpadlib import uris
3839
@@ -170,6 +171,17 @@
170 return cls(credentials, service_root, cache, timeout, proxy_info)171 return cls(credentials, service_root, cache, timeout, proxy_info)
171172
172 @classmethod173 @classmethod
174 def login_anonymously(
175 cls, consumer_name, service_root=uris.STAGING_SERVICE_ROOT,
176 launchpadlib_dir=None, timeout=None, proxy_info=None):
177 """Get access to Launchpad without providing any credentials."""
178 service_root_dir, cache_path = cls._get_paths(
179 service_root, launchpadlib_dir)
180 token = AnonymousAccessToken()
181 credentials = Credentials(consumer_name, access_token=token)
182 return cls(credentials, service_root, cache_path, timeout, proxy_info)
183
184 @classmethod
173 def login_with(cls, consumer_name,185 def login_with(cls, consumer_name,
174 service_root=uris.STAGING_SERVICE_ROOT,186 service_root=uris.STAGING_SERVICE_ROOT,
175 launchpadlib_dir=None, timeout=None, proxy_info=None,187 launchpadlib_dir=None, timeout=None, proxy_info=None,
@@ -210,27 +222,13 @@
210 :rtype: `Launchpad`222 :rtype: `Launchpad`
211223
212 """224 """
213 if launchpadlib_dir is None:225 service_root_dir, cache_path = cls._get_paths(
214 home_dir = os.environ['HOME']226 service_root, launchpadlib_dir)
215 launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
216 launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
217 if not os.path.exists(launchpadlib_dir):
218 os.makedirs(launchpadlib_dir,0700)
219 os.chmod(launchpadlib_dir,0700)
220 # Determine the real service root.
221 service_root = uris.lookup_service_root(service_root)
222 # Each service root has its own cache and credential dirs.
223 scheme, host_name, path, query, fragment = urlparse.urlsplit(
224 service_root)
225 service_root_dir = os.path.join(launchpadlib_dir, host_name)
226 cache_path = os.path.join(service_root_dir, 'cache')
227 if not os.path.exists(cache_path):
228 os.makedirs(cache_path)
229 credentials_path = os.path.join(service_root_dir, 'credentials')227 credentials_path = os.path.join(service_root_dir, 'credentials')
230 if not os.path.exists(credentials_path):228 if not os.path.exists(credentials_path):
231 os.makedirs(credentials_path)229 os.makedirs(credentials_path)
232 if credentials_file is None:230 if credentials_file is None:
233 consumer_credentials_path = os.path.join(credentials_path, 231 consumer_credentials_path = os.path.join(credentials_path,
234 consumer_name)232 consumer_name)
235 else:233 else:
236 consumer_credentials_path = credentials_file234 consumer_credentials_path = credentials_file
@@ -250,3 +248,33 @@
250 launchpad.credentials.save_to_path(consumer_credentials_path)248 launchpad.credentials.save_to_path(consumer_credentials_path)
251 os.chmod(consumer_credentials_path, stat.S_IREAD | stat.S_IWRITE)249 os.chmod(consumer_credentials_path, stat.S_IREAD | stat.S_IWRITE)
252 return launchpad250 return launchpad
251
252 @classmethod
253 def _get_paths(cls, service_root, launchpadlib_dir=None):
254 """Locate launchpadlib-related user paths and ensure they exist.
255
256 This is a helper function used by login_with() and
257 login_anonymously().
258
259 :param service_root: The service root the user wants to connect to.
260 :param launchpadlib_dir: The user's base launchpadlib directory,
261 if known.
262 :return: A 2-tuple: (cache_dir, service_root_dir)
263 """
264 if launchpadlib_dir is None:
265 home_dir = os.environ['HOME']
266 launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
267 launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
268 if not os.path.exists(launchpadlib_dir):
269 os.makedirs(launchpadlib_dir,0700)
270 os.chmod(launchpadlib_dir,0700)
271 # Determine the real service root.
272 service_root = uris.lookup_service_root(service_root)
273 # Each service root has its own cache and credential dirs.
274 scheme, host_name, path, query, fragment = urlparse.urlsplit(
275 service_root)
276 service_root_dir = os.path.join(launchpadlib_dir, host_name)
277 cache_path = os.path.join(service_root_dir, 'cache')
278 if not os.path.exists(cache_path):
279 os.makedirs(cache_path)
280 return (cache_path, service_root_dir)

Subscribers

People subscribed via source and target branches