Este tutorial adapta pedagógicamente el Día 40. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso y de sus soluciones publicadas (gists enlazados en cada paso). Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: con Python 3.14 se comprobó, el 26 de septiembre de 2026: el significado del parámetro
stopsen la documentación oficial de SerpAPI y el comportamiento delNotificationManagerde la solución oficial contra un servidor de correo local (aiosmtpd, Día 32). No se crearon cuentas en Google Forms, Sheety, SerpAPI ni Twilio.
🔐 Datos personales (adaptación para el aula): hoy el programa recoge correos de otras personas y les envía mensajes. Eso exige:
- Consentimiento: en Colombia, la Ley 1581 de 2012 exige autorización para recolectar y usar datos personales. Un formulario real debe explicar para qué se usarán los datos y pedir que la persona acepte.
- Poder darse de baja: todo servicio de correos debe permitir dejar de recibirlos.
- En el aula: solo correos ficticios (por ejemplo,
ana@colegio.test) y envíos al servidor local del Día 32.
Tabla de contenidos
- Antes de empezar: de proyecto personal a producto
- Paso 1: un formulario compartible conectado a la hoja
- Paso 2: el proyecto inicial (el final de la parte 1)
- Paso 3: destinos sin vuelos directos
- Paso 4: obtener los correos de los clientes
- Paso 5: enviar un correo a todos los clientes
- 🚀 Proyecto del día: el Club de Vuelos
- 🌟 Proyecto extra: el Boletín de Becas y Convocatorias
- 🏆 Proyecto integrador del bloque de APIs y automatización (Días 32 a 40): la Plataforma de Alertas del Colegio
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 40
0. Antes de empezar: de proyecto personal a producto
(Fuente: 302 - Day 40 Goals- what you will make by the end of the day.md)
Ayer construimos un buscador de ofertas para una sola persona. Hoy lo convertimos en un producto: cualquier persona se inscribe y recibe por correo las mejores ofertas.
La profesora se inspiró en un servicio real, Jack's Flight Club: te inscribes con tu correo y te envían ofertas de vuelos muy por debajo del precio normal. Además, cobran por un servicio premium. Su observación: "Su servicio es esencialmente el mismo que hemos creado con Python; quizá tengan una hoja de cálculo más grande con más destinos". (El chiste del inicio, "la primera regla del Flight Club es no hablar del Flight Club", es una referencia a la película El club de la pelea.)
Lo nuevo hoy:
- Un formulario para que los usuarios se inscriban y queden guardados en una hoja
users. - Buscar vuelos con escalas cuando no hay directos.
- Enviar un correo con las ofertas a todos los inscritos.
Nota sobre el material: el video de objetivos muestra una inscripción por texto (el usuario escribe su nombre y correo en la consola y, tras una validación, se agrega a la hoja). Las instrucciones actuales del curso (
303) usan en su lugar un formulario de Google. Este tutorial sigue las instrucciones actuales y propone la versión por texto como reto (sección 1).
Prerequisito general: todo el Día 39;
smtplib(Día 32); comprensión de listas; parámetros con valor por defecto (Día 10); variables de entorno y.env(Días 35 y 39).
1. Paso 1: un formulario compartible conectado a la hoja
(Fuente: 303-step-1-create-a-sharable-form-linked-to-your-sheet.md)
📝 Contenido original del curso
Crear el formulario (Google Forms):
- En Google Drive, crear un formulario nuevo.
- Desactivar la recolección automática de correos y no limitar a una respuesta por persona (facilita las pruebas con datos ficticios).
- Agregar 3 preguntas: nombre, apellido y correo.
- Vincular el formulario con la hoja de ofertas de vuelos que ya existe.
- Renombrar la hoja de respuestas a
users. - Probar: enviar datos ficticios, copiar el enlace y abrirlo en una ventana de incógnito para enviar más respuestas. Deben aparecer en la hoja.
Configurar Sheety:
- Sincronizar el proyecto: la hoja
usersdebe aparecer debajo deprices(puede que haya que volver a iniciar sesión). - Habilitar GET (y POST y PUT) para el endpoint
users.
📖 Del formulario a las claves de la API
Sheety convierte los títulos de las columnas en claves camelCase (Día 38). Si tu pregunta del formulario dice "What is your email?", la clave será whatIsYourEmail? (así aparece en la solución del curso). Por eso el curso pide anotar el nombre exacto de la columna.
🏫 Para el aula
- Si se usa Google Forms, que sea la cuenta del docente y con respuestas ficticias.
- Un formulario real debe incluir una casilla de autorización de tratamiento de datos y explicar para qué se usarán.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: redacta el texto de autorización de datos que pondrías al inicio del formulario (máximo 4 líneas).
- 🟡 Reto 2: inscripción por texto (como en el video de objetivos): una función que pida nombre, apellido y correo por consola, pida el correo dos veces y solo continúe si coinciden.
- 🟠 Reto 3: guarda la inscripción del Reto 2 en
users.csv(modo aula) o con un POST a Sheety (modo real), con la clave raíz en singular:{"user": {...}}. - 🔴 Reto 4: valida que el correo tenga una forma razonable (un
@, un punto después del@, sin espacios) y que no esté ya inscrito.
2. Paso 2: el proyecto inicial (el final de la parte 1)
(Fuente: 304-step-2-download-the-starting-project.md)
📝 Contenido original del curso
El proyecto inicial de hoy es idéntico al proyecto terminado del Día 39. Puedes usar el tuyo, siempre que:
- lea los destinos de la hoja;
- busque vuelos para cada destino;
- imprima el precio más barato de cada ciudad;
- envíe una notificación si encuentra un precio menor.
El .env debe contener: SERPAPI_API_KEY, SHEETY_PRICES_ENDPOINT, SHEETY_USERNAME, SHEETY_PASSWORD y los datos de Twilio (TWILIO_SID, TWILIO_AUTH_TOKEN, TWILIO_VIRTUAL_NUMBER, TWILIO_VERIFIED_NUMBER, TWILIO_WHATSAPP_NUMBER).
Las tres mejoras de hoy: vuelos con escalas cuando no hay directos, lista de correos de clientes y envío de correos a todos.
💡 Reflexión: que el proyecto inicial de la parte 2 sea el final de la parte 1 muestra por qué valió la pena separar responsabilidades: cada mejora de hoy toca pocas clases.
3. Paso 3: destinos sin vuelos directos
(Fuente: 305-step-3-destinations-without-direct-flights.md)
🎯 Objetivo de aprendizaje
Usar un parámetro con valor por defecto para cambiar el comportamiento de un método, y programar una búsqueda de respaldo.
📝 Ejercicio original del curso
Muchos destinos populares no tienen vuelos directos (de Londres a Bali, por ejemplo, hay que hacer escala en Singapur). El programa debe:
- Buscar primero solo vuelos directos.
- Si no encuentra ninguno, buscar de nuevo permitiendo escalas.
- Guardar el número de escalas (
stops) para mostrarlo en la notificación.
Especificación del curso:
check_flights(..., is_direct=True): un parámetro nuevo con valor por defectoTrue.- En
FlightData.__init__, un atributo nuevostops. - Cada opción de vuelo trae una lista
flightscon los tramos: un vuelo directo tiene 1 tramo; con una escala, 2. Por eso:nr_stops = len(flight["flights"]) - 1. - En
main.py: sicheapest_flight.price == "N/A", buscar otra vez conis_direct=Falsee imprimir un aviso.
📖 El parámetro stops de SerpAPI
Según la documentación oficial de SerpAPI (verificada), stops no es "el número de escalas", sino una opción de filtro:
| Valor | Significado |
|---|---|
0 |
Cualquier número de escalas (por defecto) |
1 |
Solo vuelos directos |
2 |
1 escala o menos |
3 |
2 escalas o menos |
Es contraintuitivo: "stops": "1" significa "sin escalas". Otra razón para leer siempre la documentación en lugar de adivinar por el nombre.
🔎 La solución del curso
flight_search.py:
def check_flights(self, origin_city_code, destination_city_code, from_time, to_time, is_direct=True):
query = {
"engine": "google_flights",
# ... los mismos parámetros del Día 39 ...
"api_key": self._api_key,
}
if is_direct:
query["stops"] = "1" # solo vuelos directos
# ... petición y manejo de errores iguales ...flight_data.py: se agrega stops a FlightData y, en find_cheapest_flight, nr_stops = len(flight["flights"]) - 1 para el primer vuelo y para cada vuelo más barato.
main.py:
for destination in sheet_data:
print(f"Getting direct flights for {destination['city']}...")
flights = flight_search.check_flights(
ORIGIN_CITY_IATA, destination["iataCode"],
from_time=tomorrow, to_time=six_month_from_today
)
cheapest_flight = find_cheapest_flight(flights, return_date=six_month_from_today.strftime("%Y-%m-%d"))
if cheapest_flight.price == "N/A":
print(f"No direct flight to {destination['city']}. Looking for indirect flights...")
stopover_flights = flight_search.check_flights(
ORIGIN_CITY_IATA, destination["iataCode"],
from_time=tomorrow, to_time=six_month_from_today,
is_direct=False
)
cheapest_flight = find_cheapest_flight(stopover_flights, return_date=six_month_from_today.strftime("%Y-%m-%d"))- Como
is_directvaleTruepor defecto, las llamadas antiguas siguen funcionando sin cambios. - La segunda búsqueda no envía
stops, así que acepta cualquier número de escalas.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
"stops": "0" para vuelos directos |
Busca cualquier vuelo | Según la documentación, directos = "1" |
Olvidar pasar nr_stops al crear FlightData |
TypeError: missing 1 required positional argument: 'stops' |
Actualiza todas las llamadas, incluida la de "N/A" |
Contar las escalas como len(flight["flights"]) |
Un directo aparece con 1 escala | Resta 1 |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: con el
flights.jsonde ejemplo del Día 39, imprime cuántas escalas tiene cada opción. - 🟡 Reto 2: ¿cuántas búsquedas de SerpAPI gasta ahora una ejecución con 3 destinos en el peor caso? ¿Y en el mejor?
- 🟠 Reto 3: agrega a la hoja una columna "acepta escalas" (sí/no) por destino, y haz la segunda búsqueda solo si el destino lo permite.
- 🔴 Reto 4: en lugar de
is_direct(verdadero/falso), usa un parámetromax_stops(0, 1 o 2) y conviértelo al valor correcto destopssegún la tabla de la documentación.
4. Paso 4: obtener los correos de los clientes
(Fuente: 306-step-4-retrieve-your-customer-emails.md)
📝 Ejercicio original del curso
Objetivo: obtener los correos de la hoja users como una lista de Python en main.py.
Requisitos técnicos:
- Agregar al
.envlos endpoints depricesy deusers. - Crear
DataManager.get_customer_emails(), que devuelva los datos de la hojausers. - Leer todas las variables de entorno en un solo lugar:
__init__.
🔎 La solución del curso
class DataManager:
def __init__(self):
self._user = os.environ["SHEETY_USERNAME"]
self._password = os.environ["SHEETY_PASSWORD"]
self._authorization = HTTPBasicAuth(self._user, self._password)
self.users_endpoint = os.environ["SHEETY_USERS_ENDPOINT"]
self.destination_data = {}
self.customer_data = {}
def get_customer_emails(self):
response = requests.get(url=self.users_endpoint, auth=self._authorization)
data = response.json()
self.customer_data = data["users"]
return self.customer_dataY en main.py, una comprensión de listas extrae solo los correos:
customer_data = data_manager.get_customer_emails()
customer_email_list = [row["whatIsYourEmail?"] for row in customer_data]El curso advierte: el nombre de la columna depende de tu formulario; el tuyo puede ser distinto.
🛠️ Nota técnica: la solución publicada de este paso incluye en
main.pyla líneadata_manager.update_destination_codes(), un método que no existe en elDataManagerde esa misma solución: si la copias tal cual, obtendrásAttributeError: 'DataManager' object has no attribute 'update_destination_codes'. Su.envtambién traeAMADEUS_API_KEYyAMADEUS_SECRET. Son restos de una versión anterior del proyecto, que usaba la API de vuelos Amadeus en lugar de SerpAPI. Elimínalos. Moraleja: el código de las soluciones también se desactualiza; léelo antes de copiarlo.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: en modo aula, lee
users.csvcon pandas y obtén la misma lista con una comprensión. - 🟡 Reto 2: elimina correos duplicados y vacíos de la lista, conservando el orden.
- 🟠 Reto 3: en vez de una lista de correos, crea una lista de diccionarios
{"nombre": ..., "correo": ...}para poder saludar a cada persona por su nombre. - 🔴 Reto 4: agrega una columna
activo(sí/no) y envía solo a los activos. Así implementas la baja de un usuario sin borrar su fila.
5. Paso 5: enviar un correo a todos los clientes
(Fuente: 307-step-5-email-all-our-customers.md)
📝 Ejercicio original del curso
Objetivo: enviar a todos los usuarios de la hoja un correo con la oferta.
Requisitos técnicos:
- Agregar al
.envla dirección SMTP, el correo y la contraseña de aplicación (Día 32). - En
NotificationManager.__init__, leer todas las variables de entorno en un solo lugar. - Crear el método
send_emails(). - En
main.py, redactar un mensaje distinto según si el vuelo es directo o tiene escalas.
Advertencia del curso: el símbolo £ produce un UnicodeEncodeError. Soluciones: escribir "GBP" en lugar del símbolo, o codificar el mensaje en UTF-8.
🔎 La solución del curso
class NotificationManager:
def __init__(self):
self.smtp_address = os.environ["EMAIL_PROVIDER_SMTP_ADDRESS"]
self.email = os.environ["MY_EMAIL"]
self.email_password = os.environ["MY_EMAIL_PASSWORD"]
# ... variables de Twilio ...
self.client = Client(os.environ["TWILIO_SID"], os.environ["TWILIO_AUTH_TOKEN"])
self.connection = smtplib.SMTP(os.environ["EMAIL_PROVIDER_SMTP_ADDRESS"])
def send_emails(self, email_list, email_body):
with self.connection:
self.connection.starttls()
self.connection.login(self.email, self.email_password)
for email in email_list:
self.connection.sendmail(
from_addr=self.email,
to_addrs=email,
msg=f"Subject:New Low Price Flight!\n\n{email_body}".encode("utf-8")
)El mensaje según las escalas (main.py):
if cheapest_flight.stops == 0:
message = (f"Low price alert! Only GBP {cheapest_flight.price} to fly direct "
f"from {cheapest_flight.origin_airport} to {cheapest_flight.destination_airport}, "
f"on {cheapest_flight.out_date} until {cheapest_flight.return_date}.")
else:
message = (f"Low price alert! Only GBP {cheapest_flight.price} to fly "
f"from {cheapest_flight.origin_airport} to {cheapest_flight.destination_airport}, "
f"with {cheapest_flight.stops} stop(s) "
f"departing on {cheapest_flight.out_date} and returning on {cheapest_flight.return_date}.")
notification_manager.send_emails(email_list=customer_email_list, email_body=message).encode("utf-8")convierte el mensaje en bytes;sendmailacepta bytes sin intentar convertirlos a ASCII. Es la solución del curso al problema que viste en el Día 32 con la ñ.- Se envía un correo por persona (un
sendmailpor dirección). Así ningún cliente ve los correos de los demás. Poner a todos juntos en el campo "Para" revelaría la lista completa.
📖 Dos problemas del NotificationManager (análisis de esta adaptación)
Se probó la clase de la solución contra el servidor local del Día 32 (verificado):
- La conexión se usa una sola vez. La conexión se crea en
__init__, perosend_emailsla usa dentro dewith self.connection:, que la cierra al terminar. La primera llamada funciona; la segunda falla conSMTPServerDisconnected: please run connect() first. Enmain.py,send_emailsse llama dentro delforde destinos: si en una ejecución hay ofertas para dos destinos, los clientes no reciben la segunda. Solución: crear la conexión dentro desend_emails:with smtplib.SMTP(self.smtp_address, port=587) as connection:. - Falta el puerto.
smtplib.SMTP(dirección)sin puerto usa el 25 (verificado:smtplib.SMTP_PORTes 25). La lección244del Día 32 explicaba que la mayoría de servidores lo bloquean y que se debe usar 587.
Además, .encode("utf-8") evita el error, pero el correo no declara que está en UTF-8, así que algunos programas de correo podrían mostrar mal los símbolos. La alternativa del Día 32, EmailMessage con send_message, declara la codificación correctamente.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
| Mensaje con £ sin codificar | UnicodeEncodeError |
"GBP", .encode("utf-8") o EmailMessage |
Conexión creada en __init__ y cerrada por with |
El segundo envío falla | Crea la conexión dentro del método |
smtplib.SMTP(dirección) sin puerto |
Puerto 25, a menudo bloqueado | port=587 |
| Todos los clientes en un solo "Para" | Cada cliente ve los correos de los demás | Un envío por persona (o copia oculta) |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: en modo aula, reescribe
send_emailspara el servidor local (sinstarttlsnilogin) y envía a 3 correos ficticios. - 🟡 Reto 2: corrige el problema de la conexión y demuestra, con el servidor local, que dos llamadas seguidas funcionan.
- 🟠 Reto 3: reescribe el envío con
EmailMessage, un saludo con el nombre de cada persona y el precio en pesos colombianos con separador de miles. - 🔴 Reto 4: agrega al final de cada correo una línea "Para dejar de recibir estos correos, responde BAJA". Explica qué tendría que hacer el programa para procesar esas respuestas (no hace falta implementarlo).
6. 🚀 Proyecto del día: el Club de Vuelos
(Fuentes: 302 a 307)
Problema
Convertir el buscador personal de ofertas en un servicio que envía las ofertas a todos los inscritos.
Requisitos funcionales
- Inscripción de usuarios (Google Forms en modo real; consola o Tkinter con
users.csven modo aula). - Búsqueda de vuelos directos y, si no hay, con escalas.
- Si hay oferta, un mensaje distinto para vuelos directos y con escalas.
- Un correo por persona, a todos los usuarios activos.
Restricciones técnicas
Arquitectura de cuatro clases del Día 39; todos los secretos en .env; EmailMessage o .encode("utf-8"); puerto 587 (o el servidor local en modo aula).
Pruebas
| Caso | Esperado |
|---|---|
| Destino con vuelo directo más barato que el límite | Mensaje "fly direct" a todos |
| Destino sin directos, con escalas más baratas | Mensaje "with 1 stop(s)" |
| Destino sin ningún vuelo | Ningún mensaje; sin errores |
| Dos destinos con oferta en la misma ejecución | ⚠️ (ver 🧪) |
| Correo duplicado en la hoja | ⚠️ (ver 🧪) |
| Usuario inactivo | No recibe correo |
🧪 Prueba y depuración
- Dos ofertas, un solo correo. Con el
NotificationManagerde la solución, la segunda llamada asend_emailsfalla (sección 5). Reprodúcelo con el servidor local y dos ofertas simuladas, y corrígelo. - Duplicados. Si alguien se inscribió dos veces, recibe dos correos. Elimina duplicados antes de enviar.
- Un correo por oferta o un resumen. Con 3 ofertas y 50 usuarios, la solución envía 150 correos. Propón enviar un solo correo por usuario con todas las ofertas del día. ¿Qué cambias en
main.pyy enNotificationManager? - El
AttributeErrorheredado. Si copiaste la solución del paso 4, revisa que no quedeupdate_destination_codes()ni variables de Amadeus.
⭐ Extensión del proyecto
- Cada usuario elige desde qué ciudad sale (columna nueva) y recibe solo las ofertas desde su ciudad.
- Página de inscripción con Tkinter (Días 27 a 31).
- Programación diaria con GitHub Actions y los secretos del
.envcomo secretos del repositorio.
7. 🌟 Proyecto extra: el Boletín de Becas y Convocatorias
Situación
La orientadora escolar recibe muchas convocatorias (becas, olimpiadas académicas, cursos gratuitos, concursos) y no sabe a quién le interesa cada una. Quiere un servicio donde cada estudiante se inscriba con sus intereses y reciba solo las convocatorias que le correspondan.
Requisitos
- Inscripción por consola o Tkinter: nombre, correo (ficticio), grado e intereses (elegidos de una lista: ciencias, arte, deporte, tecnología, idiomas…), con autorización de datos obligatoria.
inscritos.csvyconvocatorias.csv(título, categoría, grado mínimo, fecha límite, enlace), con al menos 8 convocatorias ficticias.- Clases con responsabilidades separadas, como en el Club de Vuelos:
GestorDatos,Convocatoria,Buscador(qué convocatorias le sirven a cada inscrito) yNotificador. - Cada inscrito recibe un solo correo con sus convocatorias vigentes (fecha límite posterior a hoy y grado adecuado), ordenadas por fecha límite.
- Las convocatorias que vencen en menos de 7 días aparecen marcadas como "¡Última semana!".
- Opción de baja (marcar como inactivo).
- Envío al servidor local, con
EmailMessagey la conexión abierta dentro del método de envío. - Registro en
envios.csvpara no enviar dos veces la misma convocatoria a la misma persona.
Decisiones que debes tomar
- ¿Cada cuánto se envía el boletín? ¿Qué pasa si un estudiante no tiene convocatorias nuevas?
- ¿Cómo evitas que el correo sea tan largo que nadie lo lea?
Casos de prueba
| Caso | Esperado |
|---|---|
| Inscrito con interés "tecnología", grado 10 | Solo convocatorias de tecnología para grado ≤ 10 |
| Convocatoria vencida | No aparece |
| Ejecutar dos veces el mismo día | No se repiten envíos |
| Inscrito dado de baja | No recibe correo |
8. 🏆 Proyecto integrador del bloque de APIs y automatización (Días 32 a 40): la Plataforma de Alertas del Colegio
Proyecto integrador creado para esta adaptación. Cierra el bloque "intermedio+" del curso: correo y fechas (Día 32), APIs y parámetros (Día 33), APIs en interfaces con clases (Día 34), autenticación y variables de entorno (Día 35), combinación de APIs y caché (Día 36), POST, PUT, DELETE y encabezados (Día 37), hojas de cálculo como API (Día 38) y un producto multiusuario con arquitectura de clases (Días 39 y 40).
Situación problema
El colegio quiere una plataforma de alertas a la que cada miembro de la comunidad (estudiantes, docentes, acudientes; todos ficticios en el ejercicio) se inscriba para recibir solo las alertas que le interesan: lluvia antes de la jornada, cambio fuerte de la TRM, paso visible de la ISS, pregunta de trivia del día y recordatorios de tareas.
Pregunta orientadora
¿Cómo diseñar un servicio automatizado, seguro y respetuoso de los datos personales, que combine varias fuentes de datos y entregue a cada persona solo lo que eligió recibir?
Objetivo
Construir la Plataforma de Alertas del Colegio: inscripción, fuentes de datos, reglas de alerta y envío diario personalizado.
Requerimientos
- Inscripción (consola o Tkinter) con: nombre, correo ficticio, rol, alertas elegidas y autorización de datos. Opción de baja.
- Almacenamiento intercambiable (CSV local o Sheety con autenticación Bearer), elegido en
config.json. - Fuentes (una clase por fuente, con la misma interfaz): Open-Meteo (lluvia), TRM (datos.gov.co), ISS y horas de sol, Open Trivia DB y un
tareas.csv. - Reglas configurables en
config.json(umbrales de lluvia y TRM, distancia de la ISS, etc.). - Un correo por persona y por día, con
EmailMessage, solo con las alertas que eligió; las urgentes, al principio. - Caché con tiempos distintos por fuente (
urls_expire_after) y reintentos con límite ante errores temporales. - Secretos en
.envcon.gitignore; el programa verifica al iniciar que existan los necesarios. - Registro de envíos para no repetir alertas; registro de fallas de cada fuente.
- Programación: expresión cron para las 5:30 a. m. de Colombia y explicación de la hora UTC.
- Documento de privacidad de una página: qué datos se recogen, para qué, dónde se guardan, cómo darse de baja y cómo se relaciona con la Ley 1581 de 2012.
Restricciones
- Solo datos ficticios y el servidor de correo local.
- Ninguna fuente que exija cuentas personales de estudiantes.
- La lógica de reglas se prueba con datos simulados, sin internet.
Etapas de desarrollo
| Etapa | Entregable parcial |
|---|---|
| 1. Diseño | Diagrama de clases, flujo diario y documento de privacidad (borrador) |
| 2. Inscripción | Alta, baja y validaciones, con almacenamiento en CSV |
| 3. Fuentes | Cada fuente probada por separado, con caché y manejo de errores |
| 4. Reglas | Pruebas con 10 escenarios simulados |
| 5. Envío | Correos personalizados al servidor local, sin duplicados |
| 6. Automatización | Expresión cron y ejecución simulada de 5 días |
| 7. Presentación | Demostración y defensa del diseño ante el grupo |
Entregables
- Código organizado en paquetes o módulos,
config.json,.env.ejemplo(sin valores reales) y.gitignore. - Diagrama de clases y documento de privacidad.
- Al menos 15 casos de prueba con resultados.
- Registro de errores con al menos 8 errores reales y su solución.
- Video corto de la demostración.
Casos de prueba mínimos
| Caso | Esperado |
|---|---|
| Persona suscrita solo a "lluvia", día sin lluvia | No recibe correo (o un correo breve, según tu diseño documentado) |
| Persona suscrita a 3 alertas, 2 activas | Un solo correo con esas 2 |
| Una fuente cae | El correo sale con las demás alertas y se registra la falla |
| Ejecutar dos veces el mismo día | No se duplican correos |
| Persona dada de baja | No recibe nada |
| Cambiar de CSV a Sheety | Solo cambia config.json |
Criterios técnicos de funcionamiento
- Cada clase tiene una sola responsabilidad;
main.pysolo coordina. - Ninguna URL, clave ni umbral está escrito dentro de las funciones de lógica.
- El programa termina en menos de 1 minuto aunque todas las fuentes fallen.
Posibles extensiones
Panel Tkinter para la administración; estadísticas mensuales de alertas enviadas; análisis de datos de las lecturas con pandas y gráficas (se estudian en la parte avanzada del curso).
9. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué diferencia hay entre el proyecto del Día 39 y el del Día 40, desde el punto de vista del usuario?
- ¿Por qué el parámetro
is_direct=Trueno rompe las llamadas que ya existían? - ¿Qué significa
"stops": "1"en SerpAPI? ¿Por qué es fácil equivocarse? - ¿Por qué se envía un correo por persona en lugar de uno con todos en el campo "Para"?
- ¿Qué exige la Ley 1581 de 2012 para recoger correos de otras personas?
Análisis de código
customer_email_list = [row["whatIsYourEmail?"] for row in customer_data]¿De dónde sale el nombre whatIsYourEmail?? ¿Qué pasa si cambias el texto de la pregunta en el formulario?
Predicción
- Un vuelo con
flightsde 3 tramos, ¿cuántas escalas tiene? - ```python conexion = smtplib.SMTP("localhost", 8025) with conexion: conexion.sendmail(...) with conexion: conexion.sendmail(...)
¿Qué ocurre en el segundo `sendmail`?
3. ¿Qué puerto usa `smtplib.SMTP("smtp.gmail.com")`?
### Depuraciónpython
class NotificationManager:
def init(self):
self.connection = smtplib.SMTP(os.environ["SMTP"])
def send_emails(self, email_list, body):
with self.connection:
self.connection.starttls()
self.connection.login(os.environ["EMAIL"], os.environ["PASSWORD"])
self.connection.sendmail(os.environ["EMAIL"], email_list, f"Subject:Oferta £\n\n{body}")
``
Encuentra **cuatro** problemas (piensa en el puerto, en elwith`, en el símbolo £ y en la privacidad de los destinatarios).
Modificación
Cambia el mensaje para que diga "vuelo directo", "1 escala" o "2 escalas" en español, según stops, con una sola línea que use un diccionario.
Creación
Escribe correos_para_enviar(usuarios: list[dict]) -> list[str] que devuelva los correos únicos de los usuarios activos, sin espacios sobrantes y en minúsculas. Pruébala con 6 usuarios simulados, incluidos duplicados y un inactivo.
10. 🤖 Programar con apoyo de IA
Actividad 1: revisar código publicado. Las soluciones de este día tenían restos de una versión anterior (Amadeus). Pega en una IA el main.py y el data_manager.py de la solución del paso 4 (sin claves) y pregúntale: "¿Este código se ejecuta sin errores?". ¿Detecta que update_destination_codes no existe? Confírmalo ejecutando.
Actividad 2: la IA y la privacidad. Pide a una IA que redacte el texto de autorización de datos para el formulario de inscripción del Club de Vuelos, según la Ley 1581 de 2012. Luego:
- Compara el texto con la información oficial sobre la ley (por ejemplo, de la Superintendencia de Industria y Comercio).
- Revisa si menciona: finalidad, derechos del titular y cómo ejercerlos.
- Corrige lo que falte. Recuerda: la IA puede ayudar a redactar, pero la responsabilidad es de quien publica el formulario.
Actividad 3: depurar un error que solo aparece a veces. El error del segundo send_emails solo aparece cuando hay dos ofertas en la misma ejecución. Describe el síntoma a una IA sin mostrarle el código y pídele posibles causas. ¿Llega a la conexión cerrada? Compara con tu análisis de la sección 5.
11. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
302 |
Inscripción por texto con validación | Las instrucciones actuales (303) usan Google Forms |
Nota de la sección 0; Retos 2–4 de la sección 1 |
305 |
"Set stops to "1" or "0" depending on your needs" |
En SerpAPI, 1 = solo directos; 0 = cualquier número de escalas (verificado en la documentación) |
Sección 3 |
306 |
main.py de la solución llama update_destination_codes() |
El método no existe en el DataManager de esa solución: AttributeError. Restos de la versión con Amadeus (también en el .env) |
Nota técnica de la sección 4 |
307 |
Conexión SMTP creada en __init__ y usada con with en send_emails |
La segunda llamada falla con SMTPServerDisconnected (verificado con servidor local) |
Sección 5; 🧪 del proyecto |
307 |
smtplib.SMTP(dirección) sin puerto |
Usa el puerto 25 por defecto (verificado); el Día 32 recomienda 587 | Sección 5 |
307 |
.encode("utf-8") para el símbolo £ |
Evita el error, pero el correo no declara la codificación; EmailMessage es más correcto |
Sección 5 |
307 |
Un correo por oferta y por usuario | Con varias ofertas, cada usuario recibe varios correos | 🧪 del proyecto |
12. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Del proyecto personal al producto, Jack's Flight Club, inscripción de usuarios | 302 |
Sección 0 | — | Nota de privacidad | Secciones 6, 7 y 8 |
Google Forms vinculado a la hoja, hoja users, Sheety sincronizado, POST y PUT |
303 |
Sección 1 | Paso 1 | Retos 1–4 de la sección 1 | Secciones 6, 7 y 8 |
Proyecto inicial = final del Día 39, .env, tres mejoras |
304 |
Sección 2 | Paso 2 | — | Sección 6 |
Vuelos directos y con escalas, is_direct=True, stops, len(...) - 1 |
305 |
Sección 3 | Paso 3 | Retos 1–4 de la sección 3 | Sección 6 |
get_customer_emails, endpoint de users, comprensión de listas, variables en __init__ |
306 |
Sección 4 | Paso 4 | Retos 1–4 de la sección 4 | Secciones 6, 7 y 8 |
send_emails, SMTP en .env, £ y UTF-8, mensaje según escalas |
307 |
Sección 5 | Paso 5 | Retos 1–4 de la sección 5 | Secciones 6, 7 y 8 |
| (No existe en el curso original) | — | — | — | Boletín de Becas y Convocatorias | Sección 7 |
| (No existe en el curso original) | — | — | — | Plataforma de Alertas del Colegio (Días 32–40) | Sección 8 |
13. 📦 Evidencias de aprendizaje del Día 40
- El texto de autorización de datos de tu formulario o de tu inscripción por consola.
customer_email_listimpresa (con correos ficticios), sin duplicados ni inactivos.- La salida del programa mostrando "No direct flight to … Looking for indirect flights…" (con datos reales en la demostración del docente o con respuestas guardadas).
- Captura del servidor local recibiendo correos de dos ofertas en la misma ejecución, después de corregir la conexión.
- Tu propuesta de "un solo correo por usuario con todas las ofertas".
- El Boletín de Becas y Convocatorias (proyecto extra) o la Plataforma de Alertas del Colegio (proyecto integrador).
- Cuaderno de errores:
stopsmal interpretado,FlightDatasinstops, nombre de columna del formulario,AttributeErrorpor código heredado,UnicodeEncodeErrorcon £, conexión SMTP cerrada, puerto 25, destinatarios visibles y correos duplicados.