Claude commited on
Commit
1fa7ddd
Β·
unverified Β·
1 Parent(s): f842a06

fix(profiles): resolve __file__ to absolute path before parent traversal

Browse files

Corrige 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 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 _PROFILES_DIR.is_dir():
45
  return []
46
  profiles = []
47
- for path in sorted(_PROFILES_DIR.glob("*.json")):
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 = _PROFILES_DIR / f"{profile_id}.json"
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
  ),