Este tutorial adapta pedagógicamente el Día 49. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso (lecciones 358 y 360 a 367). La lección 359 (paso 1) no está en la carpeta; lo que se sabe de ese paso viene del video de objetivos (
358). Las soluciones oficiales están en los 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: el 26 de septiembre de 2026, con Python 3.14, Selenium 4.49 y Chrome 153 (headless), se comprobó el sitio de práctica Snack & Lift (
appbrewery.github.io/gym/): el inicio de sesión con la cuenta de ejemplo del curso, los ids de los elementos y la estructura del horario de clases. También se probó una funciónretrycon fallos simulados.
Tabla de contenidos
- Antes de empezar: el gimnasio Snack & Lift
- Pasos 1 y 2: perfil persistente e inicio de sesión automático
- Paso 3: reservar la clase del martes a las 6 p. m.
- Pasos 4 y 5: decidir según el estado del botón y contar resultados
- Pasos 6 y 7: martes y jueves, y verificar las reservas
- Paso 8: control de calidad viajando en el tiempo
- Paso 9: resistencia a fallos de red con una función
retry - 🚀 Proyecto del día: el bot de reservas del gimnasio
- 🌟 Proyecto extra: reservas de la sala de informática
- 🏆 Proyecto integrador del bloque (Días 45 a 51) — Etapa 5
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 49
0. Antes de empezar: el gimnasio Snack & Lift
(Fuente: 358 - Day 49 Goals- what you will make by the end of the day.md)
¿Te ha pasado que cuando por fin te acuerdas de reservar algo (una cancha, una sala, un turno), ya no hay cupo? A la profesora le pasaba con las clases de spinning: el sistema de reservas abría a medianoche, "cuando ya estás en pijama soñando con código en lugar de cardio".
Para practicar, el curso creó un gimnasio ficticio: Snack & Lift ("Levanta pesas. Come snacks. Repite."), en appbrewery.github.io/gym/ (verificado).
Cómo funciona el sitio (según el video):
- Todo se simula en tu navegador: al cargar la página se crea una base de datos local (IndexedDB) con usuarios y reservas de ejemplo. Puedes verla en DevTools → pestaña Application → IndexedDB.
- Cuenta de estudiante de ejemplo:
student@test.com/password123(verificado). - Panel de administración (
admin@test.com/admin123): muestra el estado de la base de datos y tiene botones para borrar solo las reservas o restablecer todos los datos (esto último borra también los usuarios que hayas registrado). - Simulación de red (desactivada por defecto): hace que un porcentaje de las peticiones falle, para probar el paso 9.
- Simulación de tiempo: permite "adelantar días" para probar el paso 8.
- Como la base de datos vive en el navegador, cada perfil de Chrome tiene su propia copia: lo que reserves en un perfil no aparece en otro.
Habilidades nuevas del día (según el curso): perfiles de navegador persistentes; contenido que cambia según el día y la hora; botones con distintos estados; funciones de orden superior para reintentar; y control de calidad (QA) verificando que las reservas sí quedaron hechas.
Prerequisitos: Selenium (Día 48), funciones de orden superior (Día 19), excepciones (Día 30),
datetime(Día 32), diccionarios y listas.
🏫 Nota ética (adaptación): automatizar reservas en un sitio real puede ser injusto: si un bot reserva en milisegundos, las personas que lo hacen a mano nunca alcanzan cupo. Muchos sistemas de reservas prohíben los bots en sus términos de uso. Por eso trabajamos en un sitio ficticio, creado para practicar.
1. Pasos 1 y 2: perfil persistente e inicio de sesión automático
(Fuentes: 358 (paso 1, según el video) y 360-step-2-automated-login.md)
🎯 Objetivo de aprendizaje
Usar siempre el mismo perfil de Chrome, iniciar sesión automáticamente y esperar a los elementos con WebDriverWait en lugar de time.sleep().
📖 Explicación conceptual
Paso 1 — Perfil persistente (según el video de objetivos): como la base de datos del gimnasio vive en el navegador, el bot debe usar siempre el mismo perfil de Chrome para "recordar" usuarios y reservas, igual que un usuario real. Una forma de hacerlo es la opción --user-data-dir:
import os
from selenium import webdriver
chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option("detach", True)
user_data_dir = os.path.join(os.getcwd(), "chrome_profile") # una carpeta propia para el bot
chrome_options.add_argument(f"--user-data-dir={user_data_dir}")
driver = webdriver.Chrome(options=chrome_options)Usa una carpeta nueva y exclusiva para el bot, no el perfil de Chrome con el que navegas: así no mezclas tus contraseñas y tu historial con el programa, y evitas errores porque ese perfil ya está abierto.
Paso 2 — Esperas explícitas. time.sleep(3) espera siempre 3 segundos, haga falta o no; si la página tarda 4, falla igual. WebDriverWait espera hasta que se cumpla una condición, con un tiempo máximo:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
wait = WebDriverWait(driver, 10) # como máximo 10 segundos
login_button = wait.until(EC.element_to_be_clickable((By.ID, "login-button")))
login_button.click()Si en 10 segundos no se cumple, lanza TimeoutException. EC.presence_of_element_located(...) espera a que el elemento exista; EC.element_to_be_clickable(...), a que además se pueda hacer clic en él.
Ids verificados del inicio de sesión (26/09/2026): el curso agregó ids "amigables con Selenium": login-button, email-input, password-input, submit-button. Tras iniciar sesión aparece la página del horario, con id="schedule-page".
📝 Ejercicio original del curso: paso 2
Escribir código que:
- haga clic en el botón de inicio de sesión;
- escriba el correo y la contraseña;
- envíe el formulario;
- verifique que inició sesión comprobando que aparece la página "Class Schedule".
Consejo del curso: WebDriverWait en lugar de time.sleep(), y presence_of_element_located para verificar que un elemento está en la página.
🔍 Antes de programar
- ¿Qué elemento demuestra que el inicio de sesión funcionó?
- ¿Dónde guardarás el correo y la contraseña? (Aunque sean de prueba, practica con
.env, Día 35.) - ¿Qué debe hacer el programa si en 10 segundos no aparece el horario?
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
NoSuchElementException justo después de un clic |
La página nueva aún no cargó | WebDriverWait |
TimeoutException |
La condición nunca se cumplió (id mal escrito, credenciales incorrectas) | Revisa el id en DevTools y los datos de acceso |
EC.presence_of_element_located(By.ID, "x") |
La condición recibe una tupla: faltan paréntesis | EC.presence_of_element_located((By.ID, "x")) |
| Las reservas "desaparecen" entre ejecuciones | Cada ejecución usa un perfil nuevo y vacío | --user-data-dir con la misma carpeta |
SessionNotCreatedException: user data directory is already in use |
Hay otra ventana de Chrome usando esa carpeta (por ejemplo, la que dejó abierta detach) |
Cierra esa ventana antes de volver a ejecutar |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: crea una cuenta nueva con el formulario de registro del sitio (botón
toggle-login-register) y usa esa cuenta en el bot. - 🟡 Reto 2: intenta iniciar sesión con una contraseña incorrecta y haz que el programa detecte el mensaje de error del sitio e imprima "Credenciales inválidas".
- 🟠 Reto 3: mide cuánto tarda el inicio de sesión con
time.sleep(5)y conWebDriverWait. ¿Cuánto tiempo se ahorra? - 🔴 Reto 4: escribe una función
esperar_y_clic(driver, by, valor, tiempo=10)que combine la espera y el clic, y úsala en todo el proyecto. ¿Qué otras funciones auxiliares te ahorrarían código repetido?
2. Paso 3: reservar la clase del martes a las 6 p. m.
(Fuente: 361-step-3-book-the-upcoming-tuesday-class.md)
🎯 Objetivo de aprendizaje
Seleccionar elementos por patrones en sus ids y navegar del hijo al ancestro en el árbol HTML.
📖 Explicación conceptual
Tarea: encontrar la próxima clase del martes a las 6:00 p. m. (Yoga, Spin o HIIT), hacer clic en "Book Class" o "Join Waitlist" e imprimir un mensaje como ✓ Booked: Spin Class on Tue, Aug 12. El filtro por defecto del sitio muestra los próximos 7 días.
Pista 1 del curso — selectores de atributo con "empieza por":
find_element(By.CSS_SELECTOR, "p[id^='class-time-']")^= significa "el valor empieza por". Así se seleccionan todos los elementos cuyos ids siguen un mismo patrón (class-time-123, class-time-789…), aunque el final cambie. (Es el selector de atributo del Día 43, con un operador nuevo.)
Pista 2 del curso — ancestros: el día no está en la tarjeta de la clase, sino en un elemento que la envuelve. El HTML es como un árbol genealógico: la tarjeta es el hijo; el div que la contiene, el padre; el siguiente hacia arriba, el abuelo… Todos son ancestros. Con XPath:
dia = tarjeta.find_element(By.XPATH, "./ancestor::div[...]") # sube desde la tarjetaPatrones verificados en el sitio (26/09/2026):
- Tarjetas de clase: ids como
class-card-yoga-2026-09-27-0700(tipo, fecha y hora). - Botones:
book-button-...con el mismo final que la tarjeta. - Grupos de cada día: ids que empiezan por
day-group-, con un encabezado como "Tomorrow (Sun, Sep 27)" o "Tue, Sep 29".
Con esos patrones puedes armar tu propia solución. ¡Inspecciona tú mismo antes de programar!
📝 Ejercicio original del curso: paso 3
Encontrar el próximo martes a las 6 p. m., reservar (o unirse a la lista de espera) e imprimir el mensaje de confirmación. Al terminar, Chrome debe quedar abierto para revisar a mano la página "My Bookings".
🔍 Antes de programar
- ¿Qué información necesitas de cada tarjeta? ¿Cuál está en la tarjeta y cuál en el ancestro?
- ¿Cómo reconoces "6:00 PM" en el texto de una tarjeta?
- ¿Qué pasa si el martes es "Tomorrow"? ¿El encabezado sigue diciendo "Tue"?
⚠️ Errores frecuentes
| Error | Causa | Solución |
|---|---|---|
| El XPath de ancestro busca en toda la página | Falta el punto inicial: //ancestor... en lugar de ./ancestor... |
El . hace que la búsqueda empiece en la tarjeta |
| Reservar la clase de las 6 a. m. | Buscar solo "6:00" en el texto |
Busca "6:00 PM" completo |
[id^=class-card-] sin comillas |
Algunos valores requieren comillas en CSS | "[id^='class-card-']" |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: imprime todas las clases de la semana con su día, hora y tipo, sin reservar nada.
- 🟡 Reto 2: cuenta cuántos cupos quedan en cada clase (el texto dice "Available: X / Y spots") y muestra la clase con más cupos libres.
- 🟠 Reto 3: en lugar del martes, pide al usuario el día y la hora con
input()y reserva esa clase. - 🔴 Reto 4 — Pensamiento computacional: escribe tres formas de encontrar el día de una tarjeta: (a) con el ancestro, (b) con la fecha que contiene el id de la tarjeta y
datetime, (c) con el texto del encabezado. ¿Cuál es más robusta si el sitio cambia sus textos a español?
3. Pasos 4 y 5: decidir según el estado del botón y contar resultados
(Fuentes: 362-step-4-check-if-a-class-is-already-booked.md, 363-step-5-add-counters-to-your-script-to-provide-a-neat-summary.md)
🎯 Objetivo de aprendizaje
Tomar decisiones según el texto de un botón y llevar contadores para un resumen final.
📖 Explicación conceptual
El botón de cada clase puede decir:
| Texto del botón | Significa | Acción del bot | Mensaje |
|---|---|---|---|
Book Class |
Hay cupo | Clic | ✓ Booked: ... |
Join Waitlist |
Clase llena | Clic | ✓ Joined waitlist for: ... |
Booked |
Ya reservaste | Nada | ✓ Already booked: ... |
Waitlisted |
Ya estás en espera | Nada | ✓ Already on waitlist: ... |
Paso 5 — Contadores: llevar la cuenta de reservas nuevas, listas de espera nuevas y clases que ya estaban reservadas o en espera, y al final imprimir:
--- BOOKING SUMMARY ---
Classes booked: 0
Waitlists joined: 0
Already booked/waitlisted: 1
Total Tuesday 6pm classes processed: 1Un buen resumen sirve también para depurar: si los números no cuadran, algo falló.
🛠️ Nota técnica (verificado): el curso imprime
✓y emojis. En la consola de PyCharm se ven bien, pero si en Windows rediriges la salida a un archivo (python bot.py > registro.txt, útil para guardar el historial del bot), Python usa la codificacióncp1252y lanzaUnicodeEncodeError: 'charmap' codec can't encode character '✓'. Solución: al inicio del programa,import sysysys.stdout.reconfigure(encoding="utf-8"), o ejecutar con la variable de entornoPYTHONUTF8=1.
📝 Ejercicio original del curso: pasos 4 y 5
- Paso 4: antes de hacer clic, revisar si la clase ya está reservada (
Booked), si ya estás en espera (Waitlisted) o si está llena (Join Waitlist), e imprimir el mensaje correspondiente. - Paso 5: agregar los contadores e imprimir el resumen.
🔍 Antes de programar
- ¿Qué estructura es más clara para cuatro casos: varios
if/elifo un diccionario que asocie cada texto con su acción? - ¿Qué haces si el botón tiene un texto inesperado?
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: agrega el caso "texto desconocido": imprime una advertencia y súmalo a un contador de "revisar a mano".
- 🟡 Reto 2: reemplaza los contadores sueltos por un solo diccionario
resumen = {"reservadas": 0, ...}. - 🟠 Reto 3: guarda el resumen de cada ejecución, con fecha y hora, en
historial.csv. - 🔴 Reto 4: escribe una función pura
decidir(texto_boton) -> str(sin Selenium) y pruébala conassertpara los cinco casos. ¿Por qué es más fácil probar la lógica cuando está separada del navegador?
4. Pasos 6 y 7: martes y jueves, y verificar las reservas
(Fuentes: 364-step-6-class-booking-book-every-tuesday-and-thursday-class.md, 365-step-7-verify-class-bookings-on-the-my-bookings-page.md)
🎯 Objetivo de aprendizaje
Generalizar el bot a varios días y verificar el resultado en otra página ("confía, pero verifica").
📖 Explicación conceptual
Paso 6: reservar todas las clases de martes y jueves a las 6 p. m., llevar la lista de clases procesadas e imprimir un detalle:
--- DETAILED CLASS LIST ---
• [New Booking] Spin Class on Tomorrow (Thu, Aug 14)
• [New Waitlist] Spin Class on Tue, Aug 19Consejo: para volver a probar desde cero, entra como administrador y borra las reservas.
Paso 7 — Verificar: que Selenium haya hecho clic no garantiza que el gimnasio registró la reserva. Hay que ir a "My Bookings", contar las reservas de martes y jueves a las 6 p. m. y compararlas con las que se intentaron:
--- VERIFICATION RESULT ---
Expected: 2 bookings
Found: 2 bookings
✅ SUCCESS: All bookings verified!Si no coinciden, mostrar la diferencia. Consejo del curso: usar NoSuchElementException de Selenium; incluso puedes lanzarla tú (raise) si no encuentras las reservas que buscabas.
🛠️ Nota técnica (análisis de esta adaptación): el ejemplo de "discrepancia" del curso imprime
❌ MISMATCH: Missing -1 bookings(se esperaba 1 y se encontraron 2). Un número negativo de "faltantes" confunde: en realidad sobra una reserva. Un mensaje más claro distingue los dos casos: "Faltan N reservas" o "Hay N reservas de más". Además, comparar solo cantidades puede engañar: 2 esperadas y 2 encontradas podrían ser clases distintas. Es más confiable comparar conjuntos de identificadores (set).
📝 Ejercicio original del curso: pasos 6 y 7
- Paso 6: todas las clases de martes y jueves a las 6 p. m., resumen y lista detallada.
- Paso 7: navegar a "My Bookings", contar, comparar con lo intentado e imprimir el resultado de la verificación.
🔍 Antes de programar
- ¿Qué identificador único tiene cada clase para compararla entre las dos páginas?
- ¿Qué cuenta como "esperada": solo las que reservaste hoy o también las que ya estaban reservadas?
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: haz que los días y la hora sean constantes al inicio del programa (
DIAS = ["Tue", "Thu"],HORA = "6:00 PM"). - 🟡 Reto 2: implementa la verificación con conjuntos y reporta por separado las que faltan y las que sobran.
- 🟠 Reto 3: usa singular y plural correctamente en los mensajes ("1 reserva", "2 reservas"), como sugiere el curso.
- 🔴 Reto 4: el bot debe reservar como máximo 3 clases por semana (regla del gimnasio). Si hay más martes y jueves disponibles, ¿cuáles eliges? Define un criterio y justifícalo.
5. Paso 8: control de calidad viajando en el tiempo
(Fuente: 366-step-8-time-travel-quality-assurance-qa.md)
🎯 Objetivo de aprendizaje
Probar que el programa funciona en otras fechas y detectar suposiciones escondidas en el código.
📖 Explicación conceptual
QA (Quality Assurance, aseguramiento de la calidad) es comprobar que un programa funciona bien en condiciones distintas a las del día en que lo escribiste. El panel de administración permite adelantar el tiempo.
Pasos manuales del curso:
- Entrar como administrador (
admin@test.com/admin123). - Clic en "Advance 3 Days".
- Cerrar sesión y salir de Selenium.
- Ejecutar el bot de nuevo: debe encontrar y reservar las clases nuevas que ahora entran en la ventana de 7 días.
Errores comunes que el curso pide vigilar: fechas escritas "a mano" en el código, suponer qué día aparece primero y errores al interpretar fechas. Ejemplo de código problemático del curso:
class_cards = driver.find_elements(By.CSS_SELECTOR, "div[id^='class-card-']")
# Reserva el 2.º y el 4.º grupo suponiendo que son martes y jueves
for i, card in enumerate(class_cards):
if i % 7 == 1 or i % 7 == 3: # supone que la semana empieza el lunes
...Si el tiempo avanza, el primer día visible ya no es lunes y el bot reserva los días equivocados sin dar ningún error.
Verificado el 26/09/2026: ese día era sábado y el primer grupo visible era "Tomorrow (Sun, Sep 27)": el supuesto de que "la semana empieza el lunes" ya fallaba sin necesidad de adelantar el tiempo.
🔎 Ejemplo explicado: calcular fechas en lugar de suponerlas
from datetime import date, timedelta
def proximo(dia_semana, hoy): # lunes = 0 ... domingo = 6
dias = (dia_semana - hoy.weekday()) % 7
return hoy + timedelta(days=dias)
hoy = date(2026, 9, 26) # sábado
print(proximo(1, hoy)) # 2026-09-29 (martes)
print(proximo(3, hoy)) # 2026-10-01 (jueves)
print(proximo(5, hoy)) # 2026-09-26 (¡hoy mismo es sábado!)(Resultados verificados.) El operador % hace que el cálculo "dé la vuelta" a la semana. Observa el último caso: si hoy ya es el día buscado, devuelve hoy. ¿Es lo que quieres? Depende del problema: por eso hay que probar los casos límite.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: adelanta el tiempo 1, 3 y 6 días, ejecuta tu bot cada vez y anota si reservó las clases correctas.
- 🟡 Reto 2: escribe pruebas con
assertparaproximo()con los siete días de la semana como "hoy". - 🟠 Reto 3: busca en tu código cualquier número o fecha escrito "a mano" y reemplázalo por un cálculo o una constante con nombre.
- 🔴 Reto 4: diseña una tabla de casos de prueba de QA para el bot (al menos 6 casos: fechas, clases llenas, ya reservadas, sin clases a esa hora…) con el resultado esperado de cada uno.
6. Paso 9: resistencia a fallos de red con una función retry
(Fuente: 367-step-9-add-resilience-in-case-of-network-failures.md)
🎯 Objetivo de aprendizaje
Escribir una función de orden superior que reintente otra función cuando falla, y organizar el bot en funciones que se puedan reintentar.
📖 Explicación conceptual
"El jefe final", según el curso: los sitios reales tienen fallos de red. Preparación del caos:
- Entrar como administrador.
- "Clear all Bookings" (si usas "Reset All Data", tendrás que registrar de nuevo tu cuenta de estudiante).
- Activar "Network Simulation" con una tasa de fallos del 50 % ("¡brutal!").
- Volver al tiempo real ("Real Time").
La mitad de los inicios de sesión y reservas fallarán. ¿Cómo lograr que el bot sea "imparable"?
La estructura que propone el curso:
def retry(func, retries=7, description=None):
...- Una función
retryque recibe otra función (func) como parámetro: es una función de orden superior, como las del Día 19 (turtle.onkey(key="space", fun=...)), donde pasábamos funciones sin paréntesis. - Agrupar los pasos del inicio de sesión en
login(), probarretry(login)varias veces y solo después seguir con lo demás. - Hacer lo mismo con
book_class()yget_my_bookings(). - Pensar: ¿cómo sabe Selenium que el clic de reserva funcionó? ¿Qué debería comprobar después del clic?
🔎 Ejemplo explicado: el patrón retry con fallos simulados
Este ejemplo no usa Selenium: simula una red que falla la mitad de las veces para entender el patrón.
import random
import time
def retry(func, retries=7, description=None):
for intento in range(1, retries + 1):
try:
return func() # si funciona, devuelve el resultado y termina
except Exception as error:
print(f" Intento {intento} de {retries} falló ({description}): {error}")
time.sleep(1) # esperar antes de reintentar
raise RuntimeError(f"No se pudo completar: {description}")
def entrar_simulado():
if random.random() < 0.5:
raise ConnectionError("la red falló")
return "sesión iniciada"
print(retry(entrar_simulado, description="login"))(Verificado: con fallos simulados, la función reintenta hasta que funciona.)
Puntos clave:
- Se pasa
entrar_simuladosin paréntesis: se entrega la función, no su resultado. return func()sale del ciclo en cuanto funciona.- Si se agotan los intentos, se lanza un error claro en lugar de seguir como si nada.
- Para que
retrysirva, la función reintentada debe fallar con una excepción cuando algo sale mal. Sibook_class()hace clic y termina sin comprobar nada, nunca "falla" yretrynunca reintenta: por eso el curso pregunta qué verificar después del clic (por ejemplo, que el texto del botón cambie aBookedoWaitlisted, esperándolo conWebDriverWait).
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
retry(login(), ...) |
Se ejecuta login una vez antes de llamar a retry, y se pasa su resultado (None) |
retry(login, ...) |
except: sin tipo que atrapa todo, incluso Ctrl + C |
No puedes detener el programa | except Exception: |
| Reintentar sin pausa | Martillas el servidor, que ya tiene problemas | Espera entre intentos (y, mejor, cada vez un poco más: backoff) |
| Reintentar una reserva que en realidad sí se hizo | Posibles reservas duplicadas | Antes de reintentar, comprueba el estado actual (¿el botón ya dice Booked?) |
| Pasar argumentos a la función reintentada | retry(book_class(card)) ejecuta de inmediato |
retry(lambda: book_class(card)) |
📝 Ejercicio original del curso: paso 9
Crear retry(func, retries=7, description=None); agrupar el inicio de sesión en login() y hacerlo funcionar con reintentos; luego book_class() y get_my_bookings() con reintentos. El curso ofrece una solución parcial con pistas y la solución completa.
🔍 Antes de programar
- ¿Qué condición demuestra que cada acción funcionó?
- Si
login()falla a mitad (ya escribió el correo pero no la contraseña), ¿desde dónde debe empezar el reintento? - ¿Cuántos reintentos son razonables con un 50 % de fallos? Calcula la probabilidad de fallar 7 veces seguidas.
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: calcula (con Python) la probabilidad de que 7 intentos fallen seguidos con una tasa de fallo del 50 %. ¿Y con 3 intentos?
- 🟡 Reto 2: haz que la espera entre intentos se duplique cada vez (1, 2, 4, 8… segundos). Esto se llama exponential backoff.
- 🟠 Reto 3: convierte
retryen un decorador (@retry). Investiga cómo se escriben decoradores con parámetros. - 🔴 Reto 4: registra cada reintento en un archivo
registro.logcon fecha, hora, acción y error (investiga el módulologging). Al final, calcula la tasa de fallos real observada y compárala con el 50 % configurado.
7. 🚀 Proyecto del día: el bot de reservas del gimnasio
(Integra los pasos 1 a 9 del curso: 358 a 367.)
Problema
Las clases del martes y el jueves a las 6 p. m. se llenan rápido y el sistema abre a medianoche.
Objetivo
Un bot que inicie sesión, reserve (o se una a la lista de espera de) todas las clases de martes y jueves a las 6 p. m., verifique el resultado y resista fallos de red.
Requisitos funcionales
- Perfil persistente y credenciales fuera del código.
- Inicio de sesión verificado.
- Reserva de martes y jueves a las 6 p. m. según el estado del botón.
- Resumen con contadores y lista detallada.
- Verificación en "My Bookings".
- Funcionamiento correcto tras adelantar el tiempo.
- Resistencia a un 50 % de fallos de red.
Restricciones técnicas
WebDriverWaiten lugar detime.sleep()para esperar elementos.- Funciones separadas:
login(),book_class(card),get_my_bookings(),retry(...). - Ninguna fecha escrita "a mano".
Planificación
- Comprender el sitio usándolo a mano. 2. Entradas: credenciales, días y hora. 3. Procesos: entrar → recorrer tarjetas → decidir → reservar → verificar. 4. Salidas: resumen y verificación. 5. Dividir en funciones. 6. Diseñar el flujo con reintentos. 7. Implementar paso a paso (como el curso). 8. Probar con el panel de administración. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1: inicio de sesión (pasos 1 y 2).
- Versión 2: una sola reserva (paso 3).
- Versión 3: estados del botón y resumen (pasos 4 y 5).
- Versión 4: martes y jueves, verificación (pasos 6 y 7).
- Versión 5: QA con viaje en el tiempo (paso 8).
- Versión final: reintentos con red inestable (paso 9).
Pruebas
| # | Preparación en el panel de administración | Resultado esperado |
|---|---|---|
| 1 | Borrar reservas | Reservas nuevas para todos los martes y jueves a las 6 p. m. |
| 2 | Ejecutar dos veces seguidas | La segunda vez: "Already booked/waitlisted"; ninguna reserva nueva |
| 3 | Adelantar 3 días | Reserva las clases que entraron en la ventana de 7 días |
| 4 | Red con 50 % de fallos | Completa todo con reintentos; la verificación coincide |
| 5 | Credenciales incorrectas | Mensaje claro y el programa termina sin reservar |
| 6 | Salida redirigida a un archivo | Sin UnicodeEncodeError |
🧪 Prueba y depuración
def book_class(card):
button = card.find_element(By.CSS_SELECTOR, "button[id^='book-button-']")
if button.text == "Book Class" or "Join Waitlist":
button.click()
for card in cards:
retry(book_class(card), description="reserva")Encuentra tres problemas (la condición del if que siempre es verdadera, cómo se pasa la función a retry y la falta de una verificación que haga fallar la función cuando la reserva no se registra).
⭐ Extensión del proyecto
- Enviar el resumen por correo al servidor local (Día 47).
- Programar la ejecución a medianoche con el Programador de tareas de Windows o PythonAnywhere (y discutir si hacerlo es justo con los demás usuarios en un sitio real).
- Permitir elegir los días, la hora y el tipo de clase desde un archivo
config.json.
8. 🌟 Proyecto extra: reservas de la sala de informática
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
La sala de informática del colegio se reserva en un cuaderno y hay conflictos. Tu tarea es diseñar la lógica de un sistema de reservas y probarla sin navegador, aplicando lo aprendido sobre decisiones, verificación y reintentos.
Requisitos
- Una clase
SistemaReservas(Día 16) que guarde las reservas en un diccionario{(fecha, bloque): docente}. - Métodos
reservar(fecha, bloque, docente),cancelar(...)ydisponibles(fecha). - Reglas: no se puede reservar un bloque ocupado; un docente puede tener como máximo 2 bloques por día; no se reservan fines de semana.
- Un "servidor inestable" simulado que falle el 30 % de las veces, y la función
retryde la sección 6 para que las reservas se completen. - Una verificación final que compare las reservas intentadas con las registradas usando conjuntos.
Decisiones que debes tomar (y justificar)
- ¿Qué haces si dos docentes piden el mismo bloque? ¿Lista de espera?
- ¿Cómo evitas que un reintento cree una reserva duplicada?
Casos de prueba
- Reservar un sábado → rechazado.
- Tercer bloque del mismo docente el mismo día → rechazado.
- 20 reservas con el servidor inestable → todas registradas una sola vez.
9. 🏆 Proyecto integrador del bloque (Días 45 a 51) — Etapa 5
(Proyecto creado para esta adaptación. La situación problema está en el Día 45; la versión final, en el Día 51.)
Etapa 5 (hoy): un asistente confiable
- Reorganiza
inscriptor.py(etapa 4) en funciones:abrir_formulario(),llenar(aspirante),enviar()yverificar(aspirante). - Usa
WebDriverWaiten todas las esperas yretryen cada inscripción. - Para probar los reintentos, haz que
llenar()falle a propósito de forma aleatoria (30 %) con una excepción. - Al final, imprime un resumen (inscritos, fallidos, reintentos usados) y una verificación con conjuntos: ¿todos los aspirantes del CSV llegaron a
gracias.html? - Entregable: el código, la salida con los reintentos visibles y una tabla de pruebas.
10. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué el bot debe usar siempre el mismo perfil de Chrome en este proyecto?
- ¿Qué ventaja tiene
WebDriverWaitsobretime.sleep()? - ¿Qué es un ancestro en el árbol HTML? ¿Para qué se usó?
- ¿Qué significa QA y por qué se "viaja en el tiempo"?
- ¿Qué es una función de orden superior? Da un ejemplo del Día 19 y uno de hoy.
Análisis de código
estados = ["Book Class", "Booked", "Join Waitlist", "Waitlisted", "Booked"]
resumen = {}
for e in estados:
resumen[e] = resumen.get(e, 0) + 1
print(resumen)¿Qué imprime? ¿Cómo lo usarías para el resumen del bot?
Predicción
intentos = []
def falla_dos_veces():
intentos.append(1)
if len(intentos) < 3:
raise ValueError("todavía no")
return "¡listo!"
print(retry(falla_dos_veces, retries=5, description="prueba"))
print(len(intentos))¿Qué se imprime? ¿Y si retries=2?
Depuración
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located(By.ID, "schedule-page"))¿Qué error da y por qué?
Modificación
Cambia el bot para que, además de martes y jueves a las 6 p. m., reserve solo las clases de Yoga del sábado.
Creación
Escribe una función verificar(intentadas: set, encontradas: set) que imprima las reservas faltantes, las sobrantes y un mensaje final de éxito o error. Pruébala con tres casos.
11. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — Revisar una función de reintentos. Pide a una IA una función retry en Python. Revisa: ¿pasa la función sin paréntesis?, ¿espera entre intentos?, ¿qué excepciones atrapa?, ¿qué pasa al agotar los intentos? Compara con la sección 6 y mejórala.
Actividad 2 — Suposiciones ocultas. Muestra a una IA el código problemático del paso 8 (i % 7 == 1) sin explicarle el problema y pregúntale si tiene algún error. ¿Lo detecta? ¿Propone calcular fechas con datetime?
Actividad 3 — Ética de la automatización. Pregúntale a una IA: "¿Está bien usar un bot para reservar las canchas del polideportivo de mi barrio a medianoche?". Evalúa si la respuesta considera a los demás usuarios y los términos del servicio. Escribe tu propia conclusión.
12. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
| — | Paso 1 (lección 359) | El archivo no está en la carpeta; se reconstruye a partir del video de objetivos (358) |
Sección 1 |
361, 363 |
Mensajes con ✓ y emojis |
Al redirigir la salida a un archivo en Windows se produce UnicodeEncodeError con cp1252 (verificado); solución: sys.stdout.reconfigure(encoding="utf-8") o PYTHONUTF8=1 |
Nota técnica de la sección 3 |
365 |
❌ MISMATCH: Missing -1 bookings |
Un "faltante" negativo significa que sobra una reserva; conviene separar faltantes y sobrantes y comparar conjuntos, no solo cantidades | Nota técnica de la sección 4 |
366 |
El código problemático supone que la semana empieza el lunes | Confirmado: el 26/09/2026 (sábado) el primer grupo visible era el domingo, sin adelantar el tiempo (verificado) | Sección 5 |
367 |
retry que recibe funciones |
Si la función reintentada no verifica su resultado, nunca lanza una excepción y retry no sirve; y reintentar sin comprobar el estado puede duplicar reservas |
Sección 6 |
13. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Reservas que se agotan, Snack & Lift, base de datos en el navegador (IndexedDB), cuentas de estudiante y administrador, borrar y restablecer, perfiles de Chrome, simulación de red y de tiempo, habilidades del día | 358 |
Secciones 0 y 1 | Paso 1 (perfil persistente, según el video) | Retos de la sección 1 | Sección 7 |
Inicio de sesión automático, ids amigables, verificar el horario, WebDriverWait, presence_of_element_located |
360 |
Sección 1 | Paso 2 | Retos 1–4 de la sección 1 | Secciones 7 y 9 |
Martes 6 p. m., "Book Class" y "Join Waitlist", mensaje con emoji, filtro de 7 días, [id^='...'], ancestros con ancestor::div |
361 |
Sección 2 | Paso 3 | Retos 1–4 de la sección 2 | Sección 7 |
Estados Booked, Waitlisted, Join Waitlist y sus mensajes |
362 |
Sección 3 | Paso 4 | Retos 1–4 de la sección 3 | Sección 7 |
| Contadores y resumen | 363 |
Sección 3 | Paso 5 | Retos 1–4 de la sección 3 | Secciones 7 y 9 |
| Martes y jueves, lista detallada, borrar reservas como administrador | 364 |
Sección 4 | Paso 6 | Retos 1–4 de la sección 4 | Sección 7 |
Verificar en "My Bookings", esperadas y encontradas, NoSuchElementException, discrepancias |
365 |
Sección 4 | Paso 7 | Retos 1–4 de la sección 4 | Secciones 7, 8 y 9 |
| QA, "Advance 3 Days", fechas escritas a mano, supuestos sobre el primer día, código problemático | 366 |
Sección 5 | Paso 8 | Retos 1–4 de la sección 5 | Sección 7 |
Simulación de red al 50 %, retry(func, retries=7, description=None), login(), book_class(), get_my_bookings() |
367 |
Sección 6 | Paso 9 | Retos 1–4 de la sección 6 | Secciones 7, 8 y 9 |
| (No existe en el curso original) | — | — | — | Reservas de la sala de informática | Sección 8 |
| (No existe en el curso original) | — | — | — | Asistente automático, etapa 5 | Sección 9 |
14. 📦 Evidencias de aprendizaje del Día 49
- Captura del bot iniciando sesión con perfil persistente y
WebDriverWait. - La salida del resumen y de la lista detallada (pasos 5 y 6).
- La verificación en "My Bookings" con conjuntos (faltantes y sobrantes).
- La tabla de QA con los resultados tras adelantar el tiempo.
- La salida con reintentos visibles al 50 % de fallos.
- Las pruebas con
assertdedecidir()y deproximo(). - El sistema de reservas de la sala de informática (proyecto extra).
- Etapa 5 del proyecto integrador.
- Cuaderno de errores:
sleepen lugar de esperas, tupla faltante enEC, perfil ocupado, XPath de ancestro sin punto, "6:00" sin "PM",retry(login()), suposiciones sobre el primer día, emojis con la salida redirigida.