Creating a production ready API with Python and Django Rest Framework - part 1

Posted on Wed 28 September 2016 in Development • Tagged with API, Django, framework, Python, rest, tutorial

The aim if this tutorial is to show how to create a production ready solution for a REST API, using Python and Django Rest Framework. I will show you how to first create a very basic API, how to handle the authentication and permissions and I will cover deployment and hosting of images. The full source code of the tutorial is available at: https://github.com/andreagrandi/drf-tutorial

Summary of the complete tutorial

  1. Create the basic structure for the API
  2. Add Authentication and POST methods
  3. Handling details and changes to existing data
  4. Testing the API
  5. Switching from Sqlite to PostgreSQL
  6. Hosting the API on Heroku
  7. Add an Image field and save images to S3

Create the basic structure for the API

For this tutorial I will assume you have correctly installed at least Python (I will use Python 2.7.x), virtualenv and virtualenvwrapper on your system and I will explain how to create everything else step by step.

Note: at the time of writing, the tutorial has been based on Django 1.10.1 and Django Rest Framework 3.4.7

Creating the main project structure

mkdir drf-tutorial
mkvirtualenv drf-tutorial
cd drf-tutorial
pip install django djangorestframework
django-admin.py startproject drftutorial .
cd drftutorial
django-admin.py startapp catalog

Data Model

We will create the API for a generic products catalog, using a very simple structure (to keep things simple). Edit the file catalog/models.py adding these lines:

from __future__ import unicode_literals
from django.db import models


class Product(models.Model):
    name = models.CharField(max_length=255)
    description = models.TextField()
    price = models.DecimalField(decimal_places=2, max_digits=20)

after creating the model, we need to add 'catalog' application to INSTALLED_APPS. Edit settings.py and add the app at the end of the list:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'catalog',
]

at this point the Django application will be recognised by the project and we can create and apply the schema migration:

(drf-tutorial) ➜  drftutorial git:(235dfcc) ✗ ./manage.py makemigrations
Migrations for 'catalog':
    catalog/migrations/0001_initial.py:
        - Create model Product

(drf-tutorial) ➜  drftutorial git:(235dfcc) ✗ ./manage.py migrate
Operations to perform:
    Apply all migrations: admin, auth, catalog, contenttypes, sessions
    Running migrations:
        Applying contenttypes.0001_initial... OK
        Applying auth.0001_initial... OK
        Applying admin.0001_initial... OK
        Applying admin.0002_logentry_remove_auto_add... OK
        Applying contenttypes.0002_remove_content_type_name... OK
        Applying auth.0002_alter_permission_name_max_length... OK
        Applying auth.0003_alter_user_email_max_length... OK
        Applying auth.0004_alter_user_username_opts... OK
        Applying auth.0005_alter_user_last_login_null... OK
        Applying auth.0006_require_contenttypes_0002... OK
        Applying auth.0007_alter_validators_add_error_messages... OK
        Applying auth.0008_alter_user_username_max_length... OK
        Applying catalog.0001_initial... OK
        Applying sessions.0001_initial... OK

API Serializer

Serializers are those components used to convert the received data from JSON format to the relative Django model and viceversa. Create the new file catalog/serializers.py and place this code inside:

from .models import Product
from rest_framework import serializers


class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ('name', 'description', 'price')

In this case we are using a ModelSerializer. We need to create a new class from it, and specify the model attribute, that's it. In this case we also specify the fields we want to return.

API View

The serializer alone is not able to respond to an API request, that's why we need to implement a view. In this first version of the view (that we will improve soon) we will "manually" transform the data available in the serializer dictionary to a JSON response. In catalog/views.py add this code:

from django.http import HttpResponse
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from .models import Product
from .serializers import ProductSerializer


class JSONResponse(HttpResponse):
    """
    An HttpResponse that renders its content into JSON.
    """
    def __init__(self, data, **kwargs):
        content = JSONRenderer().render(data)
        kwargs['content_type'] = 'application/json'
        super(JSONResponse, self).__init__(content, **kwargs)


def product_list(request):
    if request.method == 'GET':
        products = Product.objects.all()
        serializer = ProductSerializer(products, many=True)
        return JSONResponse(serializer.data)

At this point we need to tell our Django app to use this API view when a certain URL is requested. We first need to add this code in catalog/urls.py

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^products/$', views.product_list),
]

and finally we need to add this to drftutorial/urls.py

from django.conf.urls import url, include
from django.contrib import admin

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^', include('catalog.urls')),
]

Testing our work

At this point we should be able to start our Django app:

./manage.py runserver

Let's install a tool that will help us to test the API:

pip install httpie

now we can use it to call our URL:

$ http http://127.0.0.1:8000/products/
HTTP/1.0 200 OK
Content-Type: application/json
Date: Wed, 28 Sep 2016 09:54:50 GMT
Server: WSGIServer/0.1 Python/2.7.11
X-Frame-Options: SAMEORIGIN

[]

It works! It's an empty response of course, because we still don't have any data to show, but we will see later how to load some example data in our database. If you have been able to follow the tutorial up to this point, that's good. If not, don't worry. You can checkout the code at exactly this point of the tutorial doing:

git checkout tutorial-1.0

Improving the API View

There is a quicker and more efficient way of implementing the same API view we have seen before. We can use a class based view, in particular the APIView class and also have the JSON response implemented automatically. Change the code inside catalog/views.py with this one:

from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializer


class ProductList(APIView):
    def get(self, request, format=None):
        products = Product.objects.all()
        serializer = ProductSerializer(products, many=True)
        return Response(serializer.data)

You will also have to change catalog/urls.py in this way:

urlpatterns = [
    url(r'^products/$', views.ProductList.as_view()),
]

You can check the source code for this step of the tutorial with:

git checkout tutorial-1.1

There is also another way of writing the same view. Let's try it with ListAPIView. Edit catalog/views.py again and substitute the code with this one:

from django.http import HttpResponse
from rest_framework import generics
from rest_framework.response import Response
from .models import Product
from .serializers import ProductSerializer


class ProductList(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

With a ListAPIView we are basically creating a read-only API that is supposed to return a list of items. We need to specify a queryset and the serializer_class parameters. Once again, you can get up to this point, checking out the related git tag:

git checkout tutorial-1.2

Creating Initial Data

An API that doesn't return any data is not very useful, right? Also, at the moment we haven't implemented yet any feature that let us insert any data. That's why I've created a data migration for you that will insert some data for you. You may notice that the example data contains some Italian products... out of the scope of this tutorial, I strongly advise you to google this products and if you ever happen to visit Italy, try them. You won't regret!

Going back to our data migration, you first have to create an empty one with:

./manage.py makemigrations --empty catalog

and then open the file that has been created under catalog/migrations/ named 0002_..... (your name will be different from mine, so just edit the one starting with 0002 and you will be fine) and fill it with this code:

from __future__ import unicode_literals
from django.db import migrations


def create_initial_products(apps, schema_editor):
    Product = apps.get_model('catalog', 'Product')

    Product(name='Salame', description='Salame Toscano', price=12).save()
    Product(name='Olio Balsamico', description='Olio balsamico di Modena', price=10).save()
    Product(name='Parmigiano', description='Parmigiano Reggiano', price=8.50).save()
    Product(name='Olio', description='Olio Oliva Toscano', price=13).save()
    Product(name='Porchetta', description='Porchetta toscana cotta a legna', price=7.50).save()
    Product(name='Cantucci', description='Cantucci di Prato', price=4).save()
    Product(name='Vino Rosso', description='Vino Rosso del Chianti', price=9.50).save()
    Product(name='Brigidini', description='Brigidini di Lamporecchio', price=3.50).save()


class Migration(migrations.Migration):

    dependencies = [
        ('catalog', '0001_initial'),
    ]

    operations = [
        migrations.RunPython(create_initial_products),
    ]

to apply the migration we just created, just do:

./manage.py migrate

If you try to test the API again from the command line, you will get these products back:

$ http http://127.0.0.1:8000/products/
HTTP/1.0 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Date: Wed, 28 Sep 2016 12:29:36 GMT
Server: WSGIServer/0.1 Python/2.7.11
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

[
    {
        "description": "Salame Toscano",
        "name": "Salame",
        "price": "12.00"
    },
    {
        "description": "Olio balsamico di Modena",
        "name": "Olio Balsamico",
        "price": "10.00"
    },
    {
        "description": "Parmigiano Reggiano",
        "name": "Parmigiano",
        "price": "8.50"
    },
    {
        "description": "Olio Oliva Toscano",
        "name": "Olio",
        "price": "13.00"
    },
    {
        "description": "Porchetta toscana cotta a legna",
        "name": "Porchetta",
        "price": "7.50"
    },
    {
        "description": "Cantucci di Prato",
        "name": "Cantucci",
        "price": "4.00"
    },
    {
        "description": "Vino Rosso del Chianti",
        "name": "Vino Rosso",
        "price": "9.50"
    },
    {
        "description": "Brigidini di Lamporecchio",
        "name": "Brigidini",
        "price": "3.50"
    }
]

Again, you can get up to this point with:

git checkout tutorial-1.3

One more thing...

No, we are not going to present a new amazing device, I'm sorry, but I want to show you a nice Django Rest Framework feature you can have without much additional work. Edit settings.py and add rest_framework to the list of INSTALLED_APPS:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'catalog',
]

Now, if you are still running the Django app, try to visit this url from your browser: http://127.0.0.1:8000/products/
That's very nice, isn't it? You can have browsable APIs at no cost.

Wrapping Up

I've mentioned at the beginning that this is just the first part of my API tutorial. In the next part I will show you how to let the consumer of your API add some products and leave reviews (we will introduce a new model for this). Also, we will see how to set proper permissions to these new API methods so that only admin users will be able to add products while normal users will be able to add reviews. So, if you feel ready, you can begin to follow the second part of this tutorial

References

Some parts of this tutorial and a few examples have been taken directly from the original Django Rest Framework tutorial.


Using Python ipdb from Jupyter

Posted on Tue 10 May 2016 in Development • Tagged with debugging, ipdb, Python

If we try to use the usual ipdb commands from a Jupyter (IPython notebook)

import ipdb; ipdb.set_trace()

we will get a similar error:

--------------------------------------------------------------------------
MultipleInstanceError                     Traceback (most recent call last)
<ipython-input-1-f2b356251c56> in <module>()
    1 a=4
----> 2 import ipdb; ipdb.set_trace()
    3 b=5
    4 print a
    5 print b

/home/nnn/anaconda/lib/python2.7/site-packages/ipdb/__init__.py in <module>()
    14 # You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.
    15 
---> 16 from ipdb.__main__ import set_trace, post_mortem, pm, run, runcall, runeval, launch_ipdb_on_exception
    17 
    18 pm                       # please pyflakes

/home/nnn/anaconda/lib/python2.7/site-packages/ipdb/__main__.py in <module>()
    71         # the instance method will create a new one without loading the config.
    72         # i.e: if we are in an embed instance we do not want to load the config.
---> 73         ipapp = TerminalIPythonApp.instance()
    74         shell = get_ipython()
    75         def_colors = shell.colors

/home/nnn/anaconda/lib/python2.7/site-packages/traitlets/config/configurable.pyc in instance(cls, *args, **kwargs)
    413             raise MultipleInstanceError(
    414                 'Multiple incompatible subclass instances of '
--> 415                 '%s are being created.' % cls.__name__
    416             )
    417

MultipleInstanceError: Multiple incompatible subclass instances of TerminalIPythonApp are being created.

The solution is to use Tracer instead:

from IPython.core.debugger import Tracer
Tracer()()

Source: http://stackoverflow.com/questions/35613249/using-ipdb-to-debug-python-code-in-one-cell-jupyter-or-ipython


How to publish a Python package to PyPI

Posted on Sun 10 April 2016 in Development • Tagged with pip, pypi, Python

PyPI is the Python Package Index, that archive that let you install a package using pip, for example: pip install Flask

In the past days I started writing a Python API client for Toshl expense manager and I decided to publish the library on PyPI. You can have a look at my library here https://github.com/andreagrandi/toshl-python (please note: it's still in development and Toshl API is not even public yet) in case you are not sure how to structure it.

I found a nice guide but it wasn't complete (for example it didn't say how to sign packages) so I decided to rewrite it adding more information.

Create PyPI accounts

To publish packages on PyPI you need to create two accounts: one for the production server and another one for the test server. When you register, please specify (if you have one, but I really hope you do) the PGP id of your public key. Once the accounts are created, you need to create a file named .pypirc in your \$HOME folder containing the following configuration:

[distutils]
index-servers =
pypi
pypitest

[pypi]
repository=https://pypi.python.org/pypi
username=your_username
password=your_password

[pypitest]
repository=https://testpypi.python.org/pypi
username=your_username
password=your_password

Please substitute your_username and your_password with the details you sent during the registration.

Preparing the package

I assume you have structured your library in the proper way and have included a setup.py with all the configuration (it's not something specific to PyPI so you should have done it already). If you haven't I remember you can give a look at my library here https://github.com/andreagrandi/toshl-python in particular to the setup.py:

from setuptools import setup, find_packages

setup(
    name='toshl',
    version='0.0.3',
    url='https://github.com/andreagrandi/toshl-python',
    download_url='https://github.com/andreagrandi/toshl-python/tarball/0.0.3',
    author='Andrea Grandi',
    author_email='[email protected]',
    description='Python client library for Toshl API.',
    packages=find_packages(exclude=['tests']),
    zip_safe=False,
    include_package_data=True,
    platforms='any',
    license='MIT',
    install_requires=[
        'requests==2.9.1',
    ],
)

Upload the package to PyPI Test server

The first time you upload the package you will need to register it:

python setup.py register -r pypitest

and then you will need to build the package and upload it (please note I'm using the --sign to sign the package with PGP):

python setup.py sdist upload --sign -r pypitest

Upload the package to PyPI production server

Once you have verified that you are able to build and upload the package to the test server (without getting any errors), you should upload it to the production server:

python setup.py register -r pypi
python setup.py sdist upload --sign -r pypi

This is everything you need to do if you want to publish a Python package on PyPI. Happy coding!


Using a light sensor with BBC micro:bit and MicroPython

Posted on Mon 08 February 2016 in Development • Tagged with bbc, breadboard, embedded, microbit, micropython, howto, python

A light sensor is a small component with a particular characteristic: it is basically a resistor and its resistance decreases if the light is more intense. To use it with micro:bit we need to use one of the analogic ports. To build this circuit you will need a breadboard, 3 jumper wires, a 10k resistance and possibly a Kitronik breadboard kit.

The project

I wanted to realise a simple project where, depending on the light intensity captured by the light sensor, the micro:bit shows an image of the Sun if the light is intense and an image of the Moon if the light is less intense.

Here is the complete circuit scheme:

microbit_breadboard_schema_light

"Image Copyright © Kitronik"

and here is a picture of the finished project I created:

microbit_breadboard_example_2

The source code I needed is available here:

and as a demo I realised this small video: