This is a web service.
It's got resources!
""" version_descriptions = { 'devel' : """The unstable development version.
Don't use this unless you like changing things.
""" } last_version_with_mutator_named_operations = None first_version_with_total_size_link = None use_https = False view_permission = 'lazr.restful.example.base.View' lazr.restful-0.19.3/src/lazr/restful/example/base/security.py 0000644 0001750 0001750 00000001707 11631755356 024471 0 ustar benji benji 0000000 0000000 # Copyright 2009 Canonical Ltd. All rights reserved. """A simple security policy for the LAZR example web service.""" __metaclass__ = type __all__ = [ 'CookbookWebServiceSecurityPolicy', ] from zope.security.simplepolicies import PermissiveSecurityPolicy from zope.security.proxy import removeSecurityProxy from lazr.restful.example.base.interfaces import ICookbook, IRecipe class CookbookWebServiceSecurityPolicy(PermissiveSecurityPolicy): """A very basic security policy.""" def checkPermission(self, permission, object): """Check against a simple policy. * Private recipes are always hidden. * Any fields protected by lazr.restful.example.base.ViewPrivate are hidden. """ if IRecipe.providedBy(object): return not removeSecurityProxy(object).private elif permission == "lazr.restful.example.base.ViewPrivate": return False else: return True lazr.restful-0.19.3/src/lazr/restful/example/base/__init__.py 0000644 0001750 0001750 00000000000 11631755356 024342 0 ustar benji benji 0000000 0000000 lazr.restful-0.19.3/src/lazr/restful/example/base/traversal.py 0000644 0001750 0001750 00000005525 11631755356 024627 0 ustar benji benji 0000000 0000000 # Copyright 2009 Canonical Ltd. All rights reserved. """Traversal rules for the LAZR example web service.""" __metaclass__ = type __all__ = [ 'BelowRootAbsoluteURL', 'CookbookSetTraverse', 'RootAbsoluteURL', 'TraverseWithGet', ] from urllib import unquote from zope.component import adapts from zope.interface import implements from zope.publisher.interfaces import IPublishTraverse, NotFound from zope.publisher.interfaces.browser import IDefaultBrowserLayer from zope.traversing.browser import absoluteURL, AbsoluteURL import grokcore.component from lazr.restful.example.base.interfaces import ICookbookSet, IHasGet from lazr.restful.example.base.root import CookbookWebServiceObject from lazr.restful.simple import ( RootResourceAbsoluteURL, SimulatedWebsiteRequest, ) from lazr.restful import RedirectResource def web_service_request_to_web_site_request(web_service_request): """Create a simulated website request for generating web_link.""" body = web_service_request.bodyStream.getCacheStream() environ = dict(web_service_request.environment) return SimulatedWebsiteRequest(body, environ) class RootAbsoluteURL(RootResourceAbsoluteURL): """A technique for generating the service's root URL. This class contains no code of its own. It's defined so that grok will pick it up. """ class BelowRootAbsoluteURL(AbsoluteURL): """A technique for generating a root URL given an ILocation. This class contains no code of its own. It's defined so that grok will pick it up. """ class TraverseWithGet(grokcore.component.MultiAdapter): """A simple IPublishTraverse that uses the get() method.""" grokcore.component.adapts(IHasGet, IDefaultBrowserLayer) grokcore.component.implements(IPublishTraverse) def __init__(self, context, request): self.context = context def publishTraverse(self, request, name): name = unquote(name) value = self.context.get(name) if value is None: raise NotFound(self, name) return value class CookbookSetTraverse(TraverseWithGet): """An IPublishTraverse that implements a custom redirect.""" grokcore.component.adapts(ICookbookSet, IDefaultBrowserLayer) grokcore.component.implements(IPublishTraverse) def publishTraverse(self, request, name): if name == 'featured': url = absoluteURL(self.context.featured, request) return RedirectResource(url, request) elif name == 'featured-invalid': # This is to enable a test case in which the redirect URL # contains characters not valid in URLs. url = absoluteURL(self.context.featured, request) + "{invalid}" return RedirectResource(url, request) else: return super(CookbookSetTraverse, self).publishTraverse( request, name) lazr.restful-0.19.3/src/lazr/restful/example/multiversion/ 0000755 0001750 0001750 00000000000 11636155340 024061 5 ustar benji benji 0000000 0000000 lazr.restful-0.19.3/src/lazr/restful/example/multiversion/tests/ 0000755 0001750 0001750 00000000000 11636155340 025223 5 ustar benji benji 0000000 0000000 lazr.restful-0.19.3/src/lazr/restful/example/multiversion/tests/test_integration.py 0000644 0001750 0001750 00000002777 11631755356 031204 0 ustar benji benji 0000000 0000000 # Copyright 2008 Canonical Ltd. All rights reserved. """Test harness for doctests for lazr.restful multiversion example service.""" __metaclass__ = type __all__ = [] import os import doctest from pkg_resources import resource_filename from zope.component import getUtility from van.testing.layer import zcml_layer, wsgi_intercept_layer from lazr.restful.example.multiversion.root import ( MultiversionWebServiceRootResource) from lazr.restful.interfaces import IWebServiceConfiguration from lazr.restful.simple import Publication from lazr.restful.testing.webservice import WebServiceApplication DOCTEST_FLAGS = ( doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF) class FunctionalLayer: zcml = os.path.abspath(resource_filename( 'lazr.restful.example.multiversion', 'site.zcml')) zcml_layer(FunctionalLayer) class WSGILayer(FunctionalLayer): @classmethod def make_application(self): getUtility(IWebServiceConfiguration).hostname = "multiversion.dev" getUtility(IWebServiceConfiguration).port = None root = MultiversionWebServiceRootResource() return WebServiceApplication(root, Publication) wsgi_intercept_layer(WSGILayer) def additional_tests(): """See `zope.testing.testrunner`.""" tests = sorted( [name for name in os.listdir(os.path.dirname(__file__)) if name.endswith('.txt')]) suite = doctest.DocFileSuite(optionflags=DOCTEST_FLAGS, *tests) suite.layer = WSGILayer return suite lazr.restful-0.19.3/src/lazr/restful/example/multiversion/tests/operation.txt 0000644 0001750 0001750 00000011663 11631755356 030003 0 ustar benji benji 0000000 0000000 **************** Named operations **************** Named operations have some special features that are too obscure to mention in the introductory doctest. total_size versus total_size_link --------------------------------- In old versions of lazr.restful, named operations that return collections always send a 'total_size' containing the total size of a collection. >>> from lazr.restful.testing.webservice import WebServiceCaller >>> webservice = WebServiceCaller(domain='multiversion.dev') In the example web service, named operations always send 'total_size' up to version '2.0'. >>> from zope.component import getUtility >>> from lazr.restful.interfaces import IWebServiceConfiguration >>> config = getUtility(IWebServiceConfiguration) >>> print config.first_version_with_total_size_link 2.0 When the 'byValue' operation is invoked in version 1.0, it always returns a total_size. >>> def get_collection(version, op='byValue', value="bar", size=2): ... url = '/pairs?ws.op=%s&value=%s&ws.size=%s' % (op, value, size) ... return webservice.get(url, api_version=version).jsonBody() >>> print sorted(get_collection('1.0').keys()) [u'entries', u'next_collection_link', u'start', u'total_size'] The operation itself doesn't change between 1.0 and 2.0, but in version 2.0, the operation starts returning total_size_link. >>> print sorted(get_collection('2.0').keys()) [u'entries', u'next_collection_link', u'start', u'total_size_link'] The same happens in 3.0. >>> print sorted(get_collection('3.0', 'by_value').keys()) [u'entries', u'next_collection_link', u'start', u'total_size_link'] However, if the total size is easy to calculate (for instance, because all the results fit on one page), a total_size is returned instead of total_size_link. >>> print sorted(get_collection('3.0', 'by_value', size=100).keys()) [u'entries', u'start', u'total_size'] >>> print get_collection('3.0', 'by_value', 'no-such-value') {u'total_size': 0, u'start': 0, u'entries': []} Mutators as named operations ---------------------------- Mutator methods (methods invoked when a field is modified) are annotated the same way as named operations, and in old versions of lazr.restful, they were actually published as named operations. In the example web service, mutator methods are published as named operations in the 'beta' and '1.0' versions. >>> print config.last_version_with_mutator_named_operations 1.0 If you look at the definition of IKeyValuePair in lazr.restful.example.multiversion.resources, you'll see that the 'a_comment' field has two different mutators at different times. Sometimes there is no mutator, sometimes the mutator is 'comment_mutator_1', and sometimes the mutator is 'comment_mutator_2'. Before version '2.0', mutators are published as named operations; in version '2.0' and afterwards, they are not. This helper method makes it easy to invoke a given mutator in a given version. >>> def call_mutator(version, op="comment_mutator_1"): ... url = '/pairs/foo' ... entity_body = "ws.op=%s&comment=value" % op ... body = webservice.post( ... url, 'application/x-www-form-urlencoded', entity_body, ... api_version=version) ... return body In version 'beta', the 'a_comment' field has no mutator at all. >>> print call_mutator("beta") HTTP/1.1 405 Method Not Allowed ... >>> print call_mutator("beta", op="comment_mutator_2") HTTP/1.1 405 Method Not Allowed ... In version '1.0', the 'comment_mutator_1' method is the mutator for 'a_comment'. Because mutators are published as named operation in version '1.0', we can also invoke the mutator method directly. >>> print call_mutator("1.0", "comment_mutator_1") HTTP/1.1 200 Ok ... 'comment_mutator_2' still doesn't work, because it's not the mutator for 'a_comment' in version '1.0'. >>> print call_mutator("1.0", "comment_mutator_2") HTTP/1.1 400 Bad Request ... Note that the response code is 400 ("Bad Request"), not 405 ("Method Not Allowed"). 405 means that the resource publishes no named POST operations at all. This resource does post one named POST operation ('comment_mutator_1'), so 405 isn't appropriate. In version '2.0', the 'comment_mutator_1' method is still the mutator for 'a_comment', but mutators are no longer published as named operations, so we can no longer invoke the mutator method directly. >>> print call_mutator("2.0") HTTP/1.1 405 Method Not Allowed ... Note that we're back to a 405 response code. That mutator was the only named POST operation on the key-value object, and now it's no longer published as a named operation. In version '3.0', the 'comment_mutator_2' method becomes the mutator for 'a_comment'. But since version '3.0' is after version '1.0', that method is not published as a named operation. >>> print call_mutator("3.0", "comment_mutator_2") HTTP/1.1 405 Method Not Allowed ... lazr.restful-0.19.3/src/lazr/restful/example/multiversion/tests/introduction.txt 0000644 0001750 0001750 00000022244 11631755356 030521 0 ustar benji benji 0000000 0000000 Multi-version web services ************************** lazr.restful lets you publish two or more mutually incompatible web services from the same underlying code. This lets you improve your web service to take advantage of new features of lazr.restful, without sacrificing backwards compatibility. The web service in example/multiversion illustrates the multiversion features of lazr.restful. >>> from lazr.restful.testing.webservice import WebServiceCaller >>> webservice = WebServiceCaller(domain='multiversion.dev') The multiversion web service serves four named versions of the same web service: "beta", "1.0", "2.0", and "3.0". Once you make a request to the service root of a particular version, the web service only serves you links within that version. >>> top_level_response = webservice.get( ... "/", api_version="beta").jsonBody() >>> print top_level_response['key_value_pairs_collection_link'] http://multiversion.dev/beta/pairs >>> top_level_response = webservice.get( ... "/", api_version="1.0").jsonBody() >>> print top_level_response['key_value_pairs_collection_link'] http://multiversion.dev/1.0/pairs >>> top_level_response = webservice.get( ... "/", api_version="2.0").jsonBody() >>> print top_level_response['key_value_pairs_collection_link'] http://multiversion.dev/2.0/pairs >>> top_level_response = webservice.get( ... "/", api_version="3.0").jsonBody() >>> print top_level_response['key_value_pairs_collection_link'] http://multiversion.dev/3.0/pairs Like all web services, the multiversion service also serves a development version which tracks the current state of the web service, including all changes that have not yet been folded into a named version. The default name for the development version is "devel" (see example/base/tests/service.txt), but in this web service it's called "trunk". >>> top_level_response = webservice.get( ... "/", api_version="trunk").jsonBody() >>> print top_level_response['key_value_pairs_collection_link'] http://multiversion.dev/trunk/pairs All versions of the web service can be accessed through Ajax. >>> from lazr.restful.testing.webservice import WebServiceAjaxCaller >>> ajax = WebServiceAjaxCaller(domain='multiversion.dev') >>> body = ajax.get('/', api_version="1.0").jsonBody() >>> print body['resource_type_link'] http://multiversion.dev/1.0/#service-root >>> body = ajax.get('/', api_version="trunk").jsonBody() >>> print body['resource_type_link'] http://multiversion.dev/trunk/#service-root An attempt to access a nonexistent version yields a 404 error. >>> print webservice.get('/', api_version="no_such_version") HTTP/1.1 404 Not Found ... Collections =========== The web service presents a single collection of key-value pairs. In versions previous to 2.0, the collection omits key-value pairs where the value is None. In 2.0 and 3.0, all key-value pairs are published. >>> from operator import itemgetter >>> def show_pairs(version): ... body = webservice.get('/pairs', api_version=version).jsonBody() ... for entry in sorted(body['entries'], key=itemgetter('key')): ... print "%s: %s" % (entry['key'], entry['value']) >>> show_pairs('beta') 1: 2 Also delete: me Delete: me foo: bar foo2: bar foo3: bar >>> show_pairs('1.0') 1: 2 Also delete: me Delete: me foo: bar foo2: bar foo3: bar >>> show_pairs('2.0') 1: 2 Also delete: me Delete: me Some: None foo: bar foo2: bar foo3: bar >>> show_pairs('3.0') 1: 2 Also delete: me Delete: me Some: None foo: bar foo2: bar foo3: bar Entries ======= Let's take a look at 'comment' and 'deleted', two fields with interesting properties. The 'comment' field is not modified directly, but by internal mutator methods which append some useless text to your comment. >>> import simplejson >>> def get_comment(version): ... response = webservice.get("/pairs/foo", api_version=version) ... return response.jsonBody()['comment'] >>> def change_comment(comment, version, get_comment_afterwards=True): ... ignored = webservice.patch( ... "/pairs/foo/", 'application/json', ... simplejson.dumps({"comment": comment}), ... api_version=version) ... if get_comment_afterwards: ... return get_comment(version) ... return None >>> get_comment('1.0') u'' >>> print change_comment('I changed 1.0', '1.0') I changed 1.0 (modified by mutator #1) >>> print change_comment('I changed 2.0', '2.0') I changed 2.0 (modified by mutator #1) >>> print change_comment('I changed 3.0', '3.0') I changed 3.0 (modified by mutator #2) You can try to modify the 'comment' field from a version that doesn't publish that field, but lazr.restful will ignore your request. >>> change_comment('I changed beta', 'beta', False) >>> print get_comment('1.0') I changed 3.0 (modified by mutator #2) A field called 'deleted' is published starting in version '3.0'. A comment field is called 'a_comment' in version 'beta' and 'comment' in all later versions. >>> def show_fields(version): ... entry_body = webservice.get( ... '/pairs/foo', api_version=version).jsonBody() ... for key in sorted(entry_body.keys()): ... print key >>> show_fields('beta') a_comment http_etag key resource_type_link self_link value >>> show_fields('1.0') comment http_etag key resource_type_link self_link value >>> show_fields('3.0') comment deleted http_etag key resource_type_link self_link value In the 'beta' version, attempting to delete a key-value pair will result in a status code of 405 ("Method Not Available"). >>> response = webservice.delete('/pairs/Delete', api_version='beta') >>> response.status 405 As of '1.0', attempting to delete a key-value pair results in the key-value pair being totally removed from the web service. >>> ignore = webservice.delete('/pairs/Delete', api_version='1.0') >>> show_pairs('beta') 1: 2 Also delete: me foo: bar foo2: bar foo3: bar In '3.0', deleting a key-value pair simply sets its 'deleted' field to True. (This is an abuse of the HTTP DELETE method, but it makes a good demo.) >>> body = webservice.get( ... '/pairs/Also%20delete', api_version='3.0').jsonBody() >>> body['deleted'] False >>> ignore = webservice.delete('/pairs/Also%20delete', api_version='3.0') The "deleted" key-value pair is still visible in all versions: >>> show_pairs('beta') 1: 2 Also delete: me foo: bar foo2: bar foo3: bar And in a version which publishes the 'delete' field, we can check the key-value pair's value for that field. >>> body = webservice.get( ... '/pairs/Also%20delete', api_version='3.0').jsonBody() >>> body['deleted'] True Fields ====== If an entry field is not published in a certain version, the corresponding field resource does not exist for that version. >>> print webservice.get('/pairs/foo/deleted', api_version='beta').body Object: <...>, name: u'deleted' >>> print webservice.get( ... '/pairs/foo/deleted', api_version='3.0').jsonBody() False Named operations ================ The collection of key-value pairs defines a named operation for finding pairs, given a value. This operation is present in some versions of the web service but not others. In some versions it's called "byValue"; in others, it's called "by_value". >>> def show_value(version, op): ... url = '/pairs?ws.op=%s&value=bar' % op ... body = webservice.get(url, api_version=version).jsonBody() ... return body['entries'][0]['key'] The named operation is not published at all in the 'beta' version of the web service. >>> print show_value("beta", 'byValue') Traceback (most recent call last): ... ValueError: No such operation: byValue >>> print show_value("beta", 'by_value') Traceback (most recent call last): ... ValueError: No such operation: by_value In the '1.0' and '2.0' versions, the named operation is published as 'byValue'. 'by_value' does not work. >>> print show_value("1.0", 'byValue') foo >>> print show_value("2.0", 'byValue') foo >>> print show_value("2.0", 'by_value') Traceback (most recent call last): ... ValueError: No such operation: by_value In the '3.0' version, the named operation is published as 'by_value'. 'byValue' does not work. >>> print show_value("3.0", "by_value") foo >>> print show_value("3.0", 'byValue') Traceback (most recent call last): ... ValueError: No such operation: byValue In the 'trunk' version, the named operation has been removed. Neither 'byValue' nor 'by_value' work. >>> print show_value("trunk", 'byValue') Traceback (most recent call last): ... ValueError: No such operation: byValue >>> print show_value("trunk", 'by_value') Traceback (most recent call last): ... ValueError: No such operation: by_value lazr.restful-0.19.3/src/lazr/restful/example/multiversion/tests/wadl.txt 0000644 0001750 0001750 00000011023 11631755356 026720 0 ustar benji benji 0000000 0000000 Multi-version WADL documents **************************** A given version of the web service generates a WADL document which describes that version only. Let's go through the WADL documents for the different versions and see how they differ. >>> from lazr.restful.testing.webservice import WebServiceCaller >>> webservice = WebServiceCaller(domain='multiversion.dev') We'll start with a helper function that retrieves the WADL description for a given version of the key-value web service, and decomposes the top-level tags in the WADL document into a dictionary for easy access later. This works because all versions of the web service publish a single top-level collection and a single entry type, so the document's top-level structure is always the same. >>> from lxml import etree >>> from lxml.etree import _Comment >>> def wadl_contents_for_version(version): ... """Parse the key-value service's WADL into a dictionary.""" ... wadl = webservice.get( ... '/', media_type='application/vnd.sun.wadl+xml', ... api_version=version).body ... tree = etree.fromstring(wadl) ... ... keys = ("service_doc version_doc base service_root " ... "service_root_json pair_collection pair_entry" ... "pair_full_json pair_diff_jaon pair_page pair_page_json" ... "hosted_file hosted_file_representation" ... ).split() ... ... tags = [child for child in tree if not isinstance(child, _Comment)] ... contents = {} ... for i in range(0, len(keys)): ... contents[keys[i]] = tags[i] ... return contents Let's take a look at the differences. In 'beta', the 'by_value' method is not present at all. >>> contents = wadl_contents_for_version('beta') >>> print contents['version_doc'].attrib['title'] About version beta >>> print contents['base'].attrib['base'] http://multiversion.dev/beta/ >>> pair_collection = contents['pair_collection'] >>> sorted([method.attrib['id'] for method in pair_collection]) ['key_value_pairs-get'] As a side note, see that the service documentation and version documentation tags are empty, because this service's configuration doesn't specify that information: >>> len(list(contents['service_doc'])) 0 >>> len(list(contents['version_doc'])) 0 In '2.0', the by_value method is called 'byValue'. >>> contents = wadl_contents_for_version('2.0') >>> print contents['version_doc'].attrib['title'] About version 2.0 >>> print contents['base'].attrib['base'] http://multiversion.dev/2.0/ >>> pair_collection = contents['pair_collection'] >>> sorted([method.attrib['id'] for method in pair_collection]) ['key_value_pairs-byValue', 'key_value_pairs-get'] In '3.0', the method changes its name to 'by_value'. >>> contents = wadl_contents_for_version('3.0') >>> print contents['version_doc'].attrib['title'] About version 3.0 >>> print contents['base'].attrib['base'] http://multiversion.dev/3.0/ >>> pair_collection = contents['pair_collection'] >>> sorted([method.attrib['id'] for method in pair_collection]) ['key_value_pairs-by_value', 'key_value_pairs-get'] In 'trunk', the method disappears. >>> contents = wadl_contents_for_version('trunk') >>> print contents['base'].attrib['base'] http://multiversion.dev/trunk/ >>> pair_collection = contents['pair_collection'] >>> sorted([method.attrib['id'] for method in pair_collection]) ['key_value_pairs-get'] total_size_link =============== The version in which total_size_link is introduced is controlled by the first_version_with_total_size_link attribute of the web service configuration (IWebServiceConfiguration) utility. We'll configure the web service to begin including `total_size_link` values in version 3.0: >>> from zope.component import getUtility >>> from lazr.restful.interfaces import IWebServiceConfiguration >>> config = getUtility(IWebServiceConfiguration) >>> config.first_version_with_total_size_link = '3.0' Now if we request the WADL for 3.0 it will include a description of total_size_link. >>> webservice.get('/', media_type='application/vnd.sun.wadl+xml', ... api_version='3.0').body '...A simple, reusable entry interface for use in tests.
', 'The entry publishes one field and one named operation.
', '', 'A "field"
', 'The only field that can be <> 0 in the entry.
', '', 'Print an appropriate greeting based on the message.
',], doclines[0:2]) self.assertEquals('