⚡️ ¿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:
Dispositivo | Función | Endpoint ejemplo |
---|---|---|
AVR Denon | Control de volumen, entradas | POST /denon/volumen |
TVs Samsung | Encender, cambiar canal, apps | POST /tv/canal |
Bombillas WiFi | Color, brillo, encendido | POST /luces/color |
Sensores IoT | Lecturas de temperatura, humedad | GET /sensores/datos |
Scripts de backup | Ejecutar copias de seguridad | POST /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
Preguntas Frecuentes
¿Qué es Bottle y por qué usarlo para crear APIs en Python?
Bottle es un micro-framework web de Python (un solo archivo, cero dependencias) que permite crear APIs REST y aplicaciones web minimalistas en minutos. A diferencia de Flask (más features) o Django (framework completo), Bottle es ultraligero, perfecto para servicios simples, scripts que quieres exponer como API, o prototipos rápidos. Ideal para homelabs: convierte scripts de Python que corren con cron en servicios web HTTP que puedes llamar desde n8n, webhooks, o aplicaciones. Sin overhead ni complejidad innecesaria.
¿Bottle, Flask o FastAPI para mi proyecto de API?
Bottle: para proyectos súper simples, un solo archivo, cuando no quieres instalar dependencias. Flask: para proyectos pequeños/medianos, necesitas extensiones (auth, db), comunidad enorme, balance flexibilidad/simplicidad. FastAPI: para APIs modernas con validación automática, async/await, generación de documentación OpenAPI, tipos Python 3.6+, performance máxima. Para homelab/scripts: Bottle. Para aplicación real pequeña: Flask. Para API profesional con microservicios: FastAPI. No hay respuesta única; depende de complejidad y requirements.
¿Bottle es gratis y open source?
Sí, Bottle es completamente gratis, open source (licencia MIT), sin versiones premium ni limitaciones. Se instala con pip install bottle
(40KB). No tiene telemetría, no requiere registro, y funciona offline. Para homelabs es perfecto: cero coste, cero fricción, máxima libertad. Puedes modificarlo, embebberlo, distribuirlo sin restricciones. Comparado con servicios cloud (AWS Lambda tiene free tier limitado), Bottle en tu servidor es ilimitado gratis.
¿Cómo creo una API simple con Bottle en menos de 10 minutos?
Tres pasos: 1) Instala Bottle: pip install bottle
. 2) Crea app.py: from bottle import route, run; @route('/hello'); def hello(): return {'message': 'Hello World'}; run(host='0.0.0.0', port=8080)
. 3) Ejecuta: python app.py
. Listo, tienes API REST en http://localhost:8080/hello respondiendo JSON. Desde n8n: usa HTTP Request a esa URL. Para POST con datos: usa @route('/data', method='POST')
y request.json
para leer body. Tan simple como parece.
¿Bottle es adecuado para producción o solo para desarrollo?
Bottle sirve para producción ligera (APIs internas, microservicios de homelab, webhooks, dashboards simples). NO para producción high-traffic (millones de requests/día): carece de async nativo (usa gevent para concurrency), no es tan rápido como FastAPI/Golang, y no tiene features enterprise. Para homelab/uso personal: perfectamente adecuado. Para startup/empresa: considera FastAPI o framework más robusto. Ejecuta con servidor WSGI (Gunicorn, uWSGI) en producción en lugar del servidor development de Bottle.
¿Cómo añado autenticación a una API creada con Bottle?
Opciones: 1) API Key simple: verifica header request.headers.get('X-API-Key')
en decorator custom. 2) HTTP Basic Auth: usa bottle-auth
o verifica request.auth
manualmente. 3) JWT tokens: instala PyJWT, genera tokens en endpoint /login, valida en requests subsecuentes. 4) OAuth2: usa authlib (más complejo). Para APIs de homelab que consumes desde n8n, API Key estática es suficiente y simple. NUNCA expongas API sin auth a internet; úsala solo en red interna o tras VPN/reverse proxy con SSL.
¿Puedo integrar APIs de Bottle con n8n para automatizaciones?
Sí, perfectamente. Bottle expone endpoints HTTP que n8n consume con nodo HTTP Request. Casos de uso: script de Python que procesa datos → API Bottle → n8n llama el endpoint y usa resultado en workflow. Ejemplo: API Bottle que consulta base de datos local, hace scraping, llama modelo de ML local (Ollama), o ejecuta comandos de sistema. n8n orquesta, Bottle ejecuta lógica Python custom. Es la combinación ideal para extender n8n con código Python sin limitaciones de nodos predefinidos.
¿Bottle soporta WebSockets o Server-Sent Events?
Bottle no soporta WebSockets nativamente (es síncrono). Para WebSockets, usa FastAPI (soporte nativo) o Flask con flask-socketio. Server-Sent Events (SSE) es posible con Bottle usando generadores y yield
, pero limitado. Si necesitas comunicación bidireccional real-time, Bottle no es la mejor opción. Para APIs REST simples request-response (el 90% de casos homelab), Bottle es perfecto. Para chat, notificaciones push, o streaming, considera frameworks con async/WebSocket.