fix: A25 + A21 (partiel) — plus d'exception muque, transaction protégée (v7.3.8)
FlowDeck CI / lint (push) Successful in 1m51s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 10m12s

- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
  (19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
  api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
  sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
  `create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
  transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
  qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
  unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
  restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
This commit is contained in:
2026-10-01 08:16:42 -04:00
parent 3ad2605c9e
commit ffa1fa89ab
26 changed files with 151 additions and 100 deletions
+37 -42
View File
@@ -165,7 +165,7 @@ async def get_me(request: Request, authorization: str | None = Header(default=No
try:
d[k] = json.loads(d[k] or "{}")
except Exception:
pass
logger.exception("get_me")
# never expose secrets
return d
@@ -261,7 +261,7 @@ async def create_workspace(request: Request, authorization: str | None = Header(
try:
conn.execute("INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'owner')", (wid, user["id"]))
except Exception:
pass
logger.exception("create_workspace")
conn.commit()
row = conn.execute("SELECT * FROM workspaces WHERE id=?", (wid,)).fetchone()
audit_log(user, "workspace.create", "workspace", wid, name, request)
@@ -497,17 +497,13 @@ async def create_collection_v2(request: Request, authorization: str | None = Hea
with get_conn() as conn:
cur = conn.execute("INSERT INTO collections (name, description, icon, schema_json, workspace_id, created_by) VALUES (?, ?, ?, ?, ?, ?)", (name, description, icon, schema_json, workspace_id, user["id"]))
cid = cur.lastrowid
# materialize properties if schema provided
try:
from app.services.db_templates import materialize_properties
materialize_properties(conn, cid, schema)
except Exception:
pass
# materialize properties if schema provided — A25 : PAS de try ici,
# une exception doit interrompre la transaction (sinon la collection est
# commitée sans son schéma et l'erreur disparaît).
from app.services.db_templates import materialize_properties
materialize_properties(conn, cid, schema)
# default view
try:
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json) VALUES (?, ?, ?, ?)", (cid, "Default View", "table", json.dumps({"visible_properties": ["Title"]})))
except Exception:
pass
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json) VALUES (?,?,?,?)", (cid, "Default View", "table", json.dumps({"visible_properties": ["Title"]})))
conn.commit()
row = conn.execute("SELECT * FROM collections WHERE id=?", (cid,)).fetchone()
audit_log(user, "collection.create", "collection", cid, name, request)
@@ -593,20 +589,20 @@ async def create_linked_db(collection_id: int, request: Request, authorization:
try:
conn.execute("INSERT INTO collection_data_sources (collection_id, source_collection_id, is_linked) VALUES (?, ?, 1)", (nid, collection_id))
except Exception:
pass
logger.exception("create_linked_db")
# copy views + properties (light)
rows = conn.execute("SELECT * FROM collection_properties WHERE collection_id=?", (collection_id,)).fetchall()
for p in rows:
try:
conn.execute("INSERT INTO collection_properties (collection_id, name, prop_type, options_json, position) VALUES (?, ?, ?, ?, ?)", (nid, p["name"], p["prop_type"], p["options_json"], p["position"]))
except Exception:
pass
logger.exception("create_linked_db")
vrows = conn.execute("SELECT * FROM collection_views WHERE collection_id=?", (collection_id,)).fetchall()
for v in vrows:
try:
conn.execute("INSERT INTO collection_views (collection_id, name, view_type, config_json, position) VALUES (?, ?, ?, ?, ?)", (nid, v["name"], v["view_type"], v["config_json"], v["position"]))
except Exception:
pass
logger.exception("create_linked_db")
conn.commit()
audit_log(user, "collection.linked", "collection", nid, f"src={collection_id}", request)
return {"id": nid, "name": name, "status": "created"}
@@ -766,7 +762,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
pass
# light validation: we rely on existing validators
except Exception:
pass
logger.exception("create_collection_page_v2")
max_pos = conn.execute("SELECT COALESCE(MAX(position), -1)+1 FROM collection_pages WHERE collection_id=?", (collection_id,)).fetchone()[0]
# apply auto props
try:
@@ -774,7 +770,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
from app.services.property_types import apply_auto_properties as _aap
_aap(props_list, prop_vals, user, is_create=True)
except Exception:
pass
logger.exception("create_collection_page_v2")
cur = conn.execute("INSERT INTO collection_pages (collection_id, title, icon, position, parent_id, property_values_json) VALUES (?, ?, ?, ?, ?, ?)", (collection_id, title, icon, max_pos, parent_id, json.dumps(prop_vals)))
pid = cur.lastrowid
conn.commit()
@@ -783,7 +779,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author
try:
await _fire_event("collection.page.created", {"page_id": pid, "collection_id": collection_id, "title": title})
except Exception:
pass
logger.exception("create_collection_page_v2")
data = {"id": pid, "title": title, "status": "created", "page": row_to_dict(row)}
key = (request.headers.get("Idempotency-Key") or "").strip()
if key:
@@ -857,14 +853,14 @@ async def patch_page_v2(page_id: int, request: Request, authorization: str | Non
from app.services.property_types import apply_auto_properties as _aap
_aap(props_list, stored, user, is_create=False)
except Exception:
pass
logger.exception("patch_page_v2")
conn.execute("UPDATE collection_pages SET title=?, icon=?, position=?, parent_id=?, property_values_json=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (title, icon, pos, parent_id, json.dumps(stored), page_id))
conn.commit()
audit_log(user, "page.update", "collection_page", page_id, "", request)
try:
await _fire_event("collection.page.updated", {"page_id": page_id, "collection_id": row["collection_id"], "title": title})
except Exception:
pass
logger.exception("patch_page_v2")
return {"id": page_id, "status": "updated"}
@router.delete("/pages/{page_id}")
@@ -883,7 +879,7 @@ async def delete_page_v2(page_id: int, request: Request, authorization: str | No
try:
await _fire_event("collection.page.deleted", {"page_id": page_id, "collection_id": row["collection_id"]})
except Exception:
pass
logger.exception("delete_page_v2")
return {"id": page_id, "status": "deleted"}
@router.post("/pages/{page_id}/restore")
@@ -901,7 +897,7 @@ async def restore_page_v2(page_id: int, request: Request, authorization: str | N
try:
await _fire_event("page.restored", {"page_id": page_id})
except Exception:
pass
logger.exception("restore_page_v2")
return {"id": page_id, "status": "restored"}
raise HTTPException(404, "Page not found or not deleted")
@@ -1109,7 +1105,7 @@ async def create_relation_v2(prop_id: int, request: Request, authorization: str
try:
conn.execute("INSERT INTO collection_properties (collection_id, name, prop_type, related_collection_id, reverse_name, position) VALUES (?, ?, 'relation', ?, ?, ?)", (related_id, reverse, row["collection_id"], "", max_pos))
except Exception:
pass
logger.exception("create_relation_v2")
conn.commit()
return {"id": prop_id, "status": "updated"}
@@ -1184,7 +1180,7 @@ async def create_view_v2(collection_id: int, request: Request, authorization: st
try:
await _fire_event("collection.view.created", {"view_id": vid, "collection_id": collection_id, "name": name, "view_type": vtype})
except Exception:
pass
logger.exception("create_view_v2")
return {"id": vid, "name": name, "view_type": vtype, "status": "created"}
@router.patch("/views/{view_id}")
@@ -1208,7 +1204,7 @@ async def patch_view_v2(view_id: int, request: Request, authorization: str | Non
try:
cfg.update(json.loads(body["config_json"]) if isinstance(body["config_json"], str) else body["config_json"])
except Exception:
pass
logger.exception("patch_view_v2")
# also flat keys
for k in ("group_by", "sub_group_by", "wip_limits", "card_size", "cover_property", "cover_mode", "card_properties", "visible_properties", "filters", "sorts", "date_property"):
if k in body:
@@ -1350,7 +1346,7 @@ async def create_comment_v2(page_id: int, request: Request, authorization: str |
try:
await _fire_event("comment.added", {"comment_id": nid, "page_id": page_id, "user_id": user["id"]})
except Exception:
pass
logger.exception("create_comment_v2")
return {"id": nid, "status": "created", "comment": row_to_dict(row)}
@router.patch("/comments/{comment_id}")
@@ -1378,7 +1374,7 @@ async def patch_comment_v2(comment_id: int, request: Request, authorization: str
try:
await _fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]})
except Exception:
pass
logger.exception("patch_comment_v2")
return {"id": comment_id, "status": "updated"}
@router.delete("/comments/{comment_id}")
@@ -1419,13 +1415,13 @@ async def create_mention_v2(page_id: int, request: Request, authorization: str |
conn.execute("INSERT INTO notifications (user_id, actor_id, ntype, title, message, resource_type, resource_id, url) VALUES (?, ?, 'mention', 'You were mentioned', ?, 'page', ?, ?)", (uid, user["id"], body.get("message") or f"Mentioned in page {page_id}", page_id, f"/pages/{page_id}"))
created += 1
except Exception:
pass
logger.exception("create_mention_v2")
conn.commit()
if created:
try:
await _fire_event("mention.added", {"page_id": page_id, "user_ids": [u for u in targets if isinstance(u, int)], "count": created})
except Exception:
pass
logger.exception("create_mention_v2")
return {"mentions": created, "status": "created"}
# ── Notifications ─────────────────────────────────────────────────────────
@@ -1522,7 +1518,7 @@ async def add_favorite_v2(request: Request, authorization: str | None = Header(d
try:
await _fire_event("favorite.added", {"page_id": pid, "user_id": user["id"]})
except Exception:
pass
logger.exception("add_favorite_v2")
return {"page_id": pid, "status": "added"}
@router.delete("/favorites/{page_id}")
@@ -1537,7 +1533,7 @@ async def remove_favorite_v2(page_id: int, request: Request, authorization: str
try:
await _fire_event("favorite.removed", {"page_id": page_id, "user_id": user["id"]})
except Exception:
pass
logger.exception("remove_favorite_v2")
return {"page_id": page_id, "status": "removed"}
@router.get("/tags")
@@ -1687,7 +1683,7 @@ async def create_share_v2(page_id: int, request: Request, authorization: str | N
try:
await _fire_event("page.shared", {"page_id": page_id, "share_id": nid, "permission": perm})
except Exception:
pass
logger.exception("create_share_v2")
return {"id": nid, "page_id": page_id, "status": "shared"}
@router.patch("/shares/{share_id}")
@@ -1748,7 +1744,7 @@ async def publish_page_v2(page_id: int, request: Request, authorization: str | N
try:
await _fire_event("page.published", {"page_id": page_id, "slug": slug})
except Exception:
pass
logger.exception("publish_page_v2")
return {"page_id": page_id, "slug": slug, "url": f"/p/{slug}", "status": "published"}
@router.delete("/pages/{page_id}/publish")
@@ -1763,7 +1759,7 @@ async def unpublish_page_v2(page_id: int, request: Request, authorization: str |
try:
await _fire_event("page.unpublished", {"page_id": page_id})
except Exception:
pass
logger.exception("unpublish_page_v2")
return {"page_id": page_id, "status": "unpublished"}
# ── History ───────────────────────────────────────────────────────────────
@@ -1844,7 +1840,7 @@ async def create_sprint_v2(collection_id: int, request: Request, authorization:
try:
await _fire_event("sprint.created", {"sprint_id": sid, "collection_id": collection_id, "name": name})
except Exception:
pass
logger.exception("create_sprint_v2")
return {"id": sid, "name": name, "status": "created"}
@router.patch("/sprints/{sprint_id}")
@@ -1871,7 +1867,7 @@ async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str |
try:
await _fire_event("sprint.updated", {"sprint_id": sprint_id, "collection_id": row["collection_id"], "name": name, "status": status})
except Exception:
pass
logger.exception("patch_sprint_v2")
return {"id": sprint_id, "status": "updated"}
@router.delete("/sprints/{sprint_id}")
@@ -1937,7 +1933,7 @@ async def burndown_v2(sprint_id: int, request: Request, authorization: str | Non
completed += 1
break
except Exception:
pass
logger.exception("burndown_v2")
remaining = total - completed
# ideal linear
ideal = [round(total * (1 - i / 10)) for i in range(11)]
@@ -2047,11 +2043,10 @@ async def apply_db_template_v2(template_id: int, request: Request, authorization
schema = json.loads(tpl["schema_json"] or "[]")
cur = conn.execute("INSERT INTO collections (name, description, icon, schema_json, workspace_id, created_by) VALUES (?, ?, ?, ?, ?, ?)", (name, tpl["description"], tpl["icon"] if "icon" in tpl.keys() else "📋", json.dumps(schema), body.get("workspace_id"), user["id"]))
cid = cur.lastrowid
try:
from app.services.db_templates import materialize_properties
materialize_properties(conn, cid, schema)
except Exception:
pass
# A25 : pas de try — un échec de matérialisation doit interrompre la
# transaction plutôt que de commiter une collection sans schéma.
from app.services.db_templates import materialize_properties
materialize_properties(conn, cid, schema)
conn.commit()
return {"collection_id": cid, "name": name, "status": "created"}