Haz que una plantilla sirva para cada invitado

PythonBeginner
Practicar Ahora

Introducción

Una cena comunitaria necesita una invitación para cada persona. Convertirás una lista CSV de invitados, los datos JSON del evento y un diseño HTML preparado en un nuevo lote de archivos personalizados. Dos personas con el mismo nombre deben recibir archivos distintos, y nombres como Zoë, "Z" & <Friends> deben mostrarse como texto.

Necesitas conocer cadenas, diccionarios, bucles y funciones de Python, además del uso básico del terminal. Este laboratorio explica la lectura de archivos, los registros CSV, las plantillas, el escape de texto HTML y los nuevos directorios de salida. La interfaz de línea de comandos y el diseño están preparados; implementarás el mapeo de campos, la validación completa y la generación individual. No crearás CSS, enviarás correo ni producirás PDF. Tiempo estimado por el autor: 55–75 minutos, incluida la lectura y los errores habituales.

Abre tu primera invitación

En este paso, generarás una invitación normal y relacionarás la página con sus campos de entrada. Empieza en WebIDE, abre Terminal → New Terminal y espera al indicador del shell de labex. En una ventana estrecha, Terminal puede estar dentro de More (...). Introduce todos los comandos en ese terminal. El espacio preparado es independiente de otros laboratorios.

cd cambia el directorio actual. Una ruta absoluta empieza por /; las rutas relativas se resuelven desde el directorio actual. Si un comando posterior no encuentra un archivo después de cambiar de directorio, vuelve aquí con el mismo comando:

cd /home/labex/project/invitations

cat muestra un archivo de texto. Lee los datos del evento:

cat event.json

JSON usa llaves para los objetos y comillas dobles para las claves y los valores de cadena. Estos cuatro valores serán el título, la fecha, la hora y el lugar de la invitación. El ejemplo listo para ejecutar, one.py, ya los asigna junto con un invitado al diseño proporcionado. python3 ejecuta un archivo Python. Genera el ejemplo en el nuevo directorio batches/first:

python3 one.py

Salida esperada:

Created batch: batches/first

Cambia de la pestaña de plataforma WebIDE a Web 8080. La preparación ya ha iniciado el visor de invitaciones; no necesitas Go Live. Actualiza la vista y busca first/g001.html. Pulsa Open invitation para abrir el archivo HTML real en una nueva pestaña del navegador. Lee Maya Patel, The Long Table, la hora y el lugar. Cierra esa pestaña del navegador para volver a Web 8080. Download HTML guarda el mismo archivo para utilizarlo fuera del laboratorio.

Una invitación completa para Maya Patel

El saludo identifica a la persona; las dos secciones inferiores muestran la hora y el lugar del evento.

La vista solo lee archivos que realmente existen en batches. No genera invitaciones ausentes. El directorio anterior batches/archive se conserva como trabajo previo. Tu programa final creará nuevos lotes y mantendrá estos archivos y todas las entradas. Vuelve a WebIDE para el siguiente paso.

Asigna los registros CSV al texto HTML

En este paso, harás que el renderizador reutilizable coloque cada valor del invitado en el espacio de texto adecuado. Vuelve al mismo terminal y espacio de WebIDE; las rutas relativas siguientes parten de /home/labex/project/invitations.

Lee registros en lugar de separar por comas

CSV es un formato de tabla. Su primera fila contiene los nombres de campo. Una coma separa campos, salvo cuando aparece dentro de un campo entre comillas; una comilla dentro de ese campo se duplica. Por tanto, split(",") no puede leer correctamente todas las filas. Lee la lista preparada:

cat guests.csv

Hay dos registros de Alex Chen con valores guest_id diferentes. El nombre del último invitado incluye una coma, comillas, & y corchetes angulares. Todos son caracteres válidos para un nombre. El identificador estable identifica a una persona; el nombre visible no lo hace.

El archivo preparado generate.py lee estos registros. Léelo antes de cambiar el renderizador:

cat generate.py

with ... as source abre un archivo y lo cierra al terminar el bloque indentado. encoding="utf-8" conserva los caracteres Unicode. newline="" permite que el lector CSV gestione los límites de los registros. csv.DictReader(source) utiliza la cabecera para crear un diccionario por registro; list(...) reúne los registros en una lista. Por ejemplo, guest["name"] contiene un nombre completo, incluida cualquier coma que forme parte de él. json.loads(...) convierte el texto JSON del evento en un diccionario. Estos lectores y la interfaz de comandos ya están implementados.

Distingue las comillas CSV del escape HTML

Una página HTML utiliza <...> para las etiquetas y &...; para las referencias de caracteres. Las comillas CSV protegen la estructura de la tabla, pero no impiden interpretar un nombre como <Friends> como HTML. html.escape(value, quote=True) convierte los caracteres especiales en referencias que el navegador presenta como el texto original. Aquí cada valor se introduce en un espacio de texto HTML. Esto no es una receta para insertar datos en JavaScript, CSS o URL arbitrarios.

Template usa $name, $title y los demás marcadores $field de invitation.html. substitute(mapping) sustituye cada marcador por el valor correspondiente del diccionario y rechaza las asignaciones ausentes. Los seis marcadores del cuerpo, data-field="name", message, title, date, time y venue, identifican los espacios semánticos de texto. Mantén uno de cada uno con el texto correspondiente. Conserva intacta la plantilla de entrada. Se aceptan etiquetas, diseños y entidades equivalentes en la salida generada; un nombre presente solo en el <title> de la página no ocupa su espacio en el cuerpo.

Crea el renderizador siguiente. cat > rendering.py escribe un archivo y sustituye su contenido anterior. El delimitador entre comillas de un bloque here-document <<'PYCODE' trata como texto literal todo lo que aparece hasta el cierre PYCODE; el cierre debe ocupar una línea propia. Solo sustituye tu renderizador, sin modificar las entradas.

El diccionario asigna explícitamente los campos del invitado y del evento. values.items() produce pares clave/valor. La comprensión de diccionario {key: ... for key, value in values.items()} escapa cada valor y conserva su clave. from html import escape importa la función de la biblioteca estándar.

cat > rendering.py <<'PYCODE'
from html import escape


def render_guest(guest, event, template):
    values = {"name": guest["name"], "message": guest["message"],
              "title": event["title"], "date": event["date"],
              "time": event["time"], "venue": event["venue"]}
    return template.substitute({key: escape(value, quote=True)
                                for key, value in values.items()})
PYCODE

La función devuelve texto HTML; no crea archivos. Los módulos auxiliares locales pueden ser archivos .py en el mismo directorio que generate.py; los subdirectorios de paquetes quedan fuera de este ejercicio. El escape se realiza una sola vez, antes de la sustitución, de modo que el navegador muestra & en lugar del texto visible &amp;. Ya has separado el diseño de la invitación de su texto personalizado.

Valida a todos antes de crear un lote

En este paso, generarás un archivo por identificador y protegerás el trabajo anterior. Continúa en el mismo terminal de WebIDE.

El batch.py inicial utiliza los nombres de las personas como nombres de archivo y permite directorios existentes. Dos personas llamadas Alex Chen pueden sobrescribirse entre sí. Lee esa pequeña función:

cat batch.py

El contrato exige un directorio de lote nuevo con un archivo <guest_id>.html por fila. Los identificadores solo admiten letras ASCII minúsculas, dígitos y guiones. Los nombres y mensajes siguen siendo texto UTF-8 sin esa restricción. Los campos obligatorios de invitado son guest_id, name, message; los del evento son title, date, time, venue. Debes rechazar con un error útil un valor obligatorio vacío o compuesto solo por espacios, un identificador duplicado o un directorio de salida existente. Conserva el CSV, ambos archivos de evento, la plantilla y batches/archive.

Separa la preparación de la escritura

Primero valida todos los registros y renderiza todas las páginas. Solo después de completar esa fase debes crear el directorio y escribir. De lo contrario, un último registro incorrecto podría dejar un lote parcial engañoso. Esto protege frente a los errores de entrada enseñados aquí; no garantiza una transacción frente a fallos de disco o cortes de alimentación.

isinstance(value, str) comprueba que un valor sea texto. event.get(field) devuelve None si falta una clave, lo que permite informar de un error útil. strip() comprueba si hay algo más que espacios, sin modificar el texto que se renderizará. raise ValueError(...) comunica una entrada no válida; la interfaz preparada la captura, imprime Cannot generate invitations: ... y termina con error.

Un set() guarda los identificadores vistos. in comprueba la pertenencia y seen.add(...) recuerda un identificador. all(...) comprueba que todos los caracteres cumplan la regla indicada. pages.append((guest_id, page)) guarda un par identificador/página. Finalmente, for guest_id, page in pages desempaqueta esos pares.

Path(output) representa el directorio solicitado. mkdir() lo crea y rechaza una ruta existente porque no pasamos exist_ok=True. El operador / une rutas. Una f-string como f"{guest_id}.html" inserta el identificador en el nombre. write_text(..., encoding="utf-8") escribe el HTML completo como UTF-8.

Sustituye únicamente batch.py por esta implementación central:

cat > batch.py <<'PYCODE'
from pathlib import Path
from rendering import render_guest


def write_batch(guests, event, template, output):
    for field in ("title", "date", "time", "venue"):
        if not isinstance(event.get(field), str) or not event[field].strip():
            raise ValueError(f"Missing event field: {field}")
    if not guests:
        raise ValueError("The guest list is empty")
    seen = set()
    pages = []
    for guest in guests:
        for field in ("guest_id", "name", "message"):
            if not isinstance(guest.get(field), str) or not guest[field].strip():
                raise ValueError(f"Missing guest field: {field}")
        guest_id = guest["guest_id"]
        if not all(character in "abcdefghijklmnopqrstuvwxyz0123456789-" for character in guest_id):
            raise ValueError("Guest IDs must use lowercase letters, digits, or hyphens")
        if guest_id in seen:
            raise ValueError(f"Duplicate guest ID: {guest_id}")
        seen.add(guest_id)
        pages.append((guest_id, render_guest(guest, event, template)))
    output = Path(output)
    output.mkdir()  ## An existing batch is always refused.
    for guest_id, page in pages:
        (output / f"{guest_id}.html").write_text(page, encoding="utf-8")
PYCODE

from rendering import render_guest usa tu renderizador local. El primer bucle comprueba el evento; el siguiente valida los invitados y prepara las páginas; el último escribe el lote completo. No se crea ningún archivo hasta terminar los bucles de validación y renderizado.

La interfaz recibe cuatro rutas posicionales, en este orden: CSV de invitados, JSON del evento, plantilla HTML y un directorio nuevo. Ejecútala con la lista completa:

python3 generate.py guests.csv event.json invitation.html batches/evening

Salida esperada:

Created batch: batches/evening

ls muestra los miembros del directorio:

ls batches/evening

Debes encontrar g101.html, g102.html y g103.html. Las dos invitaciones de Alex tienen identificadores independientes. Los archivos conservan el mensaje de cada persona; el tercero mantiene su coma, comillas, Unicode y corchetes angulares literales.

Reutiliza el generador y conserva el trabajo anterior

En este paso, usarás el mismo programa para otro evento y practicarás la recuperación de errores habituales. Empieza en el mismo terminal de WebIDE. Los comandos siguientes son independientes, por lo que un fallo esperado no impide continuar.

Primero repite deliberadamente la ruta de salida completada. Debe fallar: mkdir() rechaza un lote existente. El mensaje empieza por Cannot generate invitations: e incluye un error de archivo existente. El lote anterior debe permanecer intacto.

python3 generate.py guests.csv event.json invitation.html batches/evening

Lee el ejemplo preparado de identificador duplicado. El segundo invitado tiene el identificador del primero:

cat invalid-duplicate.csv

Intenta crear un lote con él. Este fallo también es esperado y comunica Duplicate guest ID: g101 antes de crear cualquier directorio:

python3 generate.py invalid-duplicate.csv event.json invitation.html batches/rejected-duplicate

El ejemplo sin mensaje falla intencionadamente con Missing guest field: message. Aunque las filas anteriores sean válidas, no debe aparecer ningún lote parcial:

python3 generate.py invalid-missing.csv event.json invitation.html batches/rejected-missing

Una ruta mal escrita es otro problema: el sistema operativo no puede abrir el archivo. El próximo comando usa deliberadamente guest.csv, que no existe. Lee el error; no reinicies el entorno:

python3 generate.py guest.csv daytime.json invitation.html batches/daytime

Recupérate usando la ruta real guests.csv. Lee el segundo evento y crea un nuevo lote con el mismo programa y lista:

cat daytime.json
python3 generate.py guests.csv daytime.json invitation.html batches/daytime

Salida esperada:

Created batch: batches/daytime

Cambia a Web 8080 y actualiza. Abre evening/g101.html y evening/g102.html por separado. Comparten nombre, pero conservan mensajes distintos. Abre evening/g103.html: la coma, las comillas, ë, el ampersand y <Friends> deben aparecer como texto literal. Abre daytime/g103.html y lee el nuevo título, hora y lugar. Cierra la pestaña del navegador de cada invitación para volver a la lista. Usa Download HTML para la invitación diurna y abre el archivo descargado: su texto personalizado debe coincidir con la vista.

Caracteres especiales mostrados como texto literal

El nombre conserva su coma, comillas y corchetes angulares. Son texto visible en el saludo.

Los dos lotes son independientes. El CSV, los eventos y la plantilla no han cambiado, y el archivo previo sigue presente. La entrada no válida, un lote existente y una ruta de entrada ausente tienen explicaciones distintas; puedes corregir cada caso sin destruir las invitaciones anteriores.

Resumen

Has generado invitaciones HTML reales y descargables a partir de registros CSV y datos JSON del evento. Separaste los identificadores estables de los nombres visibles, asignaste los campos a los espacios de texto de la plantilla, conservaste Unicode y los caracteres especiales mediante UTF-8 y el escape HTML, y validaste todo el lote antes de escribir. El generador puede crear nuevos lotes de eventos conservando las entradas y el trabajo anterior.