Merge lp:~leonardr/launchpad/launchpadlib-pagetests-take-2 into lp:launchpad/db-devel

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpad/launchpadlib-pagetests-take-2
Merge into: lp:launchpad/db-devel
Diff against target: 615 lines (+340/-92)
12 files modified
Makefile (+0/-4)
lib/canonical/buildd/debian/control (+1/-1)
lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml (+0/-10)
lib/canonical/launchpad/browser/oauth.py (+25/-13)
lib/canonical/launchpad/doc/oauth.txt (+54/-0)
lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt (+50/-0)
lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt (+68/-59)
lib/canonical/launchpad/systemhomes.py (+9/-4)
lib/canonical/launchpad/testing/pages.py (+5/-1)
lib/canonical/testing/layers.py (+7/-0)
lib/lp/testing/__init__.py (+2/-0)
lib/lp/testing/_webservice.py (+119/-0)
To merge this branch: bzr merge lp:~leonardr/launchpad/launchpadlib-pagetests-take-2
Reviewer Review Type Date Requested Status
Guilherme Salgado (community) code Approve
Michael Hudson-Doyle Approve
Review via email: mp+22444@code.launchpad.net

Description of the change

This is the second version of my branch to make it possible to use launchpadlib in pagetests. The previous branch was landed and then removed because it caused mysterious, catastrophic test failures (though not on my dev machine, on ec2, or on buildout).

The previous branch put a 'launchpad' object in the globally accessible globs. This branch instead makes available a 'launchpadlib_for' helper function that will create a Launchpad object for any user, creating the underlying OAuth credential if necessary. Most of the work happens in two more granular helper methods, launchpadlib_credentials_for and oauth_access_token_for.

I refactored the code that turns a string into an OAuth context object into a helper function, so that oauth_access_token_for can use the same code as is used when Launchpad creates an OAuth access token.

Everything else in this branch (eg. the change to the WADL code) is exactly the same as in the previous launchpadlib pagetest branch.

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

wgrant informs me that this branch _did_ cause test failures everywhere, but the failures were so bad the test suite didn't register them as failures.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

I don't understand the changes to xx-wadl.txt but they've already been reviewed once already, right?

The rest looks fine.

review: Approve
Revision history for this message
Leonard Richardson (leonardr) wrote :

ec2 test turned up some test failures. Two of them were trivial to fix; one was quite difficult and revealed that Launchpad doesn't really support a feature we thought we'd implemented. I filed bug 552732 to cover that and left the test in place but commented out. Incremental diff:

http://paste.ubuntu.com/407229/

Revision history for this message
Guilherme Salgado (salgado) wrote :

Per Leonard's request I've reviewed http://paste.ubuntu.com/407229/ and it looks good to me.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2010-03-26 13:23:20 +0000
3+++ Makefile 2010-04-05 16:09:33 +0000
4@@ -324,11 +324,7 @@
5 $(RM) -r lib/mailman
6 $(RM) -rf lib/canonical/launchpad/icing/build/*
7 $(RM) -r $(CODEHOSTING_ROOT)
8- mv $(APIDOC_DIR)/wadl-testrunner-devel.xml \
9- $(APIDOC_DIR)/wadl-testrunner-devel.xml.bak
10 $(RM) $(APIDOC_DIR)/wadl*.xml $(APIDOC_DIR)/*.html
11- mv $(APIDOC_DIR)/wadl-testrunner-devel.xml.bak \
12- $(APIDOC_DIR)/wadl-testrunner-devel.xml
13 $(RM) -rf $(APIDOC_DIR).tmp
14 $(RM) $(BZR_VERSION_INFO)
15 $(RM) _pythonpath.py
16
17=== modified file 'lib/canonical/buildd/debian/control'
18--- lib/canonical/buildd/debian/control 2009-11-17 22:28:43 +0000
19+++ lib/canonical/buildd/debian/control 2010-04-05 16:09:33 +0000
20@@ -8,7 +8,7 @@
21 Package: launchpad-buildd
22 Section: misc
23 Architecture: all
24-Depends: python-twisted, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https
25+Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https
26 Description: Launchpad buildd slave
27 This is the launchpad buildd slave package. It contains everything needed to
28 get a launchpad buildd going apart from the database manipulation required to
29
30=== added directory 'lib/canonical/launchpad/apidoc'
31=== removed directory 'lib/canonical/launchpad/apidoc'
32=== removed file 'lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml'
33--- lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml 2010-03-26 23:01:30 +0000
34+++ lib/canonical/launchpad/apidoc/wadl-testrunner-devel.xml 1970-01-01 00:00:00 +0000
35@@ -1,10 +0,0 @@
36-<?xml version="1.0"?>
37-<wadl:application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
38- xmlns="http://research.sun.com/wadl/2006/10"
39- xmlns:wadl="http://research.sun.com/wadl/2006/10"
40- xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd">
41-
42- <!--This file is for testing purposes only. See
43- canonical/launcpad/pagetests/webservice/xx-wadl.txt -->
44-
45-</wadl:application>
46
47=== modified file 'lib/canonical/launchpad/browser/oauth.py'
48--- lib/canonical/launchpad/browser/oauth.py 2009-07-17 00:26:05 +0000
49+++ lib/canonical/launchpad/browser/oauth.py 2010-04-05 16:09:33 +0000
50@@ -6,7 +6,8 @@
51 'OAuthAccessTokenView',
52 'OAuthAuthorizeTokenView',
53 'OAuthRequestTokenView',
54- 'OAuthTokenAuthorizedView']
55+ 'OAuthTokenAuthorizedView',
56+ 'lookup_oauth_context']
57
58 import simplejson
59
60@@ -172,18 +173,10 @@
61 context = self.request.form.get('lp.context')
62 if not context:
63 return
64- if '/' in context:
65- distro, package = context.split('/')
66- distro = getUtility(IDistributionSet).getByName(distro)
67- if distro is None:
68- raise UnexpectedFormData("Unknown context.")
69- context = distro.getSourcePackage(package)
70- if context is None:
71- raise UnexpectedFormData("Unknown context.")
72- else:
73- context = getUtility(IPillarNameSet).getByName(context)
74- if context is None:
75- raise UnexpectedFormData("Unknown context.")
76+ try:
77+ context = lookup_oauth_context(context)
78+ except ValueError:
79+ raise UnexpectedFormData("Unknown context.")
80 self.token_context = context
81
82 def reviewToken(self, permission):
83@@ -195,6 +188,25 @@
84 self.next_url = (
85 '+token-authorized?oauth_token=%s' % self.token.key)
86
87+def lookup_oauth_context(context):
88+ """Transform an OAuth context string into a context object.
89+
90+ :param context: A string to turn into a context object.
91+ """
92+ if '/' in context:
93+ distro, package = context.split('/')
94+ distro = getUtility(IDistributionSet).getByName(distro)
95+ if distro is None:
96+ raise ValueError(distro)
97+ context = distro.getSourcePackage(package)
98+ if context is None:
99+ raise ValueError(package)
100+ else:
101+ context = getUtility(IPillarNameSet).getByName(context)
102+ if context is None:
103+ raise ValueError(context)
104+ return context
105+
106
107 class OAuthTokenAuthorizedView(LaunchpadView):
108 """Where users who reviewed tokens may get redirected to.
109
110=== modified file 'lib/canonical/launchpad/doc/oauth.txt'
111--- lib/canonical/launchpad/doc/oauth.txt 2010-03-11 01:39:25 +0000
112+++ lib/canonical/launchpad/doc/oauth.txt 2010-04-05 16:09:33 +0000
113@@ -430,3 +430,57 @@
114 Traceback (most recent call last):
115 ...
116 TimestampOrderingError: ...
117+
118+
119+Helper methods
120+==============
121+
122+The oauth_access_token_for() helper function makes it easy to get an
123+access token for any user, consumer key, permission, and context.
124+
125+If the user already has an access token that does what you need,
126+oauth_access_token_for() returns the existing token.
127+
128+ >>> from lp.testing import oauth_access_token_for
129+ >>> existing_token = salgado.oauth_access_tokens[0]
130+ >>> token = oauth_access_token_for(
131+ ... existing_token.consumer.key, existing_token.person,
132+ ... existing_token.permission, existing_token.context)
133+
134+ >>> from zope.proxy import sameProxiedObjects
135+ >>> sameProxiedObjects(token, existing_token)
136+ True
137+
138+If the user does not already have an access token that matches your
139+requirements, oauth_access_token_for() creates a request token and
140+automatically authorizes it. Here, we create a brand new token for a
141+never-before-seen consumer.
142+
143+ >>> new_consumer = 'new consumer key to test oauth_access_token_for'
144+ >>> token = oauth_access_token_for(
145+ ... new_consumer, salgado, 'WRITE_PRIVATE', firefox)
146+
147+ >>> print token.consumer.key
148+ new consumer key to test oauth_access_token_for
149+
150+ >>> print token.person.name
151+ salgado
152+
153+ >>> token.permission
154+ <DBItem AccessLevel.WRITE_PRIVATE...>
155+
156+ >>> print token.context.name
157+ firefox
158+
159+ >>> print token.date_expires
160+ None
161+
162+You can use the token identifying one of Launchpad's OAuth permission
163+levels instead of the constant itself, but if you specify a
164+nonexistent permission you'll get an error.
165+
166+ >>> oauth_access_token_for(
167+ ... new_consumer, salgado, 'NO_SUCH_PERMISSION', firefox)
168+ Traceback (most recent call last):
169+ ...
170+ KeyError: 'NO_SUCH_PERMISSION'
171
172=== added file 'lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt'
173--- lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 1970-01-01 00:00:00 +0000
174+++ lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 2010-04-05 16:09:33 +0000
175@@ -0,0 +1,50 @@
176+Using launchpadlib in pagetests
177+===============================
178+
179+As an alternative to crafting HTTP requests with the 'webservice'
180+object, you can write pagetests using launchpadlib.
181+
182+Two helper functions make it easy to set up Launchpad objects that
183+can access the web service. With launchpadlib_for() you can set up a
184+Launchpad object for a given user with a single call.
185+
186+ >>> launchpad = launchpadlib_for(
187+ ... 'launchpadlib test', 'salgado', 'WRITE_PUBLIC')
188+ >>> print launchpad.me.name
189+ salgado
190+
191+ # XXX leonardr 2010-03-31 bug=552732
192+ # launchpadlib doesn't work with a credential scoped to a context
193+ # like 'firefox', because the service root resource is considered
194+ # out of scope. This test should pass, but it doesn't.
195+ #
196+ # When you fix this, be sure to show that an attempt to access
197+ # something that really is out of scope (like launchpad.me.name)
198+ # yields a 401 error.
199+ #
200+ #>>> launchpad = launchpadlib_for(
201+ #... 'launchpadlib test', 'no-priv', 'READ_PRIVATE', 'firefox',
202+ #... version="devel")
203+ #>>> print launchpad.projects['firefox'].name
204+ #firefox
205+
206+With launchpadlib_credentials_for() you can get a launchpadlib
207+Credentials object.
208+
209+ >>> from lp.testing import launchpadlib_credentials_for
210+ >>> credentials = launchpadlib_credentials_for(
211+ ... 'launchpadlib test', 'no-priv', 'READ_PUBLIC')
212+ >>> credentials
213+ <launchpadlib.credentials.Credentials object ...>
214+
215+ >>> print credentials.consumer.key
216+ launchpadlib test
217+ >>> print credentials.access_token
218+ oauth_token_secret=...&oauth_token=...
219+
220+This can be used to create your own Launchpad object.
221+
222+ >>> from launchpadlib.launchpad import Launchpad
223+ >>> launchpad = Launchpad(credentials, 'http://api.launchpad.dev/')
224+ >>> print launchpad.me.name
225+ no-priv
226
227=== modified file 'lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt'
228--- lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-03-26 13:23:20 +0000
229+++ lib/canonical/launchpad/pagetests/webservice/xx-wadl.txt 2010-04-05 16:09:33 +0000
230@@ -3,55 +3,83 @@
231 Because Launchpad's main WADL files are so big, we cache them
232 internally: one WADL file for every version of the web service.
233 Because the WADL only changes when the Launchpad software changes,
234-these documents are cached to files. Right now, the Launchpad
235-webservice serves a special test file
236-(canonical/launchpad/apidoc/wadl-testrunner-devel.xml) when a client
237-asks for the big WADL definition for the 'devel' version. The
238-'testrunner' part comes from canonical.config.config.instance_name, so
239-a development instance will use the file
240-canonical/launchpad/apidoc/wadl-development-{version}.xml.
241-
242- >>> test_wadl = webservice.get(
243- ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body
244- >>> print test_wadl
245- <?xml version="1.0"?>
246- <wadl:application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
247- xmlns="http://research.sun.com/wadl/2006/10"
248- xmlns:wadl="http://research.sun.com/wadl/2006/10"
249- xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd">
250- <BLANKLINE>
251- <!--This file is for testing purposes only. See
252- canonical/launcpad/pagetests/webservice/xx-wadl.txt -->
253- <BLANKLINE>
254- </wadl:application>
255- <BLANKLINE>
256-
257-Let's look at the real contents, though. To do this, we need to
258-deactivate the cache. Simply clearing it out will just cause it to be
259-filled up again.
260+these documents are cached to files.
261+
262+This test shows how the cache works. We'll start by temporarily
263+clearing the cache.
264
265 >>> from canonical.launchpad.systemhomes import WebServiceApplication
266 >>> old_cached_wadl = WebServiceApplication.cached_wadl
267- >>> WebServiceApplication.cached_wadl = None
268+ >>> WebServiceApplication.cached_wadl = {}
269+
270+If WADL is present in a certain file on disk--the filename depends on
271+the Launchpad configuration and the web service version--it will be
272+loaded from disk and not generated from scratch. But the testrunner
273+does not have any WADL files written to disk.
274+
275+ >>> import os
276+ >>> wadl_filename = WebServiceApplication.cachedWADLPath(
277+ ... 'testrunner', 'devel')
278+ >>> os.path.exists(wadl_filename)
279+ False
280+
281+Let's write some fake WADL to disk.
282+
283+ >>> fd = open(wadl_filename, "w")
284+ >>> fd.write("Some fake WADL.")
285+ >>> fd.close()
286+
287+When we request the WADL for version "devel", the fake WADL is loaded
288+from disk.
289+
290+ >>> print webservice.get(
291+ ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body
292+ Some fake WADL.
293+
294+The fake WADL is now present in the cache.
295+
296+ >>> WebServiceApplication.cached_wadl
297+ {u'devel': u'Some fake WADL.'}
298+
299+Change the cached value, and we change the document served.
300+
301+ >>> WebServiceApplication.cached_wadl['devel'] = "More fake WADL."
302+
303+ >>> print webservice.get(
304+ ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body
305+ More fake WADL.
306+
307+If there's no value in the cache and no cached file on disk, the WADL
308+is generated from scratch.
309+
310+ >>> WebServiceApplication.cached_wadl = {}
311+ >>> os.remove(wadl_filename)
312
313 >>> wadl = webservice.get(
314 ... '/', 'application/vd.sun.wadl+xml', api_version='devel').body
315 >>> wadl = wadl.decode('UTF-8')
316
317-The real file is much bigger than the test file.
318-
319- >>> len(wadl) > len(test_wadl)
320+Unlike the test strings we used earlier, this is a valid WADL file.
321+
322+ >>> from lazr.restful import WADL_SCHEMA_FILE
323+ >>> from canonical.lazr.xml import XMLValidator
324+ >>> wadl_schema = XMLValidator(WADL_SCHEMA_FILE)
325+
326+ # We need to replace the nbsp entity, because the validator
327+ # doesn't support embedded definition.
328+ >>> wadl_schema.validate(
329+ ... wadl.replace('&nbsp;', '&#160;').encode('UTF-8'))
330 True
331
332 The WADL we received is keyed to the 'devel' version of the web
333-service. This version will always be present.
334+service. The URL to this version's service root will always be
335+present.
336
337 >>> 'http://api.launchpad.dev/devel/' in wadl
338 True
339
340 If we retrieve the WADL for the '1.0' version of the web service,
341-we'll get a document keyed to the '1.0' version. The '1.0' version
342-will be deprecated along with the Lucid release of Ubuntu.
343+we'll get a document keyed to the '1.0' version.
344
345 >>> wadl_10 = webservice.get(
346 ... '/', 'application/vd.sun.wadl+xml', api_version='1.0').body
347@@ -60,31 +88,12 @@
348 >>> 'http://api.launchpad.dev/1.0/' in wadl_10
349 True
350
351-If we retrieve the WADL for the 'beta' version of the web service,
352-we'll get a document keyed to the 'beta' version. The '1.0' version
353-will be deprecated along with the Karmic release of Ubuntu.
354-
355- >>> wadl_beta = webservice.get(
356- ... '/', 'application/vd.sun.wadl+xml', api_version='beta').body
357- >>> wadl_beta = wadl_beta.decode('UTF-8')
358-
359- >>> 'http://api.launchpad.dev/beta/' in wadl_beta
360- True
361-
362-We don't need the cache anymore, so we'll reinstate the testing
363-version. This way, other tests will have a clean slate.
364+All of these documents were cached as they were generated:
365+
366+ >>> sorted(WebServiceApplication.cached_wadl.keys())
367+ [u'1.0', u'devel']
368+
369+Finally, restore the cache so that other tests will have a clean
370+slate.
371
372 >>> WebServiceApplication.cached_wadl = old_cached_wadl
373-
374-Like all lazr.restful applications, Launchpad's web service generates
375-valid WADL.
376-
377- >>> from lazr.restful import WADL_SCHEMA_FILE
378- >>> from canonical.lazr.xml import XMLValidator
379- >>> wadl_schema = XMLValidator(WADL_SCHEMA_FILE)
380-
381- # We need to replace the nbsp entity, because the validator
382- # doesn't support embedded definition.
383- >>> wadl_schema.validate(
384- ... wadl.replace('&nbsp;', '&#160;').encode('UTF-8'))
385- True
386
387=== modified file 'lib/canonical/launchpad/systemhomes.py'
388--- lib/canonical/launchpad/systemhomes.py 2010-03-26 21:03:30 +0000
389+++ lib/canonical/launchpad/systemhomes.py 2010-04-05 16:09:33 +0000
390@@ -354,6 +354,13 @@
391
392 cached_wadl = {}
393
394+ @classmethod
395+ def cachedWADLPath(cls, instance_name, version):
396+ """Helper method to calculate the path to a cached WADL file."""
397+ return os.path.join(
398+ os.path.dirname(os.path.normpath(__file__)),
399+ 'apidoc', 'wadl-%s-%s.xml' % (instance_name, version))
400+
401 def toWADL(self):
402 """See `IWebServiceApplication`.
403
404@@ -370,10 +377,8 @@
405 return super(WebServiceApplication, self).toWADL()
406 if version not in self.__class__.cached_wadl:
407 # It's not cached. Look for it on disk.
408- _wadl_filename = os.path.join(
409- os.path.dirname(os.path.normpath(__file__)),
410- 'apidoc', 'wadl-%s-%s.xml' % (config.instance_name, version))
411-
412+ _wadl_filename = self.cachedWADLPath(
413+ config.instance_name, version)
414 _wadl_fd = None
415 try:
416 _wadl_fd = codecs.open(_wadl_filename, encoding='UTF-8')
417
418=== modified file 'lib/canonical/launchpad/testing/pages.py'
419--- lib/canonical/launchpad/testing/pages.py 2010-03-26 13:23:20 +0000
420+++ lib/canonical/launchpad/testing/pages.py 2010-04-05 16:09:33 +0000
421@@ -29,6 +29,8 @@
422 from zope.testing import doctest
423 from zope.security.proxy import removeSecurityProxy
424
425+from launchpadlib.launchpad import Launchpad
426+
427 from canonical.launchpad.interfaces import (
428 IOAuthConsumerSet, OAUTH_REALM, ILaunchpadCelebrities,
429 TeamMembershipStatus)
430@@ -39,7 +41,8 @@
431 from canonical.launchpad.webapp.url import urlsplit
432 from canonical.testing import PageTestLayer
433 from lazr.restful.testing.webservice import WebServiceCaller
434-from lp.testing import ANONYMOUS, login, login_person, logout
435+from lp.testing import (
436+ ANONYMOUS, launchpadlib_for, login, login_person, logout)
437 from lp.testing.factory import LaunchpadObjectFactory
438 from lp.registry.interfaces.person import NameAlreadyTaken
439
440@@ -752,6 +755,7 @@
441 test.globs['print_table'] = print_table
442 test.globs['extract_link_from_tag'] = extract_link_from_tag
443 test.globs['extract_text'] = extract_text
444+ test.globs['launchpadlib_for'] = launchpadlib_for
445 test.globs['login'] = login
446 test.globs['login_person'] = login_person
447 test.globs['logout'] = logout
448
449=== modified file 'lib/canonical/testing/layers.py'
450--- lib/canonical/testing/layers.py 2010-03-26 13:23:20 +0000
451+++ lib/canonical/testing/layers.py 2010-04-05 16:09:33 +0000
452@@ -73,6 +73,7 @@
453 from storm.zope.interfaces import IZStorm
454 import transaction
455 import wsgi_intercept
456+from wsgi_intercept import httplib2_intercept
457
458 from lazr.restful.utils import safe_hasattr
459
460@@ -938,12 +939,18 @@
461 register_launchpad_request_publication_factories()
462 wsgi_intercept.add_wsgi_intercept(
463 'localhost', 80, lambda: wsgi_application)
464+ wsgi_intercept.add_wsgi_intercept(
465+ 'api.launchpad.dev', 80, lambda: wsgi_application)
466+ httplib2_intercept.install()
467+
468
469 @classmethod
470 @profiled
471 def tearDown(cls):
472 FunctionalLayer.isSetUp = False
473 wsgi_intercept.remove_wsgi_intercept('localhost', 80)
474+ wsgi_intercept.remove_wsgi_intercept('api.launchpad.dev', 80)
475+ httplib2_intercept.uninstall()
476 # Signal Layer cannot be torn down fully
477 raise NotImplementedError
478
479
480=== modified file 'lib/lp/testing/__init__.py'
481--- lib/lp/testing/__init__.py 2010-03-30 21:49:22 +0000
482+++ lib/lp/testing/__init__.py 2010-04-05 16:09:33 +0000
483@@ -82,6 +82,8 @@
484 # canonical.launchpad.ftests expects test_tales to be imported from here.
485 # XXX: JonathanLange 2010-01-01: Why?!
486 from lp.testing._tales import test_tales
487+from lp.testing._webservice import (
488+ launchpadlib_credentials_for, launchpadlib_for, oauth_access_token_for)
489
490 # zope.exception demands more of frame objects than twisted.python.failure
491 # provides in its fake frames. This is enough to make it work with them
492
493=== added file 'lib/lp/testing/_webservice.py'
494--- lib/lp/testing/_webservice.py 1970-01-01 00:00:00 +0000
495+++ lib/lp/testing/_webservice.py 2010-04-05 16:09:33 +0000
496@@ -0,0 +1,119 @@
497+# Copyright 2010 Canonical Ltd. This software is licensed under the
498+# GNU Affero General Public License version 3 (see the file LICENSE).
499+
500+# We like global statements!
501+# pylint: disable-msg=W0602,W0603
502+__metaclass__ = type
503+
504+__all__ = [
505+ 'launchpadlib_credentials_for',
506+ 'launchpadlib_for',
507+ 'oauth_access_token_for'
508+ ]
509+
510+from zope.component import getUtility
511+from launchpadlib.credentials import AccessToken, Credentials
512+from launchpadlib.launchpad import Launchpad
513+
514+from canonical.launchpad.webapp.interfaces import OAuthPermission
515+from canonical.launchpad.interfaces import (
516+ IOAuthConsumerSet, IPersonSet)
517+
518+from lp.testing._login import ANONYMOUS, login, logout
519+
520+def oauth_access_token_for(consumer_name, person, permission, context=None):
521+ """Find or create an OAuth access token for the given person.
522+ :param consumer_name: An OAuth consumer name.
523+ :param person: A person (or the name of a person) for whom to create
524+ or find credentials.
525+ :param permission: An OAuthPermission (or its token) designating
526+ the level of permission the credentials should have.
527+ :param context: The OAuth context for the credentials (or a string
528+ designating same).
529+
530+ :return: An OAuthAccessToken object.
531+ """
532+ if isinstance(person, basestring):
533+ # Look up a person by name.
534+ person = getUtility(IPersonSet).getByName(person)
535+ if isinstance(context, basestring):
536+ # Turn an OAuth context string into the corresponding object.
537+ # Avoid an import loop by importing from launchpad.browser here.
538+ from canonical.launchpad.browser.oauth import lookup_oauth_context
539+ context = lookup_oauth_context(context)
540+ if isinstance(permission, basestring):
541+ # Look up a permission by its token string.
542+ permission = OAuthPermission.items[permission]
543+
544+ # Find or create the consumer object.
545+ consumer_set = getUtility(IOAuthConsumerSet)
546+ consumer = consumer_set.getByKey(consumer_name)
547+ if consumer is None:
548+ consumer = consumer_set.new(consumer_name)
549+ else:
550+ # We didn't have to create the consumer. Maybe this user
551+ # already has an access token for this
552+ # consumer+person+permission?
553+ existing_token = [token for token in person.oauth_access_tokens
554+ if (token.consumer == consumer
555+ and token.permission == permission
556+ and token.context == context)]
557+ if len(existing_token) >= 1:
558+ return existing_token[0]
559+
560+ # There is no existing access token for this
561+ # consumer+person+permission+context. Create one and review it.
562+ request_token = consumer.newRequestToken()
563+ request_token.review(person, permission, context)
564+ access_token = request_token.createAccessToken()
565+ return access_token
566+
567+
568+def launchpadlib_credentials_for(
569+ consumer_name, person, permission=OAuthPermission.WRITE_PRIVATE,
570+ context=None):
571+ """Create launchpadlib credentials for the given person.
572+
573+ :param consumer_name: An OAuth consumer name.
574+ :param person: A person (or the name of a person) for whom to create
575+ or find credentials.
576+ :param permission: An OAuthPermission (or its token) designating
577+ the level of permission the credentials should have.
578+ :param context: The OAuth context for the credentials.
579+ :return: A launchpadlib Credentials object.
580+ """
581+ # Start an interaction so that oauth_access_token_for will
582+ # succeed. oauth_access_token_for may be called in any layer, but
583+ # launchpadlib_credentials_for is only called in the
584+ # PageTestLayer, when a Launchpad instance is running for
585+ # launchpadlib to use.
586+ login(ANONYMOUS)
587+ access_token = oauth_access_token_for(
588+ consumer_name, person, permission, context)
589+ logout()
590+ launchpadlib_token = AccessToken(
591+ access_token.key, access_token.secret)
592+ return Credentials(consumer_name=consumer_name,
593+ access_token=launchpadlib_token)
594+
595+
596+def launchpadlib_for(
597+ consumer_name, person, permission=OAuthPermission.WRITE_PRIVATE,
598+ context=None, version=None, service_root="http://api.launchpad.dev/"):
599+ """Create a Launchpad object for the given person.
600+
601+ :param consumer_name: An OAuth consumer name.
602+ :param person: A person (or the name of a person) for whom to create
603+ or find credentials.
604+ :param permission: An OAuthPermission (or its token) designating
605+ the level of permission the credentials should have.
606+ :param context: The OAuth context for the credentials.
607+ :param version: The version of the web service to access.
608+ :param service_root: The root URL of the web service to access.
609+
610+ :return: A launchpadlib Launchpad object.
611+ """
612+ credentials = launchpadlib_credentials_for(
613+ consumer_name, person, permission, context)
614+ version = version or Launchpad.DEFAULT_VERSION
615+ return Launchpad(credentials, service_root, version=version)

Subscribers

People subscribed via source and target branches

to status/vote changes: