Tienes el contrato (12-01); ahora toca el plano. Los proyectos personales no suelen fracasar por falta de conocimientos, sino por falta de estructura: se empieza por lo vistoso (la web), se descubre a mitad que el núcleo no aguanta, y el refactor desmoraliza. Esta lección convierte los requisitos en un plan ejecutable: seis hitos ordenados con criterio, una arquitectura en capas, la estructura de directorios definitiva, los esquemas de datos y — la parte más valiosa — los contratos de las funciones clave escritos antes de programarlas. Al terminar tendrás algo que muy poca gente tiene al empezar un proyecto: claridad sobre qué hacer cada día y cómo saber que está hecho.
Contenido
- Del requisito a la tarea: los seis hitos
- El orden de construcción y sus porqués
- Arquitectura en capas: la web traduce, el paquete resuelve
- Estructura de directorios del proyecto
- Diseño de datos: los tres ficheros
- Contratos antes que código
- Decisiones de diseño y sus porqués
- Gestión del tiempo para autodidactas
Del requisito a la tarea: los seis hitos
Un requisito ("gestión de catálogo") no se puede empezar un martes por la tarde; una tarea ("implementar RepositorioCatalogo.cargar()") sí. El puente entre ambos son los hitos: paquetes de tareas que terminan en algo demostrable. Papyrus Online se construye en seis:
| Hito | Qué entrega | Requisitos que cubre | Esfuerzo relativo | "Demo" al terminarlo |
|---|---|---|---|---|
| H1 — Dominio | modelos.py, errores.py, cupones.py limpios y anotados |
RF1 (modelo), RF2, RF3 (reglas), RNF2, RNF3 | ★★ | En un REPL: crear libros, calcular 12.35 € |
| H2 — Servicios | servicios.py con ServicioVentas integrado |
RF2, RF3, RF7 | ★★★ | Vender con socio+cupón desde un script |
| H3 — Persistencia | repositorios.py (JSON/CSV, guardado atómico) |
RF4 | ★★ | Vender, reiniciar, el stock persiste |
| H4 — Interfaz | API Flask o web Django mínima | RF5 | ★★★ (A) / ★★★★ (B) | Vender desde el navegador/curl |
| H5 — Informe | informe.py con números + PNG |
RF6 | ★★ | El PNG del mes en datos/ |
| H6 — Pulido | Logging completo, README, requirements | RF7, RNF4 | ★ | Otra persona lo instala y lo usa |
Los tests no son un hito: acompañan a cada hito (H1 se cierra con los tests de H1 en verde). Dejar los tests "para el final" es la receta del final que nunca llega; lo viste en M9 y aquí se aplica.
Las estrellas son esfuerzo relativo, no horas: si H1 te cuesta una sesión, espera unas tres para H2. Calibra con tu propia velocidad tras el primer hito — es el dato más honesto que tendrás.
El orden de construcción y sus porqués
El orden no es negociable caprichosamente: sigue las dependencias. No puedes probar la API sin servicios, ni los servicios sin dominio. El informe solo necesita el CSV, así que va tras la persistencia.
flowchart TD
H1["H1 Dominio<br/>Libro, Socio, errores, cupones"] --> H2["H2 Servicios<br/>ServicioVentas"]
H2 --> H3["H3 Persistencia<br/>repositorios JSON/CSV"]
H3 --> H4["H4 Interfaz<br/>Flask o Django"]
H3 --> H5["H5 Informe<br/>pandas + Matplotlib"]
H4 --> H6["H6 Pulido<br/>logging, README"]
H5 --> H6
¿Por qué core → persistencia → interfaz → datos y no al revés?
- El dominio primero porque todo lo demás lo consume y porque es lo más barato de probar: funciones puras, sin ficheros ni HTTP. Un error de redondeo detectado en H1 cuesta un minuto; detectado en H4, una tarde de perseguirlo a través de tres capas.
- La persistencia antes que la interfaz porque la interfaz necesita datos reales que servir, y porque el guardado atómico condiciona el diseño del servicio (¿quién guarda, el servicio o el endpoint? — lo decidimos abajo).
- La interfaz después porque es la capa que traduce, no la que resuelve: cuando llegues a ella, cada endpoint será una función de 10 líneas que llama a un servicio ya probado. Así se nota que el diseño es bueno: la parte "difícil" resulta fácil.
- El informe casi al final porque solo depende del CSV: es independiente de la interfaz (fíjate: H4 y H5 no tienen flecha entre sí — si un día te atascas en la interfaz, avanza el informe; tener trabajo paralelizable es oro para la moral).
Arquitectura en capas
La regla de M10 — "la web traduce, el paquete resuelve" — se convierte aquí en arquitectura explícita de cuatro capas:
flowchart TD
subgraph Interfaz["Capa de interfaz (app.py / views.py)"]
direction LR
A["HTTP → llamadas al servicio<br/>errores de dominio → códigos HTTP"]
end
subgraph Servicios["Capa de servicios (servicios.py)"]
B["ServicioVentas: orquesta reglas,<br/>repositorios y logging"]
end
subgraph Dominio["Capa de dominio (modelos, errores, cupones)"]
C["Reglas puras: precios, descuentos,<br/>validaciones. Sin ficheros, sin HTTP"]
end
subgraph Persistencia["Capa de persistencia (repositorios.py)"]
D["Cargar/guardar JSON y CSV.<br/>Sin reglas de negocio"]
end
Interfaz --> Servicios
Servicios --> Dominio
Servicios --> Persistencia
Dos reglas de dependencia que lo sostienen todo:
- Hacia abajo, nunca hacia arriba: el dominio no sabe que existe Flask; los repositorios no saben qué es un descuento. Si
modelos.pyimporta algo deapp.py, el diseño se ha roto (y probablemente tengas un import circular, vieja trampa de M3). - La capa de servicios es la única que orquesta: dominio (calcular precio) + persistencia (guardar) + logging. Los endpoints jamás tocan un fichero ni calculan un precio; solo traducen HTTP ↔ servicio.
El premio práctico: los tests. El dominio se prueba sin disco ni red (rápido); los repositorios con tmp_path (M9); la interfaz con el test client. Cada capa, con la herramienta que le toca — lo verás en 12-04.
Estructura de directorios del proyecto
Este es el árbol del itinerario A (el B sustituye app.py por el proyecto Django de 10-04, y el ORM asume catalogo.json):
papyrus_online/ ├── papyrus/ # el paquete: TODA la lógica vive aquí │ ├── __init__.py │ ├── modelos.py # Libro, Socio (dataclasses, M5) │ ├── errores.py # jerarquía ErrorPapyrus (M7) │ ├── cupones.py # CUPONES, aplicar_cupon (M7) │ ├── servicios.py # ServicioVentas (nuevo, H2) │ ├── repositorios.py # RepositorioCatalogo, RepositorioSocios, RegistroVentas (H3) │ └── informe.py # informe mensual con pandas (H5) ├── app.py # interfaz Flask: solo traduce (H4, itinerario A) ├── datos/ │ ├── catalogo.json # los 4 libros canónicos │ ├── socios.json # LUIS-001, MARTA-002, PAU-003 │ ├── ventas.csv # se crea al vender │ └── papyrus.log # RF7 ├── tests/ │ ├── conftest.py # fixtures canónicas (los 4 libros) │ ├── test_modelos.py │ ├── test_servicios.py │ ├── test_repositorios.py │ └── test_app.py # test client ├── DECISIONES.md # tu diario de decisiones (hito 0.1) ├── README.md # RNF4 (plantilla en 12-05) └── requirements.txt
Observa que almacen.py desaparece: sus responsabilidades se reparten entre servicios.py (las reglas: vender, reponer, cierre de caja) y repositorios.py (cargar/guardar). Es la evolución natural del diseño de M5-M6 ahora que sabes separar capas.
Diseño de datos: los tres ficheros
Fijar los esquemas antes de programar evita la peor clase de bug: dos módulos que entienden el mismo fichero de forma distinta.
datos/catalogo.json — lista de objetos, título único:
[
{"titulo": "La Odisea", "autor": "Homero", "precio": 12.50, "stock": 4},
{"titulo": "Hamlet", "autor": "Shakespeare", "precio": 9.95, "stock": 6},
{"titulo": "El Quijote", "autor": "Cervantes", "precio": 15.90, "stock": 8},
{"titulo": "Fausto", "autor": "Goethe", "precio": 21.00, "stock": 10}
]datos/socios.json — código único como clave natural:
[
{"codigo": "LUIS-001", "nombre": "Luis", "alta": "2025-03-12"},
{"codigo": "MARTA-002", "nombre": "Marta", "alta": "2025-06-30"},
{"codigo": "PAU-003", "nombre": "Pau", "alta": "2026-01-15"}
]datos/ventas.csv — una fila por venta, con cabecera:
| Columna | Tipo | Ejemplo | Nota |
|---|---|---|---|
fecha |
YYYY-MM-DD |
2026-07-13 |
formato ISO, como el date de todo el curso |
titulo |
str | Fausto |
debe existir en el catálogo en el momento de la venta |
unidades |
int ≥ 1 | 2 |
|
importe |
float, 2 decimales | 37.34 |
importe total de la línea, con descuentos ya aplicados |
Tipos claros = menos sorpresas: precio y stock son float e int en JSON, no strings; unidades en el CSV llegará como texto y alguien (el repositorio, no el informe) debe convertirlo. Decidirlo ahora, aquí, es diseño.
Contratos antes que código
Los type hints de 08-01 no eran adorno: son especificación. Escribir las firmas antes que los cuerpos te obliga a decidir entradas, salidas y errores cuando aún es barato cambiar de opinión. Estos son los contratos del corazón del sistema:
| Función / método | Firma (contrato) | Errores que lanza |
|---|---|---|
Libro.precio_final |
(self, socio: bool = False) -> float |
— |
aplicar_cupon |
(precio: float, codigo: str) -> float |
CuponInvalidoError |
ServicioVentas.vender |
(self, titulo: str, unidades: int, codigo_socio: str | None = None, cupon: str | None = None) -> Venta |
LibroNoEncontradoError, StockInsuficienteError, SocioInvalidoError, CuponInvalidoError |
ServicioVentas.cierre_de_caja |
(self, fecha: str) -> float |
— |
RepositorioCatalogo.cargar |
(self) -> dict[str, Libro] |
FileNotFoundError (fichero ausente es error técnico, no de negocio) |
RepositorioCatalogo.guardar |
(self, catalogo: dict[str, Libro]) -> None |
— (atómico: o todo o nada) |
RegistroVentas.anotar |
(self, venta: Venta) -> None |
— |
generar_informe |
(ruta_csv: Path, mes: str, salida_png: Path) -> ResumenMes |
FileNotFoundError |
Venta y ResumenMes serán dataclasses (M5): una venta con fecha, titulo, unidades, importe; un resumen con unidades_totales, importe_total, top_titulos, mejor_dia. Definir estos tipos de retorno ya es la mitad del diseño de H2 y H5.
Fíjate en lo que la tabla decide sin escribir una línea de código: que vender recibe el código de socio (no un booleano — el servicio valida contra socios.json, cosa que el precio_final(socio=True) de M5 no podía hacer), y que devuelve una Venta (no None), porque la interfaz querrá mostrar el importe.
Decisiones de diseño y sus porqués
Toda decisión descarta una alternativa. Documentar el porqué (en DECISIONES.md) es lo que diferencia un criterio de una casualidad:
| Decisión | Alternativa descartada | Criterio |
|---|---|---|
Catálogo como dict[str, Libro] (clave = título) |
Lista de libros | Búsqueda O(1) y unicidad gratis (M4); la lista obliga a recorrer y a vigilar duplicados |
| El servicio guarda tras cada venta | Guardar solo al salir | Si el programa muere, no se pierden ventas; el coste (escribir un JSON pequeño) es despreciable |
| Cupón se aplica después del descuento de socio | Antes, o excluyentes | Es la regla de negocio de M7 y la que reproducen los importes canónicos (18.67 €) |
| Errores de negocio = excepciones propias | Devolver None / códigos |
La interfaz distingue qué falló y elige el HTTP correcto (404 vs 409); None no dice nada |
| Ventas en CSV (no JSON) | JSON de ventas | Es append-only (una línea por venta, sin reescribir el fichero) y pandas lo lee directo para RF6 |
| Fechas como texto ISO | Objetos datetime en los ficheros |
JSON no tiene tipo fecha; ISO ordena bien como texto y pandas lo parsea con parse_dates |
Cuando dudes entre dos opciones y ambas parezcan válidas: elige la que sea más fácil de probar. Es un desempate que casi nunca falla.
Gestión del tiempo para autodidactas
Sin fechas de entrega ni jefe, el riesgo no es hacerlo mal: es no terminarlo. Tres prácticas que funcionan:
- Timeboxing honesto: sesiones de 60-90 minutos con un objetivo escrito antes de empezar ("hoy:
RepositorioCatalogocon sus tests"). Si el objetivo no cabe en la sesión, era demasiado grande: pártelo. - Definición de "hecho" — un hito está hecho cuando, y solo cuando: (1) su demo de la tabla de hitos funciona, (2) sus tests pasan, (3) las funciones públicas tienen type hints, y (4) has apuntado en
DECISIONES.mdcualquier decisión tomada. Sin los cuatro, está "casi hecho", que es el estado donde los proyectos van a morir. - La regla de la sesión siguiente: termina cada sesión anotando cuál es el primer paso de la próxima. Retomar un proyecto es lo que más cuesta; regálate el arranque.
Un ritmo orientativo: H1 en 1-2 sesiones, H2 en 2-3, H3 en 2, H4 en 2-3 (A) o 3-4 (B), H5 en 1-2, H6 en 1. Entre 9 y 15 sesiones: un proyecto de dos a cuatro semanas a ritmo de autodidacta. Es un plan, no una promesa — ajústalo tras H1 con tu velocidad real.
Errores Comunes y Consejos
- Diseñar de más. No necesitas interfaces abstractas ni patrones que no entiendas "por si acaso". Cuatro capas, ocho contratos y tres esquemas: eso es diseño suficiente para este tamaño. El sobrediseño es procrastinación con buena conciencia.
- Saltarse los contratos "porque ya lo tengo en la cabeza". En la cabeza caben dos firmas; a la tercera empiezan a contradecirse. Escríbelas: la tabla de contratos es la página que más consultarás en 12-03.
- Confundir capa con carpeta. Puedes tener las cuatro capas en cuatro ficheros planos (como nuestro árbol); lo que define la capa es qué importa a qué, no dónde está el fichero.
- Estimar en horas absolutas. "Esto son dos horas" falla siempre; "esto es como H1, que me costó una sesión" falla mucho menos. Estima comparando, no adivinando.
- No decidir quién convierte los tipos. El clásico: el CSV entrega
"2"y el informe suma strings. La regla de esta lección: la capa de persistencia entrega tipos correctos; a partir del repositorio, todo esint,floatoLibro.
Ejercicios
- Hito 0.4 — Esqueleto del proyecto. Crea el árbol de directorios completo (con
__init__.py, ficheros vacíos o conpass, y los tres ficheros de datos con el contenido canónico de esta lección). Verificación:python -c "import papyrus"funciona desde la raíz del proyecto, ypytestse ejecuta (aunque sea con "no tests ran"). - Hito 0.5 — Contratos completos. Copia la tabla de contratos a
DECISIONES.mdy añade los que faltan para tu itinerario: A) las 6 rutas de la API con método HTTP, entrada y códigos de respuesta; B) las vistas y forms. No implementes nada: solo firmas y errores. - Hito 0.6 — Tu plan. Escribe tu calendario de hitos (fechas o número de sesiones por hito) y tu definición de "hecho" personalizada. Compromiso realista: si solo tienes 3 sesiones por semana, que el plan lo diga.
Soluciones
- Criterio de verificación adicional: desde la raíz,
python -c "from papyrus import modelos, errores, servicios, repositorios"no debe fallar. Si falla conModuleNotFoundError, revisa quepapyrus/__init__.pyexiste y que ejecutas desde la raíz (el clásico de M3). - Ejemplo de contrato de ruta (itinerario A):
POST /api/ventas— entrada JSON{"titulo": str, "unidades": int, "socio": str|null, "cupon": str|null}— respuestas201con la venta creada,404si el título no existe,409si no hay stock,400si el socio o el cupón son inválidos o el JSON está mal formado. Si tu tabla no dice qué código devuelve cada error de dominio, aún no está terminada. - No hay solución única; hay una comprobación: enséñale el plan a tu yo escéptico. Si una semana tiene "H2 + H3 + H4", tu yo escéptico tiene razón.
Conclusión
Ya no tienes un deseo ("hacer Papyrus Online"): tienes un plan. Seis hitos con demo verificable, un orden que sigue las dependencias reales (dominio → servicios → persistencia → interfaz e informe), una arquitectura de cuatro capas donde la web traduce y el paquete resuelve, esquemas de datos que eliminan ambigüedades y contratos con type hints que son especificación, no adorno. Y algo igual de importante: una definición de "hecho" y un calendario honesto. La próxima lección es la que llevas esperando doce módulos: abrir el editor y construir, hito a hito, con esqueletos donde debas pensar tú y solución completa donde la integración lo justifique. El plano está sobre la mesa; ahora, los ladrillos.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
