"""Bot entrypoint: ensure tables → sweeper task → Bale long-poll loop. Mirrors the api's pattern of a plain asyncio background loop (no Celery/Redis). """ import asyncio import logging from app.bale.client import BaleClient, poll_loop from app.config import settings from app.db_init import ensure_schema from app.drafts import expire_stale from app.handlers import handle_update from app.runtime import BotContext from app.api.client import ErpClient from app.sync import sync_drafts_loop logging.basicConfig( level=getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO), format="%(asctime)s %(levelname)s %(name)s — %(message)s", ) logger = logging.getLogger("bot.main") async def sweeper_loop() -> None: """Expire stale drafts + sweep dead pending flows every 10 minutes.""" while True: try: expired = await expire_stale(settings.DRAFT_TTL_HOURS) if expired: logger.info("expired %s stale drafts", expired) except Exception: logger.exception("sweeper failed") await asyncio.sleep(600) async def main() -> None: if not settings.BOT_TOKEN: raise SystemExit("PROCUREMENT_BOT_BOT_TOKEN is not set — get one from Bale's BotFather") logger.info("starting bale procurement bot (api=%s, ai=%s)", settings.API_BASE_URL, "configured" if settings.AI_BASE_URL else "NOT configured") await ensure_schema() bale = BaleClient(settings.BALE_API_BASE, settings.BOT_TOKEN) api = ErpClient(settings.API_BASE_URL, settings.API_TIMEOUT_SECONDS, settings.SERVICE_TOKEN) ctx = BotContext(bale, api) sweeper = asyncio.create_task(sweeper_loop()) sync_task = asyncio.create_task(sync_drafts_loop(ctx)) try: await poll_loop(bale, lambda update: handle_update(ctx, update)) finally: sweeper.cancel() sync_task.cancel() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: pass