fix: A21 phase 1 — 352 routes async sans await → threadpool (v7.8.0)
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s

- 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
This commit is contained in:
2026-10-01 10:53:26 -04:00
parent c718fe06de
commit 224bda74d5
48 changed files with 379 additions and 358 deletions
+25 -25
View File
@@ -196,7 +196,7 @@ def _validate_meta_keys(conn, collection_id: int, properties: dict) -> None:
@router.get("", response_class=HTMLResponse)
async def list_collections(request: Request):
def list_collections(request: Request):
"""Page listing all collections in the workspace."""
with get_conn() as conn:
rows = conn.execute(
@@ -212,7 +212,7 @@ async def list_collections(request: Request):
@router.get("/api")
async def list_collections_api(request: Request):
def list_collections_api(request: Request):
"""API: list all collections."""
with get_conn() as conn:
rows = conn.execute(
@@ -502,7 +502,7 @@ async def duplicate_collection_api(request: Request, collection_id: int):
@router.get("/pages/{page_id}/api")
async def get_page_api(request: Request, page_id: int):
def get_page_api(request: Request, page_id: int):
"""API: get a single page."""
with get_conn() as conn:
page = conn.execute(
@@ -516,7 +516,7 @@ async def get_page_api(request: Request, page_id: int):
@router.get("/pages/{page_id}/open/api")
async def open_row_page_api(request: Request, page_id: int):
def open_row_page_api(request: Request, page_id: int):
"""v6.5.0 — content page of a database row (lazy-created).
Any DB view (table/board/gallery/list/calendar) opens a row through
@@ -650,7 +650,7 @@ async def delete_page_api(request: Request, page_id: int):
@router.get("/boards/api")
async def list_boards_as_collections(request: Request):
def list_boards_as_collections(request: Request):
"""API: list all Gitea boards as pseudo-collections."""
from app.services.collection_adapter import GiteaBoardCompat
boards = GiteaBoardCompat.list_boards_as_collections()
@@ -690,14 +690,14 @@ async def sync_board_to_collection(request: Request, owner: str, repo: str):
@router.get("/property-types/api")
async def list_property_types_api(request: Request):
def list_property_types_api(request: Request):
"""API: list all available property types."""
from app.services.property_types import PROPERTY_TYPES
return {"types": PROPERTY_TYPES}
@router.get("/{collection_id}/properties/api")
async def list_properties_api(request: Request, collection_id: int):
def list_properties_api(request: Request, collection_id: int):
"""API: list all properties visible to the current user."""
user = _session_user(request)
_require_view(collection_id, user)
@@ -720,7 +720,7 @@ async def list_properties_api(request: Request, collection_id: int):
@router.get("/{collection_id}/members/api")
async def list_collection_members_api(request: Request, collection_id: int):
def list_collection_members_api(request: Request, collection_id: int):
"""API: list workspace members available for a ``person`` property.
Resolves the collection's workspace and returns its members (falling back to
@@ -753,7 +753,7 @@ async def list_collection_members_api(request: Request, collection_id: int):
@router.get("/{collection_id}/calendar/api")
async def collection_calendar_api(request: Request, collection_id: int,
def collection_calendar_api(request: Request, collection_id: int,
start: str = "", end: str = "",
date_property: str = ""):
"""API (v5.8.0): expanded calendar events for a window [start, end].
@@ -838,7 +838,7 @@ async def collection_calendar_api(request: Request, collection_id: int,
@router.get("/timezones/api")
async def timezones_api(request: Request):
def timezones_api(request: Request):
"""API (v5.8.0): the user's timezone plus a picker-friendly zone list."""
user = _current_user(request)
with get_conn() as conn:
@@ -973,7 +973,7 @@ async def update_property_api(request: Request, prop_id: int):
@router.delete("/properties/{prop_id}/api")
async def delete_property_api(request: Request, prop_id: int):
def delete_property_api(request: Request, prop_id: int):
"""API: delete a property."""
with get_conn() as conn:
existing = conn.execute(
@@ -1167,7 +1167,7 @@ async def evaluate_formula(request: Request):
@router.get("/views/{view_id}/api")
async def get_view_api(request: Request, view_id: int):
def get_view_api(request: Request, view_id: int):
"""API: get a single view config."""
with get_conn() as conn:
row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
@@ -1252,7 +1252,7 @@ async def save_view_as(request: Request, collection_id: int):
@router.get("/{collection_id}/views/api")
async def list_views_api(request: Request, collection_id: int):
def list_views_api(request: Request, collection_id: int):
"""API: list views for a collection visible to the current user.
Shared/legacy views (``created_by IS NULL``) are visible to everyone;
@@ -1277,7 +1277,7 @@ async def list_views_api(request: Request, collection_id: int):
@router.delete("/views/{view_id}/api")
async def delete_view_api(request: Request, view_id: int):
def delete_view_api(request: Request, view_id: int):
"""API: delete a saved view."""
with get_conn() as conn:
existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
@@ -1333,7 +1333,7 @@ async def duplicate_view_api(request: Request, view_id: int):
@router.get("/{collection_id}/pages/{page_id}/sub-items")
async def list_sub_items(request: Request, collection_id: int, page_id: int):
def list_sub_items(request: Request, collection_id: int, page_id: int):
"""API: list sub-items of a page."""
with get_conn() as conn:
rows = conn.execute(
@@ -1396,7 +1396,7 @@ async def create_sub_item(request: Request, collection_id: int, page_id: int):
@router.get("/{collection_id}/pages/{page_id}/status-aggregate")
async def aggregate_child_status(request: Request, collection_id: int, page_id: int):
def aggregate_child_status(request: Request, collection_id: int, page_id: int):
"""API: compute aggregate status from children."""
with get_conn() as conn:
children = conn.execute(
@@ -1495,7 +1495,7 @@ async def check_dependencies(request: Request, collection_id: int, page_id: int)
@router.get("/{collection_id}/sources/api")
async def list_data_sources(request: Request, collection_id: int):
def list_data_sources(request: Request, collection_id: int):
"""API: list all data sources for a collection."""
with get_conn() as conn:
coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone()
@@ -1555,7 +1555,7 @@ async def add_data_source(request: Request, collection_id: int):
@router.delete("/{collection_id}/sources/{source_id}/api")
async def remove_data_source(request: Request, collection_id: int, source_id: int):
def remove_data_source(request: Request, collection_id: int, source_id: int):
"""API: remove a data source from a collection."""
with get_conn() as conn:
existing = conn.execute(
@@ -1668,7 +1668,7 @@ async def create_linked_database(request: Request, collection_id: int):
@router.post("/{collection_id}/toggle-inline/api")
async def toggle_inline(request: Request, collection_id: int):
def toggle_inline(request: Request, collection_id: int):
"""API: toggle a collection between full-page and inline mode."""
with get_conn() as conn:
coll = conn.execute(
@@ -1759,7 +1759,7 @@ async def create_inline_database(request: Request):
@router.put("/{collection_id}/toggle-task/api")
async def toggle_task(request: Request, collection_id: int):
def toggle_task(request: Request, collection_id: int):
"""API: toggle is_task flag on a collection (Turn into Tasks)."""
with get_conn() as conn:
coll = conn.execute("SELECT id, is_task FROM collections WHERE id=?", (collection_id,)).fetchone()
@@ -1772,7 +1772,7 @@ async def toggle_task(request: Request, collection_id: int):
@router.get("/{collection_id}/pages/{page_id}/dependencies/api")
async def list_page_dependencies(request: Request, collection_id: int, page_id: int):
def list_page_dependencies(request: Request, collection_id: int, page_id: int):
"""API: list dependencies for a page (blocks, blocked_by, related)."""
with get_conn() as conn:
rows = conn.execute(
@@ -1820,7 +1820,7 @@ async def add_page_dependency(request: Request, collection_id: int, page_id: int
@router.delete("/{collection_id}/pages/{page_id}/dependencies/{dep_id}/api")
async def remove_page_dependency(request: Request, collection_id: int, page_id: int, dep_id: int):
def remove_page_dependency(request: Request, collection_id: int, page_id: int, dep_id: int):
"""API: remove a dependency."""
with get_conn() as conn:
existing = conn.execute(
@@ -1915,7 +1915,7 @@ async def auto_shift_dates(request: Request, collection_id: int, page_id: int):
@router.get("/{collection_id}/dashboards/{dashboard_id}", response_class=HTMLResponse)
async def view_dashboard(request: Request, collection_id: int, dashboard_id: int):
def view_dashboard(request: Request, collection_id: int, dashboard_id: int):
"""v7.3.0: render a collection dashboard grid (multi-DB widgets).
Widgets live in ``collection_dashboards.layout_json`` as
@@ -1981,7 +1981,7 @@ async def view_dashboard(request: Request, collection_id: int, dashboard_id: int
@router.get("/{collection_id}", response_class=HTMLResponse)
@router.get("/{collection_id}/view/{view_type}", response_class=HTMLResponse)
async def view_collection(request: Request, collection_id: int, view_type: str = "table"):
def view_collection(request: Request, collection_id: int, view_type: str = "table"):
"""Main view — renders collection in the requested view type."""
# v6.0.0: granular collection permissions — hide restricted collections.
_require_view(collection_id, _session_user(request))
@@ -2618,7 +2618,7 @@ tr:hover td{{background:#222}}
@router.get("/{collection_id}/api")
async def get_collection_api(request: Request, collection_id: int):
def get_collection_api(request: Request, collection_id: int):
"""API: get a single collection with its pages."""
# v6.0.0: granular collection permissions — hide restricted collections.
_require_view(collection_id, _session_user(request))