Django Rest API with docker
Go to file
2023-10-12 14:14:02 -03:00
.github/workflows docker, test, db, wait_for_db, db conf, core app 2023-10-05 22:48:42 -03:00
app Implementación de filtros por tag y recetas 2023-10-12 00:44:46 -03:00
docs reorganización docs 2023-10-12 14:14:02 -03:00
.dockerignore init app recipies api 2023-10-04 00:07:55 -03:00
.gitignore init app recipies api 2023-10-04 00:07:55 -03:00
docker-compose.yml Creación Image API 2023-10-11 17:59:44 -03:00
Dockerfile Creación Image API 2023-10-11 17:59:44 -03:00
README.md reorganización docs 2023-10-12 14:14:02 -03:00
requirements.dev.txt init app recipies api 2023-10-04 00:07:55 -03:00
requirements.txt Implementación de filtros por tag y recetas 2023-10-12 00:44:46 -03:00

REST API Django

Contenido

Tecnologias

%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph " "
direction TB
SW{Swagger-UI}

subgraph APP["App Container"]
RF("REST Framework")
DJ("Django")
PY("Python")
end

subgraph DBC["DB Container"]
DB[(PostgreSQL)]
end

RF <--> SW
RF <--> DJ <--> PY
DB <--> DJ
end

Estructura del proyecto

  • app Django project
  • app/core/ código compartido entre multiples apps
  • app/user/ código relativo al usuario
  • app/recipe/ código relativo a las recetas

TDD

Test Driven Develoment

%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
WT[Write Test]
RTF["Run Test
(Fails)"]
AF[Add Feature]
RTP["Run Test
(Passes)"]
RF[Refactor]
end
WT --> RTF --> AF --> RTP --> RF
RF --> RTP
  • Esto proporciona un mejor entendimiento del código
  • Permite realizar cambios con confianza
  • Reduco bugs

Unitests

  • Código que prueba código
    • Establecer condiciones/entradas
    • Correr fragmentos de código
    • Verificar salidas con assertions
  • Beneficios
    • Asegurar que el código corre como se espera
    • Atrapar bugs
    • Mejorar fiabilidad
    • Proporciona confianza

Docker

¿Por qué usar Docker?

  • Consistencia entre ambientes de desarrollo y producción
  • Facilita la colaboración entre desarrolladores
  • Todas las dependencias como código
    • Requerimientos de Python
    • Dependencias del S.O.
  • Facilidad para limpiar el sistema (post-dev)
  • Ahorro de tiempo

¿Como usar Docker?

  • Crear dockerfile
  • Crear docker compose configuration
  • Correr todos los comandos usando Docker compose

Docker con GitHub Actions

  • Docker Hub tiene un limite de acceso:
    • 100 pulls/6 hr para usuarios sin authentificación
    • 200 pulls/6 hr para usuarios con authentificación
  • GitHub Actions es un servicio compartido
    • 100 pulls/6 hr considera TODOS los usuarios
  • Autenticación con Docker Hub
    • Crear cuenta
    • Configurar credenciales
    • Login antes de correr un trabajo (job)
    • Obtener 200 pulls/6 hr gratis

Configurar Docker

  • Creación dockerfile
  • Lista de pasos para crear imagen
    • Escoger una imagen basada en python
    • Instalar dependencias
    • Establecer usuarios

Docker Compose

  • Como se debe utlizar la imagen de docker
  • Definir servicios
    • Nombre (ej. app)
    • Mapeo de puertos
    • Mapeo de volumenes
  • Correr todos los comandos a travez de Docker Compose
    ej. docker compose run --rm app sh -c "python manage.py collectstatic"
    • docker compose Ejecuta un comando de Docker Compose
    • run comienza un contenedor específico definido en la configuración
    • --rm remueve el contenedor
    • app es el nombre del servicio/aplicación
    • sh -c pasa una orden a la shell del container
    • "python manage.py ..." comando a correr dentro del contenedor

docker build .
docker compose build

Linting

  • Instalar flake8
  • requirements.dev.txt
  • Configuración flake8
  • Correr a travez de docker compose docker compose run --rm app sh -c "flake8"

Testing

  • Django test suite
  • Configurar test por cada aplicación Django
  • Correr a travez de docker compose docker compose run --rm app sh -c "python manage.py test"

Creación del proyecto Django

docker compose run -rm app sh -c "django-admin startproject app ."

Iniciar el servidor

docker compose up

GitHub Actions

  • Herramienta de automatización
  • Similar a Travis-CI, GitLab CI/CD, Jenkins
  • Ejecuta tareaas cunado el código cambia
  • Tareas automatizadas comunes:
    • Despliege/implementación
    • Code Linting
    • Tests Unitarios

Funciona con Trigger ej. push to GitHub

¿Como funciona?

%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
TG["<b>Trigger</b>
Push to GitHub"]
JB["<b>Job</b>
Run unit tests"]
RS["<b>Result</b>
Success/fail"]
end
TG ==> JB ==> RS

Costo

  • Se cobra por minutos de uso
  • 2.000 minutos gratis

Configuranción GitHub Actions

  • Creación de archivo checks.yml
    • Set Trigger
    • Añadir passos para correr pruebas y linting
  • Configurar DockerHub auth
    • Necesitado para jalar imagenes base
    • Limites:
      • Anónimos: 100/6h
      • Atentificado 200/6h
    • GitHub Actions usan IP compartida, la limitación aplica para todos los usuarios al autenticar con DockerHub se obtienen 200/6h de uso exclusivo

Django Tests

  • Basado en la biblioteca unittest
  • Caracteristicas añadidas de Django
    • Cliente del pruebas dummy web browser
    • Simula autenticación
    • Base de datos temporal
  • Caracteristicas añadidas de REST Framework
    • Cliente de pruebas de la API

¿Donde van los tests?

  • test.py por aplicación
  • O crear un subdirectorio tests/ para dividir las pruebas
  • Recordar
    • Solo usar tests.py o directorio tests/, no ambos
    • Los moduloes de prueba deben comenzar con test_
    • Los directorios de pruebas deben contener el archivo __init__.py
demo_code
├── app_one
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── views.py
│   └── admin.py
└── app_two
    ├── tests
    │   ├── __init__.py
    │   ├── test_module_one.py
    │   └── test_module_two.py
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── views.py
    └── admin.py

Test DB

  • Codigo de pruebas que usa la base de datos
  • Base de datos específica para pruebas
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
RT["<b>Runs Tests</b>"]
CD["<b>Clears data</b>"]
end
RT ==> CD ==> RT
  • Por defecto, esto ocurre para cada test

Clases de Test

  • SimpleTestCase
    • Sin integración con BD
    • Util si no se require una BD para la lógica a probar
    • Ahorra tiempo de ejecución
  • TestCase
    • Integración con BD
    • Util para probar código que usa la BD

Ej. test

""""
Unit test for views
""""
from django.test import SimpleTestCase
form app_two import views

class ViewsTests(SimpleTestCase):

    def test_make_list_unique(self):
        """ Test making a list unique. """
        sample_items = [1, 1, 2, 2, 3, 4, 5, 5]
        res = views.remove_duplicates(sample_items)
        self.assertEqual(res, [1, 2, 3, 4, 5])

python manage.py test

Creación del primer test

Modulo a testear calc.py con tests.py

"""
Sample tests
"""
from django.test import SimpleTestCase
from app import calc

class CalcTests(SimpleTestCase):
    """ Test the calc module. """

    def test_add_numbers(self):
        """ Test adding numbers together. """
        res = calc.add(5, 6)

        self.assertEqual(res, 11)

Correr el test

docker compose run --rm app sh -c "python manage.py test"

Found 1 test(s).
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Usando TDD

  1. Crear la prueba para el comportamiento esperado tests.py
  2. La prueba debe fallar
  3. Crear el código para que el test pase (añadir la funcionalidad) calc.py

Mocking

  • Evita depender de servicios externos, pues estos
    • No garantizan disponibilidad
    • Pueden hacer que los tests sean impredecibles e incositentes
  • Evita consecuencias no intecionadas, por ejm.
    • Enviar mails accidentalmente
    • Sobrecargar servicios externos

ej.

%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
RU["<b>register_user()</b>"]
CIDB["<b>create_in_db()</b>"]
SWE["<b>send_welcome_email()</b>"]
MS[e-mail sent]

end
RU --> CIDB --> SWE --x MS
  • Previene el envio del correo electónico
  • Asegura que send_welcome_email() es llamado correctamente

Otro beneficio de Mocking

  • Acelera las pruebas
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%%
flowchart
subgraph " "
direction LR
CDB["<b>check_db()</b>"]
SLP["<b>sleep()</b>"]

end
CDB --x SLP --> CDB 

Como usar mock

  • Se usa unittest.mock
    • MagicMock/Mock reemplaza objetos reales
    • patch sobreescribe el código de las pruebas

Testing Web Request

Probando la API

  • Hacer peticiones reales
  • Comprobar resultados

Django REST Framework provee un cliente para la API basado en Django TestClient, este realiza los requests y permite verificar resultados. Incluso permite sobreescribir la autenticación, para probar la funcionalidad de la API, haciendo que esta asuma que se esta autentificado.

from django.test import SimpleTestCase
from rest_framework.test import APIClient

class TestViews(SimpleTestCase):

    def test_get_greetings(self):
        """ Test getting greetings. """
        client = APIClient()
        res = client.get('/greetings/')

        self.assertEqual(res.status_code, 200)
        self.assertEqual(
                res.data,
                ["Hello!", "Bonjour!", "Hola!"],
            )

Problemas comunes en las pruebas

El test no se ejecuta

System check identified no issues (0 silenced).

----------------------------------------------------------------------
Ran 0 test in 0.000s

OK
  • Se ejecutan menos tests que la cantidad creada

Razones posibles

  • Falta el archivo __init__.py en el directorio tests.py
  • Indentación de los casos de prueba
  • Prefijo test faltante en los metodos test_some_function(self):

ImportError al correr las pruebas

      raise ImportError(
  ImportError: 'tests' module incorrectly imported from ....
  Expected ....Is this module globally installed?
      )

Razon posible

  • Existencia de tests/ y tests.py en la misma aplicación

Configurando la base de datos

PostgreSQL se integra bien con django, es oficialmente soportada.

Se utiliza Docker compose, se define la configuración dentro del proyecto para que sea reutilizable. Datos persistentes utilizando volumes. Maneja la configuración de la red. Configuranción usando variables de entorno.

Arquitectura

%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
direction TB
subgraph "<b>Docker Compose</b>"
DB[(PostgreSQL)]
direction LR
APP("<b>App</b>
Django")
DB <-..-> APP
end

Conectividad de red

services:
  app:
    depends_on:
      - db
  
  db:
    image: postgres:16-alpine
  • Establecer depends_on para iniciar db primero
  • El servicio app puede usar el hostname de db

Volumes

  • Persistencia de datos
  • Mapea el directorio del contenedeor con la máquina local
  db:
    image: postgres:16-alphine
  volumes:
    - dev-db-data:/var/lib/postgresql/data

volumes:
  dev-db-data:
  dev-static-data:

Agregando el servicio base de datos

docker-compose.yml

services:
  app:
    build:
      context: .
      args:
        - DEV=true
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app
    command: >
      sh -c "python manage.py runserver 0.0.0.0:8000"      
    environment:
      - DB_HOST=db
      - DB_NAME=devdb
      - DB_USER=devuser
      - DB_PASS=changeme
    depends_on:
      - db

  db:
    image: postgres:16-alpine
    volumes:
      - dev-db-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=devdb
      - POSTGRES_USER=devuser
      - POSTGRES_PASSWORD=changeme


volumes:
  dev-db-data:

Probar que todo funcione correctamente docker compose up

Configuración de la BD en Django

  • Especificar como django conecta con la BD settings.py
    • Engine (tipo de BD)
    • Hostname (DB IP o dominio)
    • Port
    • DB name
    • DB user
    • DB password
  • Instalar las dependencias del conector
  • Actualizar los requerimientos
DATABASES = {
    'default': {
        'ENGINE' django.db.backends.postgresql',
        'HOST': os.environ.get('DB_HOST'),
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
    }
}

Psycopg2

Packete necesario para conectar Django con la base de datos PostgreSQL, es el conector mas popular en Python y es soportado por Django. Se puede instalar de las siguientes maneras:

  • psycopg2-binary (Ok para desarrollo, no para producción)
  • psycopg2 (se compila desde el código fuente, hay que satisfacer dependencias para esto)
  • Fácil de instalar con Docker

Lista de dependencias

  • Compilador C
  • python3-dev
  • libpq-dev

Equivalentes para Apine

  • postgresql-client
  • build-base
  • postgresql-dev
  • musl-dev

Equivalentes para Debian (python:3.12.0-slim-bookworm)

  • postgresql-client
  • python3-dev
  • libpq-dev
  • gcc confimar
  • python3-psycopg2 confirmar

Una buena practica es limpiar las dependencias que ya no serán necesarias

Previniendo Race Condition con docker compose

Usar depends_on asegura que el service comience (no asegura que la app este corriendo)

Docker services timeline

img

La solución es hace que Django espere a la base de datos db. Este chequea la disponibilidad de la base de datos y continua cuando esta disponible

img

Comando personalizado de administración de Django en app core

Creación de aplicación core

docker compose run --rm app sh -c "python manage.py startapp core"

Se eliminal el archvo tests.py para crear y usar el directorio app/core/tests/ donde se agrega el correspondiente archivo __init__.py

Se crean los directorios app/core/management/commands/ con los archivos __init__.py y commands.py

"""
Django command to wait for the DB to be available.
"""
import time

from psycopg2 import OperationalError as Psycopg2OpError

from django.db.utils import OperationalError
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    """ Django commando to wait for database. """

    def handle(self, *args, **options):
        """Entrypoint for command."""
        self.stdout.write('Waiting for database...')
        db_up = False
        while db_up is False:
            try:
                self.check(databases=['default'])
                db_up = True
            except (Psycopg2OpError, OperationalError):
                self.stdout.write('Database unavailable, waiting 1 second...')
                time.sleep(1)

        self.stdout.write(self.style.SUCCESS('Database available!'))

Correr tests

docker compose run --rm app sh -c "python manage.py test"

[+] Creating 1/0
 ✔ Container django_rest_api_udemy-db-1  Running                  0.0s
Found 4 test(s).
System check identified no issues (0 silenced).
..Waiting for database...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database unavailable, waiting 1 second...
Database available!
.Waiting for database...
Database available!
.
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK

Correr command

docker compose run --rm app sh -c "python manage.py wait_for_db"

[+] Creating 1/0
 ✔ Container django_rest_api_udemy-db-1  Running                  0.0s
Waiting for database...
Database available!

Correr linter docker compose run --rm app sh -c "flake8" y corregir

Migraciones de la base de datos

Django ORM

  • Object Reational Mapper (ORM)
  • Capa de abstracción para los datos
    • Django maneja la estructura y cambios de la BD
    • Ayuda a enfocarse en el código de Python
    • Permite usar otras bases de datos
%%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%%
flowchart
subgraph "ORM"
DM["Define models"]
GMF["Generate
migration files"]
SD["Setup database"]
STD["Store data"]
DM -.-> GMF -.-> SD -.-> STD
end

Models

  • Cada modelo mapea a una tabla
  • Los modelos contienen
    • Nombre
    • Campos
    • Otra metadata
    • Lógica en Python personalizada

Modelo ejemplo

class Ingredient(models.Model):
    """Ingredient for recipies."""
    name = models.CharField(max_length=255)
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

Creación de las migraciones

  • Asegura que la app esta activa en settings.py
  • Se utiliza el CLI de Django python manage.py makemigrations
  • Aplicar migraciones python manage.py makemigrations
  • Correr despues de esperar por la base de datos

Actualización de Docker Compose y CI/CD

    ...
    command: >
      sh -c "python manage.py wait_for_db &&
             python manage.py migrate &&
             python manage.py runserver 0.0.0.0:8000"      
    ...

docker compose down

[+] Running 3/3
 ✔ Container django_rest_api_udemy-app-1  Removed  0.0s
 ✔ Container django_rest_api_udemy-db-1   Removed  0.1s
 ✔ Network django_rest_api_udemy_default  Removed  0.1s

docker compose up

Corriendo wait_for_db antes de los tests en GitHub Actions

checks.yml

    ...
      - name: Test
        run: >
        docker compose run --rm app sh -c "python manage.py wait_for_db &&
        python manage.py test"        
    ...