Merge lp:~leonardr/launchpad/toggle-representation-cache into lp:launchpad

Proposed by Leonard Richardson
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 11011
Proposed branch: lp:~leonardr/launchpad/toggle-representation-cache
Merge into: lp:launchpad
Diff against target: 513 lines (+331/-9)
14 files modified
configs/development/launchpad-lazr.conf (+3/-0)
lib/canonical/config/schema-lazr.conf (+3/-0)
lib/canonical/database/sqlbase.py (+7/-0)
lib/canonical/launchpad/pagetests/webservice/cache.txt (+77/-0)
lib/canonical/launchpad/rest/configuration.py (+4/-0)
lib/canonical/launchpad/testing/pages.py (+12/-0)
lib/canonical/launchpad/zcml/webservice.zcml (+5/-0)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+2/-0)
lib/lp/services/memcache/doc/restful-cache.txt (+143/-0)
lib/lp/services/memcache/restful.py (+63/-0)
lib/lp/services/memcache/tests/test_doc.py (+8/-8)
lib/lp/soyuz/stories/webservice/xx-builds.txt (+1/-0)
lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt (+2/-0)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~leonardr/launchpad/toggle-representation-cache
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Graham Binns (community) code Approve
Paul Hummer (community) code Approve
Jelmer Vernooij (community) code Approve
Review via email: mp+26725@code.launchpad.net

Description of the change

This branch implements the enable_server_side_representation_cache lazr.restful configuration option using a Launchpad configuration option. That option is defined in https://code.edge.launchpad.net/~leonardr/lazr.restful/disable-cache/+merge/26713 and is still in review.

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

In lib/lp/services/memcache/doc/restful-cache.txt you're missing an asterisk on the first line.

It'd be nice to have docstrings for the methods in MemcachedStormRepresentationCache, even if they just say "See `BaseRepositoryCache`."

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

I need another review of the last couple revisions. While going through stub's review of my earlier branch, I remembered that we planned to set a default expiration time for items in the cache, so that when we tested on staging the cache wouldn't contain bad data indefinitely. I've implemented that code and a test, as well as responding to Jelmer's feedback and some more of Stuart's.

(Stuart's review is here: https://code.edge.launchpad.net/~leonardr/launchpad/test-representation-cache/+merge/26513)

I couldn't think of any way to test the expiration except from actually sleeping for 2 seconds, since I don't think we have a mocked-up memcached for use in testing. Suggestions are welcome.

Revision history for this message
Paul Hummer (rockstar) :
review: Approve (code)
Revision history for this message
Leonard Richardson (leonardr) wrote :

I need one more tiny review. I got a lots of test failures because memcached thinks space is a control character. So after creating the key, I replace any spaces with periods.

Revision history for this message
Graham Binns (gmb) :
review: Approve (code)
Revision history for this message
Leonard Richardson (leonardr) wrote :

I was still getting test failures, so I made a number of changes (which required a new version of lazr.restful: see https://code.edge.launchpad.net/~leonardr/lazr.restful/cache-unauthorized/+merge/27523). Here's a paste with the contents of the two commits I've made that weren't merges against trunk:

http://pastebin.ubuntu.com/449786/

Revision history for this message
Aaron Bentley (abentley) wrote :

As discussed in IRC
- Please catch storm.exceptions.ClassInfoError rather than Exception
- If possible, narrow the scope of the related try/except block.
- Please be more specific in applying defaults, so that cached values that evaluate to False are handled correctly.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2010-05-27 07:05:20 +0000
+++ configs/development/launchpad-lazr.conf 2010-06-15 13:39:43 +0000
@@ -275,6 +275,9 @@
275[vhost.api]275[vhost.api]
276hostname: api.launchpad.dev276hostname: api.launchpad.dev
277rooturl: https://api.launchpad.dev/277rooturl: https://api.launchpad.dev/
278# Turn this on once we've solved cache invalidation problems and are
279# ready to test.
280# enable_server_side_representation_cache: True
278281
279[vhost.blueprints]282[vhost.blueprints]
280hostname: blueprints.launchpad.dev283hostname: blueprints.launchpad.dev
281284
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2010-06-08 15:13:20 +0000
+++ lib/canonical/config/schema-lazr.conf 2010-06-15 13:39:43 +0000
@@ -1950,6 +1950,9 @@
1950[vhost.api]1950[vhost.api]
1951# This key should be removed once the production configs have been updated.1951# This key should be removed once the production configs have been updated.
1952beta_test_team: disabled1952beta_test_team: disabled
1953enable_server_side_representation_cache: False
1954# By default, cache representations for 4 hours.
1955representation_cache_expiration_time: 14400
19531956
1954[vhost.blueprints]1957[vhost.blueprints]
19551958
19561959
=== modified file 'lib/canonical/database/sqlbase.py'
--- lib/canonical/database/sqlbase.py 2010-03-24 17:37:26 +0000
+++ lib/canonical/database/sqlbase.py 2010-06-15 13:39:43 +0000
@@ -28,6 +28,8 @@
28from zope.interface import implements28from zope.interface import implements
29from zope.security.proxy import removeSecurityProxy29from zope.security.proxy import removeSecurityProxy
3030
31from lazr.restful.interfaces import IRepresentationCache
32
31from canonical.config import config, dbconfig33from canonical.config import config, dbconfig
32from canonical.database.interfaces import ISQLBase34from canonical.database.interfaces import ISQLBase
3335
@@ -246,6 +248,11 @@
246 """Inverse of __eq__."""248 """Inverse of __eq__."""
247 return not (self == other)249 return not (self == other)
248250
251 def __storm_flushed__(self):
252 """Invalidate the web service cache."""
253 cache = getUtility(IRepresentationCache)
254 cache.delete(self)
255
249alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "256alreadyInstalledMsg = ("A ZopelessTransactionManager with these settings is "
250"already installed. This is probably caused by calling initZopeless twice.")257"already installed. This is probably caused by calling initZopeless twice.")
251258
252259
=== added file 'lib/canonical/launchpad/pagetests/webservice/cache.txt'
--- lib/canonical/launchpad/pagetests/webservice/cache.txt 1970-01-01 00:00:00 +0000
+++ lib/canonical/launchpad/pagetests/webservice/cache.txt 2010-06-15 13:39:43 +0000
@@ -0,0 +1,77 @@
1************************
2The representation cache
3************************
4
5Launchpad stores JSON representations of objects in a memcached
6cache. The full cache functionality is tested in lazr.restful and in
7lib/lp/services/memcache/doc/restful-cache.txt. This is just a simple
8integration test.
9
10By default, the cache is disabled, even in the testrunner
11environment. (This will change once we improve the Launchpad cache's
12cache invalidation.) Let's enable the cache just for this test.
13
14 >>> from canonical.config import config
15 >>> config.vhost.api.enable_server_side_representation_cache
16 False
17 >>> config.push('enable cache', """\
18 ... [vhost.api]
19 ... enable_server_side_representation_cache: True
20 ... """)
21
22Now we need to get a reference to the cache object, so we can look
23inside.
24
25 >>> from zope.component import getUtility
26 >>> from lazr.restful.interfaces import IRepresentationCache
27 >>> cache = getUtility(IRepresentationCache)
28
29Since the cache is keyed by the underlying database object, we also
30need one of those objects.
31
32 >>> from lp.registry.interfaces.person import IPersonSet
33 >>> login(ANONYMOUS)
34 >>> person = getUtility(IPersonSet).getByName('salgado')
35 >>> key = cache.key_for(person, 'application/json', 'devel')
36 >>> logout()
37
38The cache starts out empty.
39
40 >>> print cache.get_by_key(key)
41 None
42
43Retrieving a representation of an object populates the cache.
44
45 >>> ignore = webservice.get("/~salgado", api_version="devel").jsonBody()
46
47 >>> cache.get_by_key(key)
48 '{...}'
49
50Once the cache is populated with a representation, the cached
51representation is used in preference to generating a new
52representation of that object. We can verify this by putting a fake
53value into the cache and retrieving a representation of the
54corresponding object.
55
56 >>> import simplejson
57 >>> cache.set_by_key(key, simplejson.dumps("Fake representation"))
58
59 >>> print webservice.get("/~salgado", api_version="devel").jsonBody()
60 Fake representation
61
62If there's a problem with the cache or the invalidation code, we can
63disable the cache by setting a configuration variable.
64
65Cleanup: re-disable the cache.
66
67 >>> ignore = config.pop('enable cache')
68
69Note that documents are never served from a disabled cache, even if the
70cache is populated.
71
72 >>> print webservice.get("/~salgado", api_version="devel").jsonBody()
73 {...}
74
75Cleanup: clear the cache.
76
77 >>> cache.delete(person)
078
=== modified file 'lib/canonical/launchpad/rest/configuration.py'
--- lib/canonical/launchpad/rest/configuration.py 2010-05-17 20:03:02 +0000
+++ lib/canonical/launchpad/rest/configuration.py 2010-06-15 13:39:43 +0000
@@ -63,6 +63,10 @@
63 return request63 return request
6464
65 @property65 @property
66 def enable_server_side_representation_cache(self):
67 return config.vhost.api.enable_server_side_representation_cache
68
69 @property
66 def default_batch_size(self):70 def default_batch_size(self):
67 return config.launchpad.default_batch_size71 return config.launchpad.default_batch_size
6872
6973
=== modified file 'lib/canonical/launchpad/testing/pages.py'
--- lib/canonical/launchpad/testing/pages.py 2010-03-30 09:40:48 +0000
+++ lib/canonical/launchpad/testing/pages.py 2010-06-15 13:39:43 +0000
@@ -40,6 +40,7 @@
40from canonical.launchpad.webapp.interfaces import OAuthPermission40from canonical.launchpad.webapp.interfaces import OAuthPermission
41from canonical.launchpad.webapp.url import urlsplit41from canonical.launchpad.webapp.url import urlsplit
42from canonical.testing import PageTestLayer42from canonical.testing import PageTestLayer
43from lazr.restful.interfaces import IRepresentationCache
43from lazr.restful.testing.webservice import WebServiceCaller44from lazr.restful.testing.webservice import WebServiceCaller
44from lp.testing import (45from lp.testing import (
45 ANONYMOUS, launchpadlib_for, login, login_person, logout)46 ANONYMOUS, launchpadlib_for, login, login_person, logout)
@@ -672,6 +673,16 @@
672 return LaunchpadWebServiceCaller(consumer_key, access_token.key)673 return LaunchpadWebServiceCaller(consumer_key, access_token.key)
673674
674675
676def ws_uncache(obj):
677 """Manually remove an object from the web service representation cache.
678
679 Directly modifying a data model object during a test may leave
680 invalid data in the representation cache.
681 """
682 cache = getUtility(IRepresentationCache)
683 cache.delete(obj)
684
685
675def setupDTCBrowser():686def setupDTCBrowser():
676 """Testbrowser configured for Distribution Translations Coordinators.687 """Testbrowser configured for Distribution Translations Coordinators.
677688
@@ -776,6 +787,7 @@
776 test.globs['print_tag_with_id'] = print_tag_with_id787 test.globs['print_tag_with_id'] = print_tag_with_id
777 test.globs['PageTestLayer'] = PageTestLayer788 test.globs['PageTestLayer'] = PageTestLayer
778 test.globs['stop'] = stop789 test.globs['stop'] = stop
790 test.globs['ws_uncache'] = ws_uncache
779791
780792
781class PageStoryTestCase(unittest.TestCase):793class PageStoryTestCase(unittest.TestCase):
782794
=== modified file 'lib/canonical/launchpad/zcml/webservice.zcml'
--- lib/canonical/launchpad/zcml/webservice.zcml 2010-03-26 19:11:50 +0000
+++ lib/canonical/launchpad/zcml/webservice.zcml 2010-06-15 13:39:43 +0000
@@ -16,6 +16,11 @@
16 provides="lazr.restful.interfaces.IWebServiceConfiguration">16 provides="lazr.restful.interfaces.IWebServiceConfiguration">
17 </utility>17 </utility>
1818
19 <utility
20 factory="lp.services.memcache.restful.MemcachedStormRepresentationCache"
21 provides="lazr.restful.interfaces.IRepresentationCache">
22 </utility>
23
19 <securedutility24 <securedutility
20 class="canonical.launchpad.systemhomes.WebServiceApplication"25 class="canonical.launchpad.systemhomes.WebServiceApplication"
21 provides="canonical.launchpad.interfaces.IWebServiceApplication">26 provides="canonical.launchpad.interfaces.IWebServiceApplication">
2227
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-06-11 18:05:59 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-06-15 13:39:43 +0000
@@ -1250,6 +1250,8 @@
12501250
1251 >>> logout()1251 >>> logout()
12521252
1253 >>> from lazr.restful.interfaces import IRepresentationCache
1254 >>> ws_uncache(mmm)
1253 >>> mmm = webservice.get("/mega-money-maker").jsonBody()1255 >>> mmm = webservice.get("/mega-money-maker").jsonBody()
1254 >>> print mmm['display_name']1256 >>> print mmm['display_name']
1255 Mega Money Maker1257 Mega Money Maker
12561258
=== added file 'lib/lp/services/memcache/doc/restful-cache.txt'
--- lib/lp/services/memcache/doc/restful-cache.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/doc/restful-cache.txt 2010-06-15 13:39:43 +0000
@@ -0,0 +1,143 @@
1****************************************
2The Storm/memcached representation cache
3****************************************
4
5The web service library lazr.restful will store the representations it
6generates in a cache, if a suitable cache implementation is
7provided. We implement a cache that stores representations of Storm
8objects in memcached.
9
10 >>> login('foo.bar@canonical.com')
11
12 >>> from lp.services.memcache.restful import (
13 ... MemcachedStormRepresentationCache)
14 >>> cache = MemcachedStormRepresentationCache()
15
16An object's cache key is derived from its Storm metadata: its database
17table name and its primary key.
18
19 >>> from zope.component import getUtility
20 >>> from lp.registry.interfaces.person import IPersonSet
21 >>> person = getUtility(IPersonSet).getByName('salgado')
22
23 >>> cache_key = cache.key_for(
24 ... person, 'media/type', 'web-service-version')
25 >>> print person.id, cache_key
26 29 Person(29,),testrunner,media/type,web-service-version
27
28 >>> from operator import attrgetter
29 >>> languages = sorted(person.languages, key=attrgetter('englishname'))
30 >>> for language in languages:
31 ... cache_key = cache.key_for(
32 ... language, 'media/type', 'web-service-version')
33 ... print language.id, cache_key
34 119 Language(119,),testrunner,media/type,web-service-version
35 521 Language(521,),testrunner,media/type,web-service-version
36
37The cache starts out empty.
38
39 >>> json_type = 'application/json'
40
41 >>> print cache.get(person, json_type, "v1", default="missing")
42 missing
43
44Add a representation to the cache, and you can retrieve it later.
45
46 >>> cache.set(person, json_type, "beta",
47 ... "This is a representation for version beta.")
48
49 >>> print cache.get(person, json_type, "beta")
50 This is a representation for version beta.
51
52If an object has no Storm metadata, it is currently not cached at all.
53
54 >>> from lp.hardwaredb.interfaces.hwdb import IHWDBApplication
55 >>> hwdb_app = getUtility(IHWDBApplication)
56 >>> cache.set(hwdb_app, 'media/type', 'web-service-version', 'data')
57 >>> print cache.get(hwdb_app, 'media/type', 'web-service-version')
58 None
59
60A single object can cache different representations for different
61web service versions.
62
63 >>> cache.set(person, json_type, '1.0',
64 ... 'This is a different representation for version 1.0.')
65
66 >>> print cache.get(person, json_type, "1.0")
67 This is a different representation for version 1.0.
68
69The web service version doesn't have to actually be defined in the
70configuration. (But you shouldn't use this--see below!)
71
72 >>> cache.set(person, json_type, 'no-such-version',
73 ... 'This is a representation for a nonexistent version.')
74
75 >>> print cache.get(person, json_type, "no-such-version")
76 This is a representation for a nonexistent version.
77
78A single object can also cache different representations for different
79media types, not just application/json. (But you shouldn't use
80this--see below!)
81
82 >>> cache.set(person, 'media/type', '1.0',
83 ... 'This is a representation for a strange media type.')
84
85 >>> print cache.get(person, "media/type", "1.0")
86 This is a representation for a strange media type.
87
88When a Launchpad object is modified, its JSON representations for
89recognized web service versions are automatically removed from the
90cache.
91
92 >>> person.addressline1 = "New address"
93 >>> from canonical.launchpad.ftests import syncUpdate
94 >>> syncUpdate(person)
95
96 >>> print cache.get(person, json_type, "beta", default="missing")
97 missing
98
99 >>> print cache.get(person, json_type, "1.0", default="missing")
100 missing
101
102But non-JSON representations, and representations for unrecognized web
103service versions, are _not_ removed from the cache. (This is why you
104shouldn't put such representations into the cache.)
105
106 >>> print cache.get(person, json_type, "no-such-version")
107 This is a representation for a nonexistent version.
108
109 >>> print cache.get(person, "media/type", "1.0")
110 This is a representation for a strange media type.
111
112Expiration time
113===============
114
115Objects will automatically be purged from the cache after a
116configurable time. The default is 4 hours (14400 seconds)
117
118 >>> from canonical.config import config
119 >>> print config.vhost.api.representation_cache_expiration_time
120 14400
121
122Let's change that.
123
124 >>> from canonical.config import config
125 >>> config.push('short expiration time', """\
126 ... [vhost.api]
127 ... representation_cache_expiration_time: 1
128 ... """)
129
130 >>> cache.set(person, "some/type", "version", "Some representation")
131 >>> print cache.get(person, "some/type", "version")
132 Some representation
133
134Now objects are purged from the cache after one second.
135
136 >>> import time
137 >>> time.sleep(2)
138 >>> print cache.get(person, "some/type", "version")
139 None
140
141Cleanup.
142
143 >>> ignore = config.pop("short expiration time")
0144
=== added file 'lib/lp/services/memcache/restful.py'
--- lib/lp/services/memcache/restful.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/memcache/restful.py 2010-06-15 13:39:43 +0000
@@ -0,0 +1,63 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Storm/memcached implementation of lazr.restful's representation cache."""
5
6import storm
7
8from zope.component import getUtility
9from zope.security.proxy import removeSecurityProxy
10from zope.traversing.browser import absoluteURL
11
12from canonical.config import config
13from lp.services.memcache.interfaces import IMemcacheClient
14from lazr.restful.simple import BaseRepresentationCache
15from lazr.restful.utils import get_current_web_service_request
16
17__metaclass__ = type
18__all__ = [
19 'MemcachedStormRepresentationCache',
20]
21
22
23class MemcachedStormRepresentationCache(BaseRepresentationCache):
24 """Caches lazr.restful representations of Storm objects in memcached."""
25
26 def __init__(self):
27 """Initialize with the memcached client."""
28 self.client = getUtility(IMemcacheClient)
29
30 def key_for(self, obj, media_type, version):
31 """See `BaseRepresentationCache`."""
32 obj = removeSecurityProxy(obj)
33 try:
34 storm_info = storm.info.get_obj_info(obj)
35 except storm.exceptions.ClassInfoError, e:
36 # There's no Storm data for this object. Don't cache it,
37 # since we don't know how to invalidate the cache.
38 return self.DO_NOT_CACHE
39 table_name = storm_info.cls_info.table
40 primary_key = tuple(var.get() for var in storm_info.primary_vars)
41 identifier = table_name + repr(primary_key)
42
43 key = (identifier
44 + ',' + config._instance_name
45 + ',' + media_type + ',' + str(version)).replace(' ', '.')
46 return key
47
48 def get_by_key(self, key, default=None):
49 """See `BaseRepresentationCache`."""
50 value = self.client.get(key)
51 if value is None:
52 value = default
53 return value
54
55 def set_by_key(self, key, value):
56 """See `BaseRepresentationCache`."""
57 self.client.set(
58 key, value,
59 time=config.vhost.api.representation_cache_expiration_time)
60
61 def delete_by_key(self, key):
62 """See `BaseRepresentationCache`."""
63 self.client.delete(key)
064
=== modified file 'lib/lp/services/memcache/tests/test_doc.py'
--- lib/lp/services/memcache/tests/test_doc.py 2010-03-03 11:00:42 +0000
+++ lib/lp/services/memcache/tests/test_doc.py 2010-06-15 13:39:43 +0000
@@ -7,9 +7,7 @@
77
8import os.path8import os.path
9from textwrap import dedent9from textwrap import dedent
10import unittest
1110
12from zope.component import getUtility
13import zope.pagetemplate.engine11import zope.pagetemplate.engine
14from zope.pagetemplate.pagetemplate import PageTemplate12from zope.pagetemplate.pagetemplate import PageTemplate
15from zope.publisher.browser import TestRequest13from zope.publisher.browser import TestRequest
@@ -17,9 +15,7 @@
17from canonical.launchpad.testing.systemdocs import (15from canonical.launchpad.testing.systemdocs import (
18 LayeredDocFileSuite, setUp, tearDown)16 LayeredDocFileSuite, setUp, tearDown)
19from canonical.testing.layers import LaunchpadFunctionalLayer, MemcachedLayer17from canonical.testing.layers import LaunchpadFunctionalLayer, MemcachedLayer
20from lp.services.memcache.interfaces import IMemcacheClient
21from lp.services.testing import build_test_suite18from lp.services.testing import build_test_suite
22from lp.testing import TestCase
2319
2420
25here = os.path.dirname(os.path.realpath(__file__))21here = os.path.dirname(os.path.realpath(__file__))
@@ -59,11 +55,15 @@
59 test.globs['MemcachedLayer'] = MemcachedLayer55 test.globs['MemcachedLayer'] = MemcachedLayer
6056
6157
58def suite_for_doctest(filename):
59 return LayeredDocFileSuite(
60 '../doc/%s' % filename,
61 setUp=memcacheSetUp, tearDown=tearDown,
62 layer=LaunchpadFunctionalLayer)
63
62special = {64special = {
63 'tales-cache.txt': LayeredDocFileSuite(65 'tales-cache.txt': suite_for_doctest('tales-cache.txt'),
64 '../doc/tales-cache.txt',66 'restful-cache.txt': suite_for_doctest('restful-cache.txt'),
65 setUp=memcacheSetUp, tearDown=tearDown,
66 layer=LaunchpadFunctionalLayer),
67 }67 }
6868
6969
7070
=== modified file 'lib/lp/soyuz/stories/webservice/xx-builds.txt'
--- lib/lp/soyuz/stories/webservice/xx-builds.txt 2010-05-17 11:30:31 +0000
+++ lib/lp/soyuz/stories/webservice/xx-builds.txt 2010-06-15 13:39:43 +0000
@@ -93,6 +93,7 @@
9393
94 >>> build = getUtility(IBinaryPackageBuildSet).getByBuildID(26)94 >>> build = getUtility(IBinaryPackageBuildSet).getByBuildID(26)
95 >>> build.upload_log = build.log95 >>> build.upload_log = build.log
96 >>> ws_uncache(build)
9697
97 >>> logout()98 >>> logout()
9899
99100
=== modified file 'lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt'
--- lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2010-02-26 14:49:00 +0000
+++ lib/lp/soyuz/stories/webservice/xx-source-package-publishing.txt 2010-06-15 13:39:43 +0000
@@ -152,6 +152,8 @@
152 ... pub.sourcepackagerelease.dscsigningkey = None152 ... pub.sourcepackagerelease.dscsigningkey = None
153 >>> logout()153 >>> logout()
154154
155 >>> ws_uncache(pub)
156
155Query the source again:157Query the source again:
156158
157 >>> pubs = webservice.named_get(159 >>> pubs = webservice.named_get(
158160
=== modified file 'versions.cfg'
--- versions.cfg 2010-06-09 19:45:18 +0000
+++ versions.cfg 2010-06-15 13:39:43 +0000
@@ -28,7 +28,7 @@
28lazr.delegates = 1.1.028lazr.delegates = 1.1.0
29lazr.enum = 1.1.229lazr.enum = 1.1.2
30lazr.lifecycle = 1.130lazr.lifecycle = 1.1
31lazr.restful = 0.9.2631lazr.restful = 0.9.29
32lazr.restfulclient = 0.9.1432lazr.restfulclient = 0.9.14
33lazr.smtptest = 1.133lazr.smtptest = 1.1
34lazr.testing = 0.1.134lazr.testing = 0.1.1