language-learning-app/api/app/routers/api/packs.py

151 lines
5.3 KiB
Python

import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import verify_token
from ...domain.services.pack_service import PackService, PackNotFoundError, DuplicateEntryError
from ...outbound.postgres.database import get_db
from ...outbound.postgres.repositories.pack_repository import PostgresPackRepository
from ...outbound.postgres.repositories.vocab_repository import PostgresVocabRepository
from ...outbound.postgres.repositories.flashcard_repository import PostgresFlashcardRepository
from ...outbound.postgres.repositories.dictionary_repository import PostgresDictionaryRepository
router = APIRouter(prefix="/packs", tags=["packs"])
# ── Response models ───────────────────────────────────────────────────────────
class PackSummaryResponse(BaseModel):
id: str
name: str
name_target: str
description: str
description_target: str
source_lang: str
target_lang: str
proficiencies: list[str]
entry_count: int
class PackDetailResponse(PackSummaryResponse):
surface_texts: list[str]
class AddTobankRequest(BaseModel):
source_lang: str
target_lang: str
class AddTobankResponse(BaseModel):
added: list[str]
# ── Dependency ────────────────────────────────────────────────────────────────
def _service(db: AsyncSession) -> PackService:
return PackService(
pack_repo=PostgresPackRepository(db),
vocab_repo=PostgresVocabRepository(db),
flashcard_repo=PostgresFlashcardRepository(db),
dict_repo=PostgresDictionaryRepository(db),
)
def _pack_repo(db: AsyncSession) -> PostgresPackRepository:
return PostgresPackRepository(db)
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get("", response_model=list[PackSummaryResponse])
async def list_packs(
source_lang: str | None = None,
target_lang: str | None = None,
db: AsyncSession = Depends(get_db),
_: dict = Depends(verify_token),
) -> list[PackSummaryResponse]:
repo = _pack_repo(db)
packs = await repo.list_packs(
source_lang=source_lang, target_lang=target_lang, published_only=True
)
responses = []
for pack in packs:
count = await repo.count_entries_for_pack(uuid.UUID(pack.id))
responses.append(
PackSummaryResponse(
id=pack.id,
name=pack.name,
name_target=pack.name_target,
description=pack.description,
description_target=pack.description_target,
source_lang=pack.source_lang,
target_lang=pack.target_lang,
proficiencies=pack.proficiencies,
entry_count=count,
)
)
return responses
@router.get("/{pack_id}", response_model=PackDetailResponse)
async def get_pack(
pack_id: str,
db: AsyncSession = Depends(get_db),
_: dict = Depends(verify_token),
) -> PackDetailResponse:
repo = _pack_repo(db)
pack = await repo.get_pack(_parse_uuid(pack_id))
if pack is None or not pack.is_published:
raise HTTPException(status_code=404, detail="Pack not found")
entries = await repo.get_entries_for_pack(uuid.UUID(pack.id))
count = len(entries)
surface_texts = [e.surface_text for e in entries]
return PackDetailResponse(
id=pack.id,
name=pack.name,
name_target=pack.name_target,
description=pack.description,
description_target=pack.description_target,
source_lang=pack.source_lang,
target_lang=pack.target_lang,
proficiencies=pack.proficiencies,
entry_count=count,
surface_texts=surface_texts,
)
@router.post("/{pack_id}/add-to-bank", response_model=AddTobankResponse, status_code=201)
async def add_pack_to_bank(
pack_id: str,
request: AddTobankRequest,
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> AddTobankResponse:
user_id = uuid.UUID(token_data["sub"])
try:
result = await _service(db).add_pack_to_user_bank(
pack_id=_parse_uuid(pack_id),
user_id=user_id,
source_lang=request.source_lang,
target_lang=request.target_lang,
)
except PackNotFoundError:
raise HTTPException(status_code=404, detail="Pack not found")
except DuplicateEntryError as exc:
raise HTTPException(status_code=409, detail=str(exc))
return AddTobankResponse(added=result.added_surface_texts)
# ── Helpers ───────────────────────────────────────────────────────────────────
def _parse_uuid(value: str) -> uuid.UUID:
try:
return uuid.UUID(value)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid UUID: {value!r}")