Skip to main content

JSON RPC server and client implementation for Zope3

Project description

This package provides an JSON-RPC server implementation for Zope3.

Detailed Documentation

JSONRPC

JSON is javascript object notation. JSON-RPC performs the same service as XML-RPC, except the transport is JSON instead of XML.

Many thanks to Jim Washington for the work on zif.jsonserver. This project uses many code writen by Jim. I implemented an additional python JSONRPC proxy which can communicate with the server. This means we can use this library to call JSON from python to python. The JSON-RPC proxy uses similar patterns like the XML-RPC implementation.

There is also an additional xmlhttp and json javascript implementation which offers a JSON-RPC proxy implementation for JavaScript.

This project provides the proposed request type “application/json”. The request type “application/json-rpc” is supported as long it is not officialy deprecated.

The goal of this project is to provide a JSON-RPC implementation. Simple Browser views which handle JSON calls with a BrowserRequest are not supported by this package. I’m still not sure if this is good or bad and in which direction I will go with this package.

Some of my goals are right now, but can change in the future if I’ll understand all the concepts around JSON, e.g. JSPON, JSONP, CrossSite etc:

  • provide a secure way to handle JSON calls from client to server. I hope we can implement JSONRequest some days. CrossSite seems to use a intereting concept

  • Simple pythonic implementation

  • Use together with JQuery (see http://www.jquery.org).

  • No other dependency then JQuery and basic zope packages.

  • well tested (this is not the case for JavaScript right now)

About JSON

See www.json.org for more information about JSON.

See http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html for more information about the JSON 1.1 specification.

What this package can’t do

JSON and this package have different limitations. This package can right now not handle the following tasks:

  • Handle fileupload

  • Handle GET request

Note that the JSONRPCRequest implementation is based on the IHTTPRequest, this means that there is no other browser page available if you call them in python, e.g. getMultiAdapter((context, request), name=’myViewName’). This is explicitly done this way. If you’d like to use content form such browser pages in a JSON request/call, you can inherit your skin form IJSONRPCLayer and IBrowserRequest and register your JSON-RPC views for this custom layer.

JSON-RPC server

The JSON server looks for content-type “application/json”, and handles those requests as JSON-RPC. The official mime-type for JSON is “application/json” The old content type application/json-rpc is supported too.

Let’s define a content object:

>>> import zope.interface
>>> class IDemoContent(zope.interface.Interface):
...     """Demo content interface."""
>>> import persistent
>>> class DemoContent(persistent.Persistent):
...     """Demo content."""
...     zope.interface.implements(IDemoContent)

And define a JSONRPC method view:

>>> from z3c.jsonrpc import publisher
>>> class DemoView(publisher.MethodPublisher):
...     """Sample JSON view."""
...
...     def hello(self):
...         return u"Hello World"
...
...     def greeting(self, name):
...         return u"Hello %s" % name
...
...     def mixedparams(self, prefix, bar=None, foo=None):
...         # Note; keyword arguments can be found in request.form
...         return u"%s %s %s" % (prefix, bar, foo)
...
...     def kws(self, adam=None, foo=None, bar=None):
...         # Note; keyword arguments can be found in request.form
...         a = self.request.get('adam')
...         b = self.request.form.get('foo')
...         c = self.request.form.get('bar')
...         return u"%s %s %s" % (a, b, c)
...
...     def showId(self):
...         return u"The json id is: %s" % self.request.jsonId
...
...     def forceValueError(self):
...         raise ValueError('Something was wrong in server method.')

Let’s define a content object that is a container:

>>> import zope.interface
>>> class IDemoContainer(zope.container.interfaces.IReadContainer):
...     """Demo container interface."""
>>> import persistent
>>> from zope.container import btree
>>> class DemoContainer(btree.BTreeContainer):
...     """Demo container."""
...     zope.interface.implements(IDemoContainer)

And define a JSONRPC method view:

>>> from z3c.jsonrpc import publisher
>>> class DemoContainerView(publisher.MethodPublisher):
...     """Sample JSON view."""
...
...     def available(self):
...         return u"Hello World"
...
...     def greeting(self, name):
...         return u"Hello %s" % name
...
...     def mixedparams(self, prefix, foo=None, bar=None):
...         # Note; keyword arguments can be found in request.form
...         return u"%s %s %s" % (prefix, foo, bar)
...
...     def kws(self, adam=None, foo=None, bar=None):
...         # Note; keyword arguments can be found in request.form
...         a = self.request.get('adam')
...         b = self.request.form.get('foo')
...         c = self.request.form.get('bar')
...         return u"%s %s %s" % (a, b, c)
...
...     def showId(self):
...         return u"The json id is: %s" % self.request.jsonId
...
...     def forceValueError(self):
...         raise ValueError('Something was wrong in server method.')

Make them available under the fake package jsonsamples:

>>> import sys
>>> sys.modules['custom'] = type('Module', (), {})()
>>> sys.modules['custom'].IDemoContent = IDemoContent
>>> sys.modules['custom'].DemoContent = DemoContent
>>> sys.modules['custom'].DemoView = DemoView
>>> sys.modules['custom'].IDemoContainer = IDemoContainer
>>> sys.modules['custom'].DemoContainer = DemoContainer
>>> sys.modules['custom'].DemoContainerView = DemoContainerView

Let’s show how we can register a jsonrpc view:

>>> from zope.configuration import xmlconfig
>>> import z3c.jsonrpc
>>> context = xmlconfig.file('meta.zcml', z3c.jsonrpc)
>>> context = xmlconfig.string("""
... <configure
...     xmlns:z3c="http://namespaces.zope.org/z3c">
...   <z3c:jsonrpc
...       for="custom.IDemoContent"
...       class="custom.DemoView"
...       permission="zope.Public"
...       methods="hello greeting mixedparams kws showId forceValueError"
...       layer="z3c.jsonrpc.testing.IJSONRPCTestSkin"
...       />
... </configure>
... """, context)

Let’s show how we can register a jsonrpc view for the container: (The container class needs permission configuration too)

>>> context = xmlconfig.file('meta.zcml', z3c.jsonrpc)
>>> context = xmlconfig.file('meta.zcml', zope.security, context)
>>> context = xmlconfig.string("""
... <configure
...     xmlns:z3c="http://namespaces.zope.org/z3c"
...     xmlns="http://namespaces.zope.org/zope">
...     <class class="custom.DemoContainer">
...       <allow
...           interface="custom.IDemoContainer"
...           />
...     </class>
...   <z3c:jsonrpc
...       for="custom.IDemoContainer"
...       class="custom.DemoContainerView"
...       permission="zope.Public"
...       methods="available greeting mixedparams kws showId forceValueError"
...       layer="z3c.jsonrpc.testing.IJSONRPCTestSkin"
...       />
... </configure>
... """, context)

Now we will setup a content object in our site:

>>> site  = getRootFolder()
>>> content = DemoContent()
>>> site['content'] = content
>>> container = DemoContainer()
>>> site['container'] = container

Now we can call the method from our JSONRPC view:

>>> from z3c.jsonrpc import testing
>>> request = testing.TestRequest()
>>> demoView = DemoView(content, request)
>>> demoView.hello()
u'Hello World'

But this is not intuitive. Let’s see how we can traverse to the method hello with the traverser:

>>> from z3c.jsonrpc.publisher import MethodTraverser
>>> methodTraverser = MethodTraverser(demoView, request)
>>> methodTraverser.publishTraverse(request, 'hello')()
u'Hello World'

Now we try to access the JSON-RPC view method with a test browser. As you can see, there is no view accessible. This is because the JSONRPC view is not a browser view and is not traversable. The error shows that the request factory falls back to the browser request factory:

>>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
>>> browser.handleErrors = False
>>> browser.addHeader('Accept-Language', 'en')
>>> browser.addHeader('Content-Type', 'application/json')
>>> siteURL = 'http://localhost/++skin++JSONRPCTestSkin'
>>> browser.open(siteURL + '/content/hello')
Traceback (most recent call last):
...
NotFound: Object: <zope.site.folder.Folder...: u'++skin++JSONRPCTestSkin'

Testing

If you need to test a JSONRPC view you can use the test proxy like shown below in the JSON-RPC proxy section.

JSON-RPC proxy

The jsonrpc package provides also a JSON-RPC proxy implementation. This implementation is similar to the one known from xmlrpclib except that it can handle JSON instead of XML.

Let’s try to call our method called hello we defined before:

>>> from z3c.jsonrpc.testing import JSONRPCTestProxy
>>> proxy = JSONRPCTestProxy(siteURL + '/content')
>>> proxy.hello()
u'Hello World'

As defined in the jsonrpc spec it is also allowed to omit the params completly we need to test this with a post directly because the testing proxy always sets the params.

>>> browser.post(siteURL + '/content', "{'method':'hello', 'id':1}",
...              content_type='application/json')
>>> browser.contents
'{"jsonrpc":"2.0","result":"Hello World","id":1}'
>>> browser.post(siteURL + '/content', "{'method':'hello', 'params':null, 'id':1}",
...              content_type='application/json')
>>> browser.contents
'{"jsonrpc":"2.0","result":"Hello World","id":1}'
>>> proxy2 = JSONRPCTestProxy(siteURL + '/container')
>>> proxy2.available()
u'Hello World'

Now let’s make a remote procedure call with a argument:

>>> proxy.greeting(u'Jessy')
u'Hello Jessy'

Let’s call named arguments:

>>> proxy.kws(bar=u'BAR', foo=u'FOO')
u'None FOO BAR'

There is also an id in the json response. Let’s use such a json request id in our JSONRPCProxy:

>>> proxy = JSONRPCTestProxy(siteURL + '/content', jsonId = u'my id')
>>> proxy.showId()
u'The json id is: my id'

The proxy also knows this id as jsonId:

>>> proxy.jsonId
u'my id'

JSON-RPC Versions

Let’s test the different JSON-RPC versions starting with version 1.0:

>>> v1 = JSONRPCTestProxy(siteURL + '/container', jsonVersion='1.0')
>>> v1.available()
u'Hello World'
>>> v1.greeting(u'Jessy')
u'Hello Jessy'
>>> v1.kws(bar=u'BAR', foo=u'FOO')
u'None FOO BAR'
>>> v1 = JSONRPCTestProxy(siteURL + '/content', jsonId = u'my id',
...     jsonVersion='1.0')
>>> v1.showId()
u'The json id is: my id'
>>> v1.jsonId
u'my id'

Now test with JSON-RPC version 1.1:

>>> v11 = JSONRPCTestProxy(siteURL + '/container', jsonVersion='1.1')
>>> v11.available()
u'Hello World'
>>> v11.greeting(u'Jessy')
u'Hello Jessy'
>>> v11.kws(bar=u'BAR', foo=u'FOO')
u'None FOO BAR'
>>> v11 = JSONRPCTestProxy(siteURL + '/content', jsonId = u'my id',
...     jsonVersion='1.1')
>>> v11.showId()
u'The json id is: my id'
>>> v11.jsonId
u'my id'

Now test with JSON-RPC version 2.0:

>>> v2 = JSONRPCTestProxy(siteURL + '/container', jsonVersion='2.0')
>>> v2.available()
u'Hello World'
>>> v2.greeting(u'Jessy')
u'Hello Jessy'
>>> v2.kws(bar=u'BAR', foo=u'FOO')
u'None FOO BAR'
>>> v2 = JSONRPCTestProxy(siteURL + '/content', jsonId = u'my id',
...     jsonVersion='2.0')
>>> v2.showId()
u'The json id is: my id'
>>> v2.jsonId
u'my id'

Mixed parameters

Note the keyword arguments will get stored in the request.form. Important to know is that JSON-RPC does not support positional and named arguments in one method call.

>>> v1.mixedparams('Hello', foo=u'FOO', bar=u'BAR')
Traceback (most recent call last):
...
ValueError: Mixing positional and named parameters in one call is not possible
>>> v11.mixedparams('Hello', foo=u'FOO', bar=u'BAR')
Traceback (most recent call last):
...
ValueError: Mixing positional and named parameters in one call is not possible
>>> v2.mixedparams('Hello', foo=u'FOO', bar=u'BAR')
Traceback (most recent call last):
...
ValueError: Mixing positional and named parameters in one call is not possible

Error handling

See what happens if the server raises an Exception. We will get a response error with additional error content:

>>> proxy.forceValueError()
Traceback (most recent call last):
...
ResponseError: Received error from server: ...

and the error content looks like:

>>> proxy.error
{u'message': u'Internal error', u'code': -32603, u'data': {u'i18nMessage': u'Internal error'}}

The error property gets reset on the next successfull call:

>>> x = proxy.showId()
>>> proxy.error is None
True

And now we force a ResponseError with a fake JSONReader. But first we need to replace our IJSONReader utility:

>>> from z3c.json.interfaces import IJSONReader
>>> sm = site.getSiteManager()
>>> fakeJSONReader = testing.ForceResponseErrorJSONReader()
>>> sm.registerUtility(fakeJSONReader, IJSONReader)

also setup the site hook:

>>> from zope.component import hooks
>>> hooks.setSite(site)

and just call a method this will now raise a ResponseError:

>>> proxy = JSONRPCTestProxy(siteURL + '/content')
>>> proxy.hello()
Traceback (most recent call last):
...
ResponseError: Unacceptable JSON expression: {"id":"jsonrpc", "method":"hello", "no-params"}

the error message is stored in the proxy too:

>>> proxy.error
u'Unacceptable JSON expression: {"id":"jsonrpc", "method":"hello", "no-params"}'
Transport

We used the JSONRPCTestProxy here for testing. This JSON-RPC proxy is a wrapper for the original JSONRPCProxy and adds handleErrors support and a special Transport layer which uses a testing caller. You can use one of the different Transport layers defined in the z3c.json.transport module in real usecases together with the default JSONRPCProxy implementation.

cleanup

Now we need to clean up the custom module.

>>> del sys.modules['custom']

Directives

JSONRPC directive

Show how we can use the jsonrpc directive. Register the meta configuration for the directive.

>>> from zope.configuration import xmlconfig
>>> import z3c.jsonrpc
>>> context = xmlconfig.file('meta.zcml', z3c.jsonrpc)

Now register the view defined in the testing module within the z3c:jsonrpc directive:

>>> context = xmlconfig.string("""
... <configure
...     xmlns:z3c="http://namespaces.zope.org/z3c">
...   <z3c:jsonrpc
...       for="z3c.jsonrpc.testing.IA"
...       class="z3c.jsonrpc.testing.MethodsA"
...       permission="zope.Public"
...       methods="hello"
...       />
... </configure>
... """, context)

Let’s check if the view is registered as adapter:

>>> import zope.component
>>> from z3c.jsonrpc.testing import A
>>> from z3c.jsonrpc.testing import TestRequest
>>> a = A()
>>> request = TestRequest()
>>> zope.component.queryMultiAdapter((a, request), name='hello')
<z3c.jsonrpc.zcml.MethodsA object at ...>

We can also use a layer interface wich will restrict our view registration to a specific request type. Provide such a request type layer:

>>> from z3c.jsonrpc.testing import IJSONRPCTestLayer
>>> demoRequest = TestRequest()
>>> zope.interface.directlyProvides(demoRequest, IJSONRPCTestLayer)

And register a new JSON-RPC view:

>>> context = xmlconfig.string("""
... <configure
...     xmlns:z3c="http://namespaces.zope.org/z3c">
...   <z3c:jsonrpc
...       for="z3c.jsonrpc.testing.IB"
...       class="z3c.jsonrpc.testing.MethodsB"
...       permission="zope.Public"
...       methods="hello"
...       layer="z3c.jsonrpc.testing.IJSONRPCTestLayer"
...       />
... </configure>
... """, context)

Setup a new content stub:

>>> from z3c.jsonrpc.testing import B
>>> b = B()

And test the view within our new layer:

>>> zope.component.queryMultiAdapter((b, demoRequest), name='hello')
<z3c.jsonrpc.zcml.MethodsB object at ...>

Note the object b does not know the view within the default request layer:

>>> zope.component.queryMultiAdapter((b, request), name='hello') is None
True

setDefaultJSONRPCSkin

>>> from z3c.jsonrpc import interfaces
>>> import z3c.jsonrpc.zcml
>>> class IMySkin(zope.interface.Interface):
...     pass
>>> zope.interface.directlyProvides(IMySkin, interfaces.IJSONRPCSkinType)

Before we setup a default request, we try to set a default request for our request:

>>> from zope.publisher.skinnable import setDefaultSkin
>>> setDefaultSkin(request)

Our request should not provide any default kins since we didn’t register any:

>>> IMySkin.providedBy(request)
False

Now let’s register a default skin:

>>> zope.component.provideUtility(IMySkin, interfaces.IJSONRPCSkinType,
...     name='JSONRPC')
>>> z3c.jsonrpc.zcml.setDefaultJSONRPCSkin('JSONRPC')

We can lookup a default skin from the adapter registry:

>>> from zope.publisher.interfaces import IDefaultSkin
>>> adapters = zope.component.getSiteManager().adapters
>>> default = adapters.lookup((interfaces.IJSONRPCRequest,), IDefaultSkin, '')
>>> default is IMySkin
True

Since we have a default skin utility registered as a skin type for our request, a new request instance should provide the default skin:

>>> request = TestRequest()
>>> setDefaultSkin(request)
>>> IMySkin.providedBy(request)
True

We can get the applied default skin by look for our skin type:

>>> for iface in zope.interface.providedBy(request):
...     if interfaces.IJSONRPCSkinType.providedBy(iface):
...         print "%s" % iface
<InterfaceClass README.IMySkin>

CHANGES

0.7.2 (2013-10-11)

  • handleException: provide human readable traceback

0.7.1 (2012-11-27)

  • Fix JSONRPCTestTransport to include the request full host. Until now it ate the port.

0.7.0 (2012-03-25)

  • Fix: added missing exception import for ParseError in publisher.processInputs

  • import doctest from python

0.6.0 (2010-01-27)

  • cleanup setup dependencies, adjust ftesting.zcml

  • adjust coverage report setup

  • implemented error view concept which will work with ZopePublication

  • implemented default error view for known zope and JSON-RPC errors

  • use DirectResult in response

  • removed unauthenticated error view. This was not working and requires a custom concept supported by the used java script library used at client side

Version 0.5.4 (2009-04-07)

  • handle empty and none-existing params in jsonrpc requests

Version 0.5.3 (2009-03-10)

  • Fix: reflect skin lookup changes in zope.publisher. Use the new skinnable concept.

  • Fix: The default skin didn’t get applied based on the inherited concept give from the zope.publisher.browser implementation because our JSON-RPC request doesn’t provide IBrowserRequest. Added a workaround which will apply a given IDefaultSkin during request instance creation.

Version 0.5.2 (2009-02-24)

  • added tests for all JSON-RPC versions

  • Feature: implemented defaultJSONRPCSkin directive

  • Feature: support non positional arguments for all jsonrpc versions. There is now no distinction in handling method parameters for all supported versions.

  • Fix: for jsonrpc version 1.1 :
    • must not provide “error” property in case of success

    • must not provide “result” property in case of error

  • Fix: removed develop path for z3c.json from buildout.cfg

  • Fix: publisher checks for version id as a string not a float

  • Feature: Implemented JSON-RPC 2.0 specification. Use JSON-RPC 2.0 version as default. Optional the version 1.0 and 1.1 can be set. See JSON-RPC 2.0 specification for more information.

  • Feature: Added initial version of JSON-RPC exceptions.

  • Added explicit test cleanup since some zope testing change left over a global adapter registry from old connections

  • Removed unused dependency to z3c.layer in test setup

  • Removed unused dependency on z3c.i18n.

Version 0.5.1 (2008-01-24)

  • Improve meta-data.

  • Bug: The skin code relied on un-released API that was actually later reverted.

Version 0.5.0 (2008-01-21)

  • Initial Release

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

z3c.jsonrpc-0.7.2.zip (55.0 kB view hashes)

Uploaded Source

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page