[Checkins] SVN: grok/trunk/ Merge the faassen-rest branch. see doc/minitutorials/rest.txt for

Martijn Faassen faassen at infrae.com
Fri Oct 26 10:17:35 EDT 2007


Log message for revision 81122:
  Merge the faassen-rest branch. see doc/minitutorials/rest.txt for
  usage information.
  

Changed:
  U   grok/trunk/CHANGES.txt
  A   grok/trunk/doc/minitutorials/rest.txt
  U   grok/trunk/src/grok/__init__.py
  U   grok/trunk/src/grok/components.py
  U   grok/trunk/src/grok/configure.zcml
  A   grok/trunk/src/grok/ftests/rest/
  U   grok/trunk/src/grok/ftests/test_grok_functional.py
  U   grok/trunk/src/grok/interfaces.py
  U   grok/trunk/src/grok/meta.py
  U   grok/trunk/src/grok/publication.py
  A   grok/trunk/src/grok/rest.py

-=-
Modified: grok/trunk/CHANGES.txt
===================================================================
--- grok/trunk/CHANGES.txt	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/CHANGES.txt	2007-10-26 14:17:35 UTC (rev 81122)
@@ -20,6 +20,9 @@
   special name 'fields'. This was abandoned in favor the usual Zope 3 way of
   defining schemas in interfaces and implementing them in our Grok models.
 
+* Integrated REST support. See doc/minitutorials/rest.txt for usage
+  information.
+
 Bug fixes
 ---------
 

Added: grok/trunk/doc/minitutorials/rest.txt
===================================================================
--- grok/trunk/doc/minitutorials/rest.txt	                        (rev 0)
+++ grok/trunk/doc/minitutorials/rest.txt	2007-10-26 14:17:35 UTC (rev 81122)
@@ -0,0 +1,92 @@
+REST support in Grok
+====================
+
+:Author: Martijn Faassen
+
+REST_ is a way to build web services, i.e. a web application where the
+user is another computer, not a human being. REST takes the approach
+to make the web service look very similar to a normal web application,
+using well-known semantics of HTTP.
+
+.. _REST: http://en.wikipedia.org/wiki/Representational_State_Transfer
+
+Grok has support that helps you implement REST-based protocols. That
+is, Grok doesn't actually implement any RESTful protocols itself, but
+it allows you to easily add them in your own application.
+
+To implement a REST protocol, you do something very similar to
+implementing a skin. This way, REST requests are separated from other
+requests on objects. This means you can have a normal web UI with
+views on a set of objects in parallel to the implementation of one or
+more REST protocols.
+
+Let's see how you define a REST protocol. Similar to the way skins
+work, first you need to define a layer. In the case of REST, your
+layer must derive from grok.IRESTLayer::
+
+  class AtomPubLayer(grok.IRESTLayer):
+     pass
+
+REST handlers are very much like views like JSON or XMLRPC views. In
+the case of REST, you implement the HTTP methods on the view::
+
+  class MyREST(grok.REST):
+      grok.context(MyContainer)
+
+    def GET(self):
+        return "GET request, retrieve container listing"
+
+    def POST(self):
+        return "POST request, add something to container"
+
+    def PUT(self):
+        return "PUT request, replace complete contents"
+
+    def DELETE(self):
+        return "DELETE request, delete this object entirely"
+
+When handling a REST request, you often want to get to the raw body of
+the request. You can access a special ``body`` attributre that contains
+the body as a string::
+
+  class MyREST2(grok.REST):
+      def POST(self):
+          return "This is the body: " + self.body
+
+This body should be parsed accordingly by your REST protocol
+implementation - it could for instance be some form of XML or JSON.
+
+To actually issue REST requests over a URL, you need to define a REST
+protocol that uses this layer::
+
+  class AtomPubProtocol(grok.RESTProtocol):
+     grok.layer(AtomPubLayer)
+     grok.name('atompub') # a nicer name
+
+Again this is very similar to the way skins work - in order to use a
+layer you need to define a ``grok.Skin`` first.
+
+Now you can access the object with the REST protocol, through requests
+like this (issuing GET, POST, PUT or DELETE)::
+
+  http://localhost:8080/++rest++atompub/mycontainer
+
+As you can see, you need to use the ++rest++<protocolname> pattern
+somewhere in the URL in order to access the REST view for your
+objects. If you don't like the ++rest++ bit you can also provide
+(``directlyProvides``) the layer manually to the request during traversal,
+or if you're using Apache, use a few rewrite rules. (just like with
+skins).
+
+Using protocols like this means you could have a single object
+implement several different REST protocols. Since layers are used, you
+could also compose a single REST protocols out of multiple protocols
+should you so desire.
+
+If you don't explicitly set a layer using ``grok.layer`` for a REST
+subclass, it'll use the grok.IRESTLayer by default. This layer is the
+base of all REST layers.
+
+Similar again to XMLRPC or JSON views, security works with all this:
+you can use @grok.require() on the REST methods to shield them from
+public use.

Modified: grok/trunk/src/grok/__init__.py
===================================================================
--- grok/trunk/src/grok/__init__.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/__init__.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -30,13 +30,15 @@
     IContainerModifiedEvent, ContainerModifiedEvent)
 
 from martian import ClassGrokker, InstanceGrokker, GlobalGrokker
-from grok.components import Model, Adapter, MultiAdapter, View, XMLRPC, JSON
+from grok.components import Model, Adapter, MultiAdapter, View
+from grok.components import XMLRPC, REST, JSON
 from grok.components import PageTemplate, PageTemplateFile, Container, Traverser
 from grok.components import Site, GlobalUtility, LocalUtility, Annotation
 from grok.components import Application, Form, AddForm, EditForm, DisplayForm
 from grok.components import Indexes
 from grok.components import Permission, Role
 from grok.components import Skin, IGrokLayer
+from grok.components import RESTProtocol, IRESTLayer
 from grok.directive import (context, name, title, template, templatedir,
                             provides, baseclass, global_utility, local_utility,
                             permissions, require, site, layer)

Modified: grok/trunk/src/grok/components.py
===================================================================
--- grok/trunk/src/grok/components.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/components.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -44,6 +44,7 @@
 from zope.app.container.contained import Contained
 from zope.app.container.interfaces import IReadContainer
 from zope.app.component.site import SiteManagerContainer
+from zope.app.publication.http import MethodNotAllowed
 
 import z3c.flashmessage.interfaces
 
@@ -177,6 +178,29 @@
     pass
 
 
+class GrokMethodNotAllowed(MethodNotAllowed):
+    pass
+
+class REST(object):
+    interface.implements(interfaces.IREST)
+    
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        self.body = request.bodyStream.getCacheStream().read()
+    
+    def GET(self):
+        raise GrokMethodNotAllowed(self.context, self.request)
+    
+    def POST(self):
+        raise GrokMethodNotAllowed(self.context, self.request)
+    
+    def PUT(self):
+        raise GrokMethodNotAllowed(self.context, self.request)
+    
+    def DELETE(self):
+        raise GrokMethodNotAllowed(self.context, self.request)
+
 class JSON(BrowserPage):
 
     def __call__(self):
@@ -264,6 +288,14 @@
         self.request = request
 
     def browserDefault(self, request):
+        # if we have a RESTful request, we will handle
+        # GET, POST and HEAD differently (PUT and DELETE are handled already
+        # but not on the BrowserRequest layer but the HTTPRequest layer)
+        if IRESTLayer.providedBy(request):
+            rest_view = component.getMultiAdapter(
+                (self.context, self.request),
+                name=request.method)
+            return rest_view, ()
         view_name = getDefaultViewName(self.context, request)
         view_uri = "@@%s" % view_name
         return self.context, (view_uri,)
@@ -478,5 +510,11 @@
 class IGrokLayer(interface.Interface):
     pass
 
+class IRESTLayer(interface.Interface):
+    pass
+
 class Skin(object):
     pass
+
+class RESTProtocol(object):
+    pass

Modified: grok/trunk/src/grok/configure.zcml
===================================================================
--- grok/trunk/src/grok/configure.zcml	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/configure.zcml	2007-10-26 14:17:35 UTC (rev 81122)
@@ -28,6 +28,13 @@
   <securityPolicy
       component="zope.app.securitypolicy.zopepolicy.ZopeSecurityPolicy" />
 
+  <!-- we register a ++rest++ traversal namespace -->
+  <view
+    name="rest" type="zope.publisher.interfaces.browser.IHTTPRequest"
+    provides="zope.traversing.interfaces.ITraversable" for="*"
+    factory=".rest.rest_skin"
+    />
+
   <!-- this overrides Zope 3's publication factories because they have
        the same name; we also need to change the priority because of
        the ZCML discriminator -->
@@ -47,6 +54,17 @@
       priority="11"
       />
 
+  <publisher
+      name="HTTP"
+      factory=".publication.GrokHTTPFactory"
+      methods="*"
+      mimetypes="*"
+      priority="1"
+      />
+
+  <!-- need to grok this for some basic REST support -->
+  <grok:grok package=".rest" />
+
   <grok:grok package=".admin" />
 
 </configure>

Copied: grok/trunk/src/grok/ftests/rest (from rev 80892, grok/branches/faassen-rest/src/grok/ftests/rest)

Modified: grok/trunk/src/grok/ftests/test_grok_functional.py
===================================================================
--- grok/trunk/src/grok/ftests/test_grok_functional.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/ftests/test_grok_functional.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -22,6 +22,25 @@
     (re.compile(r'httperror_seek_wrapper:', re.M), 'HTTPError:'),
     ])
 
+def http_call(method, path, data=None, **kw):
+    """Function to help make RESTful calls.
+
+    method - HTTP method to use
+    path - testbrowser style path
+    data - (body) data to submit
+    kw - any request parameters
+    """
+    
+    if path.startswith('http://localhost'):
+        path = path[len('http://localhost'):]
+    request_string = '%s %s HTTP/1.1\n' % (method, path)
+    for key, value in kw.items():
+        request_string += '%s: %s\n' % (key, value)
+    if data is not None:
+        request_string += '\r\n'
+        request_string += data
+    return HTTPCaller()(request_string, handle_errors=False)
+
 def suiteFromPackage(name):
     files = resource_listdir(__name__, name)
     suite = unittest.TestSuite()
@@ -36,6 +55,7 @@
             dottedname, setUp=setUp, tearDown=tearDown,
             checker=checker,
             extraglobs=dict(http=HTTPCaller(),
+                            http_call=http_call,
                             getRootFolder=getRootFolder,
                             sync=sync),
             optionflags=(doctest.ELLIPSIS+
@@ -49,8 +69,8 @@
 
 def test_suite():
     suite = unittest.TestSuite()
-    for name in ['view', 'staticdir', 'xmlrpc', 'traversal', 'form', 'url',
-                 'security', 'utility', 'catalog', 'admin']:
+    for name in ['view', 'staticdir', 'xmlrpc', 'rest', 'traversal',
+                 'form', 'url', 'security', 'utility', 'catalog', 'admin']:
         suite.addTest(suiteFromPackage(name))
     return suite
 

Modified: grok/trunk/src/grok/interfaces.py
===================================================================
--- grok/trunk/src/grok/interfaces.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/interfaces.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -36,6 +36,8 @@
     LocalUtility = interface.Attribute("Base class for local utilities.")
     View = interface.Attribute("Base class for browser views.")
     XMLRPC = interface.Attribute("Base class for XML-RPC methods.")
+    JSON = interface.Attribute("Base class for JSON methods.")
+    REST = interface.Attribute("Base class for REST views.")
     Traverser = interface.Attribute("Base class for custom traversers.")
     Form = interface.Attribute("Base class for forms.")
     AddForm = interface.Attribute("Base class for add forms.")
@@ -411,7 +413,15 @@
         doesn't have to update an object, the dictionary is empty.
         """
 
+class IREST(interface.Interface):
+    context = interface.Attribute("Object that the REST handler presents.")
 
+    request = interface.Attribute("Request that REST handler was looked"
+                                  "up with.")
+    
+    body = interface.Attribute(
+        """The text of the request body.""")
+
 class IApplication(interface.Interface):
     """Marker-interface for grok application factories.
 

Modified: grok/trunk/src/grok/meta.py
===================================================================
--- grok/trunk/src/grok/meta.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/meta.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -16,6 +16,7 @@
 import os
 
 import zope.component.interface
+import zope.location
 from zope import interface, component
 from zope.publisher.interfaces.browser import (IDefaultBrowserLayer,
                                                IBrowserRequest,
@@ -47,8 +48,8 @@
 import grok
 from grok import components, formlib
 from grok.util import check_adapts, get_default_permission, make_checker
+from grok.rest import IRESTSkinType
 
-
 class AdapterGrokker(martian.ClassGrokker):
     component_class = grok.Adapter
 
@@ -120,7 +121,59 @@
             make_checker(factory, method_view, permission)
         return True
 
+class RestPublisher(zope.location.Location):
+    interface.implements(IBrowserPublisher)
 
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        self.__parent__ = self.context
+    
+class RESTGrokker(martian.ClassGrokker):
+    component_class = grok.REST
+    
+    def grok(self, name, factory, context, module_info, templates):
+        view_context = util.determine_class_context(factory, context)
+        # XXX We should really not make __FOO__ methods available to
+        # the outside -- need to discuss how to restrict such things.
+        methods = util.methods_from_class(factory)
+
+        default_permission = get_default_permission(factory)
+
+        # grab layer from class or module
+        view_layer = determine_class_directive('grok.layer', factory,
+                                               module_info,
+                                               default=grok.IRESTLayer)
+
+        for method in methods:
+            # determine if the method is not allowed (it's the same
+            # as the method on the superclass)
+            is_not_allowed_method = (method.im_func is
+                                     getattr(grok.REST,
+                                             method.__name__).im_func)
+
+            # Make sure that the class inherits RestPublisher, so that the
+            # views have a location
+            method_view = type(
+                factory.__name__, (factory, RestPublisher),
+                {'__call__': method,
+                 'is_not_allowed': is_not_allowed_method }
+                )
+
+            component.provideAdapter(
+                method_view, (view_context, view_layer),
+                interface.Interface,
+                name=method.__name__)
+
+            # Protect method_view with either the permission that was
+            # set on the method, the default permission from the class
+            # level or zope.Public.
+            permission = getattr(method, '__grok_require__',
+                                 default_permission)
+            make_checker(factory, method_view, permission)
+        return True
+    
+
 class ViewGrokker(martian.ClassGrokker):
     component_class = grok.View
 
@@ -176,7 +229,9 @@
                                 "'render' method." % factory, factory)
 
         # grab layer from class or module
-        view_layer = determine_class_directive('grok.layer', factory, module_info, default=IDefaultBrowserLayer)
+        view_layer = determine_class_directive('grok.layer',
+                                               factory, module_info,
+                                               default=IDefaultBrowserLayer)
 
         view_name = util.class_annotation(factory, 'grok.name',
                                           factory_name)
@@ -650,14 +705,28 @@
     component_class = grok.Skin
 
     def grok(self, name, factory, context, module_info, templates):
-
-        layer = determine_class_directive('grok.layer', factory, module_info, default=IBrowserRequest)
-        name = grok.util.class_annotation(factory, 'grok.name', factory.__name__.lower())
-        zope.component.interface.provideInterface(name, layer, IBrowserSkinType)
+        layer = determine_class_directive('grok.layer', factory,
+                                          module_info, default=IBrowserRequest)
+        name = grok.util.class_annotation(factory, 'grok.name',
+                                          factory.__name__.lower())
+        zope.component.interface.provideInterface(name, layer,
+                                                  IBrowserSkinType)
         return True
 
+class RESTProtocolGrokker(martian.ClassGrokker):
+    component_class = grok.RESTProtocol
 
-def determine_class_directive(directive_name, factory, module_info, default=None):
+    def grok(self, name, factory, context, module_info, templates):
+        layer = determine_class_directive('grok.layer', factory,
+                                          module_info, default=IBrowserRequest)
+        name = grok.util.class_annotation(factory, 'grok.name',
+                                          factory.__name__.lower())
+        zope.component.interface.provideInterface(name, layer,
+                                                  IRESTSkinType)
+        return True
+    
+def determine_class_directive(directive_name, factory, module_info,
+                              default=None):
     directive = util.class_annotation(factory, directive_name, None)
     if directive is None:
         directive = module_info.getAnnotation(directive_name, None)

Modified: grok/trunk/src/grok/publication.py
===================================================================
--- grok/trunk/src/grok/publication.py	2007-10-26 09:58:26 UTC (rev 81121)
+++ grok/trunk/src/grok/publication.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -14,15 +14,19 @@
 """Grok publication objects
 """
 
+from grok.components import GrokMethodNotAllowed
+
+from zope import component
 from zope.security.proxy import removeSecurityProxy
 from zope.security.checker import selectChecker
+from zope.publisher.publish import mapply
 
-from zope.app.publication.http import BaseHTTPPublication
+from zope.app.publication.http import BaseHTTPPublication, HTTPPublication
 from zope.app.publication.browser import BrowserPublication
 from zope.app.publication.requestpublicationfactories import \
-     BrowserFactory, XMLRPCFactory
+     BrowserFactory, XMLRPCFactory, HTTPFactory
+from zope.app.http.interfaces import IHTTPException
 
-
 class ZopePublicationSansProxy(object):
 
     def getApplication(self, request):
@@ -59,9 +63,28 @@
 class GrokXMLRPCPublication(ZopePublicationSansProxy, BaseHTTPPublication):
     pass
 
-
 class GrokXMLRPCFactory(XMLRPCFactory):
 
     def __call__(self):
         request, publication = super(GrokXMLRPCFactory, self).__call__()
         return request, GrokXMLRPCPublication
+
+
+class GrokHTTPPublication(ZopePublicationSansProxy, HTTPPublication):
+   def callObject(self, request, ob):
+       orig = ob
+       if not IHTTPException.providedBy(ob):
+           ob = component.queryMultiAdapter((ob, request),
+                                            name=request.method)
+           checker = selectChecker(ob)
+           if checker is not None:
+               checker.check(ob, '__call__')
+           ob = getattr(ob, request.method, None)
+           if ob is None:
+               raise GrokMethodNotAllowed(orig, request)
+       return mapply(ob, request.getPositionalArguments(), request)
+
+class GrokHTTPFactory(HTTPFactory):
+    def __call__(self):
+        request, publication = super(GrokHTTPFactory, self).__call__()
+        return request, GrokHTTPPublication

Copied: grok/trunk/src/grok/rest.py (from rev 80892, grok/branches/faassen-rest/src/grok/rest.py)
===================================================================
--- grok/trunk/src/grok/rest.py	                        (rev 0)
+++ grok/trunk/src/grok/rest.py	2007-10-26 14:17:35 UTC (rev 81122)
@@ -0,0 +1,48 @@
+import grok
+
+from zope import component
+
+from zope.traversing.namespace import skin
+from zope.interface import Interface
+from zope.interface.interfaces import IInterface
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.publisher.interfaces.http import IHTTPRequest
+
+from grok.components import GrokMethodNotAllowed
+
+class IRESTSkinType(IInterface):
+    """Skin for REST requests.
+    """
+
+class MethodNotAllowedView(grok.MultiAdapter):
+    grok.adapts(GrokMethodNotAllowed, IHTTPRequest)
+    grok.name('index.html')
+    grok.implements(Interface)
+    
+    def __init__(self, error, request):
+        self.error = error
+        self.request = request
+        self.allow = self._getAllow()
+        
+    def _getAllow(self):
+        allow = []
+        for method in ['GET', 'PUT', 'POST', 'DELETE']:
+            view = component.queryMultiAdapter(
+                (self.error.object, self.error.request),
+                name=method)
+            if view is not None and not view.is_not_allowed:
+                allow.append(method)
+        allow.sort()
+        return allow
+    
+    def __call__(self):
+        self.request.response.setHeader('Allow', ', '.join(self.allow))
+        self.request.response.setStatus(405)
+        return 'Method Not Allowed'
+    
+class rest_skin(skin):
+    skin_type = IRESTSkinType
+
+class DefaultRest(grok.REST):
+    grok.context(Interface)
+    grok.layer(grok.IRESTLayer)



More information about the Checkins mailing list