From 90b9dad59c26e9250634cd2b52020c62fc6de963 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 2 Apr 2026 14:47:03 -0400 Subject: [PATCH] Add configurable adapter selection to app runner --- .env.example | 1 + README.md | 15 +++++++++++++++ pm/tasks-v2.org | 10 ++++++---- youdis.py | 33 ++++++++++++++++++++++++++------- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 6022761..a9e3ba1 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ YOUDIS_RUN_BACKEND=1 YOUDIS_RUN_DISCORD=1 +YOUDIS_ENABLED_ADAPTERS=discord YOUDIS_BACKEND_HOST=127.0.0.1 YOUDIS_BACKEND_PORT=8000 YOUDIS_BACKEND_HEALTH_TIMEOUT=20 diff --git a/README.md b/README.md index 3808b4c..63822b5 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,27 @@ default v2 app run: python3 ./youdis.py ``` +`youdis.py` is the app-level runner. It starts the backend first, waits for health, then starts each enabled adapter explicitly. + direct component runs still work for testing: ``` python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000 python3 -m youdis.adapters.discord ``` +adapter selection is controlled by env, currently: +``` +YOUDIS_RUN_BACKEND=1 +YOUDIS_ENABLED_ADAPTERS=discord +``` + +to add a new adapter: +1. add `youdis/adapters/.py` +2. make it independently runnable +3. add `start_()` to `youdis.py` +4. register it in the explicit adapter starter map in `youdis.py` +5. add any required env vars to `.env.example` + build and run the docker container ``` DISCORD_BOT_TOKEN = [discord bot token] diff --git a/pm/tasks-v2.org b/pm/tasks-v2.org index cc7efb8..8d4bb08 100644 --- a/pm/tasks-v2.org +++ b/pm/tasks-v2.org @@ -272,21 +272,23 @@ Build a simple Python orchestration layer in youdis.py so the standard app stack - leave room for future Zulip/XMPP adapters without redesigning the runner ** evidence -- commit: +- commit: 309ce87 - tests: 1. `python3 -m py_compile ./youdis.py ./youdis/adapters/discord.py` 2. backend direct run still works: `python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000` 3. discord direct run still works: `python3 -m youdis.adapters.discord` - 4. app runner backend-only smoke test: `YOUDIS_RUN_DISCORD=0 timeout 5s python3 ./youdis.py` + 4. app runner backend-only smoke test: `YOUDIS_ENABLED_ADAPTERS= timeout 5s python3 ./youdis.py` 5. app runner default path: `python3 ./youdis.py` -- date: +- date: [2026-04-02 Thu 14:13] ** notes - `youdis.py` is now the default v2 app runner and starts the standard stack explicitly rather than dynamically discovering adapters - backend starts first and must pass a health check before the Discord adapter is launched - backend and Discord adapter remain directly runnable for debugging and tests - Docker is intended to invoke `python3 /app/youdis.py`; Python owns app orchestration while Docker owns packaging and runtime environment -- runner flags currently include `YOUDIS_RUN_BACKEND`, `YOUDIS_RUN_DISCORD`, `YOUDIS_BACKEND_HOST`, and `YOUDIS_BACKEND_PORT` +- adapter startup remains explicit in `youdis.py`, while `YOUDIS_ENABLED_ADAPTERS` selects which known adapters to run +- adding an adapter means creating `youdis/adapters/.py`, adding `start_()` in `youdis.py`, registering it in the explicit adapter map, and documenting any env vars in `.env.example` +- runner flags currently include `YOUDIS_RUN_BACKEND`, `YOUDIS_ENABLED_ADAPTERS`, `YOUDIS_BACKEND_HOST`, and `YOUDIS_BACKEND_PORT` * ==== BACKLOG ==== diff --git a/youdis.py b/youdis.py index 0252c6a..977dbe8 100644 --- a/youdis.py +++ b/youdis.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""App runner for the standard v2 stack.""" +"""App runner for FastAPI backend and discord adapter.""" from __future__ import annotations @@ -22,6 +22,7 @@ DEFAULT_BACKEND_HOST = os.getenv("YOUDIS_BACKEND_HOST", "127.0.0.1") DEFAULT_BACKEND_PORT = int(os.getenv("YOUDIS_BACKEND_PORT", "8000")) RUN_BACKEND = os.getenv("YOUDIS_RUN_BACKEND", "1") not in {"0", "false", "False"} RUN_DISCORD = os.getenv("YOUDIS_RUN_DISCORD", "1") not in {"0", "false", "False"} +ENABLED_ADAPTERS = os.getenv("YOUDIS_ENABLED_ADAPTERS", "discord") BACKEND_HEALTH_TIMEOUT = float(os.getenv("YOUDIS_BACKEND_HEALTH_TIMEOUT", "20")) BACKEND_HEALTH_INTERVAL = float(os.getenv("YOUDIS_BACKEND_HEALTH_INTERVAL", "0.5")) @@ -69,6 +70,23 @@ async def start_discord(env: dict[str, str]) -> ManagedProcess: return ManagedProcess(name="discord", process=process) +ADAPTER_STARTERS = { + "discord": start_discord, +} + + +def selected_adapters() -> list[str]: + requested = [name.strip() for name in ENABLED_ADAPTERS.split(",") if name.strip()] + if not RUN_DISCORD: + requested = [name for name in requested if name != "discord"] + + unknown = [name for name in requested if name not in ADAPTER_STARTERS] + if unknown: + raise ValueError(f"unknown adapters requested: {', '.join(sorted(unknown))}") + + return requested + + async def wait_for_backend_health(timeout_seconds: float) -> None: deadline = asyncio.get_running_loop().time() + timeout_seconds health_url = f"{backend_url()}/health" @@ -102,9 +120,10 @@ async def stop_process(proc: ManagedProcess) -> None: async def run() -> int: managed: list[ManagedProcess] = [] env = build_child_env() + adapters = selected_adapters() - if not RUN_BACKEND and not RUN_DISCORD: - print("nothing to start: both YOUDIS_RUN_BACKEND and YOUDIS_RUN_DISCORD are disabled") + if not RUN_BACKEND and not adapters: + print("nothing to start: backend is disabled and no adapters are enabled") return 1 try: @@ -115,10 +134,10 @@ async def run() -> int: await wait_for_backend_health(BACKEND_HEALTH_TIMEOUT) print("backend is healthy") - if RUN_DISCORD: - discord = await start_discord(env) - managed.append(discord) - print("started discord adapter") + for adapter_name in adapters: + process = await ADAPTER_STARTERS[adapter_name](env) + managed.append(process) + print(f"started {adapter_name} adapter") wait_tasks = [asyncio.create_task(proc.process.wait()) for proc in managed] done, pending = await asyncio.wait(wait_tasks, return_when=asyncio.FIRST_COMPLETED)