Files
office_translator/routes/prompt_routes.py
sepehr ce8e150a61 feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup
- Restructured docker-compose for Nginx Proxy Manager (no custom nginx)
- Added domain wordly.art configuration
- Added Prometheus + Grafana monitoring stack with pre-configured dashboards
- Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation)
- Added alert rules for backend, system, and Docker metrics
- Updated deployment guide for NPM + IONOS DNS homelab setup
- Added marketing plan document
- PDF translator and watermark support
- Enhanced middleware, routes, and translator modules

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 11:43:28 +02:00

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": "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.",
},
)