# REST API Django ## Contenido - [**Inicio**](./README.md) - [User API](./docs/01_user_api.md) - [Recipe API](./docs/02_recipe_api.md) - [Tag API](./docs/03_tag_api.md) - [Ingredient API](./docs/04_ingredient_api.md) - [Image API](./docs/05_image_api.md) - [Filters](./docs/06_filters.md) ## Tecnologias - [Python](https://docs.python.org/3/) 3.12.0 - [Django](https://docs.djangoproject.com/en/4.2/) 4.2.5 - [Django REST Framework](https://www.django-rest-framework.org/) 3.14 - [Django REST Swagger](https://django-rest-swagger.readthedocs.io/en/latest/) - [Docker](https://docs.docker.com/) 24.0.6 y *([Docker-compose](https://docs.docker.com/compose/)* incluido con docker cli) - [PostgreSQL](https://www.postgresql.org/about/) - Git - GitHub Actions ```mermaid %%{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 **T**est **D**riven **D**eveloment ```mermaid %%{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
- [Dockerfile](./Dockerfile) - [.dockerignore](./.dockerignore) ```sh docker build . ``` - [docker-compose.yml](./docker-compose.yml) ```sh docker compose build ``` ### Linting - Instalar `flake8` - [requirements.dev.txt](./requirements.dev.txt) - Configuración [flake8](./app/.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 ```sh docker compose run -rm app sh -c "django-admin startproject app ." ``` ### Iniciar el servidor ```sh 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? ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%% flowchart subgraph " " direction LR TG["Trigger Push to GitHub"] JB["Job Run unit tests"] RS["Result 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`](./.github/workflows/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` ```txt 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 ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%% flowchart subgraph " " direction LR RT["Runs Tests"] CD["Clears data"] 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 ```py """" 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`](./app/app/calc.py) con [`tests.py`](app/app/tests.py) ```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"` ```python 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](./app/app/tests.py) 2. La prueba debe fallar 3. Crear el código para que el test pase (añadir la funcionalidad) [calc.py](./app/app/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. ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%% flowchart subgraph " " direction LR RU["register_user()"] CIDB["create_in_db()"] SWE["send_welcome_email()"] 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 ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'natural'}}}%% flowchart subgraph " " direction LR CDB["check_db()"] SLP["sleep()"] 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. ```py 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 ```py 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 ```py 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 ```mermaid %%{init: {'theme': 'dark','themeVariables': {'clusterBkg': '#2b2f38'}, 'flowchart': {'curve': 'basis'}}}%% flowchart direction TB subgraph "Docker Compose" DB[(PostgreSQL)] direction LR APP("App Django") DB <-..-> APP end ``` ### Conectividad de red ```yml 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 ```yml 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](./docker-compose.yml) ```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`](./app/app/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 ```py 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](./docs/imgs_readme/docker_services_timeline1.png) 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](./docs/imgs_readme/docker_services_timeline2.png) 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` ```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"` ```sh [+] 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"` ```sh [+] 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 ```mermaid %%{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 ```py 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 ```yml ... 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` ```sh [+] 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](./.github/workflows/checks.yml) ```yml ... - name: Test run: > docker compose run --rm app sh -c "python manage.py wait_for_db && python manage.py test" ... ``` ---- - [**Inicio**](./README.md) - [User API](./docs/01_user_api.md) - [Recipe API](./docs/02_recipe_api.md) - [Tag API](./docs/03_tag_api.md) - [Ingredient API](./docs/04_ingredient_api.md) - [Image API](./docs/05_image_api.md) - [Filters](./docs/06_filters.md)