Skip to main content

Plone JSON API

Project description

plone.jsonapi.core

Author:

Ramon Bartl

Version:

0.7.0

Latest Build Status

Build Status

Abstract

An extensible Plone JSON API Framework

Introduction

This Package allows Users to expose content information via JSON.

Motivation

This project was born in 2012, out of the need for a data source to build a network based iOS application. Or more precise, I wanted to learn iOS programming and wanted to knit my own JSON API:)

I know, it is a little bit awkward to provide an own routing mechanism for Plone which dipatches the request after the ZPublisher did its job, but it worked and thus, I did it.

Limitations

Since the API comes after the ZPublisher, it can only make use of HTTP GET and HTTP POST methods. The other methods will never reach the API View.

Be aware that the API View comes with the permission zope2.View, so you need to programmatically check for the correct permissions on your custom routes.

See: http://developer.plone.org/security/permission_lists.html

Compatibility

The plone.jsonapi.core should work with Plone 3, 4 and 5 on Python 3.

Example:

[buildout]
...
versions = versions

[versions]
...
simplejson = 2.0.9
werkzeug = 0.7.2

Installation

There official release is on pypi, so you have to simply include plone.jsonapi.core to your buildout config.

Example:

[buildout]
...

[instance]
...
eggs =
    ...
    plone.jsonapi.core

API URL

After installation, the API View is available as a Browser View on your Plone site with the name @@API, for example http://localhost:8080/Plone/@@API.

API Framework

The main work is done in the plone.jsonapi.core.browser.api module. This module dispatches the incoming request and dispatches it to an endpoint function.

The API Router

The Router is responsible to manage and maintain API routes to endpoints.

Routes get defined by so called “Route Providers”.

A route provider is either a named Utility class, which implements the IRouteProvider interface, or simply a function, which is registered via the add_route decorator.

Basic Example

The most basic route provider is simply a decorated function:

from plone.jsonapi.core import router

@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):
    return {"hello": name}

The passed in context and request gets passed of the @@API View. It can be used to query Plone tools or other utilities or adapters.

A more complex Example

In this Example, we’re going to add a route provider named my_routes. This route provider gets registered as an named Utility.

To do so, we add a module called routes.py to our package and add the following code:

from zope import interface
from plone.jsonapi.core.interfaces import IRouteProvider

class ExampleRoutes(object):
    interface.implements(IRouteProvider)


    def initialize(self, context, request):
        """ called by the json api framework"""
        pass

    @property
    def routes(self):
        return (
            ("/hello/<string:name>", "hello", self.json_hello, dict(methods=['GET'])),
        )

    def json_hello(self, context, request, name="world"):
        return {"hello": name}

To register the Utility, we add this directive to the configure.zcml file:

<!-- Extension point for custom routes -->
<utility
    name="my_routes"
    provides="plone.jsonapi.core.interfaces.IRouteProvider"
    factory=".routes.ExampleRoutes" />

Or use grok:

from five import grok

...

grok.global_utility(ExampleRoutes, name="my_routes", direct=False)

Each route provider gets initialized with the context and the request in a method called initialize. This method gets called by the API framework.

Our route provider has to contain a routes property or method. It should return a tuple of route definitions. Each route definition contains the url rule (/hello), an endpoint name (hello), a method to be called when the url matches (self.json_hello) and an additional dictionary with routing options

The options dictionary get directly passed to the routing mechanism of Werkzeug. For details, see: http://werkzeug.pocoo.org/docs/routing/#rule-format

To test this route, browse to the /hello API url:

http://localhost:8080/Plone/@@API/hello/JSON%20Plone%20API

Result:

{
    _runtime: 0.00025200843811035156,
    hello: "JSON Plone API"
}

API URLs

If you design your custom RESTful JSON API, you probably want to insert URLs to your specified resources, e.g:

http://localhost:8080/Plone/@@API/news/news_items_1

The plone.jsonapi.core.router module comes with a url_for method.

So when you want to insert the URL for the defined hello endpoint, you simply add it like this:

from plone.jsonapi.core import router

@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):
    return {
        "url": router.url_for("hello", values={"name": name}, force_external=True),
        "hello": name,
    }

It builds the URLs using the build method of the MapAdapter of Werkzeug. For details, see http://werkzeug.pocoo.org/docs/routing/#werkzeug.routing.MapAdapter.build

The resulting JSON will look like this:

http://localhost:8080/Plone/@@API/hello/world

Result:

{
    url: "http://localhost:8080/Plone/@@API/hello/world",
    runtime: 0.002997875213623047,
    hello: "world"
}

Permissions

You have to handle the permissions for your routes manually. so if you would like to restrict the permission of the hello route, you have to do something like this:

from AccessControl import getSecurityManager
from AccessControl import Unauthorized

from plone.jsonapi.core import router

@router.add_route("/hello/<string:name>", "hello", methods=["GET"])
def hello(context, request, name="world"):

    if not getSecurityManager().checkPermission("ViewHelloAPI", context):
        raise Unauthorized("You don't have the 'ViewHelloAPI' permission")

    return {
        "url": router.url_for("hello", values={"name": name}, force_external=True),
        "hello": name,
    }

Output:

{
    runtime: 0.0021250247955322266,
    success: false,
    error: "You don't have the 'ViewHelloAPI' permission"
}

Plone JSONAPI Integration Tests

With plone.jsonapi.core enabled, it is simple to expose functions within Plone. You only have to wrap your function around the @router.add_route decorator.

The following doctest will demonstrate how the framework works and how to register new routes.

Some needed imports:

>>> import json
>>> from plone.jsonapi.core import router
>>> from plone.jsonapi.core.version import version

Prepare the browser:

>>> browser = self.getBrowser()

Remember some URLs:

>>> portal = self.getPortal()
>>> portal_url = portal.absolute_url()
>>> api_url = portal_url + "/@@API"
>>> version_url = api_url + "/version"

Check if the version URL returns the right version:

>>> browser.open(version_url)
>>> dct = json.loads(browser.contents)
>>> dct["url"] == version_url
True
>>> dct["version"] == version()
True

Testing the framework – lets add a new GET route:

>>> @router.add_route("/hello/<string:name>", "hello", methods=["GET"])
... def hello(context, request, name="world"):
...     return dict(hello=name)

>>> browser.open(api_url + "/hello/world")
>>> json.loads(browser.contents).get("hello")
'world'

Testing the framework – lets add a new POST route:

>>> @router.add_route("/hello", "hello_post", methods=["POST"])
... def hello_post(context, request):
...     return {"hello": "post"}

>>> browser.post(api_url + "/hello", "")
>>> json.loads(browser.contents).get("hello")
'post'

Check what happenss when a route throws an Error:

>>> @router.add_route("/fail", "fail", methods=["GET"])
... def fail(context, request):
...     raise RuntimeError("This failed badly")

>>> browser.open(api_url + "/fail")
Traceback ...
>>> json.loads(browser.contents).get("message")
'This failed badly'

Test XML:

>>> @router.add_route("/xml", "xml", methods=["GET"])
... def xml(context, request):
...     return {"type": "xml"}
>>> browser.open(api_url + "/xml?asxml=1")
>>> browser.contents
b'<?xml version="1.0" encoding="UTF-8" ?><root><type type="str">xml</type></root>'

Test Binary Stream:

>>> @router.add_route("/data", "data", methods=["GET"])
... def data(context, request):
...     return self.get_testfile_path()
>>> browser.open(api_url + "/data?asbinary=1")
>>> browser.contents
b'%PDF-...'

Changelog

0.7.0 - 2020-03-29

0.6 - 2017-01-10

0.5 - 2015-07-09

0.4 - 2014-03-04

0.3 - 2014-01-23

  • renamed package to plone.jsonapi.core due to namespace conflicts with plone.jsonapi.routes

  • removed default plone route configuration.

  • added version route

  • changed the url_for method of the router to provide correct urls for virtual hosting.

0.2 - 2013-08-11

  • Router implementation updated to support decorated functions as route providers.

  • url_for functionality implemented

  • documentation changed

0.1 - unreleased

  • initial start of development

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

plone.jsonapi.core-0.7.0.tar.gz (45.5 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