diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d6d5e97..ad89747 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index d55d39d..40d1837 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/README.fr.md b/README.fr.md index f259540..2957da3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. --- diff --git a/README.md b/README.md index 464da86..068b28e 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/backend/auth/router.py b/backend/auth/router.py index 9e0c132..b6c30ee 100644 --- a/backend/auth/router.py +++ b/backend/auth/router.py @@ -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") diff --git a/backend/bookslm_routes.py b/backend/bookslm_routes.py index de2b69a..a89223a 100644 --- a/backend/bookslm_routes.py +++ b/backend/bookslm_routes.py @@ -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) diff --git a/backend/export.py b/backend/export.py index 2bf396e..720cbf2 100644 --- a/backend/export.py +++ b/backend/export.py @@ -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("*")): diff --git a/backend/indexer.py b/backend/indexer.py index 0325c49..4180367 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -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() diff --git a/backend/main.py b/backend/main.py index 15972d0..ff21565 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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") diff --git a/backend/pdf_reader.py b/backend/pdf_reader.py index f7f5d1b..b7a678d 100644 --- a/backend/pdf_reader.py +++ b/backend/pdf_reader.py @@ -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 diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index cab766f..52a818d 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -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) | ---