# Content Negotiation with the Pyramid Web Framework

Ever since college, I've always enjoyed working with Python. Even though I mainly work in .NET, every now and again I take a look over to see what's happening in the Python world. Lately, I've been digging into web frameworks to see what it's like to create Web APIs using Python.

A friend of mine, [Michael Kennedy](https://twitter.com/mkennedy?ref=cecilphillip.com), has some awesome [Python courses](https://training.talkpython.fm/?ref=cecilphillip.com) that I've been going through. In one of his courses, he introduces the [Pyramid web framework](http://docs.pylonsproject.org/projects/pyramid/en/latest?ref=cecilphillip.com). Even though it's only been about a week, I've thoroughly enjoyed working with `Pyramid`. It's easy to get started with, works with Python 2.7+ and 3.4+, and there is even support for it in Pycharm.

Now since I really like building APIs, one of the first things I wanted to do was see how content negotiation works in `Pyramid`.

###### What is Content Negotiation?

If you're unfamiliar with content negotiation, it is essentially a mechanism that allows clients to state the format they expect the response from the server to be returned as. In the world of HTTP and Web APIs, it allows clients to specify whether to be given JSON or XML, what the language is, and even the content encoding.

> Check out the [RFC 2616](http://www.ietf.org/rfc/rfc2616.txt?ref=cecilphillip.com) for a more detailed definition of content negotiation.

###### Setting up the environment

The first thing I'm going to do is create my work space. I'll open up the command terminal, create new folder on my machine, and navigate into it. Next, I'll create and activate a new Python [virtual environment](https://docs.python.org/3/library/venv.html?ref=cecilphillip.com) using the following commands.

On OSX and Linux:

    $ python3 -m venv pyramid-env
    $ . pyramid-env/bin/activate
    (pyramid-env) $ pip install --upgrade pip setuptools
    

On Windows:

    C:\folder> python -m venv pyramid-env
    C:\folder> pyramid-env\Scripts\activate.bat
    (pyramid-env) C:\folder> pip install --upgrade pip setuptools
    

You'll know that activation of the environment was successful if you see the name of the environment to the left of the command terminal.

Next, we're going to need to install pyramid and some other packages into the virtual environment. We can do that easily with pip.

    (pyramid-env) $ pip install pyramid factory-boy Faker vobject requests
    

###### Creating a simple Pyramid API

Let's start off with setting up the routes and instantiating the application. Here's what my initial `app.py` file looks like:

    from wsgiref.simple_server import make_server
    from pyramid.config import Configurator
    
    def configure_renderers(config):
        json_renderer = JSON()
        json_renderer.add_adapter(Customer, lambda p, _: p.__dict__)
        config.add_renderer('json', json_renderer)
    
    if __name__ == '__main__':
        config = Configurator()
        config.add_route('customers', '/api/customers')
        config.add_route('customer', '/api/customers/{name}')
        config.scan('views')
        configure_renderers(config)
    
        app = config.make_wsgi_app()
        server = make_server('0.0.0.0', 6363, app)
        server.serve_forever()
    

Using the `Configurator`, I'm creating two simple API [routes](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html?ref=cecilphillip.com) and also creating a WSGI application. However, this application still doesn't do anything. The routes have been defined, but there isn't anything setup to handle requests to matching routes. This is the job of views in `Pyramid`. I created a `views.py` file and added some methods to handle requests.

    from pyramid.httpexceptions import HTTPNotFound
    from pyramid.view import view_config
    from customer import CustomerFactory
    
    __CUSTOMTERS = CustomerFactory.create_batch(20)
    
    @view_config(route_name='customers',
                 renderer='json',
                 request_method='GET',
                 accept="application/json")
    def retrieve_customers(request):
        return __CUSTOMTERS
    
    @view_config(route_name='customer',
                 renderer='json',
                 request_method='GET',
                 accept="application/json")
    def retrieve_customer(request):
        name = request.matchdict['name']
        customer = [cus for cus in __CUSTOMTERS if cus.first_name.lower() == name.lower()]
        return customer[0] if any(customer) else HTTPNotFound()
    

I created one view method for each of the previously defined routes. The `retrieve_customers` view method returns a list of customers, while the `retrieve_customer` view method returns a single customer based on their first name.

From the `pyramid.view` package, I imported `view_config` decorator to associate my routes with the methods that will handle them. In addition to the route name, notice I can use `view_config` to specify the HTTP method and accept header values that should be matched against the request. What's even more interesting is that I can define what render to use. The job of the render is to serialize data returned from `Pyramid` view method and turn it into string. In the example above, I'm configuring the views to return the customer data in a JSON format.

###### Generating fake data

Where exactly is this customer data coming from? Well, I didn't feel the need to setup a database or copy/paste values into a file. Instead, I used the [factory boy](https://factoryboy.readthedocs.io/en/latest/?ref=cecilphillip.com) package to generate that fake data for me.

    import factory
    import json
    
    class Customer:
        def __init__(self, first_name, last_name, email):
            self.first_name = first_name
            self.last_name = last_name
            self.email = email
    
        def __str__(self):
            return json.dumps(self.__dict__)
    
    
    class CustomerFactory(factory.Factory):
        class Meta:
            model = Customer
    
        first_name = factory.Faker('first_name')
        last_name = factory.Faker('last_name')
        email = factory.Faker('email')
    

Inside of the `views.py` file, I import`CustomerFactory` to generate 20 customer records.

###### Making your first requests

Everything should be in place now to make requests to the API. You can run the application by just issuing the following command in the command terminal.

    (pyramid-env) $ python app.py
    

Based on the configuration, the application should be running at `http://localhost:6363`. Open up Postman, cURL, or whatever your HTTP tool of choice is and make a request to the `/api/customers` endpoint with and accept header of `application/json`.

Here's an example of making a request with cURL.

    (pyramid-env) $ curl http://localhost:6363/api/customers -X "GET" -H "Accept: application/json"
    

###### Creating custom renderers

Setting up `Pyramid` to return JSON was pretty simple, but what if I wanted to support different formats? What we can do in this case is create [custom renderers](http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/renderers.html?ref=cecilphillip.com#adding-a-new-renderer).

To do this, you will need to create a renderer factory that conforms to the [IRendererFactory interface](http://docs.pylonsproject.org/projects/pyramid/en/latest/api/interfaces.html?ref=cecilphillip.com#pyramid.interfaces.IRendererFactory). When invoked, the factory will need to return a render that conforms to the [IRenderer interface](http://docs.pylonsproject.org/projects/pyramid/en/latest/api/interfaces.html?ref=cecilphillip.com#pyramid.interfaces.IRenderer).

Here are two examples of render factories. One takes a single customer and returns their Gravatar image, while the second converts the customer information into a vCard.

    import vobject
    from faker import Factory as FakeFactory
    from hashlib import md5
    import requests
    from io import BytesIO
    from pyramid import renderers
    
    class GravatarRendererFactory:
        def __call__(self, info):
            def _renderer(value, system):
                _GRAVATAR_URL_TEMPLATE = 'http://www.gravatar.com/avatar/{}'
    
                request = system.get('request')
                request.response.content_type = 'image/png'
    
                email = value.email
                gravatar_hash = md5(email.encode('utf-8')).hexdigest()
                gravatar_url = _GRAVATAR_URL_TEMPLATE.format(gravatar_hash)
                gravatar_response = requests.get(gravatar_url)
                request.response.content_type = 'image/png'
                return BytesIO(gravatar_response.content)
            return _renderer
        
    class VCardRendererFactory:
        def __call__(self, info):
            def _renderer(value, system):
                request = system.get('request')
                request.response.content_type = 'text/vcard'
                vCard = VCardRenderer._customer_to_vcard(value)
                return vCard.serialize()
            return _renderer
    
        @staticmethod
        def _customer_to_vcard(customer):
            fakeFactory = FakeFactory.create()
    
            vCard = vobject.vCard()
            vCard.add('n')
            vCard.n.value = vobject.vcard.Name(family=customer.last_name, given=customer.first_name)
            vCard.add('fn',)
            vCard.fn.value = '{} {}'.format(customer.first_name, customer.last_name)
            vCard.add('email')
            vCard.email.value = customer.email
            vCard.email.type_param = 'WORK'
            tel = vCard.add('TEL')
            tel.value = fakeFactory.phone_number()
            tel.type_param = 'WORK'
            tel = vCard.add('TEL')
            tel.value = fakeFactory.phone_number()
            tel.type_param = 'HOME'
            vCard.add('title')
            vCard.title.value = fakeFactory.job()
    
    

Now that the renderers are created, they need to get added to the `Configurator`. Here's what the updated `app.py` looks like.

    from wsgiref.simple_server import make_server
    from pyramid.config import Configurator
    from pyramid.renderers import JSON
    from customer import Customer
    from renderers import VCardRendererFactory, GravatarRendererFactory
    
    def configure_renderers(config):
        json_renderer = JSON()
        json_renderer.add_adapter(Customer, lambda p, _: p.__dict__)
        config.add_renderer('json', json_renderer)
        config.add_renderer('vcard', VCardRendererFactory())
        config.add_renderer('img', GravatarRendererFactory())
    
    if __name__ == '__main__':
        config = Configurator()
        config.add_route('customers', '/api/customers')
        config.add_route('customer', '/api/customers/{name}')
        config.scan('views')
    
        configure_renderers(config)
        app = config.make_wsgi_app()
        server = make_server('0.0.0.0', 6363, app)
        server.serve_forever()
    
    

`configure_renderers` has been updated to map the new render factories.

###### Using the new renderers

What I want to be able to do with this API is to make requests for an individual customer, but be able to control what that data looks like. Sometimes I might want JSON data, or a vCard or maybe just a profile picture from Gravatar.

To do this, we have to update the `view_config` setup in `views.py` to be aware of the new renderers.

    from pyramid.httpexceptions import HTTPNotFound
    from pyramid.view import view_config
    from customer import CustomerFactory
    
    __CUSTOMTERS = CustomerFactory.create_batch(20)
    
    @view_config(route_name='customers', renderer='json', 
                 request_method='GET', accept="application/json")
    def retrieve_customers(request):
        return __CUSTOMTERS
    
    @view_config(route_name='customer', renderer='json', 
                 request_method='GET', accept="application/json")
    @view_config(route_name='customer', renderer='vcard', 
                 request_method='GET', accept="text/vcard")
    @view_config(route_name='customer', renderer='img',
                 request_method='GET', accept="image/png")
    def retrieve_customer(request):
        name = request.matchdict['name']
        customer = [cus for cus in __CUSTOMTERS if cus.first_name.lower() == name.lower()]
        return customer[0] if any(customer) else HTTPNotFound()
    
    

On the `retrieve_customer` method, I've added multiple `view_config` decorators. They all use the same route but the renderer and accept parameters are different. So if I issue a request for a customer using a `text/vcard` value in the Accept header of the HTTP request, I expect to get back the customer data as in vcard format. Let's see what this looks like in Postman.

Here's a request with the Accept header set to `application/json`.  
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709341133630/d69f8cf0-aeeb-4cbc-ae7c-0f1d5d26440e.png)  
Here's a request with the Accept header set to `text/vcard`.  
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709341134798/75915e13-3966-4b91-91dd-620f32161c4e.png)  
Here's a request with the Accept header set to `image/png`.  
![](https://cdn.hashnode.com/res/hashnode/image/upload/v1709341135983/1dffa80f-54d0-4f4b-843b-7ae75fb1131c.png)  
Since I'm generating fake emails, Gravatar is going to return the default image. You could easily swap in your own email address to try it out.

###### Cleaning things up

Everything is running the way I wanted, but there's one last thing that's bugging me. Having to place multiple `view_config` decorators on top of a view method is bothering me. What I would like to do is place one decorator on a view method and have it select the correct renderer from a mapping of supported media types.

What I decided to do was create another custom renderer; a `NegotiatingRender`. Here's what it looks like.

    class NegotiatingRendererFactory:
        _CONNEG_MAPPINGS = {
            'text/plain': renderers.string_renderer_factory
        }
    
        def __init__(self, mappings=None, **kw):
            if mappings is None:
                mappings = {}
    
            NegotiatingRendererFactory._CONNEG_MAPPINGS.update(mappings)
            NegotiatingRendererFactory._CONNEG_MAPPINGS.update(kw)
    
        def __call__(self, info):
            def _render(value, system):
                request = system.get('request')
                accept_header = request.headers['accept']
    
                for key in NegotiatingRendererFactory._CONNEG_MAPPINGS:
                    if key in accept_header:
                        negotiated_render = NegotiatingRendererFactory._CONNEG_MAPPINGS[key]
                        result = negotiated_render(info)(value, system)
                        return result
    
            return _render
    

Essentially what this renderer does is match the accept header in the request to a renderer that's been mapped to respond to that header value. In the `app.py` file, the `configure_renderers` method needs to be updated to register the `NegotiatingRendererFactory`.

    def configure_renderers(config):
        json_renderer = JSON()
        json_renderer.add_adapter(Customer, lambda p, _: p.__dict__)
        config.add_renderer('json', json_renderer)
        config.add_renderer('vcard', VCardRendererFactory())
        config.add_renderer('img', GravatarRendererFactory())
    
        mappings = {
            'application/json' : json_renderer,
            'text/vcard': VCardRendererFactory(),
            'image/png': GravatarRendererFactory()
        }
        negotiator = NegotiatingRendererFactory(mappings)
        config.add_renderer('negotiate', negotiator)
    

With that in place, we should be able to reduce the number of `view_config` decorators to one that will use the `negotiate` renderer instead.

    from pyramid.httpexceptions import HTTPNotFound
    from pyramid.view import view_config
    from customer import CustomerFactory
    
    __CUSTOMTERS = CustomerFactory.create_batch(20)
    
    @view_config(route_name='customers', renderer='json', 
                 request_method='GET', accept="application/json")
    def retrieve_customers(request):
        return __CUSTOMTERS
    
    @view_config(route_name='customer', renderer='negotiate', request_method='GET')
    def retrieve_customer(request):
        name = request.matchdict['name']
        customer = [cus for cus in __CUSTOMTERS if cus.first_name.lower() == name.lower()]
        return customer[0] if any(customer) else HTTPNotFound()
    

Trying out the same requests as before, you'll notice the results should be almost identical. The code also feels much cleaner in my opinion, and it's pretty easy to plug in another renderer.

###### Conclusion

I would love some feedback on this approach though. Considering I've only been using Pyramid for a few weeks, there might be a better way to do this.

I shared the code in a [Github repo](https://github.com/cecilphillip/pyramid-content-negotiation/?ref=cecilphillip.com), so check it out and let me know what you think.
