🤖 Crea tu propia API con Bottle: convierte tus scripts Python en servicios web punk

⚡️ ¿Por qué necesitas una API para tus scripts Python?

¿Tienes scripts de Python que controlan tus dispositivos domóticos, procesan datos o automatizan tareas? ¿Te gustaría poder ejecutarlos desde cualquier lugar, integrarlos con n8n o que otras aplicaciones los usen? La solución es crear una API REST con Bottle, el framework más punk y minimalista de Python.

Bottle es perfecto para esto: un solo archivo, sin dependencias, y en 10 minutos tienes tu API funcionando. Ideal para convertir scripts existentes en servicios web sin complicarte la vida.

1️⃣ Métodos GET y POST para dummies

¿Qué es una API REST?

Una API REST es como un camarero en un restaurante: tú pides algo (request) y él te trae la respuesta (response). Los métodos más usados son:

  • GET: «Dame información» – como pedir ver la carta del restaurante
  • POST: «Haz algo con esta información» – como hacer un pedido específico

Ejemplos prácticos:

  • GET /luces/salon → Te devuelve si la luz del salón está encendida
  • POST /luces/salon + {«estado»: «on»} → Enciende la luz del salón
  • GET /temperatura → Te devuelve la temperatura actual
  • POST /denon/volumen + {«nivel»: 25} → Cambia el volumen a 25

2️⃣ Casos de uso reales (probados en mi garaje punk)

Dispositivos que he controlado con APIs Bottle:

DispositivoFunciónEndpoint ejemplo
AVR DenonControl de volumen, entradasPOST /denon/volumen
TVs SamsungEncender, cambiar canal, appsPOST /tv/canal
Bombillas WiFiColor, brillo, encendidoPOST /luces/color
Sensores IoTLecturas de temperatura, humedadGET /sensores/datos
Scripts de backupEjecutar copias de seguridadPOST /backup/iniciar

3️⃣ Instalación y setup inicial

# Crear directorio del proyecto
mkdir api-punk && cd api-punk

# Crear entorno virtual (recomendado)
python3 -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate   # Windows

# Instalar Bottle
pip install bottle

# Crear archivo requirements.txt
echo "bottle==0.12.25" > requirements.txt

4️⃣ API completa de ejemplo: controlador domótico punk

Vamos a crear una API que controle varios dispositivos. Crea un archivo api_punk.py:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from bottle import Bottle, run, request, response, route
import json
import time
import subprocess
import random

# Crear instancia de Bottle
app = Bottle()

# Configurar CORS para permitir requests desde el navegador
@app.hook('after_request')
def enable_cors():
    response.headers['Access-Control-Allow-Origin'] = '*'
    response.headers['Access-Control-Allow-Methods'] = 'PUT, GET, POST, DELETE, OPTIONS'
    response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type'

# Simulador de base de datos (en producción usa PostgreSQL o similar)
dispositivos = {
    "luces": {
        "salon": {"estado": "off", "brillo": 50, "color": "#FFFFFF"},
        "cocina": {"estado": "off", "brillo": 80, "color": "#FFFFFF"},
        "dormitorio": {"estado": "off", "brillo": 30, "color": "#FF6B6B"}
    },
    "clima": {
        "temperatura": 22.5,
        "humedad": 65,
        "calefaccion": "off"
    },
    "audio": {
        "volumen": 15,
        "entrada": "tv",
        "estado": "off"
    }
}

# === ENDPOINTS DE INFORMACIÓN (GET) ===

@app.route('/')
def home():
    """Página de inicio con documentación de la API"""
    return {
        "api": "Controlador Domótico Punk",
        "version": "1.0",
        "endpoints": {
            "GET /": "Esta documentación",
            "GET /status": "Estado general del sistema",
            "GET /luces": "Estado de todas las luces",
            "GET /luces/<habitacion>": "Estado de luz específica",
            "GET /clima": "Datos de temperatura y humedad",
            "GET /audio": "Estado del sistema de audio",
            "POST /luces/<habitacion>": "Controlar luz específica",
            "POST /clima/temperatura": "Cambiar temperatura",
            "POST /audio/volumen": "Cambiar volumen",
            "POST /backup": "Ejecutar backup",
            "POST /script": "Ejecutar script personalizado"
        }
    }

@app.route('/status')
def status():
    """Estado general del sistema"""
    return {
        "timestamp": time.time(),
        "uptime": "OK",
        "dispositivos_conectados": len(dispositivos),
        "sistema": "Raspberry Pi 4B",
        "memoria_libre": "1.2GB"
    }

@app.route('/luces')
def get_luces():
    """Obtener estado de todas las luces"""
    return dispositivos["luces"]

@app.route('/luces/<habitacion>')
def get_luz(habitacion):
    """Obtener estado de una luz específica"""
    if habitacion in dispositivos["luces"]:
        return dispositivos["luces"][habitacion]
    else:
        response.status = 404
        return {"error": f"Habitación '{habitacion}' no encontrada"}

@app.route('/clima')
def get_clima():
    """Obtener datos de clima"""
    # Simular lectura de sensores (aquí irían tus scripts reales)
    dispositivos["clima"]["temperatura"] = round(random.uniform(20, 25), 1)
    dispositivos["clima"]["humedad"] = random.randint(60, 70)
    return dispositivos["clima"]

@app.route('/audio')
def get_audio():
    """Obtener estado del sistema de audio"""
    return dispositivos["audio"]

# === ENDPOINTS DE CONTROL (POST) ===

@app.route('/luces/<habitacion>', method='POST')
def control_luz(habitacion):
    """Controlar una luz específica"""
    if habitacion not in dispositivos["luces"]:
        response.status = 404
        return {"error": f"Habitación '{habitacion}' no encontrada"}

    try:
        data = request.json
        luz = dispositivos["luces"][habitacion]

        # Actualizar estado
        if "estado" in data:
            luz["estado"] = data["estado"]
        if "brillo" in data:
            luz["brillo"] = max(0, min(100, int(data["brillo"])))
        if "color" in data:
            luz["color"] = data["color"]

        # Aquí ejecutarías tu script real para controlar la bombilla
        # Ejemplo: subprocess.run(["python", "control_bombilla.py", habitacion, luz["estado"]])

        return {
            "mensaje": f"Luz de {habitacion} actualizada",
            "estado_actual": luz
        }

    except Exception as e:
        response.status = 400
        return {"error": str(e)}

@app.route('/clima/temperatura', method='POST')
def set_temperatura():
    """Cambiar temperatura del termostato"""
    try:
        data = request.json
        nueva_temp = float(data["temperatura"])

        if nueva_temp < 15 or nueva_temp > 30:
            response.status = 400
            return {"error": "Temperatura debe estar entre 15 y 30 grados"}

        dispositivos["clima"]["temperatura"] = nueva_temp

        # Aquí ejecutarías tu script real de control climático
        # subprocess.run(["python", "control_clima.py", str(nueva_temp)])

        return {
            "mensaje": f"Temperatura establecida a {nueva_temp}°C",
            "estado": dispositivos["clima"]
        }

    except Exception as e:
        response.status = 400
        return {"error": str(e)}

@app.route('/audio/volumen', method='POST')
def set_volumen():
    """Cambiar volumen del sistema de audio"""
    try:
        data = request.json
        volumen = int(data["volumen"])

        if volumen < 0 or volumen > 100:
            response.status = 400
            return {"error": "Volumen debe estar entre 0 y 100"}

        dispositivos["audio"]["volumen"] = volumen

        # Aquí ejecutarías tu script real para el AVR Denon
        # subprocess.run(["python", "control_denon.py", "volumen", str(volumen)])

        return {
            "mensaje": f"Volumen establecido a {volumen}",
            "estado": dispositivos["audio"]
        }

    except Exception as e:
        response.status = 400
        return {"error": str(e)}

@app.route('/backup', method='POST')
def ejecutar_backup():
    """Ejecutar script de backup"""
    try:
        # Ejecutar tu script de backup real
        resultado = subprocess.run(
            ["python", "backup_script.py"], 
            capture_output=True, 
            text=True,
            timeout=300  # Timeout de 5 minutos
        )

        if resultado.returncode == 0:
            return {
                "mensaje": "Backup ejecutado correctamente",
                "salida": resultado.stdout,
                "timestamp": time.time()
            }
        else:
            response.status = 500
            return {
                "error": "Backup falló",
                "detalle": resultado.stderr
            }

    except subprocess.TimeoutExpired:
        response.status = 500
        return {"error": "Backup tardó demasiado tiempo"}
    except Exception as e:
        response.status = 500
        return {"error": str(e)}

@app.route('/script', method='POST')
def ejecutar_script():
    """Ejecutar script personalizado (¡cuidado con la seguridad!)"""
    try:
        data = request.json
        script_name = data.get("script", "")
        parametros = data.get("parametros", [])

        # Lista blanca de scripts permitidos (seguridad básica)
        scripts_permitidos = [
            "check_temperatura.py",
            "reset_router.py",
            "check_discos.py",
            "update_system.py"
        ]

        if script_name not in scripts_permitidos:
            response.status = 403
            return {"error": "Script no permitido"}

        # Ejecutar script
        comando = ["python", script_name] + parametros
        resultado = subprocess.run(
            comando,
            capture_output=True,
            text=True,
            timeout=60
        )

        return {
            "script": script_name,
            "salida": resultado.stdout,
            "error": resultado.stderr,
            "codigo": resultado.returncode
        }

    except Exception as e:
        response.status = 500
        return {"error": str(e)}

# === EJECUTAR SERVIDOR ===

if __name__ == '__main__':
    print("🤖 Iniciando API Punk...")
    print("📡 Accede a http://localhost:8080 para ver la documentación")
    run(app, host='0.0.0.0', port=8080, debug=True, reloader=True)

5️⃣ Probar la API

Ejecuta tu API:

python api_punk.py

Prueba los endpoints con curl o desde tu navegador:

# Ver documentación
curl http://localhost:8080/

# Obtener estado de luces
curl http://localhost:8080/luces

# Encender luz del salón
curl -X POST http://localhost:8080/luces/salon \
  -H "Content-Type: application/json" \
  -d '{"estado": "on", "brillo": 80}'

# Cambiar volumen del audio
curl -X POST http://localhost:8080/audio/volumen \
  -H "Content-Type: application/json" \
  -d '{"volumen": 25}'

# Ejecutar backup
curl -X POST http://localhost:8080/backup

6️⃣ Dockerizar tu API: hazla portable y punk

Crear Dockerfile

# Dockerfile
FROM python:3.11-slim

# Establecer directorio de trabajo
WORKDIR /app

# Variables de entorno
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Instalar dependencias del sistema
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements e instalar dependencias Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar código de la aplicación
COPY . .

# Exponer puerto
EXPOSE 8080

# Comando por defecto
CMD ["python", "api_punk.py"]

Crear docker-compose.yml

# docker-compose.yml
version: '3.8'

services:
  api-punk:
    build: .
    container_name: api-punk
    ports:
      - "8080:8080"
    volumes:
      - ./scripts:/app/scripts:ro  # Scripts de solo lectura
      - ./logs:/app/logs           # Logs persistentes
    environment:
      - ENV=production
    restart: unless-stopped

  # Opcional: base de datos para persistencia
  redis:
    image: redis:7-alpine
    container_name: api-punk-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped

volumes:
  redis_data:

Actualizar requirements.txt

bottle==0.12.25
redis==4.5.4

Ejecutar con Docker

# Construir imagen
docker-compose build

# Ejecutar servicios
docker-compose up -d

# Ver logs
docker-compose logs -f api-punk

# Parar servicios
docker-compose down

7️⃣ Integración con n8n y automatización

Una vez que tienes tu API funcionando, puedes integrarla fácilmente con n8n:

# Nodo HTTP Request en n8n para encender luces
{
  "method": "POST",
  "url": "http://tu-servidor:8080/luces/salon",
  "body": {
    "estado": "on",
    "brillo": "{{$json.brillo}}"
  },
  "headers": {
    "Content-Type": "application/json"
  }
}

8️⃣ Consejos de seguridad para APIs punk

  • Autenticación: Añade API keys o tokens JWT para proteger endpoints críticos
  • Lista blanca: Solo permite ejecutar scripts específicos (como en el ejemplo)
  • Rate limiting: Limita requests por IP para evitar abuso
  • HTTPS: Usa certificados SSL en producción
  • Validación: Siempre valida los datos de entrada

Ejemplo de autenticación simple:

API_KEY = "tu-clave-super-secreta"

def requiere_auth(func):
    def wrapper(*args, **kwargs):
        auth = request.headers.get('Authorization')
        if auth != f'Bearer {API_KEY}':
            response.status = 401
            return {'error': 'No autorizado'}
        return func(*args, **kwargs)
    return wrapper

@app.route('/luces/<habitacion>', method='POST')
@requiere_auth
def control_luz(habitacion):
    # ... tu código aquí

9️⃣ Scripts de ejemplo para integrar

Aquí tienes ejemplos de scripts que puedes integrar en tu API:

Control de bombilla WiFi (control_bombilla.py)

#!/usr/bin/env python3
import sys
import requests

def controlar_bombilla(habitacion, estado):
    # Ejemplo para bombillas Philips Hue
    bridge_ip = "192.168.1.100"
    api_key = "tu-api-key"

    url = f"http://{bridge_ip}/api/{api_key}/lights/1/state"
    data = {"on": estado == "on"}

    response = requests.put(url, json=data)
    return response.status_code == 200

if __name__ == "__main__":
    habitacion = sys.argv[1]
    estado = sys.argv[2]
    controlar_bombilla(habitacion, estado)

Control de AVR Denon (control_denon.py)

#!/usr/bin/env python3
import sys
import telnetlib

def control_denon(comando, valor=None):
    denon_ip = "192.168.1.50"
    denon_port = 23

    try:
        tn = telnetlib.Telnet(denon_ip, denon_port, timeout=5)

        if comando == "volumen":
            cmd = f"MV{valor:02d}\r"
        elif comando == "power":
            cmd = "PWON\r" if valor == "on" else "PWSTANDBY\r"

        tn.write(cmd.encode('ascii'))
        tn.close()
        return True
    except:
        return False

if __name__ == "__main__":
    comando = sys.argv[1]
    valor = sys.argv[2] if len(sys.argv) > 2 else None
    control_denon(comando, valor)

🎸 Conclusión: el poder de las APIs punk

Con Bottle has convertido cualquier script Python en un servicio web profesional. Ahora puedes controlar tus dispositivos desde n8n, crear dashboards web, integrar con Home Assistant o incluso hacer que Alexa ejecute tus scripts.

La belleza de Bottle es su simplicidad: un framework minimalista que hace exactamente lo que necesitas sin complicaciones. Perfecto para makers, hackers caseros y cualquier punk digital que quiera automatizar su vida sin depender de servicios cloud.

¿Has integrado algún dispositivo interesante? ¿Tienes scripts que quieres convertir en API? ¡Comparte tus experimentos en los comentarios y mantén vivo el espíritu maker!

🔗 Recursos útiles

Por ziru

Learning AI Agents
Resumen de privacidad

Esta web utiliza cookies para que podamos ofrecerte la mejor experiencia de usuario posible. La información de las cookies se almacena en tu navegador y realiza funciones tales como reconocerte cuando vuelves a nuestra web o ayudar a nuestro equipo a comprender qué secciones de la web encuentras más interesantes y útiles.