fix: corriger 33 erreurs mypy (CI bloquant) + lien README (BUG-003, BUG-004)
CI / lint (push) Successful in 1m1s
CI / security (push) Successful in 40s
CI / test (push) Successful in 1m17s
CI / build (push) Successful in 38s
CI / e2e (push) Successful in 10m55s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

BUG-003: annotations de types, gardes None sur get_user(), PdfReader: Any et import PROVIDERS manquant (bug latent main.py:4523). Etape mypy du CI rendue bloquante (etait advisory).

BUG-004: lien README.md -> docs/CONTRIBUTING.md corrige (+ DELIVERY_WORKFLOW.md), arbre projet mis a jour, parite README.fr.md.

Verifie: mypy 0 erreur, ruff OK, pytest 728 passed / 5 skipped, frontend OK, liens md OK.
This commit is contained in:
2026-09-11 14:57:55 -04:00
parent ce23ab38f7
commit 240fd8586e
11 changed files with 52 additions and 29 deletions
+10
View File
@@ -379,6 +379,8 @@ async def mfa_totp_enable(
from .user_store import get_user, update_user
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
secret = user.get("mfa_secret_pending")
if not secret:
raise HTTPException(400, "Aucune configuration MFA en cours. Commencez par /mfa/totp/setup")
@@ -415,6 +417,8 @@ async def mfa_totp_disable(
from .user_store import get_user, update_user
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
if not user.get("mfa_enabled"):
raise HTTPException(400, "MFA non activé")
@@ -485,6 +489,8 @@ async def mfa_webauthn_register(
from .webauthn_mfa import complete_registration
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
try:
record = complete_registration(current_user["username"], req.credential,
label=req.label)
@@ -529,6 +535,8 @@ def user_credentials_response(creds: list[dict]) -> list[dict]:
async def mfa_webauthn_list(current_user=Depends(require_auth)):
from .user_store import get_user
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
return {"credentials": user_credentials_response(user.get("webauthn_credentials", []))}
@@ -542,6 +550,8 @@ async def mfa_webauthn_remove(
from .webauthn_mfa import clear_pending
user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
if not verify_password(req.password, user["password_hash"]):
raise HTTPException(400, "Mot de passe incorrect")
+2 -4
View File
@@ -101,8 +101,7 @@ def _resolve_system_prompt(req, current_user) -> str:
mode = _normalize_mode(req.mode)
vault_path: Path | None = None
if mode != "general":
_resolve_vault_path(req.vault, current_user)
vault_path = Path(get_vault_data(req.vault)["path"]) # type: ignore[index]
_, vault_path = _resolve_vault_path(req.vault, current_user)
context = _build_context(mode, vault_path, req.directory, req.context_files)
effective_mode = context.get("scope", mode)
@@ -149,8 +148,7 @@ async def api_bookslm_context(
if mode == "general":
return empty_context("general")
_resolve_vault_path(req.vault, current_user)
vault_path = Path(get_vault_data(req.vault)["path"]) # type: ignore[index]
_, vault_path = _resolve_vault_path(req.vault, current_user)
return _build_context(mode, vault_path, req.directory, req.context_files)
+1 -1
View File
@@ -159,7 +159,7 @@ def _safe_name(name: str) -> str:
def _collect_markdown_files(vault_path: Path) -> list[Path]:
"""List all markdown files in the vault, sorted by relative path."""
vault_path = Path(vault_path)
results = []
results: list[Path] = []
if not vault_path.is_dir():
return results
for p in sorted(vault_path.rglob("*")):
+10 -12
View File
@@ -303,27 +303,27 @@ def _decompress_excalidraw(compressed: str) -> dict[str, Any] | None:
if index > length:
return None
c = _read_bits(num_bits)
if c == 0:
code = _read_bits(num_bits)
if code == 0:
dictionary.append(chr(_read_bits(8)))
dict_size += 1
c = dict_size - 1
code = dict_size - 1
enlarge_in -= 1
elif c == 1:
elif code == 1:
dictionary.append(chr(_read_bits(16)))
dict_size += 1
c = dict_size - 1
code = dict_size - 1
enlarge_in -= 1
elif c == 2:
elif code == 2:
break # end of stream
if enlarge_in == 0:
enlarge_in = 1 << num_bits
num_bits += 1
if c < len(dictionary) and dictionary[c]:
entry = dictionary[c]
elif c == dict_size:
if code < len(dictionary) and dictionary[code]:
entry = dictionary[code]
elif code == dict_size:
entry = w + w[0]
else:
return None
@@ -461,22 +461,20 @@ def _scan_vault(vault_name: str, vault_path: str, vault_cfg: dict[str, Any] | No
modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
# PDF handling — special path (binary, uses pdf_reader)
tags: list[str] = []
if ext == ".pdf":
from backend.pdf_reader import extract_pdf_metadata, extract_pdf_text
raw = extract_pdf_text(fpath, max_chars=100000)
pdf_meta = extract_pdf_metadata(fpath)
title = pdf_meta.get("title") or fpath.stem.replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip()
tags: list[str] = []
elif ext == ".excalidraw" or fpath.name.lower().endswith(".excalidraw.md"):
raw = fpath.read_text(encoding="utf-8", errors="replace")
raw = extract_excalidraw_indexable(raw)
tags: list[str] = []
title = fpath.stem.replace(".excalidraw", "").replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip()
else:
raw = fpath.read_text(encoding="utf-8", errors="replace")
tags: list[str] = []
title = fpath.stem.replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip()
+5 -5
View File
@@ -735,7 +735,7 @@ def _custom_openapi():
return app.openapi_schema
app.openapi = _custom_openapi
app.openapi = _custom_openapi # type: ignore[method-assign]
# GZip compression — reduces bandwidth by ~70% for text responses
# Custom wrapper: skip compression for SSE streams (/api/events)
@@ -2429,7 +2429,7 @@ def _list_backup_files(vault_name: str, relative_path: str) -> list[dict]:
original_name = Path(relative_path).name
prefix = original_name + "."
backups = []
backups: list[dict[str, Any]] = []
try:
dir_entries = list(backup_dir.iterdir())
@@ -3920,7 +3920,7 @@ async def api_vault_recent_files(
if not dir_path.exists() or not dir_path.is_dir():
raise HTTPException(status_code=404, detail=f"Directory not found: {dir}")
files = []
files: list[dict[str, Any]] = []
dir_prefix = (dir or "").strip("/")
try:
@@ -4022,7 +4022,7 @@ async def api_backups_list(
current_user=Depends(require_auth),
):
"""List all backups across vaults, grouped by file."""
result = []
result: list[dict[str, Any]] = []
try:
for vault_name in index:
if vault and vault_name != vault:
@@ -4357,7 +4357,7 @@ async def api_set_config(body: dict = Body(...), current_user=Depends(require_ad
# AI API Keys — stored in data/api_keys.json, fallback to .env
# ---------------------------------------------------------------------------
from backend.ai import _read_ai_keys, get_ai_key
from backend.ai import PROVIDERS, _read_ai_keys, get_ai_key
AI_KEYS_FILE = Path("data/api_keys.json")
+5 -2
View File
@@ -6,6 +6,7 @@ import os
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError as FuturesTimeout
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
@@ -16,7 +17,7 @@ PDF_MAX_SIZE_MB: int = int(os.environ.get("OBSIGATE_PDF_MAX_SIZE_MB", "50"))
PDF_EXTRACT_TIMEOUT: float = float(os.environ.get("OBSIGATE_PDF_EXTRACT_TIMEOUT", "30"))
PDF_READER: str = "pypdf"
PdfReader = None # type: ignore
PdfReader: Any = None
try:
import fitz # pymupdf
PDF_READER = "pymupdf"
@@ -90,7 +91,7 @@ def extract_pdf_metadata(file_path: Path) -> dict:
info["title"] = meta.get("title", "")
info["author"] = meta.get("author", "")
doc.close()
else:
elif PdfReader is not None:
reader = PdfReader(str(file_path))
info["pages"] = len(reader.pages)
meta = reader.metadata or {}
@@ -159,6 +160,8 @@ def _extract_pymupdf(file_path: Path, max_chars: int) -> str:
def _extract_pypdf(file_path: Path, max_chars: int) -> str:
if PdfReader is None:
return ""
reader = PdfReader(str(file_path))
parts = []
total = 0