Servidor y cliente REST: Django REST framework + requests

Muchos de los posts de éste blog son pequeñas píndolas y recordatorios que me dejo para facilitarme mi tarea diaria de administración de sistemas y últimamente de desarrollo, los comparto públicamente porque al servirme a mi, espero que sirvan a otros. Hoy os traigo un nuevo post de éstos últimos, precisamente de uno que ha sido durante varios años una espinita clavada, programar un servidor API. Hace 3 años hice un módulo de interacción con la API de un proveedor con uno de los programas que tengo en PHP, pero me quedaba lo que era realmente la espinita, la de crear yo el servidor API y permitir que otros programas interactuasen con el mío.

Hace alrededor de 5 años, un cliente que usaba un programa mío me pidió de la posibilidad de interactuar con el programa mediante una API. En aquel entonces traté de desarollarlo, pero con el lenguaje que estaba usando (PHP) y los conocimientos que tenía entonces me resultó tarea imposible, además de la actitud del desarrollador web del cliente. En fin. Así que haber superado éste hito es una inyección de felicidad, superación y autoconfianza.

Vamos a empezar.

Marco y necesidad
Me encuentro con dos aplicaciones que estoy desarrollando con django, voy a llamarlas por su nombre, colibrí y cóndor. Colibrí es un programa experto para hacer auditorías compliance y para cada empresa permite tener un inventario de máquinas. Por otro lado está Cóndor que es un programa de comunicación cliente-empresa. Lo que queremos hacer es que cuando un cliente mande una comunicación a Cóndor, al gestor de la comunicación le salga el listado de máquinas que hay en el inventario de Colibrí y cambiar el estado de las máquinas de Colibrí desde Cóndor.

Estructura
Colibrí va a ser el software que tenga el servidor API Rest y Cóndor el cliente Rest que va a hacer peticiones GET y POST.

Ambos programas están desarrollados con Django, uno de los varios frameworks de Python.

Colibrí (Servidor REST)

Para ello vamos a usar la librería Django REST framework (Documentación).

Primero de todo en nuestro proyecto vamos a instalar la librería mediante pip

laura@melatonina:~/dev/colibri$ source venv/bin/activate
(venv) laura@melatonina:~/dev/colibri$ pip install djangorestframework

A continuación lo configuramos en el fichero settings.py de nuestro proyecto (aprovechamos para añadir también el nuevo programa de nuestro proyecto que llamaremos api)

(venv) laura@melatonina:~/dev/colibri$ vi colibri/settings.py
INSTALLED_APPS = [
    ...
    'django_extensions',
    'rest_framework',
    'api',
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAdminUser',
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    #'PAGE_SIZE': 10
}

Ahora creamos ya el programa api

(venv) laura@melatonina:~/dev/colibri$ mkdir api
(venv) laura@melatonina:~/dev/colibri$ touch api/__init__.py

Modificamos las URL de nuestro proyecto para que /api apunte al programa api, aprovechamos para definir los routers, que son las ruta de la serialización de la API

(venv) laura@melatonina:~/dev/colibri$ vi colibri/urls.py 
router = routers.DefaultRouter()
router.register(r'company', api.CompanyViewSet)
router.register(r'inventory', api.InventoryViewSet)
router.register(r'inventorychange', api.InventoryChangeViewSet)

urlpatterns += [
    path('api', api.index, name='manager'),
    path('api/', include(router.urls)),
]

Ahora crearemos los ficheros apps.py, api.py y serializers.py

(venv) laura@melatonina:~/dev/colibri$ cat api/apps.py 
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.apps import AppConfig


class QuickstartConfig(AppConfig):
    name = 'api'
(venv) laura@melatonina:~/dev/colibri$ cat api/api.py 
from django.shortcuts import render, redirect
from web.models import Profile, Language, Config, Inventory, InventoryChange
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .serializers import *


def index(request):
    nav = {'menu': 'index'}
    #return render(request, 'api/dashboard.html', {'nav': nav})
    return redirect('/api/')


class CompanyViewSet(viewsets.ModelViewSet):
    queryset = Company.objects.all().order_by('name')
    serializer_class = CompanySerializer
    permission_classes = [permissions.IsAdminUser]

class InventoryViewSet(viewsets.ModelViewSet):
    queryset = Inventory.objects.all().order_by('-creation')
    serializer_class = InventorySerializer
    permission_classes = [permissions.IsAdminUser]


class InventoryChangeViewSet(viewsets.ModelViewSet):
    queryset = InventoryChange.objects.all().order_by('-creation')
    serializer_class = InventoryChangeSerializer
    permission_classes = [permissions.IsAdminUser]
(venv) laura@melatonina:~/dev/colibri$ cat api/serializers.py 
from django.contrib.auth.models import User, Group
from web.models import Inventory, InventoryChange, Company
from rest_framework import serializers
from django.utils import timezone

class CompanySerializer(serializers.HyperlinkedModelSerializer):
    name = serializers.CharField()
    inventory = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='inventory-detail')

    class Meta:
        model = Company
        fields = ('url', 'pk', 'nif', 'name', 'inventory')


class InventorySerializer(serializers.HyperlinkedModelSerializer):
    type = serializers.StringRelatedField(many=False, read_only=True)

    changes = serializers.HyperlinkedRelatedField(
        many=True,
        read_only=True,
        view_name='inventorychange-detail'
    )

    class Meta:
        model = Inventory

        fields = ('url', 'pk', 'code', 'brand', 'model', 'type', 'location', 'used_by', 'status', 'creation', 'deletion', 'changes')



class InventoryChangeSerializer(serializers.Serializer):
    STATE = (
        ('online', 'online'),  # green
        ('maintenance', 'maintenance'),  # yellow
        ('incidence', 'incidence'),  # red
        ('inactive', 'inactive'),  # grey
        ('unsubscribed', 'unsubscribed')  # grey
    )
    id = serializers.IntegerField(read_only=True)
    name = serializers.CharField(required=True, allow_blank=False, max_length=100)
    text = serializers.CharField(required=False, allow_blank=True)
    status = serializers.ChoiceField(choices=STATE, default='incidence')
    inventory = serializers.IntegerField(required=False)

    def create(self, validated_data):
        #Create and return a new `Snippet` instance, given the validated data.
        _out = InventoryChange.objects.create(**validated_data)
        _inventory_change = InventoryChange.objects.last()
        _inventory = Inventory.objects.get(pk=validated_data['inventory'])
        _inventory.changes.add(_inventory_change)
        _inventory.status = validated_data['status']
        _inventory.save()
        return _out
        #return InventoryChange.objects.create(**validated_data)

    def update(self, instance, validated_data):
        #Update and return an existing `Snippet` instance, given the validated data.
        instance.name = validated_data.get('name', instance.name)
        instance.text = validated_data.get('text', instance.code)
        instance.status = validated_data.get('status', instance.status)
        instance.save()
        return instance

El serializer es lo que da formato a los objetos del ORM y también permite definir qué acciones realizar al hacer un create o un update.

Si nos fijamos en ningún momento hemos tocado ningún fichero de vista ni modelo del proyecto “web” que es donde tengo todo el programa de colibrí.

A partir de aquí podemos realizar consultas con curl, con un cliente REST o mediante la URL de nuestro programa https://colibri.capa8.net/api/ (al definir en el fichero api.py la variable permission_classes como permisos permissions.IsAdminUser, necesitaremos un usuario con permisos de Admin para hacer consultas a la API.

En la API que estoy programando, tengo los esquemas company, inventory e inventorychange tal como vemos en los routers en el fichero urls.py.

curl -H 'Accept: application/json; indent=4' -u apiuser:******* https://colibri.capa8.net/api/inventory/

Referencia https://www.paradigmadigital.com/dev/introduccion-django-rest-framework/

Cóndor (Cliente REST)

Una vez hecha la primera parte, queda la segunda, que un cliente REST interactue con el servidor REST. Para ello vamos a hacer uso de la librería requests (inicialmente me embarqué con coreapi pero me quedé atascada en hacer el post, tras una recomendación de un par de amigos, opté por requests, mucho más fácil y menos líneas!)

Para preparar el programa sólo tendremos que tener instalada la librería requests

laura@melatonina:~/dev/whistleblowerbox$ source venv/bin/activate
(venv) laura@melatonina:~/dev/whistleblowerbox$ pip install requests

A continuación os muestro el trozo de código que dado un fiscal_id introducido en la comunicación se lista todo el inventario de máquinas de ésta empresa en Colibrí (servidor REST).

# load inventories if api enabled for this box
if _box.box_type.api_enabled == 1:
    _count = _box.apis.filter(fiscal_id=_complaint.fiscal_id).count()

    if _count != 0:
        try:
            _api_customer = _box.apis.filter(fiscal_id=_complaint.fiscal_id)
            _api_customer = _api_customer[0]

            # connect API
            _companies = views_api.get(request, _api_customer.api_instance, "/company").json()

            for _c in _companies:
                if _c['nif'] == _complaint.fiscal_id:
                    _company = _c

            _api_inventories = _company['inventory']
            _inventories = []

            for _api_url in _api_inventories:
                _url = _api_url.split(_api_customer.api_instance.url)
                _inventory = views_api.get(request, _api_customer.api_instance, _url[1]).json()
                _inventories.append(_inventory)

        except Exception:
            _inventories = []
else:
    _inventories = []

Y ésta es la parte que recoge las distintas maquinas seleccionadas afectadas por la incidencia notificada y las manda por POST a la API REST

# connect to API and send InventoryChange
if _box.box_type.api_enabled == 1:
    # connect API
    _api_customer = _box.apis.filter(fiscal_id=_complaint.fiscal_id)
    _api_customer = _api_customer[0]

    # prepare data from post
    _api_status = post['api_status']
    _name = post['abstract_api']
    _text = post['text_api']

    _inventories = post.getlist('inventories')
    for _inventory in _inventories:
        _inventory = int(_inventory)
        _data = {"inventory": _inventory, "name": _name, "text": _text, "status": _api_status}
        _inventory = views_api.post(request, _api_customer.api_instance, '/inventorychange/', _data)

En ambos casos vinculamos a un fichero views_api.py que contiene éstas dos funciones

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
import requests

@login_required
def get(request, api_instance, schema):
    client = requests.get(api_instance.url + schema, auth=(api_instance.username, api_instance.password))
    return client


@login_required
def post(request, api_instance, schema, data):
    post = requests.post(api_instance.url + schema, auth=(api_instance.username, api_instance.password), data=data)
    return post

Y éstos son mis breves apuntes sobre como he hecho la comunicación API entre dos de mis programas :) espero que os sea de ayuda y permita solucionar el problema que te ha llevado a visitar ésta página.

Happy coding day!

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *

Aquest lloc utilitza Akismet per reduir els comentaris brossa. Apreneu com es processen les dades dels comentaris.