Algunos tips de django rest framework que quizás no conocías

django rest framework es una librería  que corre sobre django y permite de manera bastante limpia y sencilla construir una web API, esta herramienta tiene un conjunto de paquetes que fortalecen todo su ecosistema, ha tenido una gran aceptación dentro de la comunidad como se aprecia en la Figura 1 con mas de 20k estrellas en github

Figura 1 Evolución de estrellas en github de DRF

Actualmente trabajo con esta librería y he decidido comentar algunos tips que me parecen interesantes a la hora de trabajar con ella, quizas algunos ya los conocías quizás no

para instalar django rest framework es realmente sencillo yo utilizare python 3.7 y virtualenv

pip install django
pip install djangorestframework

y en la configuración de django en apps instaladas se agrega la siguiente configuración

INSTALLED_APPS = [
    ...
    'rest_framework',
]

Paginación

django rest framework utiliza las implementaciones que tiene django y a partir de aquí las especializa para lo que necesite hacer cada uno, en este ejemplo voy a mostrar como personalizar la paginación utilizando APIView como clase base para construir mi servicio, por defecto cuando se implementa la paginacion se tiene como respuesta lo siguiente

HTTP 200 OK
{
    "count": 1023
    "next": "https://api.example.org/accounts/?page=5",
    "previous": "https://api.example.org/accounts/?page=3",
    "results": [
       …
    ]
}

pero muchas veces dentro del equipo de desarrollo hay un estándar definido para las respuesta y este debe adaptarse a él, por ello en el siguiente código mostrare como se puede modificar en mi caso quiero que el formato de salida se vea como a continuación

HTTP 200 OK
{	
"data":{
    "totalItems": 1023
    "next": "https://api.example.org/accounts/?page=5",
    "previous": "https://api.example.org/accounts/?page=3",
    "items": [
       …
    ]
    }
}

Para ello definimos un archivo llamado pagination.py y agregamos el siguiente codigo

from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response


class BasicPagination(PageNumberPagination):
    page_size_query_param = "limit"
    page_size = 2


class PaginationHandlerMixin(object):
    @property
    def paginator(self):
        if not hasattr(self, "_paginator"):
            if self.pagination_class is None:
                self._paginator = None
            else:
                self._paginator = self.pagination_class()
        else:
            pass
        return self._paginator

    def paginate_queryset(self, queryset):

        if self.paginator is None:
            return None
        return self.paginator.paginate_queryset(queryset, self.request, view=self)

    def get_paginated_response(self, data):

        return Response(
            {
                "next": self.paginator.get_next_link(),
                "previous": self.paginator.get_previous_link(),
                "totalItems": self.paginator.page.paginator.count,
                "items": data,
            }
        )
Archivo pagination.py

aqui defino un tipo de paginación que es la PageNumberPagination la cual tiene unos métodos que voy a sobreescribir para adaptarlo a lo que necesito, para ello creó una clase llamada PaginationHandlerMixin en donde me me ubicare en el método que me interesa que es get_paginated_response este recibe un parámetro data que es donde viene la información serializada y con ella utilizando la clase Response de DRF se construye la nueva respuesta

def get_paginated_response(self, data):

        return Response(
            {
                "next": self.paginator.get_next_link(),
                "previous": self.paginator.get_previous_link(),
                "totalItems": self.paginator.page.paginator.count,
                "items": data,
            }
        )

Para poder implementarlo y sobreescribir este método la clase UserList en el archivo views.py debe heredar de esta clase como se muestra a continuación en el módulo views

from datetime import date, datetime
from pydantic import BaseModel
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .serializers import UserSerializer
from .pagination import PaginationHandlerMixin, BasicPagination


class User(BaseModel):
    name: str
    created_at: datetime
    birthdate: date


class UserList(APIView, PaginationHandlerMixin):
    pagination_class = BasicPagination
    serializer_class = UserSerializer

    def get(self, request, format=None):
        users = []
        users.append(
            User(
                name="andres",
                birthdate=date(day=15, year=1990, month=10),
                created_at=datetime.now(),
            )
        )
        users.append(
            User(
                name="jairo",
                birthdate=date(day=21, year=1990, month=11),
                created_at=datetime.now(),
            )
        )
        users.append(
            User(
                name="andrea",
                birthdate=date(day=17, year=1970, month=10),
                created_at=datetime.now(),
            )
        )
        page = self.paginate_queryset(users)
        if page is not None:

            serializer = self.get_paginated_response(
                self.serializer_class(page, many=True).data
            )

        else:

            serializer = self.serializer_class(users, many=True)

        return Response(status=status.HTTP_200_OK, data={"data": serializer.data})
Modulo views.py

Hemos creado una información para probar con una lista de 3 usuarios, entrando a la url tendríamos el siguiente formato de salida como se aprecia en la Figura 2

Figura 2 Resultado de listar usuarios usando paginación personalizada

Con este código podemos adaptar la paginación al formato de salida que tengamos definido con el equipo de desarrollo y no tener que dejar el formato que django rest framework trae por defecto

Fechas

Cuando se utilizan serializers que es básicamente convertir la data de tipo nativo de python a json format a veces podemos tener algunos inconvenientes con los formatos de las fechas, aquí veremos dos sencillos trucos que pueden ser útiles

  • El primero es con relación al formato de salida, en python manejamos dos tipos para fechas date y datetime pero en desarrollo puede requerir un formato en especial, para setear este formato podemos utilizar la siguiente porcion de codigo
from rest_framework import serializers


class UserSerializer(serializers.Serializer):
    name = serializers.CharField()
    created_at = serializers.DateTimeField(
         format="iso-8601"
    )
    birthdate = serializers.DateField(format="%Y-%m-%d")

como podemos notar en ejemplo anterior  podemos utilizar el parámetro format para darle el formato de fecha que mejor se nos adapte, en el ejemplo utilizo el formato de iso oficial o un formato personalizado aquí se puede utilizar los formatos de fecha de python y adaptar la salida a lo que se necesite

  • El segundo es en relación a la zona horaria, en la documentación de django se sugiere que es buena idea guardar la fecha en UTC sin zona horaria, yo específicamente lo guardo sin zona horaria pero a la hora de entregar la información puede requerirse que si la tenga, para eso en los serializadores podemos agregar  el siguiente código
import pytz
from rest_framework import serializers


class UserSerializer(serializers.Serializer):
    name = serializers.CharField()
    created_at = serializers.DateTimeField(
        default_timezone=pytz.timezone("America/Bogota"), format="iso-8601"
    )
    birthdate = serializers.DateField(format="%Y-%m-%d")

aquí agregue la librería pytz que facilita trabajar con zonas horarias, al mismo ejemplo anterior agregue el parámetro default_timezone indicando timezone "America/Bogota" la implementación completa  utilizando el código en los views sería el siguiente

from datetime import date, datetime
from pydantic import BaseModel
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from.serializers import UserSerializer

class User(BaseModel):
    name:str
    created_at:datetime
    birthdate:date

class UserList(APIView):
    def get(self, request, format=None):
        user = User(
            name="andres",
            birthdate=date(day=15,year=1990,month=10),
            created_at=datetime.today()
        )
        serializer = UserSerializer(user)
        return Response(status=status.HTTP_200_OK, data={"data":serializer.data})

y entrando a la url el json de respuesta se vería de la siguiente forma como se aprecia en la Figura 3

Figura 3  Resultado de información de usuario utilizando time zones

Validación de múltiples permisos

En la documentación de DRF se puede ver que existe la posibilidad de hacer validación por clases restringiendo por roles quienes pueden acceder a partir de un token a cada recurso, pero algunas veces se necesitará validaciones para varios roles y algunos solamente de lectura (GET) para ello es posible utilizar el operador '&' y '|' para complementar estas validaciones como se puede ver en el codigo a continuacion donde agregare un archivo permissions.py que tendra los permisos disponibles y en el arrchivo views.py agregare la implementacion

from rest_framework.permissions import BasePermission, SAFE_METHODS


class ValidatePermissionTypeUserAdmin(BasePermission):
    def has_permission(self, request, view) -> bool:
        return True


class ValidatePermissionTypeUserMod(BasePermission):
    def has_permission(self, request, view) -> bool:
        return True

class ReadOnlyPermission(BasePermission):
    def has_permission(self, request, view) -> bool:
        return request.method in SAFE_METHODS
Archivo permissions.py
from datetime import date, datetime
from pydantic import BaseModel
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
from .serializers import UserSerializer
from .permissions import (
    ReadOnlyPermission,
    ValidatePermissionTypeUserAdmin,
    ValidatePermissionTypeUserMod,
)


class User(BaseModel):
    name: str
    created_at: datetime
    birthdate: date


class UserList(APIView):
    authentication_classes = (TokenAuthentication,)
    permission_classes = [
        IsAuthenticated
        & (
            ValidatePermissionTypeUserAdmin
            | (ValidatePermissionTypeUserMod & ReadOnlyPermission)
        )
    ]

    def get(self, request, format=None):
        user = User(
            name="andres",
            birthdate=date(day=15, year=1990, month=10),
            created_at=datetime.now(),
        )
        serializer = UserSerializer(user)
        return Response(status=status.HTTP_200_OK, data={"data": serializer.data})
Archivo views.py

En el codigo anterior agrege los permissions.py que simulan la validacion de permisos, aqui deberia ir la implementación para validar cada rol, en el archivo views.py el cual es el segundo agrege el atributo authentication_classes para decirle a DRF como se autentica el usuario y el atributo permission_classes el cual es el importante, este recibe una lista pero dentro de la lista cumple las siguientes condiciones

  • La primera es que debe estar autenticado, y como ven usa el operador & por lo tanto IsAuthenticated debe ser true si no no seguirá validando
  • La siguiente está dentro de un or por lo tanto el usuario puede ser tipo Admin o Mod pero si es Mod debe cumplirse ReadOnlyPermission esta validación asegura que solo permite hacer llamados GET, es decir si un usuario de tipo Mod realiza un llamado POST generará un error de permisos insuficientes

de esta manera se puede hacer las validaciones que se necesiten para cada recursos por rol, algunas pueden llegar a ser bastante complejas dependiendo de cada rol y que restricciones tiene, existen algunas librerias para hacerlo mas sencillo, o tambien pueden hacer validaciones conjuntas ya que al final son clases que retornan booleanos

Swagger documentación

Hay una librería llamada drf-yasg dentro del ecosistema de DRF que permite generar documentación de swagger a partir de decorators aquí puede existir un gran debate si usar directamente un archivo de configuración,o usar los decorators pero el punto es poder implementarla y ahorrar tiempo utilizando los serializadores para recibir y retornar información como veremos a continuación

from datetime import date, datetime
from typing import List
from pydantic import BaseModel
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from .serializers import UserSerializer, ErrorSerializer


class User(BaseModel):
    id: int = 0
    name: str
    created_at: datetime
    birthdate: date


users: List[User] = []


class UserDetail(APIView):
    id_param = openapi.Parameter(
        "id", openapi.IN_PATH, description="user id", type=openapi.TYPE_INTEGER
    )

    user_response = openapi.Response("user detail", UserSerializer)

    @swagger_auto_schema(
        manual_parameters=[id_param],
        responses={200: user_response, 404: ErrorSerializer},
    )
    def get(self, request, id: int, format=None):
        for user in users:
            if user.id == id:
                serializer = UserSerializer(user)
                return Response(status=status.HTTP_200_OK, data=serializer.data)

        serializer = ErrorSerializer({"error": "USERDOESNOTEXISTS"})
        return Response(status=status.HTTP_404_NOT_FOUND, data=serializer.data)
Modulo views.py

En el código anterior he construido una clase UserDetail  que retornara la información de un usuario por id, en este caso se usará el decorador @swagger_auto_schema este permite generar la documentación automática basada en los serializadores, por lo tanto a medida que cambie el serializador la documentación se ira actualizando

  user_response = openapi.Response("user detail", UserSerializer)

    @swagger_auto_schema(
        manual_parameters=[id_param],
        responses={200: user_response, 404: ErrorSerializer},
    )

en esta porcion de codigo le indico a swagger que las posibles respuestas son 200 y 404 y las estructuras que responde en cada una pertenece a la clase UserSerializer y ErrorSerializer, esta librería automáticamente hace la introspección de los atributos y los documenta cómo se puede ver a continuación en la Figura 4

Figura 4 Documentación de swagger generada por decorators

esto es de gran ayuda por que permite que a medida que el código cambie a largo plazo la documentación se va actualizando al mismo tiempo de manera automática

Conclusión

Estos son algunos trucos o tips que me han servido trabajando con esta librería y que en la documentación no se pueden encontrar o por lo menos no de forma directa, existen muchas más cosas que pueden aplicarse para lograr también mejorar rendimiento pero estas espero mostrarlas en próximos post.