378 lines
11 KiB
Python
378 lines
11 KiB
Python
"""
|
|
Custom Prompt CRUD routes for Pro users
|
|
Story 3.11: Custom Prompts - Endpoint CRUD
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from routes.deps import require_pro_user, ProUser
|
|
from database.connection import get_sync_session
|
|
from database.models import CustomPrompt
|
|
from schemas.prompt_schemas import (
|
|
PromptCreate,
|
|
PromptUpdate,
|
|
PromptResponse,
|
|
PromptListItem,
|
|
PromptListResponse,
|
|
PromptDetailResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/v1/prompts", tags=["Prompts v1"])
|
|
|
|
DEFAULT_PAGE_SIZE = 50
|
|
MAX_PAGE_SIZE = 100
|
|
|
|
|
|
def _validate_uuid(prompt_id: str) -> tuple[bool, dict | None]:
|
|
"""Validate UUID format. Returns (is_valid, error_response)."""
|
|
import uuid as uuid_lib
|
|
|
|
try:
|
|
uuid_lib.UUID(prompt_id)
|
|
return True, None
|
|
except ValueError:
|
|
return False, {
|
|
"error": "INVALID_PROMPT_ID",
|
|
"message": "Format d'identifiant de prompt invalide.",
|
|
}
|
|
|
|
|
|
def _format_prompt(prompt: CustomPrompt) -> dict:
|
|
"""Format a CustomPrompt for JSON response."""
|
|
return {
|
|
"id": prompt.id,
|
|
"name": prompt.name,
|
|
"content": prompt.content,
|
|
"created_at": prompt.created_at.isoformat() if prompt.created_at else None,
|
|
"updated_at": prompt.updated_at.isoformat() if prompt.updated_at else None,
|
|
}
|
|
|
|
|
|
def _format_prompt_list_item(prompt: CustomPrompt) -> dict:
|
|
"""Format a CustomPrompt for list view with content preview."""
|
|
content_preview = prompt.content[:100] if prompt.content else ""
|
|
return {
|
|
"id": prompt.id,
|
|
"name": prompt.name,
|
|
"content_preview": content_preview,
|
|
"created_at": prompt.created_at.isoformat() if prompt.created_at else None,
|
|
"updated_at": prompt.updated_at.isoformat() if prompt.updated_at else None,
|
|
}
|
|
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=PromptDetailResponse,
|
|
status_code=201,
|
|
summary="Créer un prompt",
|
|
description="""
|
|
Crée un nouveau prompt système personnalisé.
|
|
|
|
**Restriction:** Uniquement disponible pour les utilisateurs Pro.
|
|
|
|
**Exemple de requête:**
|
|
```json
|
|
{
|
|
"name": "Prompt Technique FR-EN",
|
|
"content": "Tu es un traducteur technique expert. Traduis en préservant la terminologie technique..."
|
|
}
|
|
```
|
|
""",
|
|
)
|
|
async def create_prompt(
|
|
body: PromptCreate,
|
|
user: ProUser = Depends(require_pro_user),
|
|
):
|
|
"""Create a new prompt for the authenticated Pro user."""
|
|
try:
|
|
with get_sync_session() as session:
|
|
prompt = CustomPrompt(
|
|
user_id=user.id,
|
|
name=body.name,
|
|
content=body.content,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
session.add(prompt)
|
|
session.commit()
|
|
session.refresh(prompt)
|
|
|
|
logger.info(
|
|
f"Prompt created: id={prompt.id}, user_id={user.id}, name={prompt.name}"
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=201,
|
|
content={
|
|
"data": _format_prompt(prompt),
|
|
"meta": {},
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create prompt for user {user.id}: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "DATABASE_ERROR",
|
|
"message": "Une erreur est survenue lors de la création du prompt.",
|
|
},
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"",
|
|
response_model=PromptListResponse,
|
|
summary="Lister les prompts",
|
|
description="Retourne la liste paginée des prompts de l'utilisateur.",
|
|
)
|
|
async def list_prompts(
|
|
page: int = Query(1, ge=1, description="Numéro de page"),
|
|
per_page: int = Query(
|
|
DEFAULT_PAGE_SIZE, ge=1, le=MAX_PAGE_SIZE, description="Éléments par page"
|
|
),
|
|
user: ProUser = Depends(require_pro_user),
|
|
):
|
|
"""List all prompts for the authenticated Pro user with pagination."""
|
|
try:
|
|
with get_sync_session() as session:
|
|
total_count = (
|
|
session.query(CustomPrompt)
|
|
.filter(CustomPrompt.user_id == user.id)
|
|
.count()
|
|
)
|
|
|
|
offset = (page - 1) * per_page
|
|
prompts = (
|
|
session.query(CustomPrompt)
|
|
.filter(CustomPrompt.user_id == user.id)
|
|
.order_by(CustomPrompt.created_at.desc())
|
|
.offset(offset)
|
|
.limit(per_page)
|
|
.all()
|
|
)
|
|
|
|
items = [
|
|
PromptListItem(
|
|
id=p.id,
|
|
name=p.name,
|
|
content_preview=p.content[:100] if p.content else "",
|
|
created_at=p.created_at,
|
|
updated_at=p.updated_at,
|
|
)
|
|
for p in prompts
|
|
]
|
|
|
|
total_pages = (total_count + per_page - 1) // per_page
|
|
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": [item.model_dump(mode="json") for item in items],
|
|
"meta": {
|
|
"total": total_count,
|
|
"page": page,
|
|
"per_page": per_page,
|
|
"total_pages": total_pages,
|
|
},
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to list prompts for user {user.id}: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "DATABASE_ERROR",
|
|
"message": "Une erreur est survenue lors de la récupération des prompts.",
|
|
},
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/{prompt_id}",
|
|
response_model=PromptDetailResponse,
|
|
summary="Détail d'un prompt",
|
|
description="Retourne les détails d'un prompt spécifique.",
|
|
)
|
|
async def get_prompt(
|
|
prompt_id: str,
|
|
user: ProUser = Depends(require_pro_user),
|
|
):
|
|
"""Get a specific prompt by ID."""
|
|
is_valid, error = _validate_uuid(prompt_id)
|
|
if not is_valid:
|
|
return JSONResponse(status_code=400, content=error)
|
|
|
|
try:
|
|
with get_sync_session() as session:
|
|
prompt = (
|
|
session.query(CustomPrompt)
|
|
.filter(CustomPrompt.id == prompt_id, CustomPrompt.user_id == user.id)
|
|
.first()
|
|
)
|
|
|
|
if not prompt:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={
|
|
"error": "PROMPT_NOT_FOUND",
|
|
"message": "Prompt introuvable ou vous n'avez pas accès à cette ressource.",
|
|
},
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": _format_prompt(prompt),
|
|
"meta": {},
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to get prompt {prompt_id}: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "DATABASE_ERROR",
|
|
"message": "Une erreur est survenue.",
|
|
},
|
|
)
|
|
|
|
|
|
@router.patch(
|
|
"/{prompt_id}",
|
|
response_model=PromptDetailResponse,
|
|
summary="Mettre à jour un prompt",
|
|
description="Met à jour le nom et/ou le contenu d'un prompt existant.",
|
|
)
|
|
async def update_prompt(
|
|
prompt_id: str,
|
|
body: PromptUpdate,
|
|
user: ProUser = Depends(require_pro_user),
|
|
):
|
|
"""Update a prompt's name and/or content."""
|
|
if not body.has_updates():
|
|
return JSONResponse(
|
|
status_code=400,
|
|
content={
|
|
"error": "NO_UPDATE_FIELDS",
|
|
"message": "Au moins un champ (name ou content) doit être fourni.",
|
|
},
|
|
)
|
|
|
|
is_valid, error = _validate_uuid(prompt_id)
|
|
if not is_valid:
|
|
return JSONResponse(status_code=400, content=error)
|
|
|
|
try:
|
|
with get_sync_session() as session:
|
|
prompt = (
|
|
session.query(CustomPrompt)
|
|
.filter(CustomPrompt.id == prompt_id, CustomPrompt.user_id == user.id)
|
|
.first()
|
|
)
|
|
|
|
if not prompt:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={
|
|
"error": "PROMPT_NOT_FOUND",
|
|
"message": "Prompt introuvable ou vous n'avez pas accès à cette ressource.",
|
|
},
|
|
)
|
|
|
|
old_name = prompt.name
|
|
|
|
if body.name is not None:
|
|
prompt.name = body.name
|
|
|
|
if body.content is not None:
|
|
prompt.content = body.content
|
|
|
|
prompt.updated_at = datetime.now(timezone.utc)
|
|
session.commit()
|
|
session.refresh(prompt)
|
|
|
|
logger.info(
|
|
f"Prompt updated: id={prompt.id}, user_id={user.id}, "
|
|
f"old_name={old_name}, new_name={prompt.name}"
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=200,
|
|
content={
|
|
"data": _format_prompt(prompt),
|
|
"meta": {},
|
|
},
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to update prompt {prompt_id}: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "DATABASE_ERROR",
|
|
"message": "Une erreur est survenue lors de la mise à jour.",
|
|
},
|
|
)
|
|
|
|
|
|
@router.delete(
|
|
"/{prompt_id}",
|
|
status_code=204,
|
|
summary="Supprimer un prompt",
|
|
description="Supprime un prompt.",
|
|
)
|
|
async def delete_prompt(
|
|
prompt_id: str,
|
|
user: ProUser = Depends(require_pro_user),
|
|
):
|
|
"""Delete a prompt."""
|
|
is_valid, error = _validate_uuid(prompt_id)
|
|
if not is_valid:
|
|
return JSONResponse(status_code=400, content=error)
|
|
|
|
try:
|
|
with get_sync_session() as session:
|
|
prompt = (
|
|
session.query(CustomPrompt)
|
|
.filter(CustomPrompt.id == prompt_id, CustomPrompt.user_id == user.id)
|
|
.first()
|
|
)
|
|
|
|
if not prompt:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={
|
|
"error": "PROMPT_NOT_FOUND",
|
|
"message": "Prompt introuvable ou vous n'avez pas accès à cette ressource.",
|
|
},
|
|
)
|
|
|
|
prompt_name = prompt.name
|
|
session.delete(prompt)
|
|
session.commit()
|
|
|
|
logger.info(
|
|
f"Prompt deleted: id={prompt_id}, user_id={user.id}, name={prompt_name}"
|
|
)
|
|
|
|
return JSONResponse(
|
|
status_code=204,
|
|
content=None,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete prompt {prompt_id}: {e}")
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={
|
|
"error": "DATABASE_ERROR",
|
|
"message": "Une erreur est survenue lors de la suppression.",
|
|
},
|
|
)
|