Spaces:
Build error
Build error
File size: 5,001 Bytes
bd09498 eec7490 bd09498 c0b5d70 d03b796 bd09498 eec7490 bd09498 d03b796 bd09498 eec7490 bd09498 c0b5d70 bd09498 fc1a5f0 bd09498 eec7490 5d9b495 eec7490 5d9b495 eec7490 5d9b495 eec7490 | 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | """
Endpoints CRUD pour les corpus (R10 — préfixe /api/v1/).
GET /api/v1/corpora
POST /api/v1/corpora
GET /api/v1/corpora/{corpus_id}
DELETE /api/v1/corpora/{corpus_id}
GET /api/v1/corpora/{corpus_id}/manuscripts
Règle : toute logique métier est dans les services, jamais dans les routers.
"""
# 1. stdlib
import uuid
from datetime import datetime, timezone
# 2. third-party
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
# 3. local
from app.models.corpus import CorpusModel, ManuscriptModel
from app.models.database import get_db
router = APIRouter(prefix="/corpora", tags=["corpora"])
# ── Schémas de requête / réponse ─────────────────────────────────────────────
class CorpusCreate(BaseModel):
slug: str = Field(..., pattern=r"^[a-z0-9][a-z0-9_-]{0,63}$")
title: str = Field(..., min_length=1, max_length=256)
profile_id: str = Field(..., pattern=r"^[a-z0-9][a-z0-9_-]*$")
class CorpusResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
slug: str
title: str
profile_id: str
created_at: datetime
updated_at: datetime
class ManuscriptResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
corpus_id: str
title: str
shelfmark: str | None
date_label: str | None
total_pages: int
# ── Endpoints ────────────────────────────────────────────────────────────────
@router.get("", response_model=list[CorpusResponse])
async def list_corpora(
db: AsyncSession = Depends(get_db),
skip: int = Query(0, ge=0, description="Nombre d'éléments à sauter"),
limit: int = Query(100, ge=1, le=1000, description="Nombre maximum d'éléments"),
) -> list[CorpusModel]:
"""Retourne les corpus enregistrés (paginé)."""
result = await db.execute(select(CorpusModel).offset(skip).limit(limit))
return list(result.scalars().all())
@router.post("", response_model=CorpusResponse, status_code=201)
async def create_corpus(
body: CorpusCreate, db: AsyncSession = Depends(get_db)
) -> CorpusModel:
"""Crée un nouveau corpus. Le slug doit être unique."""
# Vérifier que le profil existe dans profiles_dir
from app.config import settings as _settings
profile_path = _settings.profiles_dir / f"{body.profile_id}.json"
if not profile_path.is_file():
raise HTTPException(
status_code=422,
detail=f"Profil «{body.profile_id}» introuvable dans {_settings.profiles_dir}",
)
# Vérifier unicité du slug
existing = await db.execute(
select(CorpusModel).where(CorpusModel.slug == body.slug)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(status_code=409, detail=f"Slug «{body.slug}» déjà utilisé")
now = datetime.now(timezone.utc)
corpus = CorpusModel(
id=str(uuid.uuid4()),
slug=body.slug,
title=body.title,
profile_id=body.profile_id,
created_at=now,
updated_at=now,
)
db.add(corpus)
await db.commit()
await db.refresh(corpus)
return corpus
@router.get("/{corpus_id}", response_model=CorpusResponse)
async def get_corpus(corpus_id: str, db: AsyncSession = Depends(get_db)) -> CorpusModel:
"""Retourne un corpus par son id."""
corpus = await db.get(CorpusModel, corpus_id)
if corpus is None:
raise HTTPException(status_code=404, detail="Corpus introuvable")
return corpus
@router.delete("/{corpus_id}", status_code=204)
async def delete_corpus(corpus_id: str, db: AsyncSession = Depends(get_db)) -> None:
"""Supprime un corpus (cascade sur les manuscrits et pages associés)."""
corpus = await db.get(CorpusModel, corpus_id)
if corpus is None:
raise HTTPException(status_code=404, detail="Corpus introuvable")
await db.delete(corpus)
await db.commit()
@router.get("/{corpus_id}/manuscripts", response_model=list[ManuscriptResponse])
async def list_manuscripts(
corpus_id: str,
db: AsyncSession = Depends(get_db),
skip: int = Query(0, ge=0, description="Nombre d'éléments à sauter"),
limit: int = Query(100, ge=1, le=1000, description="Nombre maximum d'éléments"),
) -> list[ManuscriptModel]:
"""Retourne les manuscrits d'un corpus (paginé)."""
corpus = await db.get(CorpusModel, corpus_id)
if corpus is None:
raise HTTPException(status_code=404, detail="Corpus introuvable")
result = await db.execute(
select(ManuscriptModel)
.where(ManuscriptModel.corpus_id == corpus_id)
.offset(skip)
.limit(limit)
)
return list(result.scalars().all())
|