Saltar a contenido

🛠 Unidad ISS-01 · Settings y selección del motor — capa 🛠 CONSTRUIR

🧠 Comprender este bloque → · ✅ GATE de la unidad

Capa Página Para qué
🧠 Aprender Guía de estudio comprender, explicar y relacionar
🛠 Construir esta página ejecutar, programar y verificar
✅ GATE Condiciones de cierre condición para pasar al bloque siguiente

Esta es la guía ejecutable. El cuerpo de abajo es el ISS técnico verbatim: comandos, rutas, versiones, verificaciones y criterios, sin simplificar.


ISS-01 — Settings y selección del motor

Objetivo

Elegir un solo motor por proceso, desde .env, sin escribir SQL.

Requisitos

  • ISS-00 superado y la terminal con (.venv) activo.
  • Variables separadas para PostgreSQL, MySQL, SQL Server y Oracle.

Construcción

Confirme el entorno antes de instalar. Si which python no apunta a .venv/bin/python, active el entorno. Instalar con el python3 del sistema deja los paquetes fuera del proyecto y manage.py sigue sin verlos.

source .venv/bin/activate
which python

Este ISS instala la lectura de .env y el driver de cada motor. Django no trae esos drivers. Sin ellos, migrate falla al importar el backend, aunque database.py ya esté escrito.

python -m pip install "python-dotenv==1.2.3"
python -m pip install "psycopg[binary]==3.3.6"
python -m pip install "mysqlclient==2.3.0"
python -m pip install "mssql-django==2.0.0"
python -m pip install "oracledb==26.0.1"
python -m pip freeze > requirements.txt

Qué hace cada paquete:

  • python-dotenv expone load_dotenv. manage.py, wsgi.py y asgi.py lo importan antes de cargar settings. Si el import falla, el proceso ni llega a Django.
  • psycopg es el driver que usa django.db.backends.postgresql. El extra binary incluye la librería C ya compilada.
  • mysqlclient es el driver que Django documenta para django.db.backends.mysql.
  • mssql-django registra el motor mssql. Arrastra pyodbc. En la máquina hace falta ODBC Driver 18; el paquete Python no lo instala.
  • oracledb reemplaza a cx_Oracle, deprecado desde Django 5.0. Se instala aunque este entorno no llegue a abrir Oracle.

settings.py ya existe: lo creó startproject. Un archivo y un paquete no pueden llamarse igual, así que se borra el archivo y la CLI no interviene: el paquete se escribe con cat.

rm config/settings.py
mkdir -p config/settings

.env.example es nuevo. No lleva secretos. Es la plantilla: nombres de variables y valores vacíos. El proceso no lo lee.

cat > .env.example <<'EOF'
# Copie este archivo a .env y complete los valores.
# .env no se versiona.

DJANGO_SECRET_KEY=
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,testserver

# Motor activo. Uno solo por ejecución: postgresql | mysql | mssql | oracle
DB_ENGINE=postgresql

POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=5432

MYSQL_DB=
MYSQL_USER=
MYSQL_PASSWORD=
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306

MSSQL_DB=
MSSQL_USER=
MSSQL_PASSWORD=
MSSQL_HOST=127.0.0.1
MSSQL_PORT=1433

ORACLE_DB=
ORACLE_USER=
ORACLE_PASSWORD=
ORACLE_HOST=127.0.0.1
ORACLE_PORT=1521
ORACLE_SERVICE_NAME=

# Laboratorio académico. En producción liste orígenes explícitos.
CORS_ALLOW_ALL_ORIGINS=True
EOF
cp .env.example .env

Complete en .env al menos DJANGO_SECRET_KEY y las cinco variables del motor que va a usar. .env no se versiona.

config/database.py es nuevo. No es una app. Traduce DB_ENGINE a un solo diccionario DATABASES["default"]. Cada función _postgresql, _mysql, _mssql y _oracle exige sus variables y elige el ENGINE. El de SQL Server es mssql, el nombre que registra mssql-django. El de Oracle arma un DSN con SERVICE_NAME y deja PORT vacío para que Django no lo trate como SID.

cat > config/database.py <<'EOF'
"""Selección del motor de base de datos a partir del entorno.

Un solo alias, default, queda activo en cada ejecución.
"""

from django.core.exceptions import ImproperlyConfigured

ALLOWED_ENGINES = ("postgresql", "mysql", "mssql", "oracle")


def database_from_environ(env):
    engine = str(env.get("DB_ENGINE", "postgresql")).strip().lower()
    if engine not in ALLOWED_ENGINES:
        allowed = ", ".join(ALLOWED_ENGINES)
        raise ImproperlyConfigured(
            f"DB_ENGINE='{engine}' no es válido. Valores permitidos: {allowed}."
        )
    builders = {
        "postgresql": _postgresql,
        "mysql": _mysql,
        "mssql": _mssql,
        "oracle": _oracle,
    }
    return {"default": builders[engine](env)}


def _required(env, key):
    value = env.get(key)
    if value is None or str(value).strip() == "":
        raise ImproperlyConfigured(
            f"Falta la variable de entorno {key} para DB_ENGINE={env.get('DB_ENGINE')}."
        )
    return str(value).strip()


def _postgresql(env):
    return {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": _required(env, "POSTGRES_DB"),
        "USER": _required(env, "POSTGRES_USER"),
        "PASSWORD": _required(env, "POSTGRES_PASSWORD"),
        "HOST": _required(env, "POSTGRES_HOST"),
        "PORT": _required(env, "POSTGRES_PORT"),
    }


def _mysql(env):
    return {
        "ENGINE": "django.db.backends.mysql",
        "NAME": _required(env, "MYSQL_DB"),
        "USER": _required(env, "MYSQL_USER"),
        "PASSWORD": _required(env, "MYSQL_PASSWORD"),
        "HOST": _required(env, "MYSQL_HOST"),
        "PORT": _required(env, "MYSQL_PORT"),
        "OPTIONS": {"charset": "utf8mb4"},
    }


def _mssql(env):
    return {
        "ENGINE": "mssql",
        "NAME": _required(env, "MSSQL_DB"),
        "USER": _required(env, "MSSQL_USER"),
        "PASSWORD": _required(env, "MSSQL_PASSWORD"),
        "HOST": _required(env, "MSSQL_HOST"),
        "PORT": _required(env, "MSSQL_PORT"),
        "OPTIONS": {
            "driver": "ODBC Driver 18 for SQL Server",
            "extra_params": "TrustServerCertificate=yes",
        },
    }


def _oracle(env):
    host = _required(env, "ORACLE_HOST")
    port = _required(env, "ORACLE_PORT")
    service = _required(env, "ORACLE_SERVICE_NAME")
    dsn = (
        f"(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST={host})(PORT={port}))"
        f"(CONNECT_DATA=(SERVICE_NAME={service})))"
    )
    return {
        "ENGINE": "django.db.backends.oracle",
        "NAME": dsn,
        "USER": _required(env, "ORACLE_USER"),
        "PASSWORD": _required(env, "ORACLE_PASSWORD"),
        "HOST": host,
        "PORT": "",
    }
EOF

El paquete de settings, en este ISS, todavía no conoce apps de negocio, DRF ni JWT. Esos bloques se parchean en el ISS que los necesita.

cat > config/settings/__init__.py <<'EOF'
"""Configuración de StoreLab.

El paquete sustituye al settings.py generado por startproject.
DJANGO_SETTINGS_MODULE sigue siendo config.settings.
"""

import os
from pathlib import Path

# python-dotenv, instalado en este ISS. Lee .env antes de SECRET_KEY y DATABASES.
from dotenv import load_dotenv

from config.database import database_from_environ

BASE_DIR = Path(__file__).resolve().parent.parent.parent
load_dotenv(BASE_DIR / ".env")

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "").strip()
if not SECRET_KEY:
    from django.core.exceptions import ImproperlyConfigured

    raise ImproperlyConfigured("Defina DJANGO_SECRET_KEY en el archivo .env.")

DEBUG = os.environ.get("DJANGO_DEBUG", "False").strip().lower() in {"1", "true", "yes", "on"}

ALLOWED_HOSTS = [
    host.strip()
    for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
    if host.strip()
]

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "config.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]

WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"

DATABASES = database_from_environ(os.environ)

AUTH_PASSWORD_VALIDATORS = [
    {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
    {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
    {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
    {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]

LANGUAGE_CODE = "es"
TIME_ZONE = "America/Bogota"
USE_I18N = True
USE_TZ = True

STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
EOF

config/project_python.py es nuevo. Solo usa la biblioteca estándar. Si python3 manage.py arrancó con el Python del sistema, vuelve a ejecutar el mismo comando con .venv/bin/python. Ahí están python-dotenv y Django. El binario de .venv es un enlace al Python del sistema; por eso la comparación mira sys.prefix, no la ruta resuelta del ejecutable.

cat > config/project_python.py <<'EOF'
"""Intérprete del proyecto.

Los paquetes se instalan dentro de .venv. El comando python3 manage.py
usa el Python del sistema y no ve esos paquetes.
use_project_python() vuelve a lanzar el mismo comando con .venv/bin/python.
"""

import os
import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent


def use_project_python():
    """Reejecuta el proceso con el Python de .venv si aún no es ese intérprete."""
    venv_root = PROJECT_ROOT / ".venv"
    venv_python = venv_root / "bin" / "python"
    if not venv_python.is_file():
        return
    try:
        if Path(sys.prefix).resolve() == venv_root.resolve():
            return
    except OSError:
        return
    os.execv(venv_python, [str(venv_python), *sys.argv])
EOF

manage.py, wsgi.py y asgi.py ya existen. No se regeneran. Sin load_dotenv(), Django arranca sin leer .env y SECRET_KEY o POSTGRES_DB aparecen vacíos.

ARCHIVO: manage.py

UBICAR:
import os
import sys

REEMPLAZAR POR:
import sys
from pathlib import Path

_ROOT = Path(__file__).resolve().parent
if str(_ROOT) not in sys.path:
    sys.path.insert(0, str(_ROOT))

from config.project_python import use_project_python

use_project_python()

import os

# python-dotenv se instaló en este ISS, dentro de .venv.
from dotenv import load_dotenv

DEBAJO DE:
    """Run administrative tasks."""

AGREGAR:
    load_dotenv()
ARCHIVO: config/wsgi.py

UBICAR:
import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

REEMPLAZAR POR:
import os

# python-dotenv (este ISS) carga .env en el intérprete que arrancó el proceso.
from dotenv import load_dotenv

from django.core.wsgi import get_wsgi_application

load_dotenv()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

El mismo parche en config/asgi.py, cambiando wsgi por asgi y get_wsgi_application por get_asgi_application.

El test que comprueba los cuatro motores no cabe todavía: no hay una app instalada donde Django lo descubra. Se escribe en el ISS-03, dentro de apps/security/tests.py. Hasta entonces la verificación de este ISS es python manage.py check.

Explicación por bloques

  • Intérprete. use_project_python() permite python3 manage.py runserver sin activar el entorno. pip sigue exigiendo source .venv/bin/activate: si se instala con el Python del sistema, .venv queda vacío y el salto no encuentra los paquetes.
  • Lectura del entorno. load_dotenv() no pisa variables que ya existan. Por eso DB_ENGINE=mysql python manage.py test cambia el motor sin editar .env.
  • Un alias. Solo existe DATABASES["default"]. El ORM no sabe cuál motor responde.
  • Oracle. El DSN se arma con SERVICE_NAME y PORT queda vacío para que Django use NAME como DSN literal. El driver es oracledb.
  • SQL Server. ENGINE es mssql, el valor que registra mssql-django, no django.db.backends.mssql.
  • Zona e idioma. LANGUAGE_CODE = "es" y TIME_ZONE = "America/Bogota", con USE_TZ = True.
.env → DB_ENGINE → database_from_environ → DATABASES["default"] → ORM

Criterios de aceptación

  • AC-01-01: postgresql, mysql, mssql y oracle producen el ENGINE documentado.
  • AC-01-02: un motor desconocido falla con ImproperlyConfigured.
  • AC-01-03: falta una variable obligatoria y la configuración falla antes de conectar.
  • AC-01-04: el DSN de Oracle contiene SERVICE_NAME.

Verificación

DatabaseSelectionTests usa SimpleTestCase. No abre el motor. Comprueba la decisión de configuración. La conectividad se exige en el cierre.

Evidencias

  • EVI-01-01: DatabaseSelectionTests PASS dentro de la suite.

GATE

AC Verificación Evidencia Resultado
AC-01-01 test por motor EVI-01-01 PASS
AC-01-02 test de motor inválido EVI-01-01 PASS
AC-01-03 test de variable vacía EVI-01-01 PASS
AC-01-04 aserción SERVICE_NAME EVI-01-01 PASS

✅ GATE de la unidad ISS-01 — este bloque no añade ningún criterio nuevo.

Las condiciones de cierre son las de esta misma página:

Con el GATE en verde queda habilitado el bloque siguiente de la ruta.

Navegación de la ruta: ← ISS-01 · 🧠 Aprender · ↑ Ruta Django · → ISS-02 · 🧠 Aprender