fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m23s
FlowDeck CI / docker (push) Canceled after 0s

4 passes (283 → 93 routes async sur 667 = 86 % hors loop, avant 61 %) :

A. RACINE AUTH — `get_current_user` (auth/session.py) était `async def`
   SANS aucun await (cookie decode = synchrone) ; idem ses clones :
   `agent._current_user_id/_workspace_id/_current_admin` (34 sites) et
   `sso._require_admin` (corps 0 await, 6 sites) → `def` +
   47 `await` supprimés. Piège : 3 call sites passaient par l'alias `gcu`
   (grep littéral aveugle) — 8 tests en échec → corrigés.

B. Re-scan : 19 routes devenues SANS await → `def` (agent 8, sso 5,
   web_clipper 3, projects 2, auth 1…).

C/D. 155 routes dont les seuls awaits = `request.json()` / événements :
   - try/except `body = {}` → `Body(default={})` (même tolérance)
   - try/except `raise HTTPException(400)` → `Body(...)` REQUIS
     (422 FastAPI — aucun test ne couvrait le 400)
   - forme conditionnelle `request.json() if content-type else {}`
     (54 sites) → défaut `{}` (sans corps = `{}` dans les 2 cas)
   - `await fire_*` → `run_event_sync(...)` ; imports `Body` /
     `run_event_sync` ajoutés aux routers convertis

Reste async (93, justifié) : form/upload/file (22), réseau gitea/llm/oidc,
`_json_body` (9), 2 JSON inline en argument, 1 fallback logique
(capture_frontend_error), 1 lecture conditionnelle (web_clipper), mixtes.

suite **1089/1089** · ruff OK · docs à jour
This commit is contained in:
2026-10-01 22:17:48 -04:00
parent 8d0d69e7b8
commit 45e59009c3
34 changed files with 2479 additions and 680 deletions
+52 -140
View File
@@ -6,12 +6,12 @@ import json
import logging
import sqlite3
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import HTMLResponse
from app.auth.session import SessionManager
from app.db import get_conn
from app.services.automations import fire_event
from app.services.automations import fire_event, run_event_sync
from app.services.db_templates import materialize_properties
from app.services.permission_manager import PermissionManager
from app.services.property_types import (
@@ -222,12 +222,8 @@ def list_collections_api(request: Request):
@router.post("/api")
async def create_collection_api(request: Request):
def create_collection_api(request: Request, body: dict = Body(default={})):
"""API: create a new collection, optionally from a database template."""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "").strip()
if not name:
@@ -277,7 +273,7 @@ async def create_collection_api(request: Request):
)
conn.commit()
await fire_event("collection.created", {"collection_id": collection_id, "name": name, "icon": icon})
run_event_sync(fire_event("collection.created", {"collection_id": collection_id, "name": name, "icon": icon}))
return {"id": collection_id, "name": name, "status": "created"}
@@ -285,12 +281,8 @@ async def create_collection_api(request: Request):
@router.put("/api/{collection_id}")
async def update_collection_api(request: Request, collection_id: int):
def update_collection_api(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: update a collection."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute(
@@ -316,12 +308,12 @@ async def update_collection_api(request: Request, collection_id: int):
)
conn.commit()
await fire_event("collection.updated", {"collection_id": collection_id, "name": name})
run_event_sync(fire_event("collection.updated", {"collection_id": collection_id, "name": name}))
return {"id": collection_id, "status": "updated"}
@router.delete("/api/{collection_id}")
async def delete_collection_api(request: Request, collection_id: int):
def delete_collection_api(request: Request, collection_id: int):
"""API: delete a collection and its pages (CASCADE)."""
# v6.0.0: granular collection permissions — owner/admin only.
user = _session_user(request)
@@ -340,13 +332,13 @@ async def delete_collection_api(request: Request, collection_id: int):
conn.execute("DELETE FROM collections WHERE id=?", (collection_id,))
conn.commit()
await fire_event("collection.deleted", {"collection_id": collection_id,
"name": existing["name"] if existing else ""})
run_event_sync(fire_event("collection.deleted", {"collection_id": collection_id,
"name": existing["name"] if existing else ""}))
return {"id": collection_id, "status": "deleted"}
@router.post("/{collection_id}/duplicate")
async def duplicate_collection_api(request: Request, collection_id: int):
def duplicate_collection_api(request: Request, collection_id: int):
"""v5.4.0: deep-duplicate a database (views + properties + pages + data
sources) into a new collection named '<original> (copy)'."""
with get_conn() as conn:
@@ -494,7 +486,7 @@ async def duplicate_collection_api(request: Request, collection_id: int):
conn.commit()
await fire_event("collection.created", {"collection_id": new_id, "name": new_name})
run_event_sync(fire_event("collection.created", {"collection_id": new_id, "name": new_name}))
return {"id": new_id, "name": new_name, "status": "duplicated"}
@@ -542,12 +534,8 @@ def open_row_page_api(request: Request, page_id: int):
@router.put("/pages/{page_id}/api")
async def update_page_api(request: Request, page_id: int):
def update_page_api(request: Request, page_id: int, body: dict = Body(default={})):
"""API: update a page's properties."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute(
@@ -599,18 +587,18 @@ async def update_page_api(request: Request, page_id: int):
sync_row_title_to_page(conn, page_id)
conn.commit()
await fire_event("page.updated", {
run_event_sync(fire_event("page.updated", {
"page_id": page_id,
"collection_id": existing["collection_id"],
"title": title,
"icon": icon,
"properties": props,
})
await fire_event("collection.page.updated", {
}))
run_event_sync(fire_event("collection.page.updated", {
"page_id": page_id,
"collection_id": existing["collection_id"],
"title": title,
})
}))
# Notify newly assigned people (person properties) — v5.8.0.
from app.services.notifications import notify_assignment
user = _current_user(request)
@@ -620,7 +608,7 @@ async def update_page_api(request: Request, page_id: int):
@router.delete("/pages/{page_id}/api")
async def delete_page_api(request: Request, page_id: int):
def delete_page_api(request: Request, page_id: int):
"""API: delete a page from its collection."""
with get_conn() as conn:
existing = conn.execute(
@@ -634,15 +622,15 @@ async def delete_page_api(request: Request, page_id: int):
conn.execute("DELETE FROM collection_pages WHERE id=?", (page_id,))
conn.commit()
await fire_event("page.deleted", {
run_event_sync(fire_event("page.deleted", {
"page_id": page_id,
"collection_id": existing["collection_id"],
"title": existing["title"],
})
await fire_event("collection.page.deleted", {
}))
run_event_sync(fire_event("collection.page.deleted", {
"page_id": page_id,
"collection_id": existing["collection_id"],
})
}))
return {"id": page_id, "status": "deleted"}
@@ -851,16 +839,12 @@ def timezones_api(request: Request):
@router.post("/{collection_id}/property-groups/api")
async def set_property_groups_api(request: Request, collection_id: int):
def set_property_groups_api(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: (re)assign properties to collapsible groups in the table header.
Body: ``{"groups": [{"name": "Basics", "property_ids": [1, 2]}]}``. Properties
omitted from any group have their group cleared. Empty group names clear.
"""
try:
body = await request.json()
except Exception:
body = {}
groups = body.get("groups", [])
with get_conn() as conn:
@@ -887,12 +871,8 @@ async def set_property_groups_api(request: Request, collection_id: int):
@router.post("/{collection_id}/properties/api")
async def create_property_api(request: Request, collection_id: int):
def create_property_api(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: create a new property on a collection."""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "").strip()
if not name:
@@ -934,12 +914,8 @@ async def create_property_api(request: Request, collection_id: int):
@router.put("/properties/{prop_id}/api")
async def update_property_api(request: Request, prop_id: int):
def update_property_api(request: Request, prop_id: int, body: dict = Body(default={})):
"""API: update a property."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute(
@@ -991,12 +967,8 @@ def delete_property_api(request: Request, prop_id: int):
@router.post("/{collection_id}/properties/relation")
async def create_relation_property(request: Request, collection_id: int):
def create_relation_property(request: Request, collection_id: int, body: dict = Body(default={})):
"""Create a relation property between two collections."""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "").strip()
related_collection_id = body.get("related_collection_id")
@@ -1043,12 +1015,8 @@ async def create_relation_property(request: Request, collection_id: int):
@router.post("/{collection_id}/properties/relation/link")
async def link_pages(request: Request, collection_id: int):
def link_pages(request: Request, collection_id: int, body: dict = Body(default={})):
"""Link two pages via a relation property."""
try:
body = await request.json()
except Exception:
body = {}
property_id = body.get("property_id")
source_page_id = body.get("source_page_id")
@@ -1117,12 +1085,8 @@ async def link_pages(request: Request, collection_id: int):
@router.post("/rollup/compute")
async def compute_rollup(request: Request):
def compute_rollup(request: Request, body: dict = Body(default={})):
"""Compute a rollup aggregation."""
try:
body = await request.json()
except Exception:
body = {}
collection_id = body.get("collection_id")
relation_property_id = body.get("relation_property_id")
@@ -1143,12 +1107,8 @@ async def compute_rollup(request: Request):
@router.post("/formula/evaluate")
async def evaluate_formula(request: Request):
def evaluate_formula(request: Request, body: dict = Body(default={})):
"""Evaluate a formula expression."""
try:
body = await request.json()
except Exception:
body = {}
expression = body.get("expression", "")
context = body.get("context", {})
@@ -1177,12 +1137,8 @@ def get_view_api(request: Request, view_id: int):
@router.put("/views/{view_id}/config")
async def update_view_config(request: Request, view_id: int):
def update_view_config(request: Request, view_id: int, body: dict = Body(default={})):
"""API: update view configuration (group_by, card_size, visible_properties, etc.)."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
@@ -1209,12 +1165,8 @@ async def update_view_config(request: Request, view_id: int):
@router.post("/{collection_id}/views/save-as")
async def save_view_as(request: Request, collection_id: int):
def save_view_as(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: save current view state as a new named view."""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "New View")
config = body.get("config", {})
@@ -1241,12 +1193,12 @@ async def save_view_as(request: Request, collection_id: int):
conn.commit()
new_view_id = cur.lastrowid
await fire_event("collection.view.created", {
run_event_sync(fire_event("collection.view.created", {
"view_id": new_view_id,
"collection_id": collection_id,
"name": name,
"view_type": view_type,
})
}))
return {"id": new_view_id, "name": name, "view_type": view_type,
"config_json": json.dumps(config), "created_by": user_id, "status": "saved"}
@@ -1289,12 +1241,8 @@ def delete_view_api(request: Request, view_id: int):
@router.post("/views/{view_id}/duplicate")
async def duplicate_view_api(request: Request, view_id: int):
def duplicate_view_api(request: Request, view_id: int, body: dict = Body(default={})):
"""API: duplicate a view (config + type), owned by the current user."""
try:
body = await request.json()
except Exception:
body = {}
with get_conn() as conn:
existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone()
@@ -1319,12 +1267,12 @@ async def duplicate_view_api(request: Request, view_id: int):
conn.commit()
dup_view_id = cur.lastrowid
await fire_event("collection.view.created", {
run_event_sync(fire_event("collection.view.created", {
"view_id": dup_view_id,
"collection_id": existing["collection_id"],
"name": name,
"view_type": existing["view_type"],
})
}))
return {"id": dup_view_id, "name": name, "view_type": existing["view_type"],
"status": "duplicated"}
@@ -1344,12 +1292,8 @@ def list_sub_items(request: Request, collection_id: int, page_id: int):
@router.post("/{collection_id}/pages/{page_id}/sub-items")
async def create_sub_item(request: Request, collection_id: int, page_id: int):
def create_sub_item(request: Request, collection_id: int, page_id: int, body: dict = Body(default={})):
"""API: create a sub-item under a page."""
try:
body = await request.json()
except Exception:
body = {}
title = body.get("title", "New sub-item").strip()
if not title:
@@ -1380,18 +1324,18 @@ async def create_sub_item(request: Request, collection_id: int, page_id: int):
conn.commit()
new_id = cur.lastrowid
await fire_event("page.created", {
run_event_sync(fire_event("page.created", {
"page_id": new_id,
"collection_id": collection_id,
"parent_id": page_id,
"title": title,
"properties": body.get("properties", {}),
})
await fire_event("collection.page.created", {
}))
run_event_sync(fire_event("collection.page.created", {
"page_id": new_id,
"collection_id": collection_id,
"title": title,
})
}))
return {"id": new_id, "title": title, "parent_id": page_id, "status": "created"}
@@ -1420,12 +1364,8 @@ def aggregate_child_status(request: Request, collection_id: int, page_id: int):
@router.post("/{collection_id}/pages/{page_id}/dependencies")
async def set_dependencies(request: Request, collection_id: int, page_id: int):
def set_dependencies(request: Request, collection_id: int, page_id: int, body: dict = Body(default={})):
"""API: set blocking dependencies for a page (stored as 'blocks' property)."""
try:
body = await request.json()
except Exception:
body = {}
blocks_ids = body.get("blocks", [])
@@ -1447,12 +1387,8 @@ async def set_dependencies(request: Request, collection_id: int, page_id: int):
@router.post("/{collection_id}/pages/{page_id}/check-deps")
async def check_dependencies(request: Request, collection_id: int, page_id: int):
def check_dependencies(request: Request, collection_id: int, page_id: int, body: dict = Body(default={})):
"""API: check if a page can transition to a new status."""
try:
body = await request.json()
except Exception:
body = {}
body.get("new_status", "Done")
@@ -1510,12 +1446,8 @@ def list_data_sources(request: Request, collection_id: int):
@router.post("/{collection_id}/sources/api")
async def add_data_source(request: Request, collection_id: int):
def add_data_source(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: add a data source to a collection."""
try:
body = await request.json()
except Exception:
body = {}
source_collection_id = body.get("source_collection_id")
if not source_collection_id:
@@ -1572,15 +1504,11 @@ def remove_data_source(request: Request, collection_id: int, source_id: int):
@router.post("/{collection_id}/linked/api")
async def create_linked_database(request: Request, collection_id: int):
def create_linked_database(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: create a linked database view from a source collection.
A linked database copies the structure (views, filters, sorts) of a source
but shares the same pages — edits to pages propagate to the source.
"""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "").strip()
body.get("workspace_id")
@@ -1693,12 +1621,8 @@ def toggle_inline(request: Request, collection_id: int):
@router.post("/inline/api")
async def create_inline_database(request: Request):
def create_inline_database(request: Request, body: dict = Body(default={})):
"""API: create an inline database within a parent page (optionally from a template)."""
try:
body = await request.json()
except Exception:
body = {}
name = body.get("name", "").strip()
if not name:
@@ -1792,12 +1716,8 @@ def list_page_dependencies(request: Request, collection_id: int, page_id: int):
@router.post("/{collection_id}/pages/{page_id}/dependencies/api")
async def add_page_dependency(request: Request, collection_id: int, page_id: int):
def add_page_dependency(request: Request, collection_id: int, page_id: int, body: dict = Body(default={})):
"""API: add a dependency (blocks/blocked_by/related) between two pages."""
try:
body = await request.json()
except Exception:
body = {}
dependency_id = body.get("dependency_id")
if not dependency_id:
raise HTTPException(status_code=400, detail="dependency_id is required")
@@ -1834,15 +1754,11 @@ def remove_page_dependency(request: Request, collection_id: int, page_id: int, d
@router.post("/{collection_id}/pages/{page_id}/auto-shift/api")
async def auto_shift_dates(request: Request, collection_id: int, page_id: int):
def auto_shift_dates(request: Request, collection_id: int, page_id: int, body: dict = Body(default={})):
"""API: auto-shift dates based on blocking dependencies."""
from datetime import date as dt_date
from datetime import timedelta
try:
body = await request.json()
except Exception:
body = {}
skip_weekends = body.get("skip_weekends", False)
with get_conn() as conn:
@@ -2646,15 +2562,11 @@ def get_collection_api(request: Request, collection_id: int):
@router.post("/{collection_id}/pages/api")
async def create_page_api(request: Request, collection_id: int):
def create_page_api(request: Request, collection_id: int, body: dict = Body(default={})):
"""API: create a page in a collection."""
# v6.0.0: granular collection permissions — viewer/commenter cannot create.
_require_view(collection_id, _session_user(request))
_require_edit(collection_id, _session_user(request))
try:
body = await request.json()
except Exception:
body = {}
title = body.get("title", "").strip()
if not title:
@@ -2695,16 +2607,16 @@ async def create_page_api(request: Request, collection_id: int):
conn.commit()
page_id = cur.lastrowid
await fire_event("page.created", {
run_event_sync(fire_event("page.created", {
"page_id": page_id,
"collection_id": collection_id,
"title": title,
"icon": icon,
"properties": property_values,
})
await fire_event("collection.page.created", {
}))
run_event_sync(fire_event("collection.page.created", {
"page_id": page_id,
"collection_id": collection_id,
"title": title,
})
}))
return {"id": page_id, "title": title, "status": "created"}