""" 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": "Invalid prompt ID format.", } 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": "An error occurred while creating the 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": "An error occurred while retrieving 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 not found or you do not have access to this resource.", }, ) 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": "An error occurred.", }, ) @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": "At least one field (name or content) must be provided.", }, ) 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 not found or you do not have access to this resource.", }, ) 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": "An error occurred while updating.", }, ) @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 not found or you do not have access to this resource.", }, ) 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": "An error occurred while deleting.", }, )