Saltar a contenido

Lab guiado: Flask como nueva capa de presentación


Antes de empezar

Descarga expendedora.zip del aula virtual y descomprímelo. Es el resultado de las fases anteriores del proyecto: arquitectura por capas completa, base de datos SQLite funcionando y menú de consola operativo.

Sobre ese código añadirás Flask como nueva capa de presentación. No vas a reescribir nada — vas a añadir una nueva forma de interactuar con el proyecto.

Entrega

Al terminar el lab entregarás un zip con todo el proyecto resultante (expendedora + los ficheros nuevos y modificados). Ese zip será la base de la siguiente sesión, así que asegúrate de que todo funciona antes de entregarlo.


Cómo trabajar

Puedes hacer este lab de forma individual o en parejas. Si eliges pareja, sigue el procedimiento conductor/navegante descrito a continuación. Si trabajas en solitario, salta a la sección "Antes de empezar: repasemos los conceptos".

Trabajo en parejas (opcional)

Rol Qué hace
Conductor Escribe el código y ejecuta los pasos. Solo el conductor toca el teclado.
Navegante Lee el enunciado en voz alta, propone soluciones, detecta errores y formula las preguntas.
  • Cambiad de rol al final de cada paso.
  • Al rotar, el navegante ocupa el teclado y el conductor se convierte en navegante.
  • Ambos debéis poder explicar cualquier parte del trabajo al terminar el lab.

Antes de empezar: repasemos los conceptos

Arquitectura por capas
La expendedora está dividida en presentación, aplicación, dominio e infraestructura. Cada capa tiene una responsabilidad clara y no debe conocer los detalles internos de las demás.

Capa de presentación
Es la que interactúa con el usuario: recibe su input y le muestra resultados. Hasta ahora era menu.py con print() e input(). Flask será una nueva forma de escribir esta misma capa.

HTTP y URLs
Cuando escribes una URL en el navegador, envías una petición HTTP a un servidor. Flask es el código Python que recibe esa petición y decide qué responder.

Route
En Flask, una route asocia una URL con una función Python. Cuando el navegador pide /productos, Flask ejecuta la función que hayas asociado a esa ruta.

Entorno virtual
Carpeta aislada donde se instalan las dependencias del proyecto. Evita conflictos entre paquetes de distintos proyectos.


Paso 1 — Preparar el entorno requirements.txt

Conceptos

Flask se instala como cualquier paquete Python con pip. Usaremos un entorno virtual para no contaminar el sistema. El fichero requirements.txt guarda las dependencias del proyecto.

Objetivo

Crear el entorno virtual e instalar Flask en el proyecto de la expendedora.

Tarea

Descomprime expendedora.zip y entra en la carpeta expendedora/ (la que contiene presentation/, application/, domain/...).

Empezamos declarando flask como dependencia: abre requirements.txt y añade una línea nueva con flask. El fichero ya contenía coverage de las fases anteriores; ahora declara también Flask. Debe quedar así:

coverage
flask

Luego crea el entorno virtual e instala las dependencias. Elige la línea de activación según tu sistema operativo (deja la otra comentada con #):

1
2
3
4
python -m venv .venv               # En Linux puede ser: python3 -m venv .venv
source .venv/Scripts/activate    # Windows con terminal Git Bash
# source .venv/bin/activate          # Linux/Mac
pip install -r requirements.txt

¿Tenías otro venv activo?

Si al abrir el terminal ves el prompt de otro entorno virtual (por ejemplo (otro_proyecto)), desactívalo con el comando deactivate antes de crear el nuevo.

Ubicación del entorno virtual

El .venv/ y el requirements.txt viven dentro de expendedora/, como ya se hacía en las fases anteriores del proyecto (ver docs/EJECUCION.md).

Pregunta

¿Por qué guardamos las dependencias en requirements.txt y no simplemente las instalamos?

Respuesta

Para que cualquier persona que clone el proyecto pueda instalar exactamente las mismas versiones con pip install -r requirements.txt, sin tener que adivinar qué librerías hacen falta.

Comprobación

pip show flask
Debe mostrar la versión de Flask instalada.

Reflexión

Respuestas
  • El entorno virtual aísla las dependencias del proyecto del resto del sistema.
  • requirements.txt hace el proyecto reproducible en cualquier máquina.
  • Flask se instala como cualquier otro paquete Python.

Paso 2 — Primera app Flask expendedora/presentation/app.py

Conceptos

Una app Flask se crea instanciando la clase Flask. Los routes se definen con el decorador @app.route(url). La función decorada es la que se ejecuta cuando el navegador pide esa URL.

Objetivo

Crear app.py con el primer route, y arrancarlo como módulo igual que se hace con el menú.

Tarea

Crea el fichero expendedora/presentation/app.py con este contenido:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def inicio():
    return '<h1>Expendedora — Interfaz web</h1>'

if __name__ == '__main__':
    app.run(debug=True)

¿Por qué en presentation/?

Flask es una nueva capa de presentación que convivirá con menu.py. Si menu.py vive en presentation/ porque es la presentación de consola, app.py vive en el mismo sitio porque es la presentación web. La estructura de carpetas refleja la arquitectura.

Sitúate en la carpeta padre de expendedora/ (la misma desde la que se lanza el menú). Si estás dentro de expendedora/, sube un nivel con cd ... Luego ejecuta:

python -m expendedora.presentation.app

Luego abre http://localhost:5000 en el navegador.

El servidor no termina

El comando no devuelve el prompt — el servidor queda escuchando peticiones. Esto es normal. Para detenerlo pulsa Ctrl+C. Para seguir trabajando mientras el servidor corre, abre otra terminal (y actívale el entorno virtual si vas a ejecutar pip o python).

Pista

Ejecutamos con python -m expendedora.presentation.app por la misma razón que el menú usa python -m expendedora.presentation.menu: para que Python trate expendedora/ como un paquete y los imports internos (from expendedora.infrastructure...) funcionen correctamente cuando los añadamos en el paso siguiente. Fíjate en la simetría: un módulo por capa de presentación. debug=True activa el modo de depuración: el servidor se reinicia automáticamente cuando cambias el código y muestra errores detallados en el navegador. Si ves errores raros al guardar un archivo, detén el servidor con Ctrl+C y reinícialo.

Comprobación

El navegador debe mostrar: Expendedora — Interfaz web
La terminal debe mostrar Running on http://127.0.0.1:5000

Reflexión

Respuestas
  • Flask es una clase Python. Crear la app es instanciarla.
  • @app.route('/') es un decorador que registra la función como manejadora de esa URL.
  • debug=True es solo para desarrollo, nunca para producción.

Paso 3 — Conectar Flask con la expendedora app.py

Conceptos

Flask es la nueva capa de presentación. Igual que menu.py, necesita un ServicioExpendedora para acceder a los datos. En lugar de construir la cadena repo → maquina → servicio a mano, reutilizamos crear_servicio_sqlite(), la función de inicio que ya usa menu.py.

¿Por qué se llama bootstrap?

En informática, bootstrap (arranque) es el proceso que monta todas las piezas de una aplicación antes de que empiece a funcionar — igual que un ordenador que se "autoinicia" al encenderse. crear_servicio_sqlite() es la función de inicio (bootstrap) del proyecto: abre la base de datos, crea el repositorio, lo pasa a la máquina, y esta a su vez al servicio. Con una sola llamada obtienes el servicio listo para usar.

Objetivo

Conectar app.py con la capa de aplicación usando la misma función de inicio que menu.py.

Tarea

from flask import Flask
from expendedora.infrastructure.datos_iniciales import crear_servicio_sqlite

servicio = crear_servicio_sqlite()

app = Flask(__name__)

@app.route('/')
def inicio():
    return '<h1>Expendedora — Interfaz web</h1>'

if __name__ == '__main__':
    app.run(debug=True)
Pista

crear_servicio_sqlite() devuelve un ServicioExpendedora completamente listo: abre la base de datos, instancia la máquina y monta el servicio. No modifiques nada en application/, domain/ ni infrastructure/ en este paso.

Pregunta

Abre presentation/menu.py y mira la función main(). ¿Qué línea crea el servicio? Compárala con la de app.py. ¿Qué conclusión sacas sobre qué sabe y qué no sabe la capa de presentación?

Respuesta

Ambos archivos llaman a crear_servicio_sqlite() y reciben un ServicioExpendedora. Ninguno importa sqlite3 directamente ni clases de dominio como Item. La presentación solo conoce la capa de aplicación (el servicio) — el dominio y la infraestructura quedan detrás del servicio.

Comprobación

El servidor debe seguir arrancando sin errores tras añadir el import y la línea del servicio.
Si aparece ModuleNotFoundError: No module named 'expendedora', revisa que estás ejecutando python -m expendedora.presentation.app desde la carpeta padre (la que contiene la carpeta expendedora/), no desde dentro del paquete (en la carpeta expendedora/).

Reflexión

Respuestas
  • app.py es la nueva menu.py: mismo punto de entrada a la capa de aplicación.
  • El dominio y la infraestructura no necesitan saber que ahora existe Flask.
  • La arquitectura por capas permite añadir una nueva presentación sin tocar nada más.

Paso 4 — Route /productos con datos reales app.py

Conceptos

Un route puede llamar a métodos del servicio y devolver los resultados al navegador. Es lo mismo que hacía los if/elif/else en menu.py. Por ahora devolveremos texto plano. En la siguiente sesión usaremos plantillas para generar HTML de respuesta.

Objetivo

Crear un route /productos que devuelva la lista de productos de la base de datos.

Tarea

Añade el siguiente route debajo del route /:

@app.route('/productos')
def listar_productos():
    productos = servicio.listar_productos()
    if not productos:
        return 'No hay productos.'
    lineas = [
        f"{codigo}{nombre}{precio_final:.2f} EUR (stock {cantidad})"
        for codigo, nombre, _precio_base, precio_final, cantidad, _descuento in productos
    ]
    return '<br>'.join(lineas)

Abre http://localhost:5000/productos en el navegador.

Pista

servicio.listar_productos() devuelve una lista de tuplas con el formato (codigo, nombre, precio_base, precio_final, cantidad, porcentaje_descuento). Lo mismo que ya usaba opcion_mostrar() en menu.py. El zip ya incluye una expendedora.db con datos de prueba. Si por algún motivo la BD está vacía, ejecuta python crear_bd.py desde la carpeta expendedora/ para recrearla.

Dos detalles del código

  • Guion bajo delante del nombre (_precio_base, _descuento): convención de Python para indicar que esa variable no se usa en el cuerpo del bucle. Si más adelante la necesitas, le quitas el guion bajo.
  • '<br>'.join(lineas): el navegador ignora los saltos de línea \n de Python al renderizar HTML. Para separar líneas visualmente se usa <br>, que es el salto de línea HTML.

Pregunta

En menu.py, la función opcion_mostrar() usa print() para mostrar cada producto. En el route, ¿qué sustituye a print()?

Respuesta

El return. En Flask, lo que la función devuelve es lo que el navegador recibe. print() escribe en la consola; return escribe el cuerpo de la respuesta HTTP que es enviada usando el protocolo del mismo nombre al navegador.

Comprobación

El navegador en /productos debe mostrar los productos de tu base de datos, uno por línea.

Reflexión

Respuestas
  • return en un route Flask equivale a print() en el menú de consola.
  • El servicio se usa exactamente igual que en menu.py — es la misma llamada, mismo resultado.
  • La base de datos no cambió, solo la forma de mostrar los datos.

Paso 5 — Preparar el servicio para consultar un producto servicios.py + maquina.py

Conceptos

Para consultar un producto concreto en Flask necesitamos un método que devuelva su información. seleccionar() no sirve — solo guarda la selección internamente sin devolver nada. Vamos a añadir obtener_producto(codigo) a la capa de aplicación y a la capa de dominio.

Objetivo

Añadir el método obtener_producto(codigo) al servicio y a la máquina.

Tarea

En expendedora/domain/maquina.py, añade este método inmediatamente después de seleccionar():

1
2
3
4
def obtener_producto(self, codigo):
    """Devuelve la informacion del producto indicado o lanza ProductoNoEncontradoError."""
    item = self._repo.obtener(codigo)   # lanza ProductoNoEncontradoError si no existe
    return item.mostrar_producto()

En expendedora/application/servicios.py, añade el método equivalente inmediatamente después de seleccionar():

1
2
3
def obtener_producto(self, codigo):
    """Devuelve la informacion del producto indicado o lanza ProductoNoEncontradoError."""
    return self._maquina.obtener_producto(codigo)
Pista

self._repo.obtener(codigo) ya lanza ProductoNoEncontradoError si el código no existe. No hace falta ningún if — la excepción llegará hasta el route.

Pregunta

Acabas de tocar domain/ y application/. ¿Contradice esto lo que vimos en la presentación sobre qué capas "no deberían cambiar" al añadir Flask?

Respuesta

No. Lo que no cambia por culpa de Flask es todo el código existente. Aquí estás añadiendo un caso de uso nuevo (consultar un producto por código) que también podría existir desde el menú. El cambio lo pide el dominio, no la interfaz web.

Comprobación

Guarda los dos ficheros (maquina.py y servicios.py) antes de probar.

Necesitas una terminal libre

Si dejaste el servidor Flask corriendo del Paso 2, su terminal está ocupada. Abre otra terminal, sitúate en la carpeta padre de expendedora/ y activa el entorno virtual (source .venv/bin/activate o equivalente). Sabrás que está activo cuando veas (.venv) al inicio del prompt. Si prefieres usar la misma terminal, detén el servidor con Ctrl+C antes.

Desde esa terminal abre el intérprete:

python

Y ejecuta línea por línea:

from expendedora.infrastructure.datos_iniciales import crear_servicio_sqlite
servicio = crear_servicio_sqlite()
print(servicio.obtener_producto('A1'))   # tupla con los datos
print(servicio.obtener_producto('ZZZ'))  # lanza ProductoNoEncontradoError

Reflexión

Respuestas
  • Añadir un caso de uso nuevo puede requerir tocar dominio y aplicación.
  • La excepción sube desde infraestructura hasta el route sin que ninguna capa intermedia la capture.
  • El repositorio ya hacía el trabajo; solo hay que dejar pasar el resultado.

Paso 6 — Route con parámetro app.py

Conceptos

Flask permite capturar partes de la URL como parámetros. @app.route('/producto/<codigo>') captura el segmento de la URL y lo pasa como argumento a la función. Las excepciones de dominio se capturan en el route, igual que menu.py las capturaba en su bucle main().

Objetivo

Crear un route /producto/<codigo> que devuelva los datos de un producto concreto y gestione el caso de producto no encontrado.

Tarea

Añade el import de la excepción al inicio del archivo, junto a los otros imports (no dentro del route):

from expendedora.infrastructure.errores import ProductoNoEncontradoError

Dónde va cada cosa

El import se añade arriba, al lado de los demás. El @app.route y la función ver_producto van después del route /productos.

Añade el route debajo del anterior:

1
2
3
4
5
6
7
8
9
@app.route('/producto/<codigo>')
def ver_producto(codigo):
    try:
        codigo, nombre, precio_base, precio_final, cantidad, descuento = \
            servicio.obtener_producto(codigo)
        return (f"{codigo}{nombre}{precio_final:.2f} EUR "
                f"(stock {cantidad})")
    except ProductoNoEncontradoError as e:
        return f"Error: {e}", 404
Pista

El segundo valor del return (404) es el código HTTP de respuesta. Cuando el producto no existe, el navegador recibe un error 404 para indicar dicho error, igual que en cualquier web cuando una URL no existe.

Pregunta

¿Qué URL escribirías para ver el producto con código A1? ¿Y para uno que no existe?
Antes de probarlo, predice qué verás en cada caso.

Respuesta
  • http://localhost:5000/producto/A1 → datos del producto A1.
  • http://localhost:5000/producto/ZZZ → "Error: No existe ningún producto con código 'ZZZ'" con código HTTP 404.

Comprobación

Prueba con un código que existe y con uno que no. Verifica que el manejo de la excepción funciona sin que el servidor se caiga.

Reflexión

Respuestas
  • <codigo> en la URL captura ese segmento y lo pasa a la función como argumento.
  • Las excepciones de dominio se capturan en el route, igual que en menu.py.
  • El route no sabe nada de SQLite — solo ve ProductoNoEncontradoError.

Paso 7 — Reflexión arquitectónica (sin teclado)

Conceptos

Antes de entregar, es momento de comprobar si la arquitectura por capas ha cumplido su promesa.

Objetivo

Escribir por escrito las conclusiones del lab.

Tarea

Responde por escrito en un comentario al inicio de app.py a las siguientes tres preguntas. Si trabajas en pareja, responded juntos.

# REFLEXIÓN ARQUITECTÓNICA — UT4 Lab 1
#
# 1. ¿Qué archivos del proyecto has tocado para añadir Flask?
#    ¿Cuáles no has tocado?
#
# 2. Dibuja el camino completo desde que el usuario escribe
#    http://localhost:5000/productos en el navegador hasta que recibe
#    la lista de productos. Indica qué capa interviene en cada paso.
#
# 3. ¿Por qué el try/except de ProductoNoEncontradoError está en el route
#    y no en servicios.py?
Orientación de respuesta
  1. Has creado presentation/app.py y actualizado requirements.txt, y has añadido el método obtener_producto en application/servicios.py y domain/maquina.py. No has tocado infrastructure/, item.py, repositorio_productos.py, repositorio_sqlite.py, repositorio_memoria.py ni errores.py. Fíjate en que la nueva capa de presentación (app.py) convive con la anterior (menu.py) en la misma carpeta — exactamente lo que prometía la arquitectura por capas.
  2. Navegador → petición HTTP → Flask (@app.route('/productos')) → listar_productos() del servicio (aplicación) → mostrar_productos() de la máquina (dominio) → listar() del repositorio (infraestructura) → SQLite → y vuelta.
  3. Porque el route es la nueva capa de presentación. La capa de aplicación no debe saber nada sobre cómo se le informa al usuario (si es un print, un HTML o un JSON). Esa decisión vive en la capa de presentación, igual que en menu.py.

Paso 8 — Actualizar la documentación del proyecto

Conceptos

Un proyecto bien mantenido tiene su documentación sincronizada con el código. En este paso actualizarás los ficheros afectados por los cambios de este lab.

Objetivo

Dejar README.md, CHANGELOG.md y docs/ en sintonía con la nueva capa de presentación web.

Tarea

CHANGELOG.md

Añade una entrada nueva al inicio del fichero:

## [0.5.0] - (Fase 05: interfaz web con Flask)

### Added
- `presentation/app.py`: interfaz web con Flask. Routes `/`, `/productos` y `/producto/<codigo>`.

### Changed
- `domain/maquina.py`: añadido `obtener_producto(codigo)`.
- `application/servicios.py`: añadido `obtener_producto(codigo)`.
- `requirements.txt`: añadida dependencia `flask`.

README.md

  1. En Requisitos, menciona que requirements.txt ahora incluye también Flask (quien clone el proyecto lo instalará junto al resto con pip install -r requirements.txt).
  2. En Quickstart y Uso, añade la sección de la interfaz web:
## Uso (interfaz web)
Ejecuta desde la carpeta padre que contiene el paquete `expendedora/`:

```bash
python -m expendedora.presentation.app
```

Abre `http://localhost:5000` en el navegador. Rutas disponibles:
- `/` — mensaje de bienvenida.
- `/productos` — lista todos los productos.
- `/producto/<codigo>` — detalle de un producto (404 si no existe).
  1. En Estructura del proyecto, añade app.py en presentation/:
presentation/
  menu.py
  app.py

docs/ARQUITECTURA_POR_CAPAS.md

  1. En Capas y responsabilidades, actualiza la línea de Presentation:

    Presentation: entrada/salida por consola (menu.py) o web (app.py). No contiene reglas de negocio.

  2. En Mapa de archivos, añade:

    presentation/app.py: interfaz web con Flask.

docs/EJECUCION.md

Añade una sección nueva tras "Ejecutar menu":

## Ejecutar interfaz web
```bash
python -m expendedora.presentation.app
```
Abre `http://localhost:5000` en el navegador.

Comprobación

Abre README.md y comprueba que alguien que no conozca el proyecto puede saber, solo leyéndolo, que ahora tiene dos interfaces: consola y web.


Problemas comunes

ModuleNotFoundError: No module named 'expendedora'

Estás ejecutando python -m expendedora.presentation.app desde dentro de la carpeta expendedora/. Debes lanzarlo desde la carpeta padre (la que contiene expendedora/). Usa cd .. para subir un nivel.

ModuleNotFoundError: No module named 'flask'

Has olvidado activar el entorno virtual. Actívalo con source .venv/bin/activate (Linux/Mac) o source .venv/Scripts/activate (Windows Git Bash). Sabrás que está activo cuando veas (.venv) al inicio del prompt.

Address already in use o Port 5000 is in use

Ya hay otro servidor Flask corriendo en el puerto 5000. Busca la terminal donde lo lanzaste y detenlo con Ctrl+C. Si no la encuentras, cierra todos los procesos python o reinicia el equipo.


Checklist de entrega

  • requirements.txt actualizado dentro de expendedora/ con flask declarado.
  • app.py creado en expendedora/presentation/ (al lado de menu.py), con servicio = crear_servicio_sqlite() arriba del archivo y usado en los routes. Ejecutable con python -m expendedora.presentation.app.
  • Método obtener_producto(codigo) añadido a application/servicios.py y a domain/maquina.py.
  • Route / con mensaje de bienvenida.
  • Route /productos que lista los productos usando servicio.listar_productos().
  • Route /producto/<codigo> con manejo de ProductoNoEncontradoError y respuesta 404.
  • Las capas infrastructure/ e item.py no se han tocado.
  • El menú de consola (menu.py) sigue funcionando sin cambios.
  • Reflexión arquitectónica escrita al inicio de app.py.
  • CHANGELOG.md, README.md, docs/ARQUITECTURA_POR_CAPAS.md y docs/EJECUCION.md actualizados.
  • Entregado un zip con todo el proyecto — será la base del siguiente lab.