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
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 NEWS for launchpadlib
6 =====================
7
8-Development
9-===========
10+1.5.4 (2009-12-17)
11+==================
12+
13+- Made it easy to get anonymous access to a Launchpad instance.
14
15 - Made it easy to plug in different clients that take the user's
16 Launchpad login and password for purposes of authorizing a request
17
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 # You should have received a copy of the GNU Lesser General Public License
23 # along with launchpadlib. If not, see <http://www.gnu.org/licenses/>.
24
25-__version__ = '1.5.3'
26+__version__ = '1.5.4'
27
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 __metaclass__ = type
33 __all__ = [
34 'AccessToken',
35+ 'AnonymousAccessToken',
36 'RequestTokenAuthorizationEngine',
37 'Consumer',
38 'Credentials',
39@@ -169,6 +170,15 @@
40 return cls(key, secret, context)
41
42
43+class AnonymousAccessToken(_AccessToken):
44+ """An OAuth access token that doesn't authenticate anybody.
45+
46+ This token can be used for anonymous access.
47+ """
48+ def __init__(self):
49+ super(AnonymousAccessToken, self).__init__('','')
50+
51+
52 class SimulatedLaunchpadBrowser(object):
53 """A programmable substitute for a human-operated web browser.
54
55
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 'test'
61
62
63+Anonymous access
64+================
65+
66+An anonymous access token doesn't authenticate any particular
67+user. Using it will give a client read-only access to the public parts
68+of the Launchpad dataset.
69+
70+ >>> from launchpadlib.credentials import AnonymousAccessToken
71+ >>> anonymous_token = AnonymousAccessToken()
72+
73+ >>> from launchpadlib.credentials import Credentials
74+ >>> credentials = Credentials(
75+ ... consumer_name="a consumer", access_token=anonymous_token)
76+ >>> launchpad = Launchpad(credentials=credentials)
77+
78+ >>> salgado = launchpad.people['salgado']
79+ >>> print salgado.display_name
80+ Guilherme Salgado
81+
82+An anonymous client can't modify the dataset, or read any data that's
83+permission-controlled or scoped to a particular user.
84+
85+ >>> launchpad.me
86+ Traceback (most recent call last):
87+ ...
88+ HTTPError: HTTP Error 401: Unauthorized
89+ ...
90+
91+ >>> salgado.display_name = "This won't work."
92+ >>> salgado.lp_save()
93+ Traceback (most recent call last):
94+ ...
95+ HTTPError: HTTP Error 401: Unauthorized
96+ ...
97+
98 Convenience
99 ===========
100
101-When the consumer name, access token and access secret are all known up-front,
102-a convenience method is available for logging into the web service in one
103-function call.
104+When you want anonymous access, a convenience method is available for
105+setting up a web service connection in one function call. All you have
106+to provide is the consumer name.
107+
108+ >>> launchpad = Launchpad.login_anonymously('launchpad-library')
109+ >>> sorted(launchpad.people)
110+ [...]
111+
112+ >>> launchpad.me
113+ Traceback (most recent call last):
114+ ...
115+ HTTPError: HTTP Error 401: Unauthorized
116+ ...
117+
118+Another function call is useful when the consumer name, access token
119+and access secret are all known up-front.
120
121 >>> launchpad = Launchpad.login(
122 ... 'launchpad-library', 'salgado-change-anything', 'test')
123 >>> sorted(launchpad.people)
124 [...]
125
126-If that is not the case the application should obtain authorization from
127-the user and get the credentials directly from Launchpad.
128+ >>> print launchpad.me.name
129+ salgado
130+
131+Otherwise, the application should obtain authorization from the user
132+and get a new set of credentials directly from Launchpad.
133
134 First we must get a request token.
135
136@@ -402,16 +453,6 @@
137 Bad credentials
138 ===============
139
140-The application is not allowed to access Launchpad if there are no
141-credentials.
142-
143- >>> credentials = Credentials(consumer)
144- >>> launchpad = Launchpad(credentials=credentials)
145- Traceback (most recent call last):
146- ...
147- HTTPError: HTTP Error 401: Unauthorized
148- ...
149-
150 The application is not allowed to access Launchpad with a bad access token.
151
152 >>> access_token = AccessToken('bad', 'no-secret')
153@@ -424,6 +465,19 @@
154 HTTPError: HTTP Error 401: Unauthorized
155 ...
156
157+The application is not allowed to access Launchpad with a consumer
158+name that doesn't match the credentials.
159+
160+ >>> access_token = AccessToken('salgado-change-anything', 'test')
161+ >>> credentials = Credentials(
162+ ... consumer_name='not-the-launchpad-library',
163+ ... access_token=access_token)
164+ >>> launchpad = Launchpad(credentials=credentials)
165+ Traceback (most recent call last):
166+ ...
167+ HTTPError: HTTP Error 401: Unauthorized
168+ ...
169+
170 The application is not allowed to access Launchpad with a bad access secret.
171
172 >>> access_token = AccessToken('hgm2VK35vXD6rLg5pxWw', 'bad-secret')
173
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 from lazr.restfulclient.resource import (
179 CollectionWithKeyBasedLookup, HostedFile, ServiceRoot)
180 from launchpadlib.credentials import (
181- AccessToken, Credentials, AuthorizeRequestTokenWithBrowser)
182+ AccessToken, AnonymousAccessToken, Credentials,
183+ AuthorizeRequestTokenWithBrowser)
184 from oauth.oauth import OAuthRequest, OAuthSignatureMethod_PLAINTEXT
185 from launchpadlib import uris
186
187@@ -170,6 +171,17 @@
188 return cls(credentials, service_root, cache, timeout, proxy_info)
189
190 @classmethod
191+ def login_anonymously(
192+ cls, consumer_name, service_root=uris.STAGING_SERVICE_ROOT,
193+ launchpadlib_dir=None, timeout=None, proxy_info=None):
194+ """Get access to Launchpad without providing any credentials."""
195+ service_root_dir, cache_path = cls._get_paths(
196+ service_root, launchpadlib_dir)
197+ token = AnonymousAccessToken()
198+ credentials = Credentials(consumer_name, access_token=token)
199+ return cls(credentials, service_root, cache_path, timeout, proxy_info)
200+
201+ @classmethod
202 def login_with(cls, consumer_name,
203 service_root=uris.STAGING_SERVICE_ROOT,
204 launchpadlib_dir=None, timeout=None, proxy_info=None,
205@@ -210,27 +222,13 @@
206 :rtype: `Launchpad`
207
208 """
209- if launchpadlib_dir is None:
210- home_dir = os.environ['HOME']
211- launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
212- launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
213- if not os.path.exists(launchpadlib_dir):
214- os.makedirs(launchpadlib_dir,0700)
215- os.chmod(launchpadlib_dir,0700)
216- # Determine the real service root.
217- service_root = uris.lookup_service_root(service_root)
218- # Each service root has its own cache and credential dirs.
219- scheme, host_name, path, query, fragment = urlparse.urlsplit(
220- service_root)
221- service_root_dir = os.path.join(launchpadlib_dir, host_name)
222- cache_path = os.path.join(service_root_dir, 'cache')
223- if not os.path.exists(cache_path):
224- os.makedirs(cache_path)
225+ service_root_dir, cache_path = cls._get_paths(
226+ service_root, launchpadlib_dir)
227 credentials_path = os.path.join(service_root_dir, 'credentials')
228 if not os.path.exists(credentials_path):
229 os.makedirs(credentials_path)
230 if credentials_file is None:
231- consumer_credentials_path = os.path.join(credentials_path,
232+ consumer_credentials_path = os.path.join(credentials_path,
233 consumer_name)
234 else:
235 consumer_credentials_path = credentials_file
236@@ -250,3 +248,33 @@
237 launchpad.credentials.save_to_path(consumer_credentials_path)
238 os.chmod(consumer_credentials_path, stat.S_IREAD | stat.S_IWRITE)
239 return launchpad
240+
241+ @classmethod
242+ def _get_paths(cls, service_root, launchpadlib_dir=None):
243+ """Locate launchpadlib-related user paths and ensure they exist.
244+
245+ This is a helper function used by login_with() and
246+ login_anonymously().
247+
248+ :param service_root: The service root the user wants to connect to.
249+ :param launchpadlib_dir: The user's base launchpadlib directory,
250+ if known.
251+ :return: A 2-tuple: (cache_dir, service_root_dir)
252+ """
253+ if launchpadlib_dir is None:
254+ home_dir = os.environ['HOME']
255+ launchpadlib_dir = os.path.join(home_dir, '.launchpadlib')
256+ launchpadlib_dir = os.path.expanduser(launchpadlib_dir)
257+ if not os.path.exists(launchpadlib_dir):
258+ os.makedirs(launchpadlib_dir,0700)
259+ os.chmod(launchpadlib_dir,0700)
260+ # Determine the real service root.
261+ service_root = uris.lookup_service_root(service_root)
262+ # Each service root has its own cache and credential dirs.
263+ scheme, host_name, path, query, fragment = urlparse.urlsplit(
264+ service_root)
265+ service_root_dir = os.path.join(launchpadlib_dir, host_name)
266+ cache_path = os.path.join(service_root_dir, 'cache')
267+ if not os.path.exists(cache_path):
268+ os.makedirs(cache_path)
269+ return (cache_path, service_root_dir)

Subscribers

People subscribed via source and target branches