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
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
run: ruff check backend/ run: ruff check backend/
- name: Mypy (type checker) - name: Mypy (type checker)
run: mypy backend/ --ignore-missing-imports || echo "mypy found type errors (advisory — 28 pre-existing issues)" run: mypy backend/ --ignore-missing-imports
- name: Frontend validation - name: Frontend validation
run: node tests/frontend/validate-imports.mjs run: node tests/frontend/validate-imports.mjs
+9
View File
@@ -34,6 +34,15 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### Corrigé ### Corrigé
- **Typage backend (mypy) — 33 erreurs corrigées** (`backend/main.py`, `indexer.py`,
`auth/router.py`, `pdf_reader.py`, `export.py`, `bookslm_routes.py`) : annotations de types,
gardes `None` sur `get_user()`, `PdfReader: Any` et import `PROVIDERS` manquant dans `main.py`
(bug latent : le modèle par défaut n'était jamais prépendé à la liste des modèles live).
L'étape `mypy` du CI devient **bloquante** (elle était en mode advisory).
- **README** — lien « Contributing » corrigé vers `docs/CONTRIBUTING.md` (était cassé vers la
racine) ; arbre du projet mis à jour.
- **#79 façade `backend/tools/api.py`** — gestion des namespace packages et du `__init__` ignoré - **#79 façade `backend/tools/api.py`** — gestion des namespace packages et du `__init__` ignoré
lors du chargement des modules d'outils. lors du chargement des modules d'outils.
+2 -1
View File
@@ -865,7 +865,8 @@ ObsiGate/
### Contribuer ### Contribuer
Voir [docs/CONTRIBUTING.md](./docs/CONTRIBUTING.md) pour les détails. Voir [docs/CONTRIBUTING.md](./docs/CONTRIBUTING.md) pour les standards de code et
[docs/DELIVERY_WORKFLOW.md](./docs/DELIVERY_WORKFLOW.md) pour la méthode de livraison obligatoire.
--- ---
+3 -2
View File
@@ -1032,12 +1032,13 @@ ObsiGate/
├── Dockerfile # Multi-stage, healthcheck, non-root ├── Dockerfile # Multi-stage, healthcheck, non-root
├── docker-compose.yml # Deployment with healthcheck and auth env vars ├── docker-compose.yml # Deployment with healthcheck and auth env vars
├── build.sh # Automated build & deployment (docker compose build + up) ├── build.sh # Automated build & deployment (docker compose build + up)
└── CONTRIBUTING.md # Contribution guide └── docs/CONTRIBUTING.md # Contribution guide
``` ```
### Contributing ### Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for details. See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for code standards and
[docs/DELIVERY_WORKFLOW.md](docs/DELIVERY_WORKFLOW.md) for the mandatory delivery process.
--- ---
+10
View File
@@ -379,6 +379,8 @@ async def mfa_totp_enable(
from .user_store import get_user, update_user from .user_store import get_user, update_user
user = get_user(current_user["username"]) user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
secret = user.get("mfa_secret_pending") secret = user.get("mfa_secret_pending")
if not secret: if not secret:
raise HTTPException(400, "Aucune configuration MFA en cours. Commencez par /mfa/totp/setup") 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 from .user_store import get_user, update_user
user = get_user(current_user["username"]) user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
if not user.get("mfa_enabled"): if not user.get("mfa_enabled"):
raise HTTPException(400, "MFA non activé") raise HTTPException(400, "MFA non activé")
@@ -485,6 +489,8 @@ async def mfa_webauthn_register(
from .webauthn_mfa import complete_registration from .webauthn_mfa import complete_registration
user = get_user(current_user["username"]) user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
try: try:
record = complete_registration(current_user["username"], req.credential, record = complete_registration(current_user["username"], req.credential,
label=req.label) 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)): async def mfa_webauthn_list(current_user=Depends(require_auth)):
from .user_store import get_user from .user_store import get_user
user = get_user(current_user["username"]) user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
return {"credentials": user_credentials_response(user.get("webauthn_credentials", []))} return {"credentials": user_credentials_response(user.get("webauthn_credentials", []))}
@@ -542,6 +550,8 @@ async def mfa_webauthn_remove(
from .webauthn_mfa import clear_pending from .webauthn_mfa import clear_pending
user = get_user(current_user["username"]) user = get_user(current_user["username"])
if user is None:
raise HTTPException(404, "Utilisateur introuvable")
if not verify_password(req.password, user["password_hash"]): if not verify_password(req.password, user["password_hash"]):
raise HTTPException(400, "Mot de passe incorrect") 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) mode = _normalize_mode(req.mode)
vault_path: Path | None = None vault_path: Path | None = None
if mode != "general": if mode != "general":
_resolve_vault_path(req.vault, current_user) _, vault_path = _resolve_vault_path(req.vault, current_user)
vault_path = Path(get_vault_data(req.vault)["path"]) # type: ignore[index]
context = _build_context(mode, vault_path, req.directory, req.context_files) context = _build_context(mode, vault_path, req.directory, req.context_files)
effective_mode = context.get("scope", mode) effective_mode = context.get("scope", mode)
@@ -149,8 +148,7 @@ async def api_bookslm_context(
if mode == "general": if mode == "general":
return empty_context("general") return empty_context("general")
_resolve_vault_path(req.vault, current_user) _, vault_path = _resolve_vault_path(req.vault, current_user)
vault_path = Path(get_vault_data(req.vault)["path"]) # type: ignore[index]
return _build_context(mode, vault_path, req.directory, req.context_files) 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]: def _collect_markdown_files(vault_path: Path) -> list[Path]:
"""List all markdown files in the vault, sorted by relative path.""" """List all markdown files in the vault, sorted by relative path."""
vault_path = Path(vault_path) vault_path = Path(vault_path)
results = [] results: list[Path] = []
if not vault_path.is_dir(): if not vault_path.is_dir():
return results return results
for p in sorted(vault_path.rglob("*")): 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: if index > length:
return None return None
c = _read_bits(num_bits) code = _read_bits(num_bits)
if c == 0: if code == 0:
dictionary.append(chr(_read_bits(8))) dictionary.append(chr(_read_bits(8)))
dict_size += 1 dict_size += 1
c = dict_size - 1 code = dict_size - 1
enlarge_in -= 1 enlarge_in -= 1
elif c == 1: elif code == 1:
dictionary.append(chr(_read_bits(16))) dictionary.append(chr(_read_bits(16)))
dict_size += 1 dict_size += 1
c = dict_size - 1 code = dict_size - 1
enlarge_in -= 1 enlarge_in -= 1
elif c == 2: elif code == 2:
break # end of stream break # end of stream
if enlarge_in == 0: if enlarge_in == 0:
enlarge_in = 1 << num_bits enlarge_in = 1 << num_bits
num_bits += 1 num_bits += 1
if c < len(dictionary) and dictionary[c]: if code < len(dictionary) and dictionary[code]:
entry = dictionary[c] entry = dictionary[code]
elif c == dict_size: elif code == dict_size:
entry = w + w[0] entry = w + w[0]
else: else:
return None 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() modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat()
# PDF handling — special path (binary, uses pdf_reader) # PDF handling — special path (binary, uses pdf_reader)
tags: list[str] = []
if ext == ".pdf": if ext == ".pdf":
from backend.pdf_reader import extract_pdf_metadata, extract_pdf_text from backend.pdf_reader import extract_pdf_metadata, extract_pdf_text
raw = extract_pdf_text(fpath, max_chars=100000) raw = extract_pdf_text(fpath, max_chars=100000)
pdf_meta = extract_pdf_metadata(fpath) pdf_meta = extract_pdf_metadata(fpath)
title = pdf_meta.get("title") or fpath.stem.replace("-", " ").replace("_", " ") title = pdf_meta.get("title") or fpath.stem.replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip() content_preview = raw[:200].strip()
tags: list[str] = []
elif ext == ".excalidraw" or fpath.name.lower().endswith(".excalidraw.md"): elif ext == ".excalidraw" or fpath.name.lower().endswith(".excalidraw.md"):
raw = fpath.read_text(encoding="utf-8", errors="replace") raw = fpath.read_text(encoding="utf-8", errors="replace")
raw = extract_excalidraw_indexable(raw) raw = extract_excalidraw_indexable(raw)
tags: list[str] = []
title = fpath.stem.replace(".excalidraw", "").replace("-", " ").replace("_", " ") title = fpath.stem.replace(".excalidraw", "").replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip() content_preview = raw[:200].strip()
else: else:
raw = fpath.read_text(encoding="utf-8", errors="replace") raw = fpath.read_text(encoding="utf-8", errors="replace")
tags: list[str] = []
title = fpath.stem.replace("-", " ").replace("_", " ") title = fpath.stem.replace("-", " ").replace("_", " ")
content_preview = raw[:200].strip() content_preview = raw[:200].strip()
+5 -5
View File
@@ -735,7 +735,7 @@ def _custom_openapi():
return app.openapi_schema 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 # GZip compression — reduces bandwidth by ~70% for text responses
# Custom wrapper: skip compression for SSE streams (/api/events) # 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 original_name = Path(relative_path).name
prefix = original_name + "." prefix = original_name + "."
backups = [] backups: list[dict[str, Any]] = []
try: try:
dir_entries = list(backup_dir.iterdir()) 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(): if not dir_path.exists() or not dir_path.is_dir():
raise HTTPException(status_code=404, detail=f"Directory not found: {dir}") raise HTTPException(status_code=404, detail=f"Directory not found: {dir}")
files = [] files: list[dict[str, Any]] = []
dir_prefix = (dir or "").strip("/") dir_prefix = (dir or "").strip("/")
try: try:
@@ -4022,7 +4022,7 @@ async def api_backups_list(
current_user=Depends(require_auth), current_user=Depends(require_auth),
): ):
"""List all backups across vaults, grouped by file.""" """List all backups across vaults, grouped by file."""
result = [] result: list[dict[str, Any]] = []
try: try:
for vault_name in index: for vault_name in index:
if vault and vault_name != vault: 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 # 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") 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 ThreadPoolExecutor
from concurrent.futures import TimeoutError as FuturesTimeout from concurrent.futures import TimeoutError as FuturesTimeout
from pathlib import Path from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__) 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_EXTRACT_TIMEOUT: float = float(os.environ.get("OBSIGATE_PDF_EXTRACT_TIMEOUT", "30"))
PDF_READER: str = "pypdf" PDF_READER: str = "pypdf"
PdfReader = None # type: ignore PdfReader: Any = None
try: try:
import fitz # pymupdf import fitz # pymupdf
PDF_READER = "pymupdf" PDF_READER = "pymupdf"
@@ -90,7 +91,7 @@ def extract_pdf_metadata(file_path: Path) -> dict:
info["title"] = meta.get("title", "") info["title"] = meta.get("title", "")
info["author"] = meta.get("author", "") info["author"] = meta.get("author", "")
doc.close() doc.close()
else: elif PdfReader is not None:
reader = PdfReader(str(file_path)) reader = PdfReader(str(file_path))
info["pages"] = len(reader.pages) info["pages"] = len(reader.pages)
meta = reader.metadata or {} 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: def _extract_pypdf(file_path: Path, max_chars: int) -> str:
if PdfReader is None:
return ""
reader = PdfReader(str(file_path)) reader = PdfReader(str(file_path))
parts = [] parts = []
total = 0 total = 0
+4 -1
View File
@@ -14,7 +14,7 @@
- **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian - **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian
- **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop) - **Stack** : Python 3.11+ (backend FastAPI) · JavaScript/Vanilla (frontend) · Tauri/Rust (desktop)
- **Dernière mise à jour** : *(à tenir à jour à chaque modification)* - **Dernière mise à jour** : 2026-09-11
--- ---
@@ -112,6 +112,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-001* | L'ouverture des fichier PDF ne fonctionne pas et donne l'erreur Internal Server Error | 🟢 corrigé | P1 | fichier PDF | IA | `backend/main.py` | `GET /api/file/{vault}/pdf/stream?path=…(pdf à nom accentué)` | `backend/main.py` : Content-Disposition encodé RFC 5987 (helper `_content_disposition`) | 500 car nom Unicode brut dans l'en-tête → header invalide. Vérifié: stream 200 / Range 206 + test `tests/test_pdf_stream.py` | | *BUG-001* | L'ouverture des fichier PDF ne fonctionne pas et donne l'erreur Internal Server Error | 🟢 corrigé | P1 | fichier PDF | IA | `backend/main.py` | `GET /api/file/{vault}/pdf/stream?path=…(pdf à nom accentué)` | `backend/main.py` : Content-Disposition encodé RFC 5987 (helper `_content_disposition`) | 500 car nom Unicode brut dans l'en-tête → header invalide. Vérifié: stream 200 / Range 206 + test `tests/test_pdf_stream.py` |
| *BUG-002* | l'ouverture d'un fichier .excalidraw ne fonctionne pas et affiche toujours Loading *Excalidraw…* | 🟢 corrigé | P1 | fichier .excalidraw | IA | `frontend/excalidraw-editor.html` | Ouvrir un fichier `.excalidraw` | `frontend/excalidraw-editor.html` : alias esm.sh supprimé (408 jotai) + React 19 cohérent + prop `excalidrawAPI` | 2 causes: 408 esm.sh sur `?alias` + prop legacy `excalidrawRef` inopérante en 0.18. Vérifié navigateur: Loading masqué + cycle save OK | | *BUG-002* | l'ouverture d'un fichier .excalidraw ne fonctionne pas et affiche toujours Loading *Excalidraw…* | 🟢 corrigé | P1 | fichier .excalidraw | IA | `frontend/excalidraw-editor.html` | Ouvrir un fichier `.excalidraw` | `frontend/excalidraw-editor.html` : alias esm.sh supprimé (408 jotai) + React 19 cohérent + prop `excalidrawAPI` | 2 causes: 408 esm.sh sur `?alias` + prop legacy `excalidrawRef` inopérante en 0.18. Vérifié navigateur: Loading masqué + cycle save OK |
| | | | | | | | | | | | | | | | | | | | | |
| *BUG-003* | `mypy` : 33 erreurs de typage (étape CI en mode advisory → bloquante) | 🟢 corrigé | P2 | ⚙️ backend | IA | `backend/{main,indexer,export,pdf_reader,bookslm_routes}.py`, `backend/auth/router.py`, `.gitea/workflows/ci.yml` | `mypy backend/ --ignore-missing-imports` | Annotations de types, gardes `None` (auth/router), import `PROVIDERS` manquant (bug latent `main.py:4523`), `PdfReader: Any` ; CI mypy rendu bloquant | 0 erreur après correction. `PROVIDERS` non importé → `NameError` avalé par le `except` (le modèle par défaut n'était jamais prépendé). Tests : 728 passed |
| *BUG-004* | Lien cassé vers `CONTRIBUTING.md` dans `README.md` | 🟢 corrigé | P3 | 📄 docs | IA | `README.md` | Cliquer le lien « Contributing » | `README.md` : lien → `docs/CONTRIBUTING.md` (+ `DELIVERY_WORKFLOW.md`) ; arbre du projet corrigé | `README.fr.md` pointait déjà correctement vers `./docs/CONTRIBUTING.md` |
### TODOs techniques (améliorations / nouvelles tâches) ### TODOs techniques (améliorations / nouvelles tâches)
@@ -132,6 +134,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|---|---|---|---|---|---| |---|---|---|---|---|---|
| *(exemple)* 2026-06-15 | BUG-001 | Correction | `frontend/app.js` | Réécriture de `renderFile()` pour préserver le DOM dashboard | 🟢 corrigé (en attente vérif) | | *(exemple)* 2026-06-15 | BUG-001 | Correction | `frontend/app.js` | Réécriture de `renderFile()` pour préserver le DOM dashboard | 🟢 corrigé (en attente vérif) |
| 2026-09-09 | BUG-001, BUG-002 | Correction | `backend/main.py`, `frontend/excalidraw-editor.html`, `tests/test_pdf_stream.py` | BUG-001: Content-Disposition RFC 5987 (nom PDF accentué ne casse plus l'en-tête → plus de 500). BUG-002: suppression alias esm.sh (408 jotai) + React 19 cohérent + prop `excalidrawAPI` → Loading masqué, save OK. Vérifié: 534 tests backend verts + E2E navigateur. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-09 | BUG-001, BUG-002 | Correction | `backend/main.py`, `frontend/excalidraw-editor.html`, `tests/test_pdf_stream.py` | BUG-001: Content-Disposition RFC 5987 (nom PDF accentué ne casse plus l'en-tête → plus de 500). BUG-002: suppression alias esm.sh (408 jotai) + React 19 cohérent + prop `excalidrawAPI` → Loading masqué, save OK. Vérifié: 534 tests backend verts + E2E navigateur. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-11 | BUG-003, BUG-004 | Correction | `backend/{main,indexer,export,pdf_reader,bookslm_routes}.py`, `backend/auth/router.py`, `.gitea/workflows/ci.yml`, `README.md`, `README.fr.md` | BUG-003: 33 erreurs mypy corrigées (annotations, gardes `None`, import `PROVIDERS` manquant → bug latent) + étape CI mypy rendue bloquante. BUG-004: lien `README.md` → `docs/CONTRIBUTING.md`. Vérifié: mypy 0 erreur, ruff OK, pytest 728 passed, frontend OK. | 🟢 corrigé (en attente vérif utilisateur) |
--- ---