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
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user