feat: Docker, health check, schema migrations y webhook opt-in
- migrations/001_initial_schema.sql: DDL idempotente con CREATE TABLE/INDEX IF NOT EXISTS - database.py: run_migrations() aplica .sql pendientes en orden; init_db() queda como alias - health.py: servidor aiohttp en puerto 8080 — GET /health, GET /health/history, POST /webhook Watcher loop alerta al admin por Telegram si el poller no actualiza en N horas - config.py: webhook_url, webhook_secret, health_port, health_alert_hours - poller.py: on_poll_done callback (conecta con health.record_poll) - main.py: arranca HealthServer, llama run_migrations, modo webhook opt-in via WEBHOOK_URL - Dockerfile + docker-compose.yml + .dockerignore - pyproject.toml: aiohttp>=3.9 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
19
poller.py
19
poller.py
@@ -9,6 +9,7 @@ Responsabilidades:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
@@ -42,9 +43,11 @@ class Poller:
|
||||
self,
|
||||
settings: Settings,
|
||||
bot: object, # PedritoBot — evitamos import circular
|
||||
on_poll_done: Callable[[int], None] | None = None,
|
||||
) -> None:
|
||||
self._settings = settings
|
||||
self._bot = bot
|
||||
self._on_poll_done = on_poll_done
|
||||
self._running = False
|
||||
self._snapshots: dict[int, Snapshot] = {}
|
||||
self._errores_consecutivos: dict[int, int] = {}
|
||||
@@ -163,23 +166,27 @@ class Poller:
|
||||
# API pública
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async def revisar_ahora(self, chat_id: int | None = None, es_manual: bool = False) -> None:
|
||||
async def revisar_ahora(self, chat_id: int | None = None, es_manual: bool = False) -> int:
|
||||
"""
|
||||
Si chat_id: revisar solo ese tenant (para /notif manual).
|
||||
Si None: revisar todos los tenants activos.
|
||||
Retorna la cantidad de tenants revisados.
|
||||
"""
|
||||
if chat_id is not None:
|
||||
tenant = await get_tenant(chat_id)
|
||||
if tenant and tenant.get("estado") == "activo":
|
||||
await self._revisar_tenant(tenant, es_manual=es_manual)
|
||||
return 1
|
||||
elif es_manual and tenant:
|
||||
logger.warning("revisar_ahora_tenant_no_activo", chat_id=chat_id, estado=tenant.get("estado"))
|
||||
return 0
|
||||
else:
|
||||
tenants = await list_active_tenants()
|
||||
if not tenants:
|
||||
logger.info("sin_tenants_activos_skip")
|
||||
return
|
||||
return 0
|
||||
await asyncio.gather(*(self._revisar_tenant(t) for t in tenants))
|
||||
return len(tenants)
|
||||
|
||||
def disparar_revision_manual(self, chat_id: int) -> None:
|
||||
"""Llamado desde el handler de /notif para disparar revisión inmediata."""
|
||||
@@ -199,7 +206,9 @@ class Poller:
|
||||
)
|
||||
|
||||
logger.info("revision_inicial_al_arrancar")
|
||||
await self.revisar_ahora()
|
||||
count = await self.revisar_ahora()
|
||||
if self._on_poll_done:
|
||||
self._on_poll_done(count)
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
@@ -211,6 +220,8 @@ class Poller:
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
if self._en_horario_habitual():
|
||||
await self.revisar_ahora()
|
||||
count = await self.revisar_ahora()
|
||||
if self._on_poll_done:
|
||||
self._on_poll_done(count)
|
||||
else:
|
||||
logger.debug("fuera_de_horario_skip")
|
||||
|
||||
Reference in New Issue
Block a user