Implementación Django de API REST con django-rest-framework.
Go to file
2023-04-05 00:50:05 -04:00
backend creación de unit tests para ecommerce app (10) 2023-03-30 16:21:22 -03:00
.gitignore init Django Rest Framework Api course 2023-03-29 14:06:11 -03:00
api_calls.sh edit README 2023-04-05 00:50:05 -04:00
env.template init Django Rest Framework Api course 2023-03-29 14:06:11 -03:00
README.md edit README 2023-04-05 00:50:05 -04:00
requirements.txt Creación del primer endpoint 2023-03-29 18:35:23 -03:00

Django REST API

Django Rest Framework

Requerimientos Rest Framework

  • Python (>=3.10)
  • Django
Opcionales
  • PyYAML, uritemplate: Schema generation support.
  • Markdown: Markdown support for the browsable API.
  • Pygments: Add syntax highlighting to Markdown processing.
  • django-filter: Filtering support.
  • django-guardian: Object level permissions support.

Instalación

Instalar requerimientos en entorno virtual

pip install -r requirements.txt

Django utiliza SQLite3 por defecto para simplificar el desarrolo, en este proyecto se utliza MariaDB, pero es opcional. Si no requiere sacarlo de requirements.txt o desinstalarlo pip uninstall mysqlclient

Inicio del proyecto

Creación del proyecto Django

mkdir backend
django-admin startproject drf_course backend

Este proyecto consta de 2 aplicaciones.

La primera es el núcleo. Esta contendrá la lógica de contacto con el endpoint. La segunda será ecommerce. Esta contendrá la ĺógica del endpoint de los items y ordenes.

cd backend
./manage.py startapp core

Editar configuración del proyecto

Archivo ./backend/drf_course/settings.py.

Importar variables de entorno usando python-dotenv del archivo en .backend/.env.

from dotenv import load_dotenv
import os

load_dotenv()

Reemplazar ALLOWED_HOSTS, SECRET_KEY y DEBUG.

SECRET_KEY = os.environ.get("SECRET_KEY")
DEBUG = int(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS").split(" ")

Añadir aplicaciones

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django_extensions', # <---
    'django_filters', # <---
    'rest_framework', # <---
    'core', # <---
]

Añadir variables del framework REST al final del arhivo.

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework_json_api.parsers.JSONParser',
        ),
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework_json_api.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
        ),
    'DEFAULT_METADATA_CLASS': 'rest_framework_json_api.metadata.JSONAPIMetadata',
    'DEFAULT_FILTER_BACKENDS': (
        'rest_framework_json_api.filters.QueryParameterValidationFilter',
        'rest_framework_json_api.filters.OrderingFilter',
        'rest_framework_json_api.django_filters.DjangoFilterBackend',
        'rest_framework_json_api.filters.SearchFilter',
        ),
    'SEARCH_PARAM': 'filter[search]',
    'TEST_REQUEST_RENDERER_CLASSES': (
        'rest_framework_json_api.renderers.JSONRenderer',
        ),
    'TEST_REQUEST_DEFAULT_FORMAT': 'vnd.api+json'
}

En caso de utilizar MariaDB, cambiar la declaración de DATABASES, para usar las variables de entorno declaradas en ./backend/.env

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': os.environ.get("MARIADB_DBASE"),
        'USER': os.environ.get("MARIADB_USER"),
        'PASSWORD': os.environ.get("MARIADB_PASS"),
        'HOST': os.environ.get("MARIADB_HOST"),
        'PORT': os.environ.get("MARIADB_PORT"),
    }
}

Modificar ./backend/drf_course/urls.py

from django.contrib import admin
from django.urls import path
from rest_framework import routers

router = routers.DefaultRouter()

urlpatterns = router.urls

urlpatterns += [
    path('admin/', admin.site.urls),
]

Migrar y probar aplicación

./manage.py migrate
./manage.py runserver

Creación del primer endoint

Creación de endpoint Contac, para que un usuario pueda enviar su nombre, email, y mensaje al backend.

Para ello se requiere:

  • Un modelo que almacene la captura de los datos entrantes.
  • Un serializador que procese los datos entrantes del usuario y envíe un mensaje de respuesta.
  • Una vista que encapsule las llamadas a los métodos REST HTTP comunes.
  • Una ruta /url llamada /contact/.

Model

./backend/core/models.py

Utiliza modelos abstractos del modulo django_extensions; TimeStampedModel (campos como created), ActivatorModel (campos status, activated date, deactivated date ), TitleDescriptionModel (campos de texto textfield y charfield). Clase Contact hereda de estos modelos. Todas las tablas del proyecto tendrán un campo uuid como id. Además de un campo email de Django. Y método de representación del modelo en cadena de texto.

Modelo abstracto

Para implementar uuid en vez de id como campo identificador en todos los modelos, se crea el modulo model_abstracts.py que hereda de models de django.db y utliza el campo id. Utiliza el campo UUID.

Serializer

Para convertir los datos de entrada json en tipos de datos de python, y viceversa, se crea el archivo ./backend/core/serializers.py en la app core. Este hereda de la clase serializers del modulo rest_framework e implementa sus campos (CharField EmailField).

View

./backend/core/views.py backend El uso de la clase APIView es muy similar al una vista regular, la petición entrante es enviada a un manejador apropiado para el método, como .get() o .post(). Además se pueden establecer otros atributos en la clase que controla varios aspectos de las normas de la API.

Route & URL

./backend/drf_course/urls.py

El framework REST añade soporte para ruteo automático de URLs a Django y provee al programador de una simple, rápida y consistente forma de enlazar la lógica de la vista a un conjunto de URLs.

Registrar app en panel de administración

Importar modelo y registrar en ./backend/core/admin.py.

Crear las migraciones y migrar.

./manage.py makemigrations
./manage.py migrate

Finalmente, crear super usuario.

./manage.py createsuperuser

Prueba manual

curl

curl -X POST -H "Content-type: application/json" \
    -d '{"name": "DevFzn", "message": "prueba", "email":"mail@mail.com"}' \
    'http://127.0.0.1:8000/contact/'

o HTTPie

http post http://127.0.0.1:8000/contact/ name="DevFzn" message="prueba" \
          email="devfzn@mail.com"
httpie output
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 155
Content-Type: application/vnd.api+json
Cross-Origin-Opener-Policy: same-origin
Date: Wed, 29 Mar 2023 20:05:32 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.10
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "data": {
        "attributes": {
            "email": "devfzn@mail.com",
            "message": "prueba",
            "name": "DevFzn"
        },
        "id": "bef5e90c-821a-4d04-98ef-c0a0adde5ec1",
        "type": "ContactAPIView"
    }
}

Se puede utilizar la shell de Django para chequear la nueva entrada en Contact

./manage.py shell

>>> from core.models import Contact
>>> c = Contact.objects.last()
>>> c.title
'DevFzn'

Tests

Creación de pruebas en ./backend/core/tests.py. Utilizando las clases APIClient que proporciona un cliente incorporado y APITestCase, similar al TestCase de Django.

Test suite para Contact

  1. test setup
  2. test ContactViewSet para método create
  3. test ContactViewSet para método create cuando nombre no está en los datos
  4. test ContactViewSet para método create cuando nombre está en blanco
  5. test ContactViewSet para método create cuando mensaje no está en los datos
  6. test ContactViewSet para método create cuando mensaje está en blanco
  7. test ContactViewSet para método create cuando email no está en los datos
  8. test ContactViewSet para método create cuando email está en blanco
  9. test ContactViewSet para método create cuando email no es un email

Correr test ./manage.py test

tests output
Found 8 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........
----------------------------------------------------------------------
Ran 8 tests in 0.028s

OK
Destroying test database for alias 'default'...

Ecommerce endpoint

Este se compone de 2 endpoints, items y order. La primera se encarga de retornar todos los items de la tienda al llamarla, además, si se llama a este endpoint con una llave primaria, retorna solo ese item. Y cuando se requiera comprar, se utiliza el order endpoint, donde se pasa el item en particular. Se implementan validaciones para asegurar que el item esten en stock, también asegurar que solo usuarios autentificado puedan llamar estos endpoints. Esta app usa token authentication. Django Rest Framework facilita esta tarea.

Creación de app ecommerce

./manage.py startapp ecommerce

Modificar settings del sitio, editando REST_FRAMEWORK con el siguiente código.

notar el nuevo DEFAULT_AUTHENTICATION_CLASSES.

REST_FRAMEWORK = {
    'EXCEPTION_HANDLER': 'rest_framework_json_api.exceptions.exception_handler',
    'DEFAULT_PARSER_CLASSES': (
        'rest_framework_json_api.parsers.JSONParser',
        ),
    'DEFAULT_AUTHENTICATION_CLASSES': [ # <---
        'rest_framework.authentication.TokenAuthentication', # <---
        ],
    ...

Agregar las siguientes apps en INSTALLED_APPS.

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken', # <---
    'core',
    'ecommerce', # <---
    ]

URLs

Agregar los nuevos endpoints en urls.py del sitio.

# importar authtoken
from rest_framework.authtoken.views import obtain_auth_token

# agregar urls
urlpatterns += [
    ...
    path('api-token-auth', obtain_auth_token),
]

Cuando un usuario visite este endpoint y pase un nombre de usuario y un password validos, este recibirá de vuelta un token de autentificación. Este token esta enlazado con el usuario especifico.

Realizar migraciones

./manage.py makemigrations
./manage.py migrate

Signals

Se requiere un mecanismo (signal) que cree un token para cada usuario que este registrado en la app. Este token es el que será devuelto cada vez que se llame al nuevo endopint.

Para ello, crear el archivo ./backend/ecommerce/signals.py.

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

@receiver(post_save, sender=User, weak=False)
def report_uploaded(sender, instance, created, **kwargs):
    if created:
        Token.objects.create(user=instance)

En Django se le llama señal al código que recibe una señal de cierta tabla, basado en una acción (pre-save o post-save). Cuando se crea un registro en la tabla, esta envía una señal, y es interceptada por signals. En este caso, se utiliza para generar un nuevo token.

Agregar el siguiente método en la clase EcommerceConfig del archivo apps.py de la app.

class EcommerceConfig(AppConfig):
    ...

    def ready(self):
        import ecommerce.signals

Creamos otro superusuario, para que se active el disparador de la señal, y se cree un nuevo token para este.

./manage.py runserver
./manage.py createsuperuser

Visitar http://127.0.0.1:8000/admin, y verificar creación del token.

Probar retorno del token a traves de la API.

ej. curl

curl -XPOST -F 'username=<tu-usuario>' -F 'password=<tu-password>' \
               'http://127.0.0.1:8000/api-token-auth/'

ej. HTTPie

http post http://127.0.0.1:8000/api-token-auth/ username=<tu-usuario> \
                                                password=<tu-password>
httpie output
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Cross-Origin-Opener-Policy: same-origin
Date: Thu, 29 Mar 2023 11:57:43 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.10.10
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "token": "2f076a6310a244283c6902a73e07a0febc59649c"
}

Ecommerce Model

Esta app hace uso obligatorio del token de autentificación. Solo usuarios autentificados pueden acceder a este endpoint.

La app ecommerce se construye con un endpoint item y otro order. Los usuarios podrán recuperar elementos de la base de datos, hacer un pedido y recuperar la información del pedido.

Se necesitan modelos, enrutadores, serializadores y vistas. (models, routers, serializers & view/sets api/view).

Creación de modelos Item y Order. Creación de serializers.

Registro de app en el panel de administración.

Migraciones ./manage.py makemigrations y ./manage.py migrate.

Ecommerce Views y URLs

Creación de views de la app, se utiliza IsAuthenticated del framework. Se agrega conjunto de vistas genericas de la app en urls.py del sitio, registrandolas en el router.

...
from ecommerce import views as ecommerce_views
...

router = routers.DefaultRouter()
router.register(r'item', ecommerce_views.ItemViewSet, basename='item')
router.register(r'order', ecommerce_views.OrderViewSet, basename='order')

...

Ecommerce Tests

Creación de test unitarios para la aplicación.

  1. test setup
  2. test ItemsViewSet método list
  3. test ItemsViewSet método retrieve
  4. test Item.check_stock cuando order.quantity > item.stock
  5. test Item.check_stock cuando order.quantity == item.stock
  6. test Item.check_stock cuando order.quantity < item.stock
  7. test OrdersViewSet método create cuando order.quantity > item.stock
  8. test OrdersViewSet método create cuando order.quantity < item.stock
  9. test OrdersViewSet método create cuando order.quantity == item.stock
  10. test OrdersViewSet método list
  11. test OrdersViewSet método retrieve

Correr tests ./manage.py test.

tests output
Found 18 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..................
----------------------------------------------------------------------
Ran 18 tests in 2.248s

OK
Destroying test database for alias 'default'...

Bash script con llamadas a la API utilizando curl


Jerarquía de directorios
📂️ .
├── 📂️ backend
│   ├── 📂️ core
│   │   ├── 📂️ migrations
│   │   │   ├── 0001_initial.py
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── 📂️ drf_course
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   ├── 📂️ ecommerce
│   │   ├── 📂️ migrations
│   │   │   ├── 0001_initial.py
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── models.py
│   │   ├── serializers.py
│   │   ├── signals.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── 📂️ utils
│   │   ├── __init__.py
│   │   └── model_abstracts.py
│   ├── .env
│   └── manage.py
├── .gitignore
├── api_calls.sh
├── env.template
├── README.md
└── requirements.txt