Merge lp:~wallyworld/launchpad/pillar-access-service-infrastructure into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Ian Booth
Approved revision: no longer in the source branch.
Merged at revision: 14863
Proposed branch: lp:~wallyworld/launchpad/pillar-access-service-infrastructure
Merge into: lp:launchpad
Diff against target: 465 lines (+363/-0)
13 files modified
lib/lp/app/browser/launchpad.py (+2/-0)
lib/lp/app/configure.zcml (+21/-0)
lib/lp/app/interfaces/services.py (+37/-0)
lib/lp/app/services.py (+37/-0)
lib/lp/app/tests/test_services.py (+45/-0)
lib/lp/registry/browser/configure.zcml (+9/-0)
lib/lp/registry/interfaces/accesspolicyservice.py (+32/-0)
lib/lp/registry/interfaces/webservice.py (+6/-0)
lib/lp/registry/services/__init__.py (+1/-0)
lib/lp/registry/services/accesspolicyservice.py (+45/-0)
lib/lp/registry/services/configure.zcml (+18/-0)
lib/lp/registry/services/tests/test_accesspolicyservice.py (+78/-0)
lib/lp/services/webservice/services.py (+32/-0)
To merge this branch: bzr merge lp:~wallyworld/launchpad/pillar-access-service-infrastructure
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Richard Harding (community) code* Approve
Review via email: mp+94338@code.launchpad.net

Commit message

[r=jcsackett,rharding][no-qa] Provides an easy way to define and use named services having methods returning json data and supporting method parameter marshalling using the lazr restful infrastructure.

Description of the change

== Implementation ==

This branch provides an easy way to define and use named services having methods returning json data and supporting method parameter marshalling using the lazr restful infrastructure.

This work is required for disclosure. I wanted to introduce this new infrastructure rather than perpetuating the broken design of attaching service interfaces to domain entities. The lazr restful stuff suits quite well, but we will likely need to extend it to support marshalling operation parameters of type dict with IEntry objects for keys or values, rather than just references or lists.

Services are located using a simple traversal mechanism: "/services/<service_name>"

To define a new named service, simply create an interface extending IService, provide an implementing class, and register the service as a named utility in zcml. The service is then accessible via the url mentioned above.

The infrastructure to provide the zope url and navigation glue is provided by ServicesLink (for the top level services entry), ServiceFactory (for the traversal off the services link), and the IService url rules defined in zcml.

As well as the infrastructure to make it work, a sample service has been implemented, AccessPolicyService. This will be fleshed out and used with the disclosure work. Note that the service infrastructure glue and bespoke service implementation logic is kept separate for ease of maintainability etc.

I had trouble getting this all glued together. Even though all the exported methods and launchpadlib instance etc use version 'devel', I had to export the service implementations themselves for version 'beta':

export_as_webservice_entry(publish_web_link=False, as_of='beta')

Until I did this, the wadl generation was broken. Thanks to wgrant for this bit.

== Demo and QA ==

Invoke a method on the access policy service from within a browser

https://launchpad.dev/api/devel/services/accesspolicy?ws.op=getAccessPolicies&ws.accept=application/json

or via launchpadlib:

from launchpadlib.launchpad import Launchpad
lp = Launchpad.login_with('testing', service_root='https://api.launchpad.dev', version='devel')
aps = lp.load('/services/accesspolicy')
aps.getAccessPolicies()

or from an XHR call using an lp.client.Launchpad instance

launchpad.named_get('/services/accesspolicy', 'getAccessPolicies')

== Tests ==

Add test for service traversal
Add test for service api invocation using a web services caller
Add test for service api invocation using launchpadlib

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/app/services.py
  lib/lp/app/browser/launchpad.py
  lib/lp/app/interfaces/services.py
  lib/lp/app/tests/test_services.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/services/
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/interfaces/accesspolicyservice.py
  lib/lp/registry/interfaces/webservice.py
  lib/lp/registry/services/__init__.py
  lib/lp/registry/services/accesspolicyservice.py
  lib/lp/registry/services/configure.zcml
  lib/lp/registry/services/tests/
  lib/lp/registry/services/tests/__init__.py
  lib/lp/registry/services/tests/test_accesspolicyservice.py
  lib/lp/services/webservice/services.py

To post a comment you must log in.
Revision history for this message
Richard Harding (rharding) :
review: Approve (code*)
Revision history for this message
Curtis Hovey (sinzui) wrote :

The services utilities in registry/configure.zcml can be moved into app/configure.zcml since none of the registrations use .registry domain objects. I think this is registering the objects used by LaunchpadRootNavigation which I think is your change in LaunchpadRootNavigation.

Revision history for this message
j.c.sackett (jcsackett) wrote :

I agree with Curtis, there's no good reason for the base service zcml stuff to be on registry; it makes a lot more sense for it to be in app.

As I don't want to create a block, I'm approving, but please make that change.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

Hi, this is neat (but I still want to talk with you and Curtis today about it).

Two nits:
 - don't accept broken stuff in our stack: that as_of beta should be
fixed at root.
 - /services/ will collide with a product called services won't it?
Please use /+services instead to mitigate this.

-Rob

Revision history for this message
Ian Booth (wallyworld) wrote :

Hi Rob

> Two nits:
> - don't accept broken stuff in our stack: that as_of beta should be
> fixed at root.

Ok. I wasn't 100% sure it was broken or me not appreciating a subtlety
of the lazr restful versioning. I'll file a bug and put in a XXX if we
think it is indeed broken.

> - /services/ will collide with a product called services won't it?
> Please use /+services instead to mitigate this.
>

I had planned on adding services to the names blacklist. My opinion is
that "services" should be a reserved word in this sense but if you fell
strongly that it should be +services I'll do that.

Revision history for this message
Ian Booth (wallyworld) wrote :

I've altered the zcml as requested.

On 24/02/12 01:58, Curtis Hovey wrote:
> The services utilities in registry/configure.zcml can be moved into app/configure.zcml since none of the registrations use .registry domain objects. I think this is registering the objects used by LaunchpadRootNavigation which I think is your change in LaunchpadRootNavigation.

Revision history for this message
Robert Collins (lifeless) wrote :

On Fri, Feb 24, 2012 at 10:01 AM, Ian Booth <email address hidden> wrote:
> I had planned on adding services to the names blacklist. My opinion is
> that "services" should be a reserved word in this sense but if you fell
> strongly that it should be +services I'll do that.

We've no reason to munge the two namespaces together - +foo is our
default choice for getting into a different namespace. So yes,
+services.

-Rob

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/app/browser/launchpad.py'
2--- lib/lp/app/browser/launchpad.py 2011-12-30 09:16:36 +0000
3+++ lib/lp/app/browser/launchpad.py 2012-02-23 23:37:18 +0000
4@@ -79,6 +79,7 @@
5 )
6 from lp.app.interfaces.headings import IMajorHeadingView
7 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
8+from lp.app.interfaces.services import IServiceFactory
9 from lp.app.widgets.project import ProjectScopeWidget
10 from lp.blueprints.interfaces.specification import ISpecificationSet
11 from lp.blueprints.interfaces.sprint import ISprintSet
12@@ -652,6 +653,7 @@
13 # hierarchical navigation model.
14 stepto_utilities = {
15 '+announcements': IAnnouncementSet,
16+ '+services': IServiceFactory,
17 'binarypackagenames': IBinaryPackageNameSet,
18 'branches': IBranchSet,
19 'bugs': IMaloneApplication,
20
21=== modified file 'lib/lp/app/configure.zcml'
22--- lib/lp/app/configure.zcml 2011-12-30 08:03:42 +0000
23+++ lib/lp/app/configure.zcml 2012-02-23 23:37:18 +0000
24@@ -55,4 +55,25 @@
25 factory="lp.app.browser.badge.HasBadgeBase"
26 permission="zope.Public"
27 />
28+
29+ <!-- Services Infrastructure -->
30+ <include
31+ package="lp.registry.services"/>
32+ <securedutility
33+ class="lp.app.services.ServiceFactory"
34+ provides="lp.app.interfaces.services.IServiceFactory">
35+ <allow
36+ interface="lp.app.interfaces.services.IServiceFactory"/>
37+ <allow
38+ interface="zope.publisher.interfaces.IPublishTraverse"/>
39+ </securedutility>
40+ <securedutility
41+ class="lp.services.webservice.services.ServicesLink"
42+ provides="lp.services.webservice.services.IServicesLink">
43+ <allow
44+ interface="lazr.restful.interfaces.ITopLevelEntryLink"/>
45+ <allow
46+ interface="lp.services.webapp.interfaces.ICanonicalUrlData"/>
47+ </securedutility>
48+
49 </configure>
50
51=== added file 'lib/lp/app/interfaces/services.py'
52--- lib/lp/app/interfaces/services.py 1970-01-01 00:00:00 +0000
53+++ lib/lp/app/interfaces/services.py 2012-02-23 23:37:18 +0000
54@@ -0,0 +1,37 @@
55+# Copyright 2012 Canonical Ltd. This software is licensed under the
56+# GNU Affero General Public License version 3 (see the file LICENSE).
57+
58+"""Interfaces used for named services."""
59+
60+
61+__metaclass__ = type
62+
63+__all__ = [
64+ 'IService',
65+ 'IServiceFactory',
66+ ]
67+
68+from zope.interface import Interface
69+from zope.schema import TextLine
70+from lazr.restful.declarations import (
71+ export_as_webservice_entry,
72+ exported,
73+ )
74+
75+from lp import _
76+
77+
78+class IService(Interface):
79+ """Base interface for services."""
80+
81+ name = exported(
82+ TextLine(
83+ title=_('Name'),
84+ description=_(
85+ 'The name of the service, used to generate the url.')))
86+
87+
88+class IServiceFactory(Interface):
89+ """Interface representing a factory used to access named services."""
90+
91+ export_as_webservice_entry(publish_web_link=False, as_of='beta')
92
93=== added file 'lib/lp/app/services.py'
94--- lib/lp/app/services.py 1970-01-01 00:00:00 +0000
95+++ lib/lp/app/services.py 2012-02-23 23:37:18 +0000
96@@ -0,0 +1,37 @@
97+# Copyright 2012 Canonical Ltd. This software is licensed under the
98+# GNU Affero General Public License version 3 (see the file LICENSE).
99+
100+"""Factory used to get named services."""
101+
102+__metaclass__ = type
103+__all__ = [
104+ 'ServiceFactory',
105+ ]
106+
107+from zope.component import getUtility
108+from zope.interface import implements
109+
110+from lp.app.interfaces.services import (
111+ IService,
112+ IServiceFactory,
113+ )
114+from lp.services.webapp.publisher import Navigation
115+
116+
117+class ServiceFactory(Navigation):
118+ """Creates a named service.
119+
120+ Services are traversed via urls of the form /services/<name>
121+ Implementation classes are registered as named zope utilities.
122+ """
123+
124+ implements(IServiceFactory)
125+
126+ def __init__(self):
127+ super(ServiceFactory, self).__init__(None)
128+
129+ def traverse(self, name):
130+ return self.getService(name)
131+
132+ def getService(self, service_name):
133+ return getUtility(IService, service_name)
134
135=== added file 'lib/lp/app/tests/test_services.py'
136--- lib/lp/app/tests/test_services.py 1970-01-01 00:00:00 +0000
137+++ lib/lp/app/tests/test_services.py 2012-02-23 23:37:18 +0000
138@@ -0,0 +1,45 @@
139+# Copyright 2012 Canonical Ltd. This software is licensed under the
140+# GNU Affero General Public License version 3 (see the file LICENSE).
141+
142+"""Tests for core services infrastructure."""
143+
144+from zope.component import getUtility
145+from zope.interface.declarations import implements
146+
147+from lazr.restful.interfaces._rest import IHTTPResource
148+
149+from lp.app.interfaces.services import IService, IServiceFactory
150+from lp.services.webapp.interaction import ANONYMOUS
151+from lp.testing import (
152+ FakeAdapterMixin,
153+ TestCaseWithFactory,
154+ )
155+from lp.testing import login
156+from lp.testing.layers import DatabaseFunctionalLayer
157+from lp.testing.publication import test_traverse
158+
159+
160+class IFakeService(IService):
161+ """Fake service interface."""
162+
163+
164+class FakeService:
165+ implements(IFakeService, IHTTPResource)
166+
167+ name = 'fake_service'
168+
169+
170+class TestServiceFactory(TestCaseWithFactory, FakeAdapterMixin):
171+ """Tests for the ServiceFactory"""
172+
173+ layer = DatabaseFunctionalLayer
174+
175+ def test_service_traversal(self):
176+ # Test that traversal to the named service works.
177+ login(ANONYMOUS)
178+ fake_service = FakeService()
179+ self.registerUtility(fake_service, IService, "fake")
180+ context, view, request = test_traverse(
181+ 'https://launchpad.dev/api/devel/+services/fake')
182+ self.assertEqual(getUtility(IServiceFactory), context)
183+ self.assertEqual(fake_service, view)
184
185=== modified file 'lib/lp/registry/browser/configure.zcml'
186--- lib/lp/registry/browser/configure.zcml 2012-02-20 05:12:41 +0000
187+++ lib/lp/registry/browser/configure.zcml 2012-02-23 23:37:18 +0000
188@@ -32,6 +32,15 @@
189 module="lp.registry.feed.announcement"
190 classes="LaunchpadAnnouncementsFeed TargetAnnouncementsFeed"
191 />
192+ <browser:url
193+ for="lp.app.interfaces.services.IServiceFactory"
194+ path_expression="string:+services"
195+ parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
196+ />
197+ <browser:url
198+ for="lp.app.interfaces.services.IService"
199+ path_expression="string:${name}"
200+ parent_utility="lp.app.interfaces.services.IServiceFactory"/>
201
202 <facet facet="overview">
203 <browser:page
204
205=== added file 'lib/lp/registry/interfaces/accesspolicyservice.py'
206--- lib/lp/registry/interfaces/accesspolicyservice.py 1970-01-01 00:00:00 +0000
207+++ lib/lp/registry/interfaces/accesspolicyservice.py 2012-02-23 23:37:18 +0000
208@@ -0,0 +1,32 @@
209+# Copyright 2011 Canonical Ltd. This software is licensed under the
210+# GNU Affero General Public License version 3 (see the file LICENSE).
211+
212+"""Interfaces for access policy service."""
213+
214+
215+__metaclass__ = type
216+
217+__all__ = [
218+ 'IAccessPolicyService',
219+ ]
220+
221+from lazr.restful.declarations import (
222+ export_as_webservice_entry,
223+ export_read_operation,
224+ operation_for_version,
225+ )
226+
227+from lp.app.interfaces.services import IService
228+
229+
230+class IAccessPolicyService(IService):
231+
232+ # XXX 2012-02-24 wallyworld bug 939910
233+ # Need to export for version 'beta' even though we only want to use it in
234+ # version 'devel'
235+ export_as_webservice_entry(publish_web_link=False, as_of='beta')
236+
237+ @export_read_operation()
238+ @operation_for_version('devel')
239+ def getAccessPolicies():
240+ """Return the access policy types."""
241
242=== modified file 'lib/lp/registry/interfaces/webservice.py'
243--- lib/lp/registry/interfaces/webservice.py 2011-12-22 16:19:11 +0000
244+++ lib/lp/registry/interfaces/webservice.py 2012-02-23 23:37:18 +0000
245@@ -4,6 +4,8 @@
246 """All the interfaces that are exposed through the webservice."""
247
248 __all__ = [
249+ 'IAccessPolicyService',
250+ 'IServiceFactory',
251 'DerivationError',
252 'ICommercialSubscription',
253 'IDistribution',
254@@ -100,5 +102,9 @@
255 from lp.registry.interfaces.teammembership import ITeamMembership
256 from lp.registry.interfaces.wikiname import IWikiName
257
258+# Services
259+from lp.app.interfaces.services import IServiceFactory
260+from lp.registry.interfaces.accesspolicyservice import IAccessPolicyService
261+
262
263 _schema_circular_imports
264
265=== added directory 'lib/lp/registry/services'
266=== added file 'lib/lp/registry/services/__init__.py'
267--- lib/lp/registry/services/__init__.py 1970-01-01 00:00:00 +0000
268+++ lib/lp/registry/services/__init__.py 2012-02-23 23:37:18 +0000
269@@ -0,0 +1,1 @@
270+"""The services namespace package."""
271
272=== added file 'lib/lp/registry/services/accesspolicyservice.py'
273--- lib/lp/registry/services/accesspolicyservice.py 1970-01-01 00:00:00 +0000
274+++ lib/lp/registry/services/accesspolicyservice.py 2012-02-23 23:37:18 +0000
275@@ -0,0 +1,45 @@
276+# Copyright 2012 Canonical Ltd. This software is licensed under the
277+# GNU Affero General Public License version 3 (see the file LICENSE).
278+
279+"""Classes for pillar and artifact access policy services."""
280+
281+__metaclass__ = type
282+__all__ = [
283+ 'AccessPolicyService',
284+ ]
285+
286+import simplejson
287+from lazr.restful import ResourceJSONEncoder
288+from zope.interface import implements
289+
290+from lp.app.enums import InformationVisibilityPolicy
291+from lp.registry.interfaces.accesspolicyservice import (
292+ IAccessPolicyService,
293+ )
294+
295+
296+class AccessPolicyService:
297+ """Service providing operations for access policies.
298+
299+ Service is accessed via a url of the form
300+ '/services/accesspolicy?ws.op=...
301+ """
302+
303+ implements(IAccessPolicyService)
304+
305+ @property
306+ def name(self):
307+ """See `IService`."""
308+ return 'accesspolicy'
309+
310+ def getAccessPolicies(self):
311+ policies = []
312+ for x, policy in enumerate(InformationVisibilityPolicy):
313+ item = dict(
314+ index=x,
315+ value=policy.token,
316+ title=policy.title,
317+ description=policy.value.description
318+ )
319+ policies.append(item)
320+ return simplejson.dumps(policies, cls=ResourceJSONEncoder)
321
322=== added file 'lib/lp/registry/services/configure.zcml'
323--- lib/lp/registry/services/configure.zcml 1970-01-01 00:00:00 +0000
324+++ lib/lp/registry/services/configure.zcml 2012-02-23 23:37:18 +0000
325@@ -0,0 +1,18 @@
326+<!-- Copyright 2012 Canonical Ltd. This software is licensed under the
327+ GNU Affero General Public License version 3 (see the file LICENSE).
328+-->
329+
330+<configure
331+ xmlns="http://namespaces.zope.org/zope"
332+ xmlns:browser="http://namespaces.zope.org/browser"
333+ i18n_domain="launchpad">
334+
335+ <!-- Named Services -->
336+ <securedutility
337+ name="accesspolicy"
338+ class="lp.registry.services.accesspolicyservice.AccessPolicyService"
339+ provides="lp.app.interfaces.services.IService">
340+ <allow
341+ interface="lp.registry.interfaces.accesspolicyservice.IAccessPolicyService"/>
342+ </securedutility>
343+</configure>
344
345=== added directory 'lib/lp/registry/services/tests'
346=== added file 'lib/lp/registry/services/tests/__init__.py'
347=== added file 'lib/lp/registry/services/tests/test_accesspolicyservice.py'
348--- lib/lp/registry/services/tests/test_accesspolicyservice.py 1970-01-01 00:00:00 +0000
349+++ lib/lp/registry/services/tests/test_accesspolicyservice.py 2012-02-23 23:37:18 +0000
350@@ -0,0 +1,78 @@
351+# Copyright 2012 Canonical Ltd. This software is licensed under the
352+# GNU Affero General Public License version 3 (see the file LICENSE).
353+
354+__metaclass__ = type
355+
356+
357+import simplejson
358+
359+from zope.component import getUtility
360+
361+from lp.app.enums import InformationVisibilityPolicy
362+from lp.registry.services.accesspolicyservice import AccessPolicyService
363+from lp.services.webapp.interfaces import ILaunchpadRoot
364+from lp.services.webapp.publisher import canonical_url
365+from lp.testing import WebServiceTestCase, TestCaseWithFactory
366+from lp.testing.layers import AppServerLayer
367+from lp.testing.pages import LaunchpadWebServiceCaller
368+
369+
370+class ApiTestMixin:
371+ """Common tests for launchpadlib and webservice."""
372+
373+ def test_getAccessPolicies(self):
374+ # Test the getAccessPolicies method.
375+ json_policies = self._getAccessPolicies()
376+ policies = simplejson.loads(json_policies)
377+ expected_polices = []
378+ for x, policy in enumerate(InformationVisibilityPolicy):
379+ item = dict(
380+ index=x,
381+ value=policy.token,
382+ title=policy.title,
383+ description=policy.value.description
384+ )
385+ expected_polices.append(item)
386+ self.assertContentEqual(expected_polices, policies)
387+
388+
389+class TestWebService(WebServiceTestCase, ApiTestMixin):
390+ """Test the web service interface for the Access Policy Service."""
391+
392+ def setUp(self):
393+ super(TestWebService, self).setUp()
394+ self.webservice = LaunchpadWebServiceCaller(
395+ 'launchpad-library', 'salgado-change-anything')
396+
397+ def test_url(self):
398+ # Test that the url for the service is correct.
399+ service = AccessPolicyService()
400+ root_app = getUtility(ILaunchpadRoot)
401+ self.assertEqual(
402+ '%s+services/accesspolicy' % canonical_url(root_app),
403+ canonical_url(service))
404+
405+ def _named_get(self, api_method, **kwargs):
406+ return self.webservice.named_get(
407+ '/+services/accesspolicy',
408+ api_method, api_version='devel', **kwargs).jsonBody()
409+
410+ def _getAccessPolicies(self):
411+ return self._named_get('getAccessPolicies')
412+
413+
414+class TestLaunchpadlib(TestCaseWithFactory, ApiTestMixin):
415+ """Test launchpadlib access for the Access Policy Service."""
416+
417+ layer = AppServerLayer
418+
419+ def setUp(self):
420+ super(TestLaunchpadlib, self).setUp()
421+ self.launchpad = self.factory.makeLaunchpadService()
422+
423+ def _getAccessPolicies(self):
424+ # XXX 2012-02-23 wallyworld bug 681767
425+ # Launchpadlib can't do relative url's
426+ service = self.launchpad.load(
427+ '%s/+services/accesspolicy' % self.launchpad._root_uri)
428+ return service.getAccessPolicies()
429
430=== added file 'lib/lp/services/webservice/services.py'
431--- lib/lp/services/webservice/services.py 1970-01-01 00:00:00 +0000
432+++ lib/lp/services/webservice/services.py 2012-02-23 23:37:18 +0000
433@@ -0,0 +1,32 @@
434+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
435+# GNU Affero General Public License version 3 (see the file LICENSE).
436+
437+"""A class for the top-level link to the services factory."""
438+
439+__metaclass__ = type
440+__all__ = [
441+ 'IServicesLink',
442+ 'ServicesLink',
443+ ]
444+
445+from lazr.restful.interfaces import ITopLevelEntryLink
446+from zope.interface import implements
447+
448+from lp.app.interfaces.services import IServiceFactory
449+from lp.services.webapp.interfaces import ICanonicalUrlData
450+
451+
452+class IServicesLink(ITopLevelEntryLink, ICanonicalUrlData):
453+ """A marker interface."""
454+
455+
456+class ServicesLink:
457+ """The top-level link to the services factory."""
458+ implements(IServicesLink)
459+
460+ link_name = 'services'
461+ entry_type = IServiceFactory
462+
463+ inside = None
464+ path = 'services'
465+ rootsite = 'api'