diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 2bb2d6e..297cfed 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -16,6 +16,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Test
- run: docker compose run --rm app sh -c "python manage.py test"
+ run: docker compose run --rm app sh -c "python manage.py wait_for_db &&
+ python manage.py test"
- name: Lint
run: docker compose run --rm app sh -c "flake8"
diff --git a/Dockerfile b/Dockerfile
index 76098ca..6a4b877 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,7 +10,12 @@ WORKDIR /app
EXPOSE 8000
ARG DEV=false
+
+# apt install -y python3-dev libpq-dev python3-psycopg2 && \
RUN python -m venv /py && \
+ apt update && \
+ apt install -y postgresql-client python3-dev libpq-dev gcc python3-psycopg2 && \
+ apt clean && \
/py/bin/pip install --upgrade pip && \
/py/bin/pip install -r /tmp/requirements.txt && \
if [ $DEV = "true" ]; \
diff --git a/README.md b/README.md
index 7b7f7ba..28c1cab 100644
--- a/README.md
+++ b/README.md
@@ -134,7 +134,7 @@ 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/applicación
+ - `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
@@ -163,7 +163,7 @@ docker-compose build
### Testing
- Django test suite
-- Configurar test por cada applicación Django
+- Configurar test por cada aplicación Django
- Correr a travez de docker-compose `docker-compose run --rm app sh -c "python
manage.py test"`
@@ -213,7 +213,7 @@ TG ==> JB ==> RS
- Se cobra por minutos de uso
- 2.000 minutos *gratis*
-#### Configranción GitHub Actions
+#### Configuranción GitHub Actions
- Creación de archivo [`checks.yml`](./.github/workflows/checks.yml)
- Set Trigger
@@ -226,3 +226,582 @@ TG ==> JB ==> RS
- 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](./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](./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"
+ ...
+```
diff --git a/app/app/calc.py b/app/app/calc.py
new file mode 100644
index 0000000..9d3f2d6
--- /dev/null
+++ b/app/app/calc.py
@@ -0,0 +1,13 @@
+"""
+Calculator functions
+"""
+
+
+def add(x, y):
+ """ Add x and y and return result. """
+ return x + y
+
+
+def subtract(x, y):
+ """ Subtract x from y and return resutl. """
+ return x - y
diff --git a/app/app/settings.py b/app/app/settings.py
index f96fa36..43a7bec 100644
--- a/app/app/settings.py
+++ b/app/app/settings.py
@@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
-
+import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -37,6 +37,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+ 'core',
]
MIDDLEWARE = [
@@ -75,8 +76,11 @@ WSGI_APPLICATION = 'app.wsgi.application'
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': BASE_DIR / 'db.sqlite3',
+ '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_PASS'),
}
}
diff --git a/app/app/tests.py b/app/app/tests.py
new file mode 100644
index 0000000..e408f8f
--- /dev/null
+++ b/app/app/tests.py
@@ -0,0 +1,21 @@
+"""
+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)
+
+ def test_subtract_numbers(self):
+ """ Test subtracting numbers. """
+ res = calc.subtract(10, 5)
+
+ self.assertEqual(res, 5)
diff --git a/app/core/__init__.py b/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/admin.py b/app/core/admin.py
new file mode 100644
index 0000000..6af52da
--- /dev/null
+++ b/app/core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin # noqa
+
+# Register your models here.
diff --git a/app/core/apps.py b/app/core/apps.py
new file mode 100644
index 0000000..8115ae6
--- /dev/null
+++ b/app/core/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'core'
diff --git a/app/core/management/commands/__init__.py b/app/core/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/management/commands/wait_for_db.py b/app/core/management/commands/wait_for_db.py
new file mode 100644
index 0000000..6db1f92
--- /dev/null
+++ b/app/core/management/commands/wait_for_db.py
@@ -0,0 +1,27 @@
+"""
+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!'))
diff --git a/app/core/migrations/__init__.py b/app/core/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/models.py b/app/core/models.py
new file mode 100644
index 0000000..9d57c55
--- /dev/null
+++ b/app/core/models.py
@@ -0,0 +1,3 @@
+from django.db import models # noqa
+
+# Create your models here.
diff --git a/app/core/tests/__init__.py b/app/core/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/core/tests/test_commands.py b/app/core/tests/test_commands.py
new file mode 100644
index 0000000..db941da
--- /dev/null
+++ b/app/core/tests/test_commands.py
@@ -0,0 +1,35 @@
+"""
+Test custom Django management commands.
+"""
+from unittest.mock import patch
+
+from psycopg2 import OperationalError as Psycopg2OpError
+
+from django.core.management import call_command
+from django.db.utils import OperationalError
+from django.test import SimpleTestCase
+
+
+@patch('core.management.commands.wait_for_db.Command.check')
+class CommandTests(SimpleTestCase):
+ """Test Comands."""
+
+ def test_wait_for_db_ready(self, patched_check):
+ """Test waiting for database if database ready."""
+ patched_check.return_value = True
+
+ call_command('wait_for_db')
+
+ patched_check.assert_called_once_with(databases=['default'])
+
+ # ojo con el orden de los parametros "desde dentro hacia fuera"
+ @patch('time.sleep')
+ def test_wait_for_db_delay(self, patched_sleep, patched_check):
+ """Test waiting for database when getting OperationalError."""
+ patched_check.side_effect = [Psycopg2OpError] * 2 + \
+ [OperationalError] * 3 + [True]
+
+ call_command('wait_for_db')
+
+ self.assertEqual(patched_check.call_count, 6)
+ patched_check.assert_called_with(databases=['default'])
diff --git a/docker-compose.yml b/docker-compose.yml
index cacfcfb..8a89582 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,9 +4,33 @@ services:
context: .
args:
- DEV=true
+ - BUILDKIT_PROGRESS=plain docker compose build
ports:
- "8000:8000"
volumes:
- ./app:/app
command: >
- sh -c "python manage.py runserver 0.0.0.0:8000"
+ sh -c "python manage.py wait_for_db &&
+ python manage.py migrate &&
+ 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:
+
diff --git a/imgs_readme/docker_services_timeline1.png b/imgs_readme/docker_services_timeline1.png
new file mode 100644
index 0000000..948db83
Binary files /dev/null and b/imgs_readme/docker_services_timeline1.png differ
diff --git a/imgs_readme/docker_services_timeline2.png b/imgs_readme/docker_services_timeline2.png
new file mode 100644
index 0000000..b3f9e03
Binary files /dev/null and b/imgs_readme/docker_services_timeline2.png differ
diff --git a/requirements.txt b/requirements.txt
index f1b3359..e40fb74 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
Django==4.2.5
djangorestframework==3.14.0
+psycopg2>=2.9.9