🛠 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.
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-dotenvexponeload_dotenv.manage.py,wsgi.pyyasgi.pylo importan antes de cargar settings. Si el import falla, el proceso ni llega a Django.psycopges el driver que usadjango.db.backends.postgresql. El extrabinaryincluye la librería C ya compilada.mysqlclientes el driver que Django documenta paradjango.db.backends.mysql.mssql-djangoregistra el motormssql. Arrastrapyodbc. En la máquina hace falta ODBC Driver 18; el paquete Python no lo instala.oracledbreemplaza acx_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.
.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()permitepython3 manage.py runserversin activar el entorno.pipsigue exigiendosource .venv/bin/activate: si se instala con el Python del sistema,.venvqueda vacío y el salto no encuentra los paquetes. - Lectura del entorno.
load_dotenv()no pisa variables que ya existan. Por esoDB_ENGINE=mysql python manage.py testcambia 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_NAMEyPORTqueda vacío para que Django useNAMEcomo DSN literal. El driver esoracledb. - SQL Server.
ENGINEesmssql, el valor que registramssql-django, nodjango.db.backends.mssql. - Zona e idioma.
LANGUAGE_CODE = "es"yTIME_ZONE = "America/Bogota", conUSE_TZ = True.
Criterios de aceptación
- AC-01-01:
postgresql,mysql,mssqlyoracleproducen elENGINEdocumentado. - 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:
DatabaseSelectionTestsPASS 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:
- 📋 Criterios de aceptación → Criterios de aceptación
- ✅ GATE → GATE
- 🔎 Verificación → Verificación
- 🧠 Autoevaluación → Evaluación del cuaderno
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