.github/workflows | ||
app | ||
imgs_readme | ||
.dockerignore | ||
.gitignore | ||
docker-compose.yml | ||
Dockerfile | ||
README.md | ||
requirements.dev.txt | ||
requirements.txt |
REST API Django
Tecnologias
- Python 3.12.0
- Django 4.2.5
- Django REST Framework 3.14
- Django REST Swagger
- Docker 24.0.6 y (Docker-compose incluido con docker cli)
- PostgreSQL
- Git
- GitHub Actions
%%{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 projectapp/core/
código compartido entre multiples appsapp/user/
código relativo al usuarioapp/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 Composerun
comienza un contenedor específico definido en la configuración--rm
remueve el contenedorapp
es el nombre del servicio/aplicaciónsh -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 directoriotests/
, no ambos - Los moduloes de prueba deben comenzar con
test_
- Los directorios de pruebas deben contener el archivo
__init__.py
- Solo usar
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
- Crear la prueba para el comportamiento esperado tests.py
- La prueba debe fallar
- 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 realespatch
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 directoriotests.py
- Indentación de los casos de prueba
- Prefijo
test
faltante en los metodostest_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/
ytests.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 iniciardb
primero - El servicio
app
puede usar el hostname dedb
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
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
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
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
...
- name: Test
run: >
docker compose run --rm app sh -c "python manage.py wait_for_db &&
python manage.py test"
...