- `app/services/http_client.py` : `async with shared_client(timeout=15)
as client:` remplace les 49 créations `async with httpx.AsyncClient(`
de 14 fichiers (gitea ×21, providers oidc/oauth ×11, calendar ×4,
automations ×3…) — le pool de connexions est réutilisé au lieu d'être
recréé à chaque appel. __aexit__ no-op (le client partagé ne se ferme
pas à la sortie).
- Cache par (boucle d'event, kwargs) en WeakKeyDictionary : un
AsyncClient n'est JAMAIS partagé entre deux loops (piège des tests
« Event loop is closed ») — une boucle par test = client propre
collecté avec la boucle. Clé = kwargs triés, repr() pour les valeurs
non hashables (`headers=` dict → TypeError rattrapé par la suite).
- Laissés délibérément : github_adapter (transport MockTransport
injecté), webhook_outbound (client « own_client » fermé par la
fonction).
- Tests : `test_http_client_shared_and_loop_scoped` (réutilisation mêmes
kwargs / cloisonné kwargs / cloisonné loop) ; le stub des webhooks
patche aussi la fabrique `http_client.httpx` + purge du cache (avant :
webhook_outbound.httpx patché mais la fabrique partagée créait un vrai
client → réseau réel dans les tests).
suite **1091/1091** · ruff OK · docs à jour
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient
ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique
corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié
`asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers
dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte
l'event loop, sans changer une ligne de logique.
- Répartition : api_v2 60, dashboard 40, collections 25, board 23,
workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers.
- Les 4 routers prioritaires de l'audit sont couverts par ce lot :
api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions
(le reste de leurs routes attend la phase 2 : elles ont de vrais `await`).
- Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs
DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré
(rien ne l'appellerait — YAGNI jusqu'au premier usage).
suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour