Add configurable adapter selection to app runner
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
YOUDIS_RUN_BACKEND=1
|
YOUDIS_RUN_BACKEND=1
|
||||||
YOUDIS_RUN_DISCORD=1
|
YOUDIS_RUN_DISCORD=1
|
||||||
|
YOUDIS_ENABLED_ADAPTERS=discord
|
||||||
YOUDIS_BACKEND_HOST=127.0.0.1
|
YOUDIS_BACKEND_HOST=127.0.0.1
|
||||||
YOUDIS_BACKEND_PORT=8000
|
YOUDIS_BACKEND_PORT=8000
|
||||||
YOUDIS_BACKEND_HEALTH_TIMEOUT=20
|
YOUDIS_BACKEND_HEALTH_TIMEOUT=20
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -5,12 +5,27 @@ default v2 app run:
|
|||||||
python3 ./youdis.py
|
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:
|
direct component runs still work for testing:
|
||||||
```
|
```
|
||||||
python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000
|
python3 -m uvicorn youdis.main:app --host 127.0.0.1 --port 8000
|
||||||
python3 -m youdis.adapters.discord
|
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/<adapter>.py`
|
||||||
|
2. make it independently runnable
|
||||||
|
3. add `start_<adapter>()` 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
|
build and run the docker container
|
||||||
```
|
```
|
||||||
DISCORD_BOT_TOKEN = [discord bot token]
|
DISCORD_BOT_TOKEN = [discord bot token]
|
||||||
|
|||||||
@@ -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
|
- leave room for future Zulip/XMPP adapters without redesigning the runner
|
||||||
|
|
||||||
** evidence
|
** evidence
|
||||||
- commit:
|
- commit: 309ce87
|
||||||
- tests:
|
- tests:
|
||||||
1. `python3 -m py_compile ./youdis.py ./youdis/adapters/discord.py`
|
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`
|
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`
|
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`
|
5. app runner default path: `python3 ./youdis.py`
|
||||||
- date:
|
- date: [2026-04-02 Thu 14:13]
|
||||||
|
|
||||||
** notes
|
** notes
|
||||||
- `youdis.py` is now the default v2 app runner and starts the standard stack explicitly rather than dynamically discovering adapters
|
- `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 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
|
- 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
|
- 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/<adapter>.py`, adding `start_<adapter>()` 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 ====
|
* ==== BACKLOG ====
|
||||||
|
|||||||
33
youdis.py
33
youdis.py
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""App runner for the standard v2 stack."""
|
"""App runner for FastAPI backend and discord adapter."""
|
||||||
|
|
||||||
from __future__ import annotations
|
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"))
|
DEFAULT_BACKEND_PORT = int(os.getenv("YOUDIS_BACKEND_PORT", "8000"))
|
||||||
RUN_BACKEND = os.getenv("YOUDIS_RUN_BACKEND", "1") not in {"0", "false", "False"}
|
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"}
|
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_TIMEOUT = float(os.getenv("YOUDIS_BACKEND_HEALTH_TIMEOUT", "20"))
|
||||||
BACKEND_HEALTH_INTERVAL = float(os.getenv("YOUDIS_BACKEND_HEALTH_INTERVAL", "0.5"))
|
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)
|
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:
|
async def wait_for_backend_health(timeout_seconds: float) -> None:
|
||||||
deadline = asyncio.get_running_loop().time() + timeout_seconds
|
deadline = asyncio.get_running_loop().time() + timeout_seconds
|
||||||
health_url = f"{backend_url()}/health"
|
health_url = f"{backend_url()}/health"
|
||||||
@@ -102,9 +120,10 @@ async def stop_process(proc: ManagedProcess) -> None:
|
|||||||
async def run() -> int:
|
async def run() -> int:
|
||||||
managed: list[ManagedProcess] = []
|
managed: list[ManagedProcess] = []
|
||||||
env = build_child_env()
|
env = build_child_env()
|
||||||
|
adapters = selected_adapters()
|
||||||
|
|
||||||
if not RUN_BACKEND and not RUN_DISCORD:
|
if not RUN_BACKEND and not adapters:
|
||||||
print("nothing to start: both YOUDIS_RUN_BACKEND and YOUDIS_RUN_DISCORD are disabled")
|
print("nothing to start: backend is disabled and no adapters are enabled")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -115,10 +134,10 @@ async def run() -> int:
|
|||||||
await wait_for_backend_health(BACKEND_HEALTH_TIMEOUT)
|
await wait_for_backend_health(BACKEND_HEALTH_TIMEOUT)
|
||||||
print("backend is healthy")
|
print("backend is healthy")
|
||||||
|
|
||||||
if RUN_DISCORD:
|
for adapter_name in adapters:
|
||||||
discord = await start_discord(env)
|
process = await ADAPTER_STARTERS[adapter_name](env)
|
||||||
managed.append(discord)
|
managed.append(process)
|
||||||
print("started discord adapter")
|
print(f"started {adapter_name} adapter")
|
||||||
|
|
||||||
wait_tasks = [asyncio.create_task(proc.process.wait()) for proc in managed]
|
wait_tasks = [asyncio.create_task(proc.process.wait()) for proc in managed]
|
||||||
done, pending = await asyncio.wait(wait_tasks, return_when=asyncio.FIRST_COMPLETED)
|
done, pending = await asyncio.wait(wait_tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||||
|
|||||||
Reference in New Issue
Block a user