Simple API with Django Rest Framework

Django Rest Framework(DRF - this is how it's called now) allows to get started with REST APIs very quickly. As all packages in Django, DRF has steep learning curve, but so does everything in programming.


To see end product: https://adamw.eu/prime/

First of all let's install what we need, if you are working in virtualenv then do it there or copy to the folder . Use debugger too. This one is good django-debug-panel :

pip install djangorestframework

Let's install it:

INSTALLED_APPS = [
     #...
    'rest_framework',
    'prime', # your app name
]

Make sure main urls have an entry to our app:

from django.urls import path,include

urlpatterns = [
    #...
    path(r'prime/', include('prime.urls')),
]

Direct app to it's soon-to-be-written views:

from django.urls import path, include
from . import  views
urlpatterns = [
    path('', views.PrimeHome.as_view()),
    path('<int:prime>/', views.AskPrime.as_view()),
]

 

As you can see we will be displaying Home page and also taking number from the link to display API response. Now let's start with our views. We will be using class views that will be inheriting from DRF templates.

First imports:

from rest_framework import views #ready DRF views
from rest_framework.response import Response 
from .is_prime import is_prime #our formula to check if number is prime
from .serializers import PrimeSerializer # more below 
from django.shortcuts import render

Our home view will simply render readme file:

class PrimeHome(views.APIView):
    """API Home View"""
    def get(self, request):
        """get method"""
        return render(request, 'prime/README.html')

This doesn't have to be class view but I am using it in this case.

Our AskPrime view will handle prime/<number_to_check> requests. Starting from above, name and description are just DRF class variables that will be displayed in html version of api. 

Our get method will take request and prime number from the link that we defined in urls.py above.

We will first check if number isn't too big- I am using it as protection vs some gigantic numbers and since I am not using database in this case only calculating it on demand there has to be some limit.

If number is ok, we will use formula to check if number is prime and pass positive msg.

class AskPrime(views.APIView):
    """API Ask View"""
    name = "Primality Test"
    description = \
    "Ask if number is prime number /prime/<number_to_test>.\
    Only accepts integers, for ex 10^6 is not valid query"
    """Ask if number is prime number /prime/<number_to_test>.
    Only accepts integers, for ex 10^6 is not valid query"""

    def get(self, request, prime):
        """get method"""
        if prime > 10**9:
            test_prime = None
            msg = "Number too big, max 10^9 i.e. one billion"
        else:
            test_prime = is_prime(prime)
            msg = "Prime test complete."
        data = [{"number": prime, "is_prime": test_prime, "msg":msg,}]
        results = PrimeSerializer(data, many=True).data
        return Response(results)

As you can see we will also need serializer that we will pass our data to. In new file serializers.py , there are some default serializers provided in DRF but this one is simple custom one needed for our data  :

from rest_framework import serializers

class PrimeSerializer(serializers.Serializer):
    """Custom serializer for AskPrime view"""
    number = serializers.IntegerField()
    is_prime = serializers.BooleanField()
    msg = serializers.CharField()

I am using below prime test that I have saved in is_prime.py file and imported above.

def _try_composite(a, d, n, s):
    if pow(a, d, n) == 1:
        return False
    for i in range(s):
        if pow(a, 2**i * d, n) == n-1:
            return False
    return True # n  is definitely composite
 
def is_prime(n, _precision_for_huge_n=16):
    if n in _known_primes:
        return True
    if any((n % p) == 0 for p in _known_primes) or n in (0, 1):
        return False
    d, s = n - 1, 0
    while not d % 2:
        d, s = d >> 1, s + 1
    # Returns exact according to http://primes.utm.edu/prove/prove2_3.html
    if n < 1373653: 
        return not any(_try_composite(a, d, n, s) for a in (2, 3))
    if n < 25326001: 
        return not any(_try_composite(a, d, n, s) for a in (2, 3, 5))
    if n < 118670087467: 
        if n == 3215031751: 
            return False
        return not any(_try_composite(a, d, n, s) for a in (2, 3, 5, 7))
    if n < 2152302898747: 
        return not any(_try_composite(a, d, n, s) for a in (2, 3, 5, 7, 11))
    if n < 3474749660383: 
        return not any(_try_composite(a, d, n, s) for a in (2, 3, 5, 7, 11, 13))
    if n < 341550071728321: 
        return not any(_try_composite(a, d, n, s) for a in (2, 3, 5, 7, 11, 13, 17))
    # otherwise
    return not any(_try_composite(a, d, n, s) 
                   for a in _known_primes[:_precision_for_huge_n])
 
_known_primes = [2, 3]
_known_primes += [x for x in range(5, 1000, 2) if is_prime(x)]

 

Remember to collectstatic to get prettified version of DRF interface. If everything went well you should be able to go to domain/primes/ to display README file and domain/primes/<integer> to check if number is prime. 

Once this has been done I've written simple async "stress-test" to check if my setup can handle 100 GET requests at once, managed to get all responses in 0.8 sec. I can sleep safe, it's just a blog. But yeah in real world you would probably want more advanced set-up probably with quick-database prepopulated with answers. Can be storing only prime numbers and returning False for anything that doesn't exist in database. 

Soon to be done: Throttling - to limit number of requests per day. 

Below is the script I used:

import time
import asyncio
import aiohttp


HEADERS = {'Content-Type': 'application/json; charset=utf-8'}
BASE_URL = 'https://sample.domain/prime/'
PRIMES = {}

async def fetch(session, url):
    """Fetch json response coroutine"""
    async with session.get(url,headers=HEADERS) as response:
        return await response.json()

async def main():
    """Main routine"""
    urls = [BASE_URL+str(i) for i in range(100)]
    tasks = []
    async with aiohttp.ClientSession() as session:
        for url in urls:
            tasks.append(fetch(session, url))

        htmls = await asyncio.gather(*tasks)

        for num, html in enumerate(htmls):
            PRIMES[num] = html[0]['is_prime']

if __name__ == '__main__':
    START = time.time()
    LOOP = asyncio.get_event_loop()
    LOOP.run_until_complete(main())
    END = time.time()-START
    print(END)
    print(PRIMES)

To the next time!