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/
- 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
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é
- **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é
lors du chargement des modules d'outils.
+2 -1
View File
@@ -865,7 +865,8 @@ ObsiGate/
### 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
├── docker-compose.yml # Deployment with healthcheck and auth env vars
├── build.sh # Automated build & deployment (docker compose build + up)
└── CONTRIBUTING.md # Contribution guide
└── docs/CONTRIBUTING.md # Contribution guide
```
### 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
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
+4 -1
View File
@@ -14,7 +14,7 @@
- **Projet** : ObsiGate — Porte d'entrée web pour vaults Obsidian
- **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-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)
@@ -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) |
| 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) |
---