[z3-checkins] r32995 - z3/jsonserver/trunk
jwashin at codespeak.net
jwashin at codespeak.net
Sat Oct 7 23:20:50 CEST 2006
Author: jwashin
Date: Sat Oct 7 23:20:46 2006
New Revision: 32995
Added:
z3/jsonserver/trunk/JSONViews.txt
z3/jsonserver/trunk/ftests.py
Modified:
z3/jsonserver/trunk/__init__.py
z3/jsonserver/trunk/interfaces.py
z3/jsonserver/trunk/jsonrpc.py
Log:
Added JSONViews, and JSONViews.txt as ftest
Added: z3/jsonserver/trunk/JSONViews.txt
==============================================================================
--- (empty file)
+++ z3/jsonserver/trunk/JSONViews.txt Sat Oct 7 23:20:46 2006
@@ -0,0 +1,162 @@
+JSON Views are used when an HTTP GET is a reasonable way to get some
+JSON-formatted data from the server, for example, if you have a javascript
+library that employs JSON GETs, e.g., Dojo.
+
+A JSON View is simply a page that, instead of HTML, is a JSON.representation of
+some data.
+
+To do this you need to create a view class and register it in ZCML.
+
+We'll follow the xmlrpc README to demonstrate.
+
+First, write a view class, descended from JSONView. Whatever is returned in the
+doResponse method is what will be sent as a response. The usual view instance
+variables context and request are available.
+
+ >>> from jsonserver import JSONView
+ >>> class FolderListing(JSONView):
+ ... def doResponse(self):
+ ... return list(self.context.keys())
+
+Register it as a view. Usually, this will be a browser:page or browser:view
+directive (I'm not sure which is really preferred, either should work.).
+browser2:page should also work, though I have not tried it.
+
+ >>> from zope.configuration import xmlconfig
+ >>> ignored = xmlconfig.string("""
+ ... <configure
+ ... xmlns="http://namespaces.zope.org/zope"
+ ... xmlns:browser="http://namespaces.zope.org/browser"
+ ... >
+ ... <!-- allow browser directives here -->
+ ... <include package="zope.app.publisher.browser" file="meta.zcml" />
+ ... <browser:page
+ ... name="folderlist"
+ ... for="zope.app.folder.folder.IFolder"
+ ... class="jsonserver.JSONViews.FolderListing"
+ ... permission="zope.ManageContent"
+ ... />
+ ... </configure>
+ ... """)
+
+Exactly like the xmlrpc example, we add some items to the root folder.
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 73
+ ... Content-Type: application/x-www-form-urlencoded
+ ...
+ ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f1""")
+ HTTP/1.1 303 See Other
+ ...
+
+ >>> print http(r"""
+ ... POST /@@contents.html HTTP/1.1
+ ... Authorization: Basic bWdyOm1ncnB3
+ ... Content-Length: 73
+ ... Content-Type: application/x-www-form-urlencoded
+ ...
+ ... type_name=BrowserAdd__zope.app.folder.folder.Folder&new_value=f2""")
+ HTTP/1.1 303 See Other
+ ...
+
+Before we can JSONView, there needs to be an IJSONWriter utility. Here's one.
+ >>> import zope.component
+ >>> import jsoncomponent
+ >>> from jsonserver.interfaces import IJSONWriter
+ >>> zope.component.provideUtility(jsoncomponent.JSONWriter(),IJSONWriter)
+
+Let's set up a browser.
+ >>> from zope.testbrowser.testing import Browser
+ >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+ >>> #this was how I figured out the need for the IJSONWriter utility...
+ >>> #browser.handleErrors = False
+
+Now, we can call our new JSONView and get a response:
+ >>> browser.open('/folderlist')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 401: Unauthorized
+
+That was expected Let's view again, and provide authentication this time.
+Content-type is set appropriately.
+ >>> browser.addHeader('Authorization','Basic mgr:mgrpw')
+ >>> browser.open('/folderlist')
+ >>> browser.headers['content-type']
+ 'application/json;charset=utf-8'
+ >>> browser.contents
+ '["f1","f2"]'
+
+Pretty cool, yes? This is much smaller than a similar xmlrpc response.
+
+But what about parameters? Let's create another class with some error handling.
+There's no real standard on this, so you may need to commune with the cliemt
+implementation to see how to handle errors.
+ >>> import decimal
+ >>> class FolderStupidSum(JSONView):
+ ... """return two values and their sum"""
+ ... def doResponse(self, a=0, b=0):
+ ... try:
+ ... a = decimal.Decimal(a)
+ ... b = decimal.Decimal(b)
+ ... except decimal.InvalidOperation:
+ ... self.request.response.setStatus(500)
+ ... return {'error':'bad params','a':a, 'b':b}
+ ... return {'a':a,'b':b,'sum':a+b}
+
+and register it.
+ >>> from zope.configuration import xmlconfig
+ >>> ignored = xmlconfig.string("""
+ ... <configure
+ ... xmlns="http://namespaces.zope.org/zope"
+ ... xmlns:browser="http://namespaces.zope.org/browser"
+ ... >
+ ... <!-- allow browser directives here -->
+ ... <include package="zope.app.publisher.browser" file="meta.zcml" />
+ ... <browser:page
+ ... name="sum"
+ ... for="zope.app.folder.folder.IFolder"
+ ... class="jsonserver.JSONViews.FolderStupidSum"
+ ... permission="zope.ManageContent"
+ ... />
+ ... </configure>
+ ... """)
+
+Start a new browser.
+ >>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
+ >>> #browser.handleErrors = False
+ >>> browser.addHeader('Authorization','Basic mgr:mgrpw')
+
+Let's do a couple of views. browser is already authenticated. Asssure the
+parameters in the GET match the names in the doResponse method. Default values
+are OK, and probably a good idea.
+ >>> browser.open('/sum?a=5')
+
+We are expecting something that looks like '{"a":5,"sum":5,"b":0}'
+ >>> '"a"' in browser.contents
+ True
+ >>> '"b"' in browser.contents
+ True
+ >>> '"sum"' in browser.contents
+ True
+ >>> browser.contents.count('5') == 2
+ True
+
+This request should get something that looks like '{"a":5,"sum":15,"b":10}'
+ >>> browser.open('/sum?a=5&b=10')
+ >>> browser.contents.count('15') == 1
+ True
+
+Let's see if the error handling works. We should get an HTTP error and something
+like '{"a":"zzz5","b":"10","error":"bad params"}
+ >>> browser.handleErrors = True
+ >>> browser.open('/sum?a=zzz5&b=10')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 500: Internal Server Error
+ >>> 'zzz5' in browser.contents
+ True
+ >>> 'bad params' in browser.contents
+ True
+
Modified: z3/jsonserver/trunk/__init__.py
==============================================================================
--- z3/jsonserver/trunk/__init__.py (original)
+++ z3/jsonserver/trunk/__init__.py Sat Oct 7 23:20:46 2006
@@ -1 +1,2 @@
#python package
+from jsonrpc import MethodPublisher, JSONView
Added: z3/jsonserver/trunk/ftests.py
==============================================================================
--- (empty file)
+++ z3/jsonserver/trunk/ftests.py Sat Oct 7 23:20:46 2006
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Functional tests for JSON Views
+Original file z.a.publisher.xmlrpc/ftests.py
+$Id: ftests.py 29787 2005-04-01 16:41:05Z srichter $
+Mod by jmw 7 Oct 06 for JSON Views
+"""
+import zope.interface
+import zope.app.folder.folder
+import zope.publisher.interfaces.xmlrpc
+from zope.app.testing import ztapi, functional, setup
+
+def setUp(test):
+ setup.setUpTestAsModule(test, 'jsonserver.JSONViews')
+
+def tearDown(test):
+ # clean up the views we registered:
+
+ # we use the fact that registering None unregisters whatever is
+ # registered. We can't use an unregistration call because that
+ # requires the object that was registered and we don't have that handy.
+ # (OK, we could get it if we want. Maybe later.)
+
+ ztapi.provideView(zope.app.folder.folder.IFolder,
+ zope.publisher.interfaces.IRequest,
+ zope.interface,
+ 'folderlist',
+ None,
+ )
+
+ ztapi.provideView(zope.app.folder.folder.IFolder,
+ zope.publisher.interfaces.xmlrpc.IXMLRPCRequest,
+ zope.interface,
+ 'sum',
+ None,
+ )
+
+ setup.tearDownTestAsModule(test)
+
+def test_suite():
+ return functional.FunctionalDocFileSuite(
+ 'JSONViews.txt', setUp=setUp, tearDown=tearDown)
+
+if __name__ == '__main__':
+ import unittest
+ unittest.main(defaultTest='test_suite')
Modified: z3/jsonserver/trunk/interfaces.py
==============================================================================
--- z3/jsonserver/trunk/interfaces.py (original)
+++ z3/jsonserver/trunk/interfaces.py Sat Oct 7 23:20:46 2006
@@ -23,12 +23,15 @@
from zope.publisher.interfaces.http import IHTTPApplicationRequest,\
IHTTPCredentials
from zope.interface import Interface
-from zope.component.interfaces import IView, IPresentation
+from zope.component.interfaces import IView
from zope.interface import Attribute
from zope.app.publisher.xmlrpc import IMethodPublisher
from zope.publisher.interfaces.xmlrpc import IXMLRPCPublication
from zope.app.publication.interfaces import IRequestFactory
-from zope.publisher.interfaces.browser import IDefaultBrowserLayer
+from zope.publisher.interfaces.browser import IDefaultBrowserLayer, \
+ IBrowserPage
+from zope.schema.interfaces import TextLine
+
class IJSONRPCRequestFactory(IRequestFactory):
"""Browser request factory"""
@@ -78,3 +81,12 @@
"""Premarshaller to remove security proxies"""
def __call__():
"""return the object without proxies"""
+
+class IJSONView(IBrowserPage):
+ """A view that is a JSON representation of an object"""
+ contentType = TextLine(title=u"content-type", default=u"application/json")
+ def doResponse():
+ """return the list or dict that is response for this view"""
+ def doCacheControl():
+ """set any cache headers that may be needed. Default sends 'no-cache'
+ to KHTML browsers. May be extended/overridden in subclasses"""
Modified: z3/jsonserver/trunk/jsonrpc.py
==============================================================================
--- z3/jsonserver/trunk/jsonrpc.py (original)
+++ z3/jsonserver/trunk/jsonrpc.py Sat Oct 7 23:20:46 2006
@@ -22,12 +22,14 @@
#2006-03-09 enabled gzip compression for large responses
#2006-05-10 removed gzip compression and (prematurely) enabled json-rpc 1.1 jmw
#2006-06-19 updated with ctheune's xmlrpc solution for removing proxies jmw
+#2006-09-27 added JSONView class
__docformat__ = 'restructuredtext'
from zope.app.publication.http import BaseHTTPPublication
from interfaces import IMethodPublisher, IJSONRPCView, IJSONRPCPublisher,\
- IJSONRPCRequest, IJSONReader, IJSONWriter, IJSONRPCPremarshaller
+ IJSONRPCRequest, IJSONReader, IJSONWriter, IJSONRPCPremarshaller, \
+ IJSONView
from zope.interface import implements
#from zope.publisher.http import IResult
from zope.location.location import Location
@@ -36,8 +38,11 @@
from zope.publisher.browser import BrowserRequest
from zope.security.proxy import isinstance
from zope.publisher.interfaces.browser import IBrowserRequest
+
from zope.publisher.interfaces.browser import IBrowserApplicationRequest
from zope.component import getUtility
+from zope.publisher.browser import BrowserPage
+
try:
from cStringIO import StringIO
except ImportError:
@@ -50,6 +55,8 @@
keyword_key = "pythonKwMaRkEr"
+json_charsets = ('utf-8','utf-16', 'utf-32')
+
#writeProfileData transcribes reads and writes files in the zope3
# instance directory for profiling use.
# profiledata.py has response dicts that can be read as python
@@ -208,6 +215,9 @@
def _prepareResult(self,result):
#we've asked json to return unicode; result should be unicode
encoding = getCharsetUsingRequest(self._request) or 'utf-8'
+ enc = encoding.lower()
+ if not enc in json_charsets:
+ encoding = 'utf-8'
#at outgoing boundary; encode it.
if isinstance(result,unicode):
body = result.encode(encoding)
@@ -268,7 +278,7 @@
return map(premarshal, self.data)
def premarshal(data):
- """Premarshal data before handing it to xmlrpclib for marhalling
+ """Premarshal data before handing it to JSON writer for marshalling
The initial purpose of this function is to remove security proxies
without resorting to removeSecurityProxy. This way, we can avoid
@@ -303,6 +313,69 @@
#return premarshaller(data)
#return data
+class JSONView(BrowserPage):
+ """This is a base view class for 'ordinary' JSON methods.
+ JSONViews are accessible by ordinary URLs and HTTP GETs.
+ """
+ implements(IJSONView)
+ contentType = 'application/json'
+
+ def doResponse(self, *args, **kw):
+ """return the python list or dict that will be the body of the response.
+ This needs to be overridden in subclasses"""
+ raise NotImplementedError("Subclasses should override doResponse to "
+ "provide a response body")
+ def doCacheControl(self):
+ """ at the moment, KHTML-based browsers do not handle cached JSON data
+ properly. This may be Dojo-specific, and may be only necessary for
+ a short time until Konq and Safari behave like other browsers in this
+ respect.
+ Default here is to send 'no-cache' header to KHTML browsers.
+ For other user agents, a 1-hour public cache is specified.
+
+ May be overridden/extended in subclasses.
+ """
+ agent = self.request.get('HTTP_USER_AGENT','')
+ response = self.request.response
+ if 'KHTML' in agent:
+ response.setHeader('cache-control','no-cache')
+ else:
+ response.setHeader('cache-control',
+ 'public, must-revalidate, max-age=3600')
+
+ def __call__(self, *args, **kw):
+ """the doResponse method is called.
+
+ First, anything that matches the method signature in request.form is
+ put in the method's **kw.
+
+ After call, the response is JSONized and sent out with appropriate
+ encoding.
+
+ """
+ request = self.request
+ meth = self.doResponse
+ #introspect the method and set kw params if the arg is in request.form
+ params = meth.im_func.func_code.co_varnames[1:]
+ for key in request.form.keys():
+ if key in params:
+ kw[str(key)] = request.form.get(key)
+ resp = premarshal(self.doResponse(*args,**kw))
+
+ if not isinstance(resp,dict) and not isinstance(resp,list):
+ raise ValueError("JSON responses must be dicts or lists")
+
+ self.doCacheControl()
+
+ encoding = getCharsetUsingRequest(self.request)
+ enc = encoding.lower()
+ if not enc in json_charsets:
+ #we'll allow utf-32, utf-16 or utf-8; if not specified, use utf-8
+ enc = 'utf-8'
+ request.response.setHeader('content-type','%s;charset=%s' % (self.contentType,enc))
+ json = getUtility(IJSONWriter)
+ s = json.write(resp).encode(enc)
+ return s
class JSONRPCView(object):
"""A base JSON-RPC view that can be used as mix-in for JSON-RPC views.
More information about the z3-checkins
mailing list