File size: 2,755 Bytes
bd09498
 
 
 
 
 
 
 
 
 
c0b5d70
bd09498
 
d03b796
bd09498
 
 
 
 
 
 
1fa7ddd
bd09498
 
 
 
 
 
5d9b495
99f23f1
5d9b495
 
 
99f23f1
 
 
 
 
5d9b495
 
 
99f23f1
 
5d9b495
 
 
 
99f23f1
5d9b495
 
99f23f1
5d9b495
 
bd09498
 
 
 
 
 
 
 
 
 
 
 
 
 
5d9b495
 
bd09498
 
d03b796
 
 
bd09498
 
 
d03b796
 
c0b5d70
5d9b495
 
bd09498
5d9b495
bd09498
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"""
Endpoints de lecture des profils de corpus (R10 — préfixe /api/v1/).

GET  /api/v1/profiles
GET  /api/v1/profiles/{profile_id}

Les profils sont des fichiers JSON dans profiles/ (racine du dépôt).
Ils sont validés par CorpusProfile avant d'être retournés.
"""
# 1. stdlib
import asyncio
import json
import logging
import re
from pathlib import Path

# 2. third-party
from fastapi import APIRouter, HTTPException
from pydantic import ValidationError

# 3. local
from app.config import settings
from app.schemas.corpus_profile import CorpusProfile

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/profiles", tags=["profiles"])

_profiles_cache: dict[str, CorpusProfile] | None = None
_profiles_cache_dir: Path | None = None


def _load_all_profiles() -> dict[str, CorpusProfile]:
    """Charge tous les profils depuis le disque (cache invalidé si le répertoire change)."""
    global _profiles_cache, _profiles_cache_dir

    current_dir = settings.profiles_dir
    if _profiles_cache is not None and _profiles_cache_dir == current_dir:
        return _profiles_cache

    result: dict[str, CorpusProfile] = {}
    if current_dir.is_dir():
        for path in sorted(current_dir.glob("*.json")):
            profile = _load_profile(path)
            if profile is not None:
                result[profile.profile_id] = profile
    else:
        logger.warning("profiles_dir introuvable : %s", current_dir)

    _profiles_cache = result
    _profiles_cache_dir = current_dir
    return _profiles_cache


def _load_profile(path: Path) -> CorpusProfile | None:
    """Charge et valide un fichier de profil JSON. Retourne None si invalide."""
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        return CorpusProfile.model_validate(data)
    except (json.JSONDecodeError, ValidationError) as exc:
        logger.warning("Profil invalide ignoré", extra={"path": str(path), "error": str(exc)})
        return None


@router.get("", response_model=list[dict])
async def list_profiles() -> list[dict]:
    """Retourne tous les profils valides du dossier profiles/."""
    profiles = _load_all_profiles()
    return [p.model_dump() for p in profiles.values()]


_SAFE_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")


@router.get("/{profile_id}", response_model=dict)
async def get_profile(profile_id: str) -> dict:
    """Retourne un profil par son id (nom du fichier sans extension)."""
    if not _SAFE_ID_RE.match(profile_id):
        raise HTTPException(status_code=400, detail="profile_id invalide")

    profiles = _load_all_profiles()
    profile = profiles.get(profile_id)
    if profile is None:
        raise HTTPException(status_code=404, detail="Profil introuvable")
    return profile.model_dump()