Mejores Prácticas en bots en Python

Este documento describe las mejores prácticas utilizadas por Unimate RPA para la construcción de bots RPA y Analytics en Python.

Estructura del Proyecto

Organizar el código en una estructura modular y fácil de mantener, siguiendo esta estructura:

project_name/
│
├── project_name/        # Paquete principal
│   ├── __init__.py      # Indica que es un paquete de Python
│   ├── main.py          # Punto de entrada principal
│   ├── logs/            # Carpeta para logs del bot
│   ├── tasks/           # Módulos para cada tarea del bot (si es necesario)
│   │   ├── task1.py
│   │   ├── task2.py
│   │   └── ...
│
├── .env                 # Variables de entorno (credenciales seguras)
├── config.yaml          # Archivo de configuración (rutas, parámetros)
├── README.md            # Documentación del bot
├── requirements.txt     # Dependencias del proyecto
├── launcher.py          # Script para gestionar entorno y ejecutar el bot

Código Modular y Separación de Responsabilidades

Cada funcionalidad debe estar en su propio módulo, evitando código innecesario en main.py.

El flujo del bot debe estar claro y delegar responsabilidades correctamente a módulos en tasks/.

tasks/task1.py (Módulo de tarea específica, lógica reusable)

"""
Módulo de procesamiento de datos.
"""

def process_data(data):
    """
    Procesa los datos y devuelve el resultado.

    Parámetros:
    data (list): Lista de strings a procesar.

    Retorna:
    list: Lista de strings en mayúsculas.
    """
    return [d.upper() for d in data]

main.py (Punto de entrada con toda la lógica del bot)

"""
Punto de entrada principal del bot RPA.
"""

import logging
from tasks.task1 import process_data

# Configurar logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def main():
    """Ejecuta el bot y maneja su flujo principal."""
    logger.info("Iniciando bot...")
    data = ["rpa", "automation", "bot"]
    resultado = process_data(data)
    print("Datos procesados:", resultado)

if __name__ == "__main__":
    main()

Uso de requirements.txt y Entorno Virtual (venv)

Siempre se debe usar un entorno virtual para evitar conflictos entre dependencias.

Creación y activación del entorno virtual:

python -m venv env
env\Scripts\activate

Instalación de dependencias:

pip install -r requirements.txt

Ejemplo de requirements.txt:

python-dotenv
requests
selenium
pyautogui
pandas

Manejo de Rutas y Configuración en el Bot

Para garantizar flexibilidad y escalabilidad en los bots de RPA, es fundamental evitar rutas hardcodeadas y utilizar una estrategia estándar para gestionar rutas y configuraciones. Esto se logra combinando rutas absolutas y relativas, junto con el uso de archivos de configuración (config.yaml) y variables de entorno (.env).

Importante:

  • config.yaml almacena rutas, parámetros generales y configuraciones que no son sensibles.

  • .env debe siempre estar presente, ya que almacena credenciales y datos sensibles.

  • El bot no debe ejecutarse si .env no está presente.

Uso de Configuración con config.yaml y .env

Ejemplo de config.yaml:

rutas:
    logs: "logs/bot.log"
    entrada: "data/input.xlsx"
    salida: "data/output.xlsx"

parametros:
    modo_debug: true
    max_intentos: 3

Ejemplo de .env:

API_KEY=my_secret_key
DB_PASSWORD=super_secure_password

Carga de Configuración en main.py

import os
import yaml
from dotenv import load_dotenv
import sys

# Cargar variables de entorno desde .env (el script debe fallar si .env no está presente)
if not os.path.exists(".env"):
    print("Error: Archivo .env no encontrado. El bot no puede ejecutarse sin credenciales.")
    sys.exit(1)

load_dotenv()

# Cargar configuración desde config.yaml
with open("config.yaml", "r") as file:
    config = yaml.safe_load(file)

# Obtener parámetros del YAML
LOGS_PATH = config["rutas"]["logs"]
INPUT_FILE = config["rutas"]["entrada"]
OUTPUT_FILE = config["rutas"]["salida"]
DEBUG_MODE = config["parametros"]["modo_debug"]

# Obtener credenciales desde .env
API_KEY = os.getenv("API_KEY")
DB_PASSWORD = os.getenv("DB_PASSWORD")

print(f"Ruta de logs: {LOGS_PATH}")
print(f"Archivo de entrada: {INPUT_FILE}")
print(f"Modo Debug: {DEBUG_MODE}")

Gestión de Entornos: Desarrollo y Producción

Para manejar distintos entornos (desarrollo y producción), config.yaml puede contener configuraciones separadas para cada uno, evitando modificar el código fuente al cambiar de entorno.

Estructura de config.yaml con múltiples entornos

entorno: "desarrollo"  # Puede ser "desarrollo" o "producción"

configuracion:
    desarrollo:
        rutas:
            logs: "logs/dev_bot.log"
            entrada: "data/dev_input.xlsx"
            salida: "data/dev_output.xlsx"
        parametros:
            modo_debug: true
            max_intentos: 5

    produccion:
        rutas:
            logs: "logs/prod_bot.log"
            entrada: "data/input.xlsx"
            salida: "data/output.xlsx"
        parametros:
            modo_debug: false
            max_intentos: 3

Cómo cargar la configuración en el código

El bot debe seleccionar automáticamente la configuración del entorno definido en config.yaml:

import yaml

with open("config.yaml", "r") as file:
    config = yaml.safe_load(file)

# Obtener el entorno activo
entorno_activo = config["entorno"]

# Cargar la configuración correspondiente al entorno activo
configuracion = config["configuracion"][entorno_activo]

# Acceder a las rutas y parámetros
LOGS_PATH = configuracion["rutas"]["logs"]
INPUT_FILE = configuracion["rutas"]["entrada"]
OUTPUT_FILE = configuracion["rutas"]["salida"]
DEBUG_MODE = configuracion["parametros"]["modo_debug"]

print(f"Entorno: {entorno_activo}")
print(f"Ruta de logs: {LOGS_PATH}")
print(f"Modo Debug: {DEBUG_MODE}")

Cambio de entorno sin modificar el código

Para cambiar de desarrollo a producción, basta con modificar la línea:

entorno: "produccion"

También es posible hacer que el entorno sea configurable desde una variable de entorno .env:

ENTORNO=produccion

Y en el código:

import os

entorno_activo = os.getenv("ENTORNO", "desarrollo")  # Usa "desarrollo" por defecto si no está definido

print(f"Entorno: {entorno_activo}")

Uso de Parámetros desde la Línea de Comandos

Los bots pueden recibir parámetros externos al ejecutarse, en lugar de depender solo de config.yaml.

Cuándo usar argparse en lugar de config.yaml?

  • config.yaml → Para parámetros fijos en la configuración.

  • argparse → Para valores que cambian en cada ejecución.

Ejemplo de main.py con argparse:

import argparse

def main(fecha):
    print(f"Procesando datos para la fecha: {fecha}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Ejecutar bot con fecha específica.")
    parser.add_argument("--fecha", type=str, required=True, help="Fecha en formato YYYY-MM-DD")
    args = parser.parse_args()
    main(args.fecha)

Manejo de Rutas Absolutas y Relativas

Uso de rutas relativas

Siempre que sea posible, utilizar rutas relativas dentro del proyecto en config.yaml:

import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
input_path = os.path.join(BASE_DIR, "data", "input.xlsx")
print(f"Ruta absoluta del archivo de entrada: {input_path}")

Cuándo usar rutas absolutas?

Las rutas absolutas pueden ser necesarias en algunos casos:

  1. Interacción con programas externos

    • Ejemplo: Almacenar archivos en una carpeta específica que otro programa lee.

  2. Rutas de red compartidas

    • Ejemplo: Acceder a un archivo en un servidor compartido \\servidor\carpeta\archivo.xlsx.

Ejemplo de cómo definir rutas absolutas en config.yaml:

rutas:
    reporte: "C:/Users/Usuario/Documentos/reporte.xlsx"

Ejemplo en código:

reporte_path = config["rutas"]["reporte"]
print(f"Ruta absoluta del reporte: {reporte_path}")

Logging y Manejo de Errores

Capturar y registrar errores de manera adecuada es fundamental para la depuración y mantenimiento de los bots.

Configuración del Logging

El archivo config.yaml debe definir la ubicación del archivo de logs y el nivel de logging:

rutas:
    logs: "logs/bot.log"

logging:
    nivel: "INFO"

Validación del nivel de logging Para evitar errores por valores inválidos en config.yaml, se debe validar antes de aplicarlo:

import logging
import yaml

with open("config.yaml", "r") as file:
    config = yaml.safe_load(file)

LOGS_PATH = config["rutas"]["logs"]
LOG_LEVEL = config["logging"]["nivel"].upper()

if LOG_LEVEL not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
    LOG_LEVEL = "INFO"

logging.basicConfig(
    filename=LOGS_PATH,
    level=getattr(logging, LOG_LEVEL, logging.INFO),
    format="%(asctime)s - %(levelname)s - %(message)s"
)

logger = logging.getLogger(__name__)

Manejo de Errores en main.py

main.py debe capturar y registrar errores usando traceback, asegurando que el bot no falle silenciosamente.

import traceback
from logs.logger import logger
from tasks.task1 import process_data

def main():
    """Ejecuta el bot y maneja su flujo principal."""
    logger.info("Iniciando bot...")

    try:
        data = ["rpa", "automation", "bot"]
        resultado = process_data(data)
        print("Datos procesados:", resultado)
    except FileNotFoundError as e:
        logger.error(f"Archivo no encontrado: {e}\n{traceback.format_exc()}")
    except ValueError as e:
        logger.warning(f"Error de valor: {e}\n{traceback.format_exc()}")
    except Exception as e:
        logger.critical(f"Error inesperado: {e}\n{traceback.format_exc()}")

if __name__ == "__main__":
    main()

Buenas prácticas en manejo de errores:

  1. Registrar errores en logs, no solo imprimirlos.

  2. Diferenciar errores recuperables y errores fatales.

  3. Configurar el nivel de logging en config.yaml para ajustar detalle sin cambiar código.

Uso de launcher.py para ejecución Manual y Automática

launcher.py se encarga de:

  • Activar el entorno virtual.

  • Instalar dependencias solo si es necesario.

  • Ejecutar main.py.

Ejemplo de launcher.py:

import os
import subprocess
import sys

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
VENV_PATH = os.path.join(BASE_DIR, "env")
REQUIREMENTS_FILE = os.path.join(BASE_DIR, "requirements.txt")

def setup_environment():
    """Crea el entorno virtual si no existe e instala las dependencias."""
    if not os.path.exists(VENV_PATH):
        print("Creando entorno virtual...")
        subprocess.run([sys.executable, "-m", "venv", VENV_PATH], check=True)

    print("Instalando dependencias si es necesario...")
    subprocess.run([os.path.join(VENV_PATH, "Scripts", "python.exe"), "-m", "pip", "install", "-r", REQUIREMENTS_FILE], check=True)

def run_bot():
    """Ejecuta el bot principal."""
    try:
        subprocess.run([PYTHON_EXEC, "main.py"], check=True)
    except subprocess.CalledProcessError as e:
        print(f"Error en la ejecución de main.py: {e}")
        sys.exit(1)

if __name__ == "__main__":
    try:
        setup_environment()
        run_bot()
    except Exception as e:
        print(f"Error crítico: {e}")
        sys.exit(1)

Ejecutarlo manualmente:

python launcher.py

Programar en Windows:

  • Programa: C:\ruta\al\proyecto\env\Scripts\python.exe

  • Argumentos: C:\ruta\al\proyecto\launcher.py

  • Iniciar en: C:\ruta\al\proyecto\

Comentarios y Docstrings

Para mantener el código claro y entendible, es importante documentarlo correctamente con docstrings y comentarios en línea.

Reglas generales:

  • Usar docstrings en funciones y clases para describir su propósito.

  • Usar comentarios en línea solo cuando sea necesario explicar un paso específico.

  • Mantener los comentarios actualizados, evitando información desactualizada que pueda confundir.

Ejemplo de buenos docstrings y comentarios en main.py:

"""
Punto de entrada principal del bot RPA.

Este módulo contiene la lógica principal del bot, incluyendo la inicialización
del entorno y la ejecución de tareas específicas.
"""

import logging
from tasks.task1 import process_data

logger = logging.getLogger(__name__)

def main():
    """Inicia la ejecución del bot."""
    logger.info("Bot iniciado.")
    data = ["rpa", "automation", "bot"]
    resultado = process_data(data)
    print("Datos procesados:", resultado)

if __name__ == "__main__":
    main()

Ejemplo de documentación con parámetros y retorno en tasks/task1.py:

"""
Módulo de procesamiento de datos del bot.
"""

def process_data(data):
    """
    Procesa los datos y los convierte a mayúsculas.

    Parámetros:
    data (list): Lista de strings a procesar.

    Retorna:
    list: Lista de strings en mayúsculas.
    """
    return [d.upper() for d in data]

Ejemplo de comentarios en línea cuando son necesarios en main.py:

def execute_task():
    """Ejecuta una tarea y maneja errores con traceback."""
    try:
        logger.info("Ejecutando tarea...")
        result = 10 / 0  # Esto generará un ZeroDivisionError
        return result
    except Exception as e:
        logger.error(f"Error en execute_task: {e}")
        return None

Documentación con README.md

Cada bot debe incluir un archivo README.md claro y estructurado.

Ejemplo de README.md:

# Bot RPA con Python

Este bot automatiza procesos repetitivos utilizando Python.

## Estructura del Proyecto

project_name/
│
│── project_name/       # Código fuente
│   │── main.py         # Punto de entrada
│   │── tasks/          # Módulos de tareas
│
│── logs/               # Carpeta de logs
│── .env                # Variables de entorno
│── requirements.txt    # Dependencias
│── launcher.py         # Script de ejecución

## Instalación y Configuración

1. Descomprimir el paquete en la ubicación deseada
    - Extraer el contenido del .zip en una carpeta de trabajo.

2. Ejecutar launcher.py
El archivo launcher.py se encarga de:
    - Crear y activar el entorno virtual.
    - Instalar las dependencias necesarias.
    - Ejecutar main.py.

## Uso

Para ejecutar el bot manualmente:
```sh
python launcher.py
```

Para programarlo en Windows:
- Programa: `C:\ruta\al\proyecto\env\Scripts\python.exe`
- Argumentos: `C:\ruta\al\proyecto\launcher.py`
- Iniciar en: `C:\ruta\al\proyecto\`

## Configuración

El bot utiliza un archivo `.env` para gestionar credenciales y configuraciones sensibles.

Ejemplo de `.env`:
```ini
API_KEY=my_secret_key
DB_PASSWORD=super_secure_password
DB_HOST=localhost
LOG_LEVEL=INFO
```

**Carga de variables en Python:**
```python
from dotenv import load_dotenv
import os

load_dotenv()  # Carga variables de entorno desde .env

API_KEY = os.getenv("API_KEY")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_HOST = os.getenv("DB_HOST", "default_host")  # Valor por defecto
```

Eficiencia y Buenas Prácticas en Código

  • Usa listas por comprensión en lugar de bucles innecesarios:

# Forma tradicional con for loop
squared_numbers = []
for num in range(10):
    squared_numbers.append(num**2)

# Lista por comprensión (más eficiente)
squared_numbers = [num**2 for num in range(10)]
  • Usa context managers para manejar archivos:

# Mala práctica: No se cierra el archivo automáticamente
file = open("archivo.txt", "r")
content = file.read()
file.close()

# Buena práctica: Context manager cierra automáticamente el archivo
with open("archivo.txt", "r") as file:
    content = file.read()
  • Evita esperas fijas en RPA y usa estrategias dinámicas:

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Mala práctica: Espera fija, desperdicia tiempo
import time
time.sleep(5)  # Siempre espera 5 segundos aunque el elemento ya esté disponible

# Buena práctica: Espera dinámica con WebDriverWait
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.ID, "boton")))

Conclusión

La implementación de estas buenas prácticas permite garantizar que los bots desarrollados con Unimate sean modulares, seguros, eficientes y fáciles de mantener.

Se establecen estándares en la estructura del código, el manejo de entornos virtuales, la gestión de dependencias, la documentación y la automatización, asegurando un desarrollo escalable y sostenible.