Accueil / Blog / Métier / Archive du blog de l'année 2013 / Multi-format RESTful APIs with Cornice

Multi-format RESTful APIs with Cornice

Par Alex Marandon 15/11/2013
How to create a RESTful web API that support writing and reading multiple formats using Cornice and Pyramid

Cornice is a library from the Mozilla folks that makes it really easy to create RESTful web services in Python, using the Pyramid web framework. Cornice was able to send responses in multiple formats using content negotiation based on the Accept header. Recently Makina Corpus made a contribution that unlocked the potential of creating APIs that can also receive requests in multiple formats.

Cornice comes with a Pyramid template that allows to create a project directory using Cornice in one command:

$ pcreate -t cornice myapp

In the freshly created project directory, we'll find a file myapp/views.py, which already contains a really simple service that returns a JSON response:

hello = Service(name='hello', path='/', description="Simplest app")

@hello.get()
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's start a development server (you might need to install waitress first):

$ python setup.py develop
$ pserve --reload myapp.ini

And check that we can send a request and get a response back:

$ curl -D - http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 10:39:41 GMT
Server: waitress

{"Hello": "World"}

Now let's say we'd like our API to send a response in plain text. We can use content negotiation with the Accept header and create a Pyramid renderer that will format a plain text response. First we create a plain text renderer. It can go in myapp/__init__.py:

class TextRenderer(object):

    def __init__(self, info):
        pass

    def __call__(self, value, system):
        request = system.get('request')
        if request is not None:
            response = request.response
            response.content_type = 'text/plain'
        name = value['Hello']
        return u"Hello, {}!".format(name)

We need to register this new renderer in our app main function:

def main(global_config, **settings):
    config = Configurator(settings=settings)
    config.include("cornice")
    config.scan("myapp.views")
    config.add_renderer('text', TextRenderer)
    return config.make_wsgi_app()

Now we update our service so that it uses the correct renderer based on the Accept header:

@hello.get(accept='text/plain', renderer='text')
@hello.get(accept='application/json', renderer='json')
def get_info(request):
    """Returns Hello in JSON."""
    return {'Hello': 'World'}

Let's check that it works:

$ curl -D - -H 'Accept: text/plain' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:41 GMT
Server: waitress

Hello, World!


$ curl -D - -H 'Accept: application/json' http://0.0.0.0:6543
HTTP/1.1 200 OK
Content-Length: 18
Content-Type: application/json; charset=UTF-8
Date: Fri, 15 Nov 2013 11:22:48 GMT
Server: waitress

{"Hello": "World"}

This is fine but now we'd like to produce a response based on user input. We're going to allow our users to provide their name in plain text or in JSON. We also want to validate user input to make sure the name we receive is at least two characters long. Cornice integrates well with the Colander validation library Make sure you have at least colander 1.0b1 installed.

Let's create a colander schema to implement our validation rule:

import colander

class NameSchema(colander.MappingSchema):
    name = colander.SchemaNode(colander.String(),
                               validator=colander.Length(2))

Then we write a POST request handler that is wired up with our Colander schema:

@hello.post(accept='text/plain', renderer='text', schema=NameSchema)
@hello.post(accept='application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

Let's see how this works if I post JSON containing my name:

$ curl -H 'Accept: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This isn't working because we haven't told the server what format we were using. If we don't specify the format, Cornice will assume we're sending application/x-www-form-urlencoded, which is the format sent by web browsers when we submit a form. So this will work:

$ curl -H 'Accept: application/json' http://0.0.0.0:6543 -d 'name=Alex'
{"Hello": "Alex"}

To get Cornice to validate JSON input properly, we need to specify the Content-Type header:

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "Alex"}'
{"Hello": "Alex"}

By the way, does our validation rule work as expected?

$ curl -H 'Accept: application/json' -H 'Content-Type: application/json' http://0.0.0.0:6543 -d '{"name": "A"}'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "Shorter than minimum length 2"}]}

Perfect! But remember that we'd also like to be able to send our name in plain text as the request body, without any structured format around it:

$ curl -H 'Accept: application/json' -H 'Content-Type: text/plain' http://0.0.0.0:6543 -d 'Alex'
{"status": "error", "errors": [{"location": "body", "name": "name", "description": "name is missing"}]}

This is not working because out of the box Cornice can only validate JSON and form-urlencoded data. To tell our app how to interpret plain text, we need to add a deserializer. A deserializer is just a callable taking a request argument and returning a Python data structure that Colander can understand (a cstruct). In this case the callable simply returns a dictionary containing a single entry name whose value is the request body:

def plain_text_deserializer(request):
    # Here we could be parsing XML or any complex format as long as we're
    # returning a dictionary with the same structure
    return {'name': request.body}

And we assign our new deserializer to our POST handler:

@hello.post(accept='text/plain', renderer='text', schema=NameSchema,
            deserializer=plain_text_deserializer)
@hello.post(accept='application/json', renderer='json', schema=NameSchema)
def post_info(request):
    """Returns Hello in JSON."""
    name = request.validated['name']
    return {'Hello': name}

And now we can post plain text:

$ curl -H 'Accept: text/plain' -H 'Content-Type: text/plain' http://0.0.0.0:6543 -d 'Al'
Hello, Al!

So this is basically how we implement read/write multi-format APIs in Cornice with Colander validation: we make sure we have appropriate renderers and deserializers for each format and we plug them into our view configuration. Of course in any real-world scenario your renderers and deserializers will be much more complex.

Thank you for reading so far and special thanks to Alexis Métaireau for merging our work on deserializers.

Mots-clés associés : ,
Derniers articles
Formation PostgreSQL / PostGIS du 3 au 5 décembre à ToulouseFormation PostgreSQL / PostGIS du 3 au 5 décembre à Toulouse 21/10/2014 Vous êtes technicien, responsable SIG ou même développeur ? Venez suivre la formation base de données PostgreSQL / PostGIS à Toulouse du 3 au 5 décembre. Anael Boulier
Retour sur le petit déjeuner Drupal 8 20/10/2014 Si vous n'avez pas pu assister à la présentation de Simon Georges à Toulouse ou de Pierre Rineau à Nantes, voici leurs notes... Anael Boulier
Drupal, tous des experts ?Drupal, tous des experts ? 17/10/2014 Comment (bien) faire du Drupal ? Faut-il utiliser ce module ou préférer le coder ? Mon budget permet-il de réaliser ces développements ? Ne vous posez plus ces questions et venez vous former avec des experts ! Anael Boulier
Formation Talend Data Integration du 25 au 27 novembre à ToulouseFormation Talend Data Integration du 25 au 27 novembre à Toulouse 17/10/2014 Venez prendre en main Talend Open Studio for Data Integration lors de notre prochaine formation à Toulouse. Anael Boulier