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

  1. Del requisito a la tarea: los seis hitos
  2. El orden de construcción y sus porqués
  3. Arquitectura en capas: la web traduce, el paquete resuelve
  4. Estructura de directorios del proyecto
  5. Diseño de datos: los tres ficheros
  6. Contratos antes que código
  7. Decisiones de diseño y sus porqués
  8. 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.py importa algo de app.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: RepositorioCatalogo con 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.md cualquier 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 es int, float o Libro.

Ejercicios

  1. Hito 0.4 — Esqueleto del proyecto. Crea el árbol de directorios completo (con __init__.py, ficheros vacíos o con pass, 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, y pytest se ejecuta (aunque sea con "no tests ran").
  2. Hito 0.5 — Contratos completos. Copia la tabla de contratos a DECISIONES.md y 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.
  3. 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

  1. Criterio de verificación adicional: desde la raíz, python -c "from papyrus import modelos, errores, servicios, repositorios" no debe fallar. Si falla con ModuleNotFoundError, revisa que papyrus/__init__.py existe y que ejecutas desde la raíz (el clásico de M3).
  2. Ejemplo de contrato de ruta (itinerario A): POST /api/ventas — entrada JSON {"titulo": str, "unidades": int, "socio": str|null, "cupon": str|null} — respuestas 201 con la venta creada, 404 si el título no existe, 409 si no hay stock, 400 si 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.
  3. 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados