Saltar al contenido
Inicio 100 Días de Código en Python — Día 49: Un bot de reservas resistente a fallos

100 Días de Código en Python — Día 49: Un bot de reservas resistente a fallos

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 49 (nivel intermedio+).

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ón retry con fallos simulados.


Tabla de contenidos

  1. Antes de empezar: el gimnasio Snack & Lift
  2. Pasos 1 y 2: perfil persistente e inicio de sesión automático
  3. Paso 3: reservar la clase del martes a las 6 p. m.
  4. Pasos 4 y 5: decidir según el estado del botón y contar resultados
  5. Pasos 6 y 7: martes y jueves, y verificar las reservas
  6. Paso 8: control de calidad viajando en el tiempo
  7. Paso 9: resistencia a fallos de red con una función retry
  8. 🚀 Proyecto del día: el bot de reservas del gimnasio
  9. 🌟 Proyecto extra: reservas de la sala de informática
  10. 🏆 Proyecto integrador del bloque (Días 45 a 51) — Etapa 5
  11. 🧩 Comprueba lo aprendido
  12. 🤖 Programar con apoyo de IA
  13. 🛠️ Correcciones técnicas al material original
  14. 📋 Matriz de trazabilidad
  15. 📦 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:

Python
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:

Python
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 con WebDriverWait. ¿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":

Python
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:

Python
dia = tarjeta.find_element(By.XPATH, "./ancestor::div[...]")   # sube desde la tarjeta

Patrones 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:

Texto
--- BOOKING SUMMARY ---
Classes booked: 0
Waitlists joined: 0
Already booked/waitlisted: 1
Total Tuesday 6pm classes processed: 1

Un 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ón cp1252 y lanza UnicodeEncodeError: 'charmap' codec can't encode character '✓'. Solución: al inicio del programa, import sys y sys.stdout.reconfigure(encoding="utf-8"), o ejecutar con la variable de entorno PYTHONUTF8=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/elif o 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 con assert para 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:

Texto
--- DETAILED CLASS LIST ---
  • [New Booking] Spin Class on Tomorrow (Thu, Aug 14)
  • [New Waitlist] Spin Class on Tue, Aug 19

Consejo: 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:

Texto
--- 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:

  1. Entrar como administrador (admin@test.com / admin123).
  2. Clic en "Advance 3 Days".
  3. Cerrar sesión y salir de Selenium.
  4. 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:

Python
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

Python
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 assert para proximo() 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:

  1. Entrar como administrador.
  2. "Clear all Bookings" (si usas "Reset All Data", tendrás que registrar de nuevo tu cuenta de estudiante).
  3. Activar "Network Simulation" con una tasa de fallos del 50 % ("¡brutal!").
  4. 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:

Python
def retry(func, retries=7, description=None):
    ...
  • Una función retry que 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(), probar retry(login) varias veces y solo después seguir con lo demás.
  • Hacer lo mismo con book_class() y get_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.

Python
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_simulado sin 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 retry sirva, la función reintentada debe fallar con una excepción cuando algo sale mal. Si book_class() hace clic y termina sin comprobar nada, nunca "falla" y retry nunca reintenta: por eso el curso pregunta qué verificar después del clic (por ejemplo, que el texto del botón cambie a Booked o Waitlisted, esperándolo con WebDriverWait).

⚠️ 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 retry en un decorador (@retry). Investiga cómo se escriben decoradores con parámetros.
  • 🔴 Reto 4: registra cada reintento en un archivo registro.log con fecha, hora, acción y error (investiga el módulo logging). 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

  1. Perfil persistente y credenciales fuera del código.
  2. Inicio de sesión verificado.
  3. Reserva de martes y jueves a las 6 p. m. según el estado del botón.
  4. Resumen con contadores y lista detallada.
  5. Verificación en "My Bookings".
  6. Funcionamiento correcto tras adelantar el tiempo.
  7. Resistencia a un 50 % de fallos de red.

Restricciones técnicas

  • WebDriverWait en lugar de time.sleep() para esperar elementos.
  • Funciones separadas: login(), book_class(card), get_my_bookings(), retry(...).
  • Ninguna fecha escrita "a mano".

Planificación

  1. 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

Python
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

  1. Una clase SistemaReservas (Día 16) que guarde las reservas en un diccionario {(fecha, bloque): docente}.
  2. Métodos reservar(fecha, bloque, docente), cancelar(...) y disponibles(fecha).
  3. 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.
  4. Un "servidor inestable" simulado que falle el 30 % de las veces, y la función retry de la sección 6 para que las reservas se completen.
  5. 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

  1. Reorganiza inscriptor.py (etapa 4) en funciones: abrir_formulario(), llenar(aspirante), enviar() y verificar(aspirante).
  2. Usa WebDriverWait en todas las esperas y retry en cada inscripción.
  3. Para probar los reintentos, haz que llenar() falle a propósito de forma aleatoria (30 %) con una excepción.
  4. Al final, imprime un resumen (inscritos, fallidos, reintentos usados) y una verificación con conjuntos: ¿todos los aspirantes del CSV llegaron a gracias.html?
  5. Entregable: el código, la salida con los reintentos visibles y una tabla de pruebas.

10. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué el bot debe usar siempre el mismo perfil de Chrome en este proyecto?
  2. ¿Qué ventaja tiene WebDriverWait sobre time.sleep()?
  3. ¿Qué es un ancestro en el árbol HTML? ¿Para qué se usó?
  4. ¿Qué significa QA y por qué se "viaja en el tiempo"?
  5. ¿Qué es una función de orden superior? Da un ejemplo del Día 19 y uno de hoy.

Análisis de código

Python
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

Python
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

Python
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 assert de decidir() y de proximo().
  • El sistema de reservas de la sala de informática (proyecto extra).
  • Etapa 5 del proyecto integrador.
  • Cuaderno de errores: sleep en lugar de esperas, tupla faltante en EC, perfil ocupado, XPath de ancestro sin punto, "6:00" sin "PM", retry(login()), suposiciones sobre el primer día, emojis con la salida redirigida.