launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #04206
[Merge] lp:~flacoste/launchpad/bug-801233 into lp:launchpad
Francis J. Lacoste has proposed merging lp:~flacoste/launchpad/bug-801233 into lp:launchpad.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~flacoste/launchpad/bug-801233/+merge/67353
= Summary =
This is a massive Yak shaving branch! The goal is to write an API script which
will allow copy rebuilds to be manually allocated a number of builders. To do
this, we need to find dynamically the number of available builders for a
particular processor. That information isn't currently available over the
API.
== Proposed fix ==
It exports IBuilder.processor, IBuilderSet.getBuidQueueSizes() and
IBuilderSet.getBuilderrsForQueue(). Along with numerous other yak that needed
shaving along the way. An important one worth mentioning here: I changed the
URL of IProcessor. They were living under their family and their DB id was
part of th URL. Since IProcessor name are globally unique, I added a
/+processors collection and their URL point their using their name. That will
make it easier to get processor by crafting the URL.
== Pre-implementation notes ==
No pre-implementation done.
== Implementation details ==
Here are the numerous yak shaved along the way:
* Removed getBuildersByArch() which wasn't used anywhere.
* getBuildQueueSize() returns a dict containing datetime.timedelta object.
simplejson didn't know how to serialize those, so I hooked up a very
simple one which simply str() it. That was added into
lp.services.webservice.json and hooked through zcml.
* Collections check that launchpad.View is available on the entry. So I had
to define adapters for IBuilder, IProcessor and IProcessorFamily.
* Since there is no web page for IProcessor nor IProcessorFamily, I changed
pulish_web_link to False.
Everything else should be straightforward, let me know if you have any
questions.
== Tests ==
./bin/test -vvt
'lp.soyuz.tests.test_processor|lp.soyuz.browser.tests.test_processor|lp.buildmaster.tests.test_webservice'
== Demo and Q/A ==
This will be QA-ed by checking the exported attribute and methods on
qastaging.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/buildmaster/model/builder.py
lib/lp/soyuz/tests/test_processor.py
lib/lp/services/webservice/tests/__init__.py
lib/lp/buildmaster/security.py
lib/lp/soyuz/model/processor.py
lib/lp/soyuz/interfaces/processor.py
lib/lp/soyuz/security.py
lib/lp/app/browser/launchpad.py
lib/lp/testing/publication.py
lib/lp/soyuz/configure.zcml
lib/lp/testing/factory.py
lib/lp/testing/__init__.py
lib/lp/soyuz/interfaces/webservice.py
lib/lp/buildmaster/configure.zcml
lib/lp/soyuz/browser/configure.zcml
lib/lp/services/webservice/tests/test_json.py
lib/lp/testing/tests/test_publication.py
lib/canonical/launchpad/configure.zcml
lib/lp/services/webservice/json.py
lib/lp/soyuz/browser/processor.py
lib/lp/services/webservice/configure.zcml
lib/lp/buildmaster/tests/test_webservice.py
lib/lp/buildmaster/interfaces/builder.py
lib/lp/soyuz/browser/tests/test_processor.py
./lib/lp/buildmaster/model/builder.py
174: Line exceeds 78 characters.
261: Line exceeds 78 characters.
219: W291 trailing whitespace
284: E301 expected 1 blank line, found 0
302: E301 expected 1 blank line, found 0
373: E301 expected 1 blank line, found 0
517: E301 expected 1 blank line, found 0
519: E301 expected 1 blank line, found 0
608: E301 expected 1 blank line, found 0
683: E301 expected 1 blank line, found 0
686: E301 expected 1 blank line, found 0
./lib/lp/soyuz/tests/test_processor.py
48: Line exceeds 78 characters.
55: Line exceeds 78 characters.
57: Line exceeds 78 characters.
123: Line exceeds 78 characters.
88: E202 whitespace before ')'
123: E501 line too long (123 characters)
123: E202 whitespace before ')'
./lib/lp/soyuz/model/processor.py
87: E301 expected 1 blank line, found 0
./lib/lp/soyuz/security.py
18: E302 expected 2 blank lines, found 1
22: E302 expected 2 blank lines, found 1
./lib/lp/testing/publication.py
36: E302 expected 2 blank lines, found 1
./lib/lp/services/webservice/tests/test_json.py
27: E303 too many blank lines (2)
30: W291 trailing whitespace
33: W391 blank line at end of file
./lib/lp/testing/tests/test_publication.py
49: E301 expected 1 blank line, found 0
58: W291 trailing whitespace
79: E301 expected 1 blank line, found 0
101: E301 expected 1 blank line, found 0
./lib/lp/buildmaster/interfaces/builder.py
139: Line exceeds 78 characters.
210: Line exceeds 78 characters.
249: W293 blank line contains whitespace
256: W293 blank line contains whitespace
./lib/lp/soyuz/browser/tests/test_processor.py
13: E302 expected 2 blank lines, found 1
41: W391 blank line at end of file
I'll fix the white-space lint post-review.
--
https://code.launchpad.net/~flacoste/launchpad/bug-801233/+merge/67353
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~flacoste/launchpad/bug-801233 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml 2011-02-23 21:15:23 +0000
+++ lib/canonical/launchpad/configure.zcml 2011-07-08 19:23:25 +0000
@@ -27,6 +27,7 @@
<include package="lp.coop.answersbugs" />
<include package="lp.code" />
<include package="lp.soyuz" />
+ <include package="lp.services.webservice" />
<include package="lp.translations" />
<include package="lp.testopenid" />
<include package="lp.blueprints" />
=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2011-06-30 11:28:59 +0000
+++ lib/lp/app/browser/launchpad.py 2011-07-08 19:23:25 +0000
@@ -146,7 +146,10 @@
from lp.services.worlddata.interfaces.language import ILanguageSet
from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
from lp.soyuz.interfaces.packageset import IPackagesetSet
-from lp.soyuz.interfaces.processor import IProcessorFamilySet
+from lp.soyuz.interfaces.processor import (
+ IProcessorFamilySet,
+ IProcessorSet,
+ )
from lp.testopenid.interfaces.server import ITestOpenIDApplication
from lp.translations.interfaces.translationgroup import ITranslationGroupSet
from lp.translations.interfaces.translationimportqueue import (
@@ -617,6 +620,7 @@
'people': IPersonSet,
'pillars': IPillarNameSet,
'+processor-families': IProcessorFamilySet,
+ '+processors': IProcessorSet,
'projects': IProductSet,
'projectgroups': IProjectGroupSet,
'sourcepackagenames': ISourcePackageNameSet,
=== modified file 'lib/lp/buildmaster/configure.zcml'
--- lib/lp/buildmaster/configure.zcml 2010-11-19 13:25:25 +0000
+++ lib/lp/buildmaster/configure.zcml 2011-07-08 19:23:25 +0000
@@ -10,6 +10,7 @@
xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
i18n_domain="launchpad">
<include package=".browser"/>
+ <authorizations module=".security" />
<!-- Builder -->
<class
=== modified file 'lib/lp/buildmaster/interfaces/builder.py'
--- lib/lp/buildmaster/interfaces/builder.py 2011-02-24 15:30:54 +0000
+++ lib/lp/buildmaster/interfaces/builder.py 2011-07-08 19:23:25 +0000
@@ -27,6 +27,12 @@
exported,
operation_parameters,
operation_returns_entry,
+ operation_returns_collection_of,
+ operation_for_version,
+ )
+from lazr.restful.fields import (
+ Reference,
+ ReferenceChoice,
)
from zope.interface import (
Attribute,
@@ -34,7 +40,6 @@
)
from zope.schema import (
Bool,
- Choice,
Field,
Int,
Text,
@@ -45,6 +50,7 @@
from lp.app.validators.name import name_validator
from lp.app.validators.url import builder_url_validator
from lp.registry.interfaces.role import IHasOwner
+from lp.soyuz.interfaces.processor import IProcessor
from lp.services.fields import (
Description,
PersonChoice,
@@ -106,10 +112,12 @@
id = Attribute("Builder identifier")
- processor = Choice(
+ processor = exported(ReferenceChoice(
title=_('Processor'), required=True, vocabulary='Processor',
+ schema=IProcessor,
description=_('Build Slave Processor, used to identify '
- 'which jobs can be built by this device.'))
+ 'which jobs can be built by this device.')),
+ as_of='devel', readonly=True)
owner = exported(PersonChoice(
title=_('Owner'), required=True, vocabulary='ValidOwner',
@@ -385,9 +393,8 @@
def getBuilders():
"""Return all active configured builders."""
- def getBuildersByArch(arch):
- """Return all configured builders for a given DistroArchSeries."""
-
+ @export_read_operation()
+ @operation_for_version('devel')
def getBuildQueueSizes():
"""Return the number of pending builds for each processor.
@@ -406,5 +413,13 @@
as a timedelta or None for empty queues.
"""
+ @operation_parameters(
+ processor=Reference(
+ title=_("Processor"), required=True, schema=IProcessor),
+ virtualized=Bool(
+ title=_("Virtualized"), required=False, default=True))
+ @operation_returns_collection_of(IBuilder)
+ @export_read_operation()
+ @operation_for_version('devel')
def getBuildersForQueue(processor, virtualized):
"""Return all builders for given processor/virtualization setting."""
=== modified file 'lib/lp/buildmaster/model/builder.py'
--- lib/lp/buildmaster/model/builder.py 2011-02-01 18:14:57 +0000
+++ lib/lp/buildmaster/model/builder.py 2011-07-08 19:23:25 +0000
@@ -896,13 +896,6 @@
return Builder.selectBy(
active=True, orderBy=['virtualized', 'processor', 'name'])
- def getBuildersByArch(self, arch):
- """See IBuilderSet."""
- return Builder.select('builder.processor = processor.id '
- 'AND processor.family = %d'
- % arch.processorfamily.id,
- clauseTables=("Processor",))
-
def getBuildQueueSizes(self):
"""See `IBuilderSet`."""
store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
=== added file 'lib/lp/buildmaster/security.py'
--- lib/lp/buildmaster/security.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/security.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,19 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the buildmaster package."""
+
+__metaclass__ = type
+__all__ = [
+ 'ViewBuilder',
+ ]
+
+from lp.app.security import AnonymousAuthorization
+from lp.buildmaster.interfaces.builder import (
+ IBuilder,
+ )
+
+
+class ViewBuilder(AnonymousAuthorization):
+ """Anyone can view a `IBuilder`."""
+ usedfor = IBuilder
=== added file 'lib/lp/buildmaster/tests/test_webservice.py'
--- lib/lp/buildmaster/tests/test_webservice.py 1970-01-01 00:00:00 +0000
+++ lib/lp/buildmaster/tests/test_webservice.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,68 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the builders webservice ."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
+from lp.testing import (
+ api_url,
+ logout,
+ TestCaseWithFactory,
+ )
+
+
+class TestBuildersCollection(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestBuildersCollection, self).setUp()
+ self.webservice = LaunchpadWebServiceCaller()
+
+ def test_getBuildQueueSizes(self):
+ logout()
+ results = self.webservice.named_get(
+ '/builders', 'getBuildQueueSizes', api_version='devel')
+ self.assertEquals(
+ ['nonvirt', 'virt'], sorted(results.jsonBody().keys()))
+
+ def test_getBuildersForQueue(self):
+ g1 = self.factory.makeProcessorFamily('g1').processors[0]
+ quantum = self.factory.makeProcessorFamily('quantum').processors[0]
+ self.factory.makeBuilder(
+ processor=quantum, name='quantum_builder1')
+ self.factory.makeBuilder(
+ processor=quantum, name='quantum_builder2')
+ self.factory.makeBuilder(
+ processor=quantum, name='quantum_builder3', virtualized=False)
+ self.factory.makeBuilder(
+ processor=g1, name='g1_builder', virtualized=False)
+
+ logout()
+ results = self.webservice.named_get(
+ '/builders', 'getBuildersForQueue',
+ processor=api_url(quantum), virtualized=True,
+ api_version='devel').jsonBody()
+ self.assertEquals(
+ ['quantum_builder1', 'quantum_builder2'],
+ sorted(builder['name'] for builder in results['entries']))
+
+
+class TestBuilderEntry(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestBuilderEntry, self).setUp()
+ self.webservice = LaunchpadWebServiceCaller()
+
+ def test_exports_processor(self):
+ processor_family = self.factory.makeProcessorFamily('s1')
+ builder = self.factory.makeBuilder(
+ processor=processor_family.processors[0])
+
+ logout()
+ entry = self.webservice.get(
+ api_url(builder), api_version='devel').jsonBody()
+ self.assertEndsWith(entry['processor_link'], '/+processors/s1')
=== added file 'lib/lp/services/webservice/configure.zcml'
--- lib/lp/services/webservice/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/configure.zcml 2011-07-08 19:23:25 +0000
@@ -0,0 +1,15 @@
+<!-- Copyright 2011 Canonical Ltd. This software is licensed under the
+ GNU Affero General Public License version 3 (see the file LICENSE).
+-->
+
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:i18n="http://namespaces.zope.org/i18n"
+ i18n_domain="launchpad">
+
+ <adapter
+ provides="lazr.restful.interfaces.IJSONPublishable"
+ for="zope.interface.common.idatetime.ITimeDelta"
+ factory="lp.services.webservice.json.StrJSONSerializer"
+ permission="zope.Public"/>
+</configure>
=== added file 'lib/lp/services/webservice/json.py'
--- lib/lp/services/webservice/json.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/json.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,22 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Additional JSON serializer for the web service."""
+
+__metaclass__ = type
+__all__ = []
+
+
+from lazr.restful.interfaces import IJSONPublishable
+from zope.interface import implements
+
+
+class StrJSONSerializer:
+ """Simple JSON serializer that simply str() it's context. """
+ implements(IJSONPublishable)
+
+ def __init__(self, context):
+ self.context = context
+
+ def toDataForJSON(self, media_type):
+ return str(self.context)
=== added directory 'lib/lp/services/webservice/tests'
=== added file 'lib/lp/services/webservice/tests/__init__.py'
=== added file 'lib/lp/services/webservice/tests/test_json.py'
--- lib/lp/services/webservice/tests/test_json.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/webservice/tests/test_json.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,33 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for the JSON serializer."""
+
+__metaclass__ = type
+
+from datetime import timedelta
+
+from canonical.testing.layers import FunctionalLayer
+from lazr.restful.interfaces import IJSONPublishable
+from lp.services.webservice.json import StrJSONSerializer
+from lp.testing import TestCase
+
+
+class TestStrJSONSerializer(TestCase):
+ layer = FunctionalLayer
+
+ def test_toDataForJSON(self):
+ serializer = StrJSONSerializer(
+ timedelta(days=2, hours=2, seconds=5))
+ self.assertEquals(
+ '2 days, 2:00:05',
+ serializer.toDataForJSON('application/json'))
+
+
+ def test_timedelta_users_StrJSONSerializer(self):
+ delta = timedelta(seconds=5)
+ serializer = IJSONPublishable(delta)
+ self.assertEquals('0:00:05',
+ serializer.toDataForJSON('application/json'))
+
+
=== modified file 'lib/lp/soyuz/browser/configure.zcml'
--- lib/lp/soyuz/browser/configure.zcml 2011-06-23 13:42:38 +0000
+++ lib/lp/soyuz/browser/configure.zcml 2011-07-08 19:23:25 +0000
@@ -32,18 +32,20 @@
path_expression="string:+binarypub"
attribute_to_parent="archive"
urldata="lp.soyuz.browser.publishing.BinaryPublicationURL"/>
- <browser:url
- for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
+ <browser:url for="lp.soyuz.interfaces.processor.IProcessorFamilySet"
path_expression="string:+processor-families"
parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
<browser:url
for="lp.soyuz.interfaces.processor.IProcessorFamily"
path_expression="string:${name}"
parent_utility="lp.soyuz.interfaces.processor.IProcessorFamilySet" />
+ <browser:url for="lp.soyuz.interfaces.processor.IProcessorSet"
+ path_expression="string:+processors"
+ parent_utility="canonical.launchpad.webapp.interfaces.ILaunchpadRoot"/>
<browser:url
for="lp.soyuz.interfaces.processor.IProcessor"
- path_expression="string:${id}"
- attribute_to_parent="family" />
+ path_expression="string:${name}"
+ parent_utility="lp.soyuz.interfaces.processor.IProcessorSet" />
</facet>
<browser:navigation
module="lp.soyuz.browser.binarypackagerelease"
@@ -233,7 +235,7 @@
<browser:navigation
module="lp.soyuz.browser.processor"
classes="
- ProcessorFamilySetNavigation ProcessorFamilyNavigation"/>
+ ProcessorFamilySetNavigation ProcessorSetNavigation"/>
<browser:url
for="lp.soyuz.interfaces.archive.IPPA"
path_expression="string:+archive"
=== modified file 'lib/lp/soyuz/browser/processor.py'
--- lib/lp/soyuz/browser/processor.py 2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/browser/processor.py 2011-07-08 19:23:25 +0000
@@ -8,15 +8,15 @@
__all__ = [
'ProcessorFamilySetNavigation',
- 'ProcessorFamilyNavigation',
+ 'ProcessorSetNavigation',
]
from canonical.launchpad.webapp import Navigation
from lp.app.errors import NotFoundError
from lp.soyuz.interfaces.processor import (
- IProcessorFamily,
IProcessorFamilySet,
+ IProcessorSet,
)
@@ -32,15 +32,9 @@
return family
-class ProcessorFamilyNavigation(Navigation):
- """IProcessorFamily navigation."""
-
- usedfor = IProcessorFamily
-
- def traverse(self, id_):
- id_ = int(id_)
- processors = self.processors
- for p in processors:
- if p.id == id_:
- return p
- raise NotFoundError(id_)
+class ProcessorSetNavigation(Navigation):
+ """IProcessorFamilySet navigation."""
+ usedfor = IProcessorSet
+
+ def traverse(self, name):
+ return self.context.getByName(name)
=== added file 'lib/lp/soyuz/browser/tests/test_processor.py'
--- lib/lp/soyuz/browser/tests/test_processor.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/browser/tests/test_processor.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,41 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for process navigation."""
+
+__metaclass__ = type
+
+from canonical.testing.layers import DatabaseFunctionalLayer
+from canonical.launchpad.webapp.publisher import canonical_url
+from lp.testing import TestCaseWithFactory
+from lp.testing.publication import test_traverse
+
+class TestProcessorNavigation(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_processor_family_url(self):
+ family = self.factory.makeProcessorFamily('quantum')
+ self.assertEquals(
+ '/+processor-families/quantum',
+ canonical_url(family, force_local_path=True))
+
+ def test_processor_url(self):
+ family = self.factory.makeProcessorFamily('quantum')
+ quantum = family.processors[0]
+ self.assertEquals(
+ '/+processors/quantum',
+ canonical_url(quantum, force_local_path=True))
+
+ def test_processor_family_navigation(self):
+ family = self.factory.makeProcessorFamily('quantum')
+ obj, view, request = test_traverse(
+ 'http://api.launchpad.dev/devel/+processor-families/quantum')
+ self.assertEquals(family, obj)
+
+ def test_processor_navigation(self):
+ family = self.factory.makeProcessorFamily('quantum')
+ obj, view, request = test_traverse(
+ 'http://api.launchpad.dev/'
+ 'devel/+processors/quantum')
+ self.assertEquals(family.processors[0], obj)
+
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2011-06-25 08:55:37 +0000
+++ lib/lp/soyuz/configure.zcml 2011-07-08 19:23:25 +0000
@@ -11,6 +11,7 @@
i18n_domain="launchpad">
<include
package=".browser"/>
+ <authorizations module=".security" />
<!-- PackageCloner -->
@@ -375,6 +376,12 @@
<allow
interface="lp.soyuz.interfaces.processor.IProcessorFamilySet"/>
</securedutility>
+ <securedutility
+ class="lp.soyuz.model.processor.ProcessorSet"
+ provides="lp.soyuz.interfaces.processor.IProcessorSet">
+ <allow
+ interface="lp.soyuz.interfaces.processor.IProcessorSet"/>
+ </securedutility>
<adapter
for="lp.soyuz.interfaces.distroarchseriesbinarypackagerelease.IDistroArchSeriesBinaryPackageRelease"
provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
=== modified file 'lib/lp/soyuz/interfaces/processor.py'
--- lib/lp/soyuz/interfaces/processor.py 2011-06-23 16:11:42 +0000
+++ lib/lp/soyuz/interfaces/processor.py 2011-07-08 19:23:25 +0000
@@ -11,6 +11,8 @@
'IProcessor',
'IProcessorFamily',
'IProcessorFamilySet',
+ 'IProcessorSet',
+ 'ProcessorNotFound',
]
from zope.interface import (
@@ -38,6 +40,12 @@
CollectionField,
Reference,
)
+from lp.app.errors import NameLookupFailed
+
+
+class ProcessorNotFound(NameLookupFailed):
+ """Exception raised when a processor name isn't found."""
+ _message_prefix = 'No such processor'
class IProcessor(Interface):
@@ -49,7 +57,7 @@
# the WADL generation work it must be back-dated to the earliest version.
# Note that individual attributes and methods can and must truthfully set
# 'devel' as their version.
- export_as_webservice_entry(publish_web_link=True, as_of='beta')
+ export_as_webservice_entry(publish_web_link=False, as_of='beta')
id = Attribute("The Processor ID")
family = exported(
Reference(
@@ -84,7 +92,7 @@
# 'devel' as their version.
export_as_webservice_entry(
plural_name='processor_families',
- publish_web_link=True,
+ publish_web_link=False,
as_of='beta')
id = Attribute("The ProcessorFamily ID")
@@ -123,14 +131,36 @@
"""
+class IProcessorSet(Interface):
+ """Operations related to Processor instances."""
+ export_as_webservice_collection(IProcessor)
+
+ @operation_parameters(
+ name=TextLine(required=True))
+ @operation_returns_entry(IProcessor)
+ @export_read_operation()
+ @operation_for_version('devel')
+ def getByName(name):
+ """Return the IProcessor instance with the matching name.
+
+ :param name: The name to look for.
+ :raise ProcessorNotFound: if there is no processor with that name.
+ :return: A `IProcessor` instance if found
+ """
+
+ @collection_default_content()
+ def getAll():
+ """Return all the `IProcessor` known to Launchpad."""
+
+
class IProcessorFamilySet(Interface):
"""Operations related to ProcessorFamily instances."""
- export_as_webservice_collection(Interface)
+ export_as_webservice_collection(IProcessorFamily)
@operation_parameters(
name=TextLine(required=True))
- @operation_returns_entry(Interface)
+ @operation_returns_entry(IProcessorFamily)
@export_read_operation()
@operation_for_version('devel')
def getByName(name):
=== modified file 'lib/lp/soyuz/interfaces/webservice.py'
--- lib/lp/soyuz/interfaces/webservice.py 2011-06-23 18:26:26 +0000
+++ lib/lp/soyuz/interfaces/webservice.py 2011-07-08 19:23:25 +0000
@@ -35,6 +35,7 @@
'IProcessor',
'IProcessorFamily',
'IProcessorFamilySet',
+ 'IProcessorSet',
'ISourcePackagePublishingHistory',
'IncompatibleArguments',
'InsufficientUploadRights',
@@ -96,6 +97,7 @@
IProcessor,
IProcessorFamily,
IProcessorFamilySet,
+ IProcessorSet,
)
from lp.soyuz.interfaces.publishing import (
IBinaryPackagePublishingHistory,
@@ -105,7 +107,6 @@
from canonical.launchpad.components.apihelpers import (
patch_collection_property,
- patch_entry_return_type,
patch_plain_parameter_type,
patch_reference_property,
)
@@ -115,15 +116,10 @@
from canonical.launchpad.interfaces import _schema_circular_imports
_schema_circular_imports
-from lazr.restful.declarations import LAZR_WEBSERVICE_EXPORTED
-IProcessorFamilySet.queryTaggedValue(
- LAZR_WEBSERVICE_EXPORTED)['collection_entry_schema'] = IProcessorFamily
-
# IProcessor
patch_reference_property(
IProcessor, 'family', IProcessorFamily)
-patch_entry_return_type(IProcessorFamilySet, 'getByName', IProcessorFamily)
patch_collection_property(
IArchive, 'enabled_restricted_families', IProcessorFamily)
patch_plain_parameter_type(
=== modified file 'lib/lp/soyuz/model/processor.py'
--- lib/lp/soyuz/model/processor.py 2010-11-12 02:15:28 +0000
+++ lib/lp/soyuz/model/processor.py 2011-07-08 19:23:25 +0000
@@ -25,6 +25,8 @@
IProcessor,
IProcessorFamily,
IProcessorFamilySet,
+ IProcessorSet,
+ ProcessorNotFound,
)
@@ -42,6 +44,24 @@
return "<Processor %r>" % self.title
+class ProcessorSet:
+ """See `IProcessorSet`."""
+ implements(IProcessorSet)
+
+ def getByName(self, name):
+ """See `IProcessorSet`."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ processor = store.find(Processor, Processor.name == name).one()
+ if processor is None:
+ raise ProcessorNotFound(name)
+ return processor
+
+ def getAll(self):
+ """See `IProcessorSet`."""
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ return store.find(Processor)
+
+
class ProcessorFamily(SQLBase):
implements(IProcessorFamily)
_table = 'ProcessorFamily'
=== added file 'lib/lp/soyuz/security.py'
--- lib/lp/soyuz/security.py 1970-01-01 00:00:00 +0000
+++ lib/lp/soyuz/security.py 2011-07-08 19:23:25 +0000
@@ -0,0 +1,24 @@
+# Copyright 2011 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Security adapters for the soyuz module."""
+
+__metaclass__ = type
+__all__ = [
+ 'ViewProcessor',
+ 'ViewProcessorFamily',
+ ]
+
+from lp.app.security import AnonymousAuthorization
+from lp.soyuz.interfaces.processor import (
+ IProcessor,
+ IProcessorFamily,
+ )
+
+class ViewProcessor(AnonymousAuthorization):
+ """Anyone can view an `IProcessor`."""
+ usedfor = IProcessor
+
+class ViewProcessorFamily(AnonymousAuthorization):
+ """Anyone can view an `IProcessorFamily`."""
+ usedfor = IProcessorFamily
=== modified file 'lib/lp/soyuz/tests/test_processor.py'
--- lib/lp/soyuz/tests/test_processor.py 2010-10-04 19:50:45 +0000
+++ lib/lp/soyuz/tests/test_processor.py 2011-07-08 19:23:25 +0000
@@ -5,13 +5,29 @@
from zope.component import getUtility
-from canonical.testing.layers import LaunchpadZopelessLayer
+from canonical.launchpad.webapp.interfaces import (
+ DEFAULT_FLAVOR,
+ IStoreSelector,
+ MAIN_STORE,
+ )
+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
+from canonical.testing.layers import (
+ DatabaseFunctionalLayer,
+ LaunchpadZopelessLayer,
+ )
+
from lp.soyuz.interfaces.processor import (
IProcessor,
IProcessorFamily,
IProcessorFamilySet,
- )
-from lp.testing import TestCaseWithFactory
+ IProcessorSet,
+ ProcessorNotFound,
+ )
+from lp.testing import (
+ ExpectedException,
+ logout,
+ TestCaseWithFactory,
+ )
class ProcessorFamilyTests(TestCaseWithFactory):
@@ -42,3 +58,66 @@
"Another small processor family", restricted=True)
self.assertFalse(normal_family in family_set.getRestricted())
self.assertTrue(restricted_family in family_set.getRestricted())
+
+
+class ProcessorSetTests(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def test_getByName(self):
+ processor_set = getUtility(IProcessorSet)
+ q1 = self.factory.makeProcessorFamily(name='q1')
+ self.assertEquals(q1.processors[0], processor_set.getByName('q1'))
+
+ def test_getByName_not_found(self):
+ processor_set = getUtility(IProcessorSet)
+ with ExpectedException(ProcessorNotFound, 'No such processor.*'):
+ processor_set.getByName('q1')
+
+ def test_getAll(self):
+ processor_set = getUtility(IProcessorSet)
+ # Make it easy to filter out sample data
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ store.execute("UPDATE Processor SET name = 'sample_data_' || name")
+ self.factory.makeProcessorFamily(name='q1')
+ self.factory.makeProcessorFamily(name='i686')
+ self.factory.makeProcessorFamily(name='g4')
+ self.assertEquals(
+ ['g4', 'i686', 'q1'],
+ sorted(
+ processor.name for processor in processor_set.getAll()
+ if not processor.name.startswith('sample_data_') ))
+
+
+class ProcessorSetWebServiceTests(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(ProcessorSetWebServiceTests, self).setUp()
+ self.webservice = LaunchpadWebServiceCaller()
+
+ def test_getByName(self):
+ self.factory.makeProcessorFamily(name='transmeta')
+ logout()
+
+ processor = self.webservice.named_get(
+ '/+processors', 'getByName', name='transmeta',
+ api_version='devel',
+ ).jsonBody()
+ self.assertEquals('transmeta', processor['name'])
+
+ def test_default_collection(self):
+ # Make it easy to filter out sample data
+ store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
+ store.execute("UPDATE Processor SET name = 'sample_data_' || name")
+ self.factory.makeProcessorFamily(name='q1')
+ self.factory.makeProcessorFamily(name='i686')
+ self.factory.makeProcessorFamily(name='g4')
+
+ logout()
+
+ collection = self.webservice.get(
+ '/+processors?ws.size=10', api_version='devel').jsonBody()
+ self.assertEquals(
+ ['g4', 'i686', 'q1'],
+ sorted(
+ processor['name'] for processor in collection['entries'] if not processor['name'].startswith('sample_data_') ))
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2011-07-07 12:54:38 +0000
+++ lib/lp/testing/__init__.py 2011-07-08 19:23:25 +0000
@@ -619,6 +619,17 @@
self._unfoldEmailHeader(expected),
self._unfoldEmailHeader(observed))
+ def assertStartsWith(self, s, prefix):
+ if not s.startswith(prefix):
+ raise AssertionError(
+ 'string %r does not start with %r' % (s, prefix))
+
+ def assertEndsWith(self, s, suffix):
+ """Asserts that s ends with suffix."""
+ if not s.endswith(suffix):
+ raise AssertionError(
+ 'string %r does not end with %r' % (s, suffix))
+
class TestCaseWithFactory(TestCase):
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2011-07-07 15:47:36 +0000
+++ lib/lp/testing/factory.py 2011-07-08 19:23:25 +0000
@@ -894,6 +894,9 @@
restricted=False):
"""Create a new processor family.
+ A default processor for the family will be created with the
+ same name than the family.
+
:param name: Name of the family (e.g. x86)
:param title: Optional title of the family
:param description: Optional extended description
@@ -906,11 +909,11 @@
description = "Description of the %s processor family" % name
if title is None:
title = "%s and compatible processors." % name
- family = getUtility(IProcessorFamilySet).new(name, title, description,
- restricted=restricted)
+ family = getUtility(IProcessorFamilySet).new(
+ name, title, description, restricted=restricted)
# Make sure there's at least one processor in the family, so that
# other things can have a default processor.
- self.makeProcessor(family=family)
+ self.makeProcessor(name=name, family=family)
return family
def makeProductRelease(self, milestone=None, product=None,
=== modified file 'lib/lp/testing/publication.py'
--- lib/lp/testing/publication.py 2010-10-03 15:30:06 +0000
+++ lib/lp/testing/publication.py 2011-07-08 19:23:25 +0000
@@ -20,6 +20,7 @@
from zope.interface import providedBy
from zope.publisher.interfaces.browser import IDefaultSkin
from zope.security.management import restoreInteraction
+from zope.security.proxy import removeSecurityProxy
from canonical.launchpad.interfaces.launchpad import IOpenLaunchBag
import canonical.launchpad.layers as layers
@@ -119,9 +120,10 @@
getUtility(IOpenLaunchBag).clear()
app = publication.getApplication(request)
view = request.traverse(app)
- # Since the last traversed object is the view, the second last should be
- # the object that the view is on.
- obj = request.traversed_objects[-2]
+ # Find the object from the view instead on relying that it stays
+ # in the traversed_objects stack. That doesn't apply to the web
+ # service for example.
+ obj = removeSecurityProxy(view).context
restoreInteraction()
=== modified file 'lib/lp/testing/tests/test_publication.py'
--- lib/lp/testing/tests/test_publication.py 2010-10-04 19:50:45 +0000
+++ lib/lp/testing/tests/test_publication.py 2011-07-08 19:23:25 +0000
@@ -23,6 +23,7 @@
from canonical.launchpad.webapp.publisher import get_current_browser_request
from canonical.launchpad.webapp.servers import LaunchpadTestRequest
from canonical.testing.layers import DatabaseFunctionalLayer
+from lazr.restful import EntryResource
from lp.testing import (
ANONYMOUS,
login,
@@ -47,6 +48,7 @@
name = '+' + self.factory.getUniqueString()
class new_class(simple):
def __init__(self, context, request):
+ self.context = context
view_callable()
required = {}
for n in ('browserDefault', '__call__', 'publishTraverse'):
@@ -102,3 +104,11 @@
self.registerViewCallable(record_user))
self.assertEqual(1, len(users))
self.assertEqual(person, users[0])
+
+ def test_webservice_traverse(self):
+ login(ANONYMOUS)
+ product = self.factory.makeProduct()
+ context, view, request = test_traverse(
+ 'http://api.launchpad.dev/devel/' + product.name)
+ self.assertEqual(product, context)
+ self.assertIsInstance(view, EntryResource)
Follow ups