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())