⚡️ ¿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!