Spaces:
Build error
Build error
Claude commited on
fix(profiles): resolve __file__ to absolute path before parent traversal
Browse filesCorrige un bug de production sur HuggingFace : GET /api/v1/profiles
retournait [] car Path(__file__) pouvait Γͺtre relatif au CWD selon
l'environnement d'exΓ©cution (uvicorn, PYTHONPATH, HF Spaces).
Solution : centraliser les chemins dans config.py en utilisant
Path(__file__).resolve()
ce qui garantit un chemin absolu avant la traversΓ©e des parents.
- Ajout de profiles_dir et prompts_dir dans Settings (surchargeables
via PROFILES_DIR / PROMPTS_DIR)
- _REPO_ROOT calculΓ© depuis config.py (3 parents = racine du dΓ©pΓ΄t)
- profiles.py utilise settings.profiles_dir au lieu de son propre calcul
- 503 tests, 3 skipped
https://claude.ai/code/session_018woyEHc8HG2th7V4ewJ4Kg
- backend/app/api/v1/profiles.py +4 -7
- backend/app/config.py +12 -0
backend/app/api/v1/profiles.py
CHANGED
|
@@ -17,14 +17,11 @@ from fastapi import APIRouter, HTTPException
|
|
| 17 |
from pydantic import ValidationError
|
| 18 |
|
| 19 |
# 3. local
|
|
|
|
| 20 |
from app.schemas.corpus_profile import CorpusProfile
|
| 21 |
|
| 22 |
logger = logging.getLogger(__name__)
|
| 23 |
|
| 24 |
-
# RΓ©pertoire des profils β relatif Γ la racine du dΓ©pΓ΄t
|
| 25 |
-
# (identique au chemin utilisΓ© dans test_profiles.py)
|
| 26 |
-
_PROFILES_DIR = Path(__file__).parent.parent.parent.parent.parent / "profiles"
|
| 27 |
-
|
| 28 |
router = APIRouter(prefix="/profiles", tags=["profiles"])
|
| 29 |
|
| 30 |
|
|
@@ -41,10 +38,10 @@ def _load_profile(path: Path) -> CorpusProfile | None:
|
|
| 41 |
@router.get("", response_model=list[dict])
|
| 42 |
async def list_profiles() -> list[dict]:
|
| 43 |
"""Retourne tous les profils valides du dossier profiles/."""
|
| 44 |
-
if not
|
| 45 |
return []
|
| 46 |
profiles = []
|
| 47 |
-
for path in sorted(
|
| 48 |
profile = _load_profile(path)
|
| 49 |
if profile is not None:
|
| 50 |
profiles.append(profile.model_dump())
|
|
@@ -54,7 +51,7 @@ async def list_profiles() -> list[dict]:
|
|
| 54 |
@router.get("/{profile_id}", response_model=dict)
|
| 55 |
async def get_profile(profile_id: str) -> dict:
|
| 56 |
"""Retourne un profil par son id (nom du fichier sans extension)."""
|
| 57 |
-
path =
|
| 58 |
if not path.exists():
|
| 59 |
raise HTTPException(status_code=404, detail="Profil introuvable")
|
| 60 |
profile = _load_profile(path)
|
|
|
|
| 17 |
from pydantic import ValidationError
|
| 18 |
|
| 19 |
# 3. local
|
| 20 |
+
from app.config import settings
|
| 21 |
from app.schemas.corpus_profile import CorpusProfile
|
| 22 |
|
| 23 |
logger = logging.getLogger(__name__)
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
router = APIRouter(prefix="/profiles", tags=["profiles"])
|
| 26 |
|
| 27 |
|
|
|
|
| 38 |
@router.get("", response_model=list[dict])
|
| 39 |
async def list_profiles() -> list[dict]:
|
| 40 |
"""Retourne tous les profils valides du dossier profiles/."""
|
| 41 |
+
if not settings.profiles_dir.is_dir():
|
| 42 |
return []
|
| 43 |
profiles = []
|
| 44 |
+
for path in sorted(settings.profiles_dir.glob("*.json")):
|
| 45 |
profile = _load_profile(path)
|
| 46 |
if profile is not None:
|
| 47 |
profiles.append(profile.model_dump())
|
|
|
|
| 51 |
@router.get("/{profile_id}", response_model=dict)
|
| 52 |
async def get_profile(profile_id: str) -> dict:
|
| 53 |
"""Retourne un profil par son id (nom du fichier sans extension)."""
|
| 54 |
+
path = settings.profiles_dir / f"{profile_id}.json"
|
| 55 |
if not path.exists():
|
| 56 |
raise HTTPException(status_code=404, detail="Profil introuvable")
|
| 57 |
profile = _load_profile(path)
|
backend/app/config.py
CHANGED
|
@@ -13,6 +13,11 @@ from pathlib import Path
|
|
| 13 |
# 2. third-party
|
| 14 |
from pydantic import BaseModel, ConfigDict
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
class Settings(BaseModel):
|
| 18 |
"""Paramètres d'application lus depuis les variables d'environnement.
|
|
@@ -27,6 +32,11 @@ class Settings(BaseModel):
|
|
| 27 |
base_url: str = "http://localhost:8000"
|
| 28 |
data_dir: Path = Path("data")
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# ββ Base de donnΓ©es βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
database_url: str = "sqlite+aiosqlite:///./scriptorium.db"
|
| 32 |
|
|
@@ -41,6 +51,8 @@ def _load_settings() -> Settings:
|
|
| 41 |
return Settings(
|
| 42 |
base_url=os.getenv("BASE_URL", "http://localhost:8000"),
|
| 43 |
data_dir=Path(os.getenv("DATA_DIR", "data")),
|
|
|
|
|
|
|
| 44 |
database_url=os.getenv(
|
| 45 |
"DATABASE_URL", "sqlite+aiosqlite:///./scriptorium.db"
|
| 46 |
),
|
|
|
|
| 13 |
# 2. third-party
|
| 14 |
from pydantic import BaseModel, ConfigDict
|
| 15 |
|
| 16 |
+
# Racine du dΓ©pΓ΄t β rΓ©solue depuis l'emplacement absolu de ce fichier.
|
| 17 |
+
# config.py se trouve dans backend/app/ ; 3 parents remontent Γ la racine.
|
| 18 |
+
# .resolve() garantit un chemin absolu mΓͺme si __file__ est relatif au CWD.
|
| 19 |
+
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
|
| 20 |
+
|
| 21 |
|
| 22 |
class Settings(BaseModel):
|
| 23 |
"""Paramètres d'application lus depuis les variables d'environnement.
|
|
|
|
| 32 |
base_url: str = "http://localhost:8000"
|
| 33 |
data_dir: Path = Path("data")
|
| 34 |
|
| 35 |
+
# ββ Chemins des ressources statiques βββββββββββββββββββββββββββββββββββββ
|
| 36 |
+
# CalculΓ©s depuis la racine du dΓ©pΓ΄t ; surchargeables via variables d'env.
|
| 37 |
+
profiles_dir: Path = _REPO_ROOT / "profiles"
|
| 38 |
+
prompts_dir: Path = _REPO_ROOT / "prompts"
|
| 39 |
+
|
| 40 |
# ββ Base de donnΓ©es βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
database_url: str = "sqlite+aiosqlite:///./scriptorium.db"
|
| 42 |
|
|
|
|
| 51 |
return Settings(
|
| 52 |
base_url=os.getenv("BASE_URL", "http://localhost:8000"),
|
| 53 |
data_dir=Path(os.getenv("DATA_DIR", "data")),
|
| 54 |
+
profiles_dir=Path(os.getenv("PROFILES_DIR", str(_REPO_ROOT / "profiles"))),
|
| 55 |
+
prompts_dir=Path(os.getenv("PROMPTS_DIR", str(_REPO_ROOT / "prompts"))),
|
| 56 |
database_url=os.getenv(
|
| 57 |
"DATABASE_URL", "sqlite+aiosqlite:///./scriptorium.db"
|
| 58 |
),
|