feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle
Made-with: Cursor
This commit is contained in:
98
schemas/__init__.py
Normal file
98
schemas/__init__.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Pydantic models for API documentation and validation
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from .translation import (
|
||||
TranslateResponseData,
|
||||
TranslateResponseMeta,
|
||||
TranslateResponse,
|
||||
TranslationStatusData,
|
||||
TranslationStatusMeta,
|
||||
TranslationStatusResponse,
|
||||
LanguageResponse,
|
||||
)
|
||||
from .auth import (
|
||||
RegisterRequest,
|
||||
LoginRequest,
|
||||
TokenResponse,
|
||||
LogoutResponse,
|
||||
RefreshRequest,
|
||||
)
|
||||
from .api_keys import (
|
||||
APIKeyCreateRequest,
|
||||
APIKeyResponse,
|
||||
APIKeyListResponse,
|
||||
APIKeyRevokeResponse,
|
||||
)
|
||||
from .admin import (
|
||||
AdminLoginRequest,
|
||||
AdminLoginResponse,
|
||||
AdminDashboardResponse,
|
||||
AdminUserResponse,
|
||||
AdminUserUpdateRequest,
|
||||
AdminStatsResponse,
|
||||
AdminRevokeApiKeyRequest,
|
||||
)
|
||||
from .errors import ErrorResponse, ErrorCode
|
||||
from .common import (
|
||||
SuccessResponse,
|
||||
HealthCheckResponse,
|
||||
ReadyCheckResponse,
|
||||
)
|
||||
from .glossary_schemas import (
|
||||
GlossaryTermCreate,
|
||||
GlossaryTermResponse,
|
||||
GlossaryCreate,
|
||||
GlossaryUpdate,
|
||||
GlossaryResponse,
|
||||
GlossaryListItem,
|
||||
GlossaryListResponse,
|
||||
GlossaryDetailResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Translation
|
||||
"TranslateResponseData",
|
||||
"TranslateResponseMeta",
|
||||
"TranslateResponse",
|
||||
"TranslationStatusData",
|
||||
"TranslationStatusMeta",
|
||||
"TranslationStatusResponse",
|
||||
"LanguageResponse",
|
||||
# Auth
|
||||
"RegisterRequest",
|
||||
"LoginRequest",
|
||||
"TokenResponse",
|
||||
"LogoutResponse",
|
||||
"RefreshRequest",
|
||||
# API Keys
|
||||
"APIKeyCreateRequest",
|
||||
"APIKeyResponse",
|
||||
"APIKeyListResponse",
|
||||
"APIKeyRevokeResponse",
|
||||
# Admin
|
||||
"AdminLoginRequest",
|
||||
"AdminLoginResponse",
|
||||
"AdminDashboardResponse",
|
||||
"AdminUserResponse",
|
||||
"AdminUserUpdateRequest",
|
||||
"AdminStatsResponse",
|
||||
"AdminRevokeApiKeyRequest",
|
||||
# Errors
|
||||
"ErrorResponse",
|
||||
"ErrorCode",
|
||||
# Common
|
||||
"SuccessResponse",
|
||||
"HealthCheckResponse",
|
||||
"ReadyCheckResponse",
|
||||
# Glossaries
|
||||
"GlossaryTermCreate",
|
||||
"GlossaryTermResponse",
|
||||
"GlossaryCreate",
|
||||
"GlossaryUpdate",
|
||||
"GlossaryResponse",
|
||||
"GlossaryListItem",
|
||||
"GlossaryListResponse",
|
||||
"GlossaryDetailResponse",
|
||||
]
|
||||
262
schemas/admin.py
Normal file
262
schemas/admin.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
Pydantic models for admin endpoints
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Literal, Dict, Any, List
|
||||
|
||||
|
||||
class AdminLoginRequest(BaseModel):
|
||||
"""Request model for admin login"""
|
||||
|
||||
password: str = Field(
|
||||
...,
|
||||
example="admin_secret_password",
|
||||
description="Mot de passe administrateur"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"password": "admin_secret_password"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminLoginData(BaseModel):
|
||||
"""Admin login response data"""
|
||||
|
||||
access_token: str = Field(
|
||||
...,
|
||||
example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
description="Token d'accès admin (expire dans 24h)"
|
||||
)
|
||||
token_type: str = Field(
|
||||
default="bearer",
|
||||
example="bearer",
|
||||
description="Type de token"
|
||||
)
|
||||
expires_in: int = Field(
|
||||
default=86400,
|
||||
example=86400,
|
||||
description="Durée de validité en secondes"
|
||||
)
|
||||
|
||||
|
||||
class AdminLoginResponse(BaseModel):
|
||||
"""Response model for admin login"""
|
||||
|
||||
status: str = Field(default="success", description="Statut de la connexion")
|
||||
access_token: str = Field(..., description="Token d'accès admin")
|
||||
token_type: str = Field(default="bearer", description="Type de token")
|
||||
expires_in: int = Field(default=86400, description="Durée de validité en secondes")
|
||||
message: str = Field(default="Login successful", description="Message de confirmation")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": "success",
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 86400,
|
||||
"message": "Login successful"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminUserUpdateRequest(BaseModel):
|
||||
"""Request model for updating user tier"""
|
||||
|
||||
plan: Literal["free", "starter", "pro", "business", "enterprise"] = Field(
|
||||
...,
|
||||
example="pro",
|
||||
description="Nouveau plan d'abonnement"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"plan": "pro"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminUserData(BaseModel):
|
||||
"""User data in admin responses"""
|
||||
|
||||
id: str = Field(..., description="Identifiant unique de l'utilisateur")
|
||||
email: str = Field(..., description="Adresse email")
|
||||
name: str = Field(..., description="Nom de l'utilisateur")
|
||||
plan: str = Field(..., description="Plan d'abonnement actuel")
|
||||
subscription_status: str = Field(..., description="Statut de l'abonnement")
|
||||
docs_translated_this_month: int = Field(..., description="Documents traduits ce mois")
|
||||
pages_translated_this_month: int = Field(..., description="Pages traduites ce mois")
|
||||
extra_credits: int = Field(..., description="Crédits supplémentaires")
|
||||
created_at: str = Field(..., description="Date de création du compte")
|
||||
plan_limits: Dict[str, Any] = Field(..., description="Limites du plan actuel")
|
||||
|
||||
|
||||
class AdminUserResponse(BaseModel):
|
||||
"""Response model for admin user operations"""
|
||||
|
||||
data: AdminUserData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "usr_abc123def456",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"name": "Jean Dupont",
|
||||
"plan": "pro",
|
||||
"subscription_status": "active",
|
||||
"docs_translated_this_month": 15,
|
||||
"pages_translated_this_month": 42,
|
||||
"extra_credits": 0,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"plan_limits": {
|
||||
"docs_per_month": 100,
|
||||
"max_pages_per_doc": 100
|
||||
}
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminUsersListResponse(BaseModel):
|
||||
"""Response model for listing users"""
|
||||
|
||||
total: int = Field(..., description="Nombre total d'utilisateurs")
|
||||
users: List[AdminUserData] = Field(..., description="Liste des utilisateurs")
|
||||
|
||||
|
||||
class AdminDashboardResponse(BaseModel):
|
||||
"""Response model for admin dashboard"""
|
||||
|
||||
timestamp: str = Field(..., description="Timestamp de la réponse")
|
||||
status: str = Field(..., description="Statut global du système")
|
||||
system: Dict[str, Any] = Field(..., description="Informations système")
|
||||
providers: Dict[str, Any] = Field(..., description="Statut des providers")
|
||||
cleanup: Dict[str, Any] = Field(..., description="Statut du cleanup")
|
||||
rate_limits: Dict[str, Any] = Field(..., description="Statut des rate limits")
|
||||
config: Dict[str, Any] = Field(..., description="Configuration actuelle")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"status": "healthy",
|
||||
"system": {
|
||||
"memory": {},
|
||||
"disk": {}
|
||||
},
|
||||
"providers": {
|
||||
"google": {
|
||||
"name": "google",
|
||||
"available": True,
|
||||
"last_check": "2024-01-15T10:29:00Z"
|
||||
}
|
||||
},
|
||||
"cleanup": {
|
||||
"files_cleaned": 12,
|
||||
"tracked_files_count": 5
|
||||
},
|
||||
"rate_limits": {
|
||||
"active_clients": 3
|
||||
},
|
||||
"config": {
|
||||
"max_file_size_mb": 50,
|
||||
"supported_extensions": [".xlsx", ".docx", ".pptx"],
|
||||
"translation_service": "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminStatsResponse(BaseModel):
|
||||
"""Response model for admin statistics"""
|
||||
|
||||
users: Dict[str, Any] = Field(..., description="Statistiques utilisateurs")
|
||||
translations: Dict[str, Any] = Field(..., description="Statistiques de traduction")
|
||||
cache: Dict[str, Any] = Field(..., description="Statistiques du cache")
|
||||
config: Dict[str, Any] = Field(..., description="Configuration actuelle")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"users": {
|
||||
"total": 150,
|
||||
"active_this_month": 45,
|
||||
"by_plan": {
|
||||
"free": 100,
|
||||
"pro": 40,
|
||||
"business": 10
|
||||
}
|
||||
},
|
||||
"translations": {
|
||||
"docs_this_month": 350,
|
||||
"pages_this_month": 1250
|
||||
},
|
||||
"cache": {
|
||||
"hits": 1500,
|
||||
"misses": 500,
|
||||
"size": 42
|
||||
},
|
||||
"config": {
|
||||
"translation_service": "google",
|
||||
"max_file_size_mb": 50,
|
||||
"supported_extensions": [".xlsx", ".docx", ".pptx"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminRevokeApiKeyRequest(BaseModel):
|
||||
"""Request model for admin API key revocation"""
|
||||
|
||||
reason: Optional[str] = Field(
|
||||
None,
|
||||
example="Violation des conditions d'utilisation",
|
||||
description="Raison de la révocation (optionnel)"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"reason": "Violation des conditions d'utilisation"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AdminRevokeApiKeyData(BaseModel):
|
||||
"""Data returned after admin API key revocation"""
|
||||
|
||||
id: str = Field(..., description="Identifiant de la clé révoquée")
|
||||
revoked: bool = Field(..., description="Confirmation de révocation")
|
||||
revoked_at: str = Field(..., description="Date de révocation (ISO 8601)")
|
||||
owner_user_id: str = Field(..., description="ID de l'utilisateur propriétaire")
|
||||
reason: Optional[str] = Field(None, description="Raison de la révocation")
|
||||
|
||||
|
||||
class AdminRevokeApiKeyResponse(BaseModel):
|
||||
"""Response model for admin API key revocation"""
|
||||
|
||||
data: AdminRevokeApiKeyData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"revoked": True,
|
||||
"revoked_at": "2024-01-15T16:00:00Z",
|
||||
"owner_user_id": "usr_abc123def456",
|
||||
"reason": "Violation des conditions d'utilisation"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
177
schemas/api_keys.py
Normal file
177
schemas/api_keys.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""
|
||||
Pydantic models for API key endpoints
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class APIKeyCreateRequest(BaseModel):
|
||||
"""Request model for creating an API key"""
|
||||
|
||||
name: Optional[str] = Field(
|
||||
default="Default API Key",
|
||||
max_length=100,
|
||||
example="Production API Key",
|
||||
description="Nom descriptif pour la clé API"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "Production API Key"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class APIKeyData(BaseModel):
|
||||
"""API key data in response (full key shown only on creation)"""
|
||||
|
||||
id: str = Field(
|
||||
...,
|
||||
example="550e8400-e29b-41d4-a716-446655440000",
|
||||
description="Identifiant unique de la clé API"
|
||||
)
|
||||
key: str = Field(
|
||||
...,
|
||||
example="sk_live_abc123def456ghi789...",
|
||||
description="Clé API complète (affichée UNE SEULE FOIS à la création)"
|
||||
)
|
||||
name: str = Field(
|
||||
...,
|
||||
example="Production API Key",
|
||||
description="Nom de la clé API"
|
||||
)
|
||||
key_prefix: str = Field(
|
||||
...,
|
||||
example="sk_live_",
|
||||
description="Préfixe de la clé (pour identification)"
|
||||
)
|
||||
created_at: str = Field(
|
||||
...,
|
||||
example="2024-01-15T10:30:00Z",
|
||||
description="Date de création (ISO 8601)"
|
||||
)
|
||||
|
||||
|
||||
class APIKeyResponse(BaseModel):
|
||||
"""Response model for API key creation"""
|
||||
|
||||
data: APIKeyData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"key": "sk_live_abc123def456ghi789jkl012mno345pqr678...",
|
||||
"name": "Production API Key",
|
||||
"key_prefix": "sk_live_",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class APIKeyListItem(BaseModel):
|
||||
"""API key item in list (without secret)"""
|
||||
|
||||
id: str = Field(..., description="Identifiant unique de la clé API")
|
||||
name: str = Field(..., description="Nom de la clé API")
|
||||
key_prefix: str = Field(..., description="Préfixe de la clé (pour identification)")
|
||||
is_active: bool = Field(..., description="Si la clé est active")
|
||||
last_used_at: Optional[str] = Field(None, description="Dernière utilisation (ISO 8601)")
|
||||
usage_count: int = Field(..., description="Nombre total d'utilisations")
|
||||
created_at: str = Field(..., description="Date de création (ISO 8601)")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Production API Key",
|
||||
"key_prefix": "sk_live_",
|
||||
"is_active": True,
|
||||
"last_used_at": "2024-01-15T14:30:00Z",
|
||||
"usage_count": 42,
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class APIKeyListMeta(BaseModel):
|
||||
"""Metadata for API key list response"""
|
||||
|
||||
total: int = Field(..., description="Nombre total de clés API")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class APIKeyListResponse(BaseModel):
|
||||
"""Response model for listing API keys"""
|
||||
|
||||
data: list[APIKeyListItem]
|
||||
meta: APIKeyListMeta
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Production API Key",
|
||||
"key_prefix": "sk_live_",
|
||||
"is_active": True,
|
||||
"last_used_at": "2024-01-15T14:30:00Z",
|
||||
"usage_count": 42,
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"name": "Development API Key",
|
||||
"key_prefix": "sk_live_",
|
||||
"is_active": True,
|
||||
"last_used_at": None,
|
||||
"usage_count": 0,
|
||||
"created_at": "2024-01-16T09:00:00Z"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class APIKeyRevokeData(BaseModel):
|
||||
"""Data returned after API key revocation"""
|
||||
|
||||
id: str = Field(..., description="Identifiant de la clé révoquée")
|
||||
revoked: bool = Field(..., description="Confirmation de révocation")
|
||||
revoked_at: str = Field(..., description="Date de révocation (ISO 8601)")
|
||||
|
||||
|
||||
class APIKeyRevokeResponse(BaseModel):
|
||||
"""Response model for API key revocation"""
|
||||
|
||||
data: APIKeyRevokeData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"revoked": True,
|
||||
"revoked_at": "2024-01-15T16:00:00Z"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
163
schemas/auth.py
Normal file
163
schemas/auth.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""
|
||||
Pydantic models for authentication endpoints
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, EmailStr
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
"""Request model for user registration"""
|
||||
|
||||
email: EmailStr = Field(
|
||||
...,
|
||||
example="utilisateur@exemple.com",
|
||||
description="Adresse email de l'utilisateur"
|
||||
)
|
||||
password: str = Field(
|
||||
...,
|
||||
example="MotDePasse123!",
|
||||
description="Mot de passe (min 8 caractères)",
|
||||
min_length=8
|
||||
)
|
||||
name: Optional[str] = Field(
|
||||
None,
|
||||
example="Jean Dupont",
|
||||
description="Nom complet de l'utilisateur (optionnel)"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"email": "utilisateur@exemple.com",
|
||||
"password": "MotDePasse123!",
|
||||
"name": "Jean Dupont"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Request model for user login"""
|
||||
|
||||
email: EmailStr = Field(
|
||||
...,
|
||||
example="utilisateur@exemple.com",
|
||||
description="Adresse email de l'utilisateur"
|
||||
)
|
||||
password: str = Field(
|
||||
...,
|
||||
example="MotDePasse123!",
|
||||
description="Mot de passe"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"email": "utilisateur@exemple.com",
|
||||
"password": "MotDePasse123!"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""Token data in response"""
|
||||
|
||||
access_token: str = Field(
|
||||
...,
|
||||
example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
description="Token d'accès JWT (expire dans 15 minutes)"
|
||||
)
|
||||
refresh_token: str = Field(
|
||||
...,
|
||||
example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
description="Token de rafraîchissement JWT (expire dans 7 jours)"
|
||||
)
|
||||
token_type: str = Field(
|
||||
default="bearer",
|
||||
example="bearer",
|
||||
description="Type de token"
|
||||
)
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Response model for authentication tokens"""
|
||||
|
||||
data: TokenData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "bearer"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LogoutResponse(BaseModel):
|
||||
"""Response model for logout"""
|
||||
|
||||
data: dict = Field(
|
||||
default_factory=lambda: {"message": "Déconnexion réussie"},
|
||||
description="Message de confirmation"
|
||||
)
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"message": "Déconnexion réussie"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
"""Request model for token refresh"""
|
||||
|
||||
refresh_token: str = Field(
|
||||
...,
|
||||
example="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
description="Token de rafraîchissement"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserData(BaseModel):
|
||||
"""User data in responses"""
|
||||
|
||||
id: str = Field(..., description="Identifiant unique de l'utilisateur")
|
||||
email: str = Field(..., description="Adresse email")
|
||||
tier: str = Field(..., description="Niveau d'abonnement (free, pro, etc.)")
|
||||
|
||||
|
||||
class RegisterResponse(BaseModel):
|
||||
"""Response model for registration"""
|
||||
|
||||
data: UserData
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "usr_abc123def456",
|
||||
"email": "utilisateur@exemple.com",
|
||||
"tier": "free"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
179
schemas/common.py
Normal file
179
schemas/common.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Common Pydantic models for API documentation
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
"""Generic success response"""
|
||||
|
||||
data: Dict[str, Any] = Field(..., description="Données de réponse")
|
||||
meta: Dict[str, Any] = Field(default_factory=dict, description="Métadonnées")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"message": "Opération réussie"
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class HealthCheckResponse(BaseModel):
|
||||
"""Response model for health check endpoint"""
|
||||
|
||||
status: str = Field(..., description="Statut de santé (healthy/unhealthy)")
|
||||
translation_service: str = Field(..., description="Service de traduction configuré")
|
||||
database: Dict[str, Any] = Field(..., description="Statut de la base de données")
|
||||
redis: Dict[str, Any] = Field(..., description="Statut de Redis")
|
||||
memory: Dict[str, Any] = Field(..., description="Informations mémoire")
|
||||
disk: Dict[str, Any] = Field(..., description="Informations disque")
|
||||
cleanup_service: Dict[str, Any] = Field(..., description="Statut du service de cleanup")
|
||||
rate_limits: Dict[str, Any] = Field(..., description="Configuration des rate limits")
|
||||
translation_cache: Dict[str, Any] = Field(..., description="Statut du cache de traduction")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"status": "healthy",
|
||||
"translation_service": "google",
|
||||
"database": {
|
||||
"status": "healthy"
|
||||
},
|
||||
"redis": {
|
||||
"status": "not_configured"
|
||||
},
|
||||
"memory": {},
|
||||
"disk": {},
|
||||
"cleanup_service": {
|
||||
"running": True
|
||||
},
|
||||
"rate_limits": {
|
||||
"requests_per_minute": 30,
|
||||
"translations_per_minute": 10
|
||||
},
|
||||
"translation_cache": {
|
||||
"hits": 1500,
|
||||
"misses": 500,
|
||||
"size": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ReadyCheckResponse(BaseModel):
|
||||
"""Response model for readiness check endpoint"""
|
||||
|
||||
ready: bool = Field(..., description="Si le service est prêt à recevoir du trafic")
|
||||
issues: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="Liste des problèmes si non prêt"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"examples": [
|
||||
{
|
||||
"summary": "Service prêt",
|
||||
"value": {
|
||||
"ready": True
|
||||
}
|
||||
},
|
||||
{
|
||||
"summary": "Service non prêt",
|
||||
"value": {
|
||||
"ready": False,
|
||||
"issues": ["database_unavailable", "redis_unavailable"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class RootResponse(BaseModel):
|
||||
"""Response model for root endpoint"""
|
||||
|
||||
name: str = Field(..., description="Nom de l'API")
|
||||
version: str = Field(..., description="Version de l'API")
|
||||
status: str = Field(..., description="Statut opérationnel")
|
||||
docs: str = Field(..., description="URL de la documentation Swagger")
|
||||
redoc: str = Field(..., description="URL de la documentation ReDoc")
|
||||
api_base: str = Field(..., description="Base URL des endpoints API")
|
||||
supported_formats: List[str] = Field(..., description="Formats de fichier supportés")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"name": "Office Translator API",
|
||||
"version": "1.0.0",
|
||||
"status": "operational",
|
||||
"docs": "/docs",
|
||||
"redoc": "/redoc",
|
||||
"api_base": "/api/v1",
|
||||
"supported_formats": [".xlsx", ".docx", ".pptx"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RateLimitStatusResponse(BaseModel):
|
||||
"""Response model for rate limit status"""
|
||||
|
||||
client_ip: str = Field(..., description="Adresse IP du client")
|
||||
limits: Dict[str, int] = Field(..., description="Limites configurées")
|
||||
current_usage: Dict[str, Any] = Field(..., description="Utilisation actuelle")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"client_ip": "192.168.1.1",
|
||||
"limits": {
|
||||
"requests_per_minute": 30,
|
||||
"requests_per_hour": 200,
|
||||
"translations_per_minute": 10,
|
||||
"translations_per_hour": 50
|
||||
},
|
||||
"current_usage": {
|
||||
"requests_this_minute": 5,
|
||||
"requests_this_hour": 42,
|
||||
"translations_this_minute": 1,
|
||||
"translations_this_hour": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MetricsResponse(BaseModel):
|
||||
"""Response model for metrics endpoint"""
|
||||
|
||||
system: Dict[str, Any] = Field(..., description="Métriques système")
|
||||
cleanup: Dict[str, Any] = Field(..., description="Métriques de cleanup")
|
||||
rate_limits: Dict[str, Any] = Field(..., description="Métriques de rate limiting")
|
||||
config: Dict[str, Any] = Field(..., description="Configuration actuelle")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"system": {
|
||||
"memory": {},
|
||||
"disk": {},
|
||||
"status": "healthy"
|
||||
},
|
||||
"cleanup": {
|
||||
"files_cleaned": 12,
|
||||
"last_cleanup": "2024-01-15T10:00:00Z"
|
||||
},
|
||||
"rate_limits": {
|
||||
"active_clients": 3
|
||||
},
|
||||
"config": {
|
||||
"max_file_size_mb": 50,
|
||||
"supported_extensions": [".xlsx", ".docx", ".pptx"],
|
||||
"translation_service": "google"
|
||||
}
|
||||
}
|
||||
}
|
||||
279
schemas/errors.py
Normal file
279
schemas/errors.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Error response models for API documentation
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
"""All error codes used in the API"""
|
||||
|
||||
# Client errors (4xx)
|
||||
INVALID_FORMAT = "INVALID_FORMAT"
|
||||
CORRUPTED_FILE = "CORRUPTED_FILE"
|
||||
FILE_TOO_LARGE = "FILE_TOO_LARGE"
|
||||
URL_DOWNLOAD_FAILED = "URL_DOWNLOAD_FAILED"
|
||||
URL_UNREACHABLE = "URL_UNREACHABLE"
|
||||
QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
|
||||
UNAUTHORIZED = "UNAUTHORIZED"
|
||||
FORBIDDEN = "FORBIDDEN"
|
||||
INVALID_CREDENTIALS = "INVALID_CREDENTIALS"
|
||||
USER_NOT_FOUND = "USER_NOT_FOUND"
|
||||
EMAIL_EXISTS = "EMAIL_EXISTS"
|
||||
INVALID_EMAIL = "INVALID_EMAIL"
|
||||
TOKEN_EXPIRED = "TOKEN_EXPIRED"
|
||||
TOKEN_MISSING = "TOKEN_MISSING"
|
||||
TOKEN_INVALID = "TOKEN_INVALID"
|
||||
MISSING_API_KEY = "MISSING_API_KEY"
|
||||
INVALID_API_KEY = "INVALID_API_KEY"
|
||||
API_KEY_REVOKED = "API_KEY_REVOKED"
|
||||
API_KEY_NOT_FOUND = "API_KEY_NOT_FOUND"
|
||||
API_KEY_LIMIT_REACHED = "API_KEY_LIMIT_REACHED"
|
||||
PRO_FEATURE_REQUIRED = "PRO_FEATURE_REQUIRED"
|
||||
GLOSSARY_NOT_FOUND = "GLOSSARY_NOT_FOUND"
|
||||
PROMPT_NOT_FOUND = "PROMPT_NOT_FOUND"
|
||||
INVALID_WEBHOOK_URL = "INVALID_WEBHOOK_URL"
|
||||
FILE_EXPIRED = "FILE_EXPIRED"
|
||||
NOT_READY = "NOT_READY"
|
||||
NOT_FOUND = "NOT_FOUND"
|
||||
INVALID_REQUEST = "INVALID_REQUEST"
|
||||
INVALID_JOB_ID = "INVALID_JOB_ID"
|
||||
ACCESS_DENIED = "ACCESS_DENIED"
|
||||
|
||||
# Provider errors (5xx but not 500)
|
||||
PROVIDER_UNAVAILABLE = "PROVIDER_UNAVAILABLE"
|
||||
PROVIDER_RATE_LIMITED = "PROVIDER_RATE_LIMITED"
|
||||
ALL_PROVIDERS_FAILED = "ALL_PROVIDERS_FAILED"
|
||||
WEBHOOK_FAILED = "WEBHOOK_FAILED"
|
||||
|
||||
# System errors (5xx)
|
||||
INTERNAL_ERROR = "INTERNAL_ERROR"
|
||||
AUTH_HASHING_UNAVAILABLE = "AUTH_HASHING_UNAVAILABLE"
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Standard error response format"""
|
||||
|
||||
error: ErrorCode = Field(
|
||||
...,
|
||||
description="Code d'erreur standardisé",
|
||||
example="INVALID_FORMAT"
|
||||
)
|
||||
message: str = Field(
|
||||
...,
|
||||
description="Message d'erreur lisible en français",
|
||||
example="Format de fichier non supporté. Formats acceptés: .xlsx, .docx, .pptx"
|
||||
)
|
||||
details: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Détails supplémentaires sur l'erreur"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"error": "INVALID_FORMAT",
|
||||
"message": "Format PDF non supporté. Formats acceptés: .xlsx, .docx, .pptx",
|
||||
"details": {
|
||||
"accepted_formats": [".xlsx", ".docx", ".pptx"],
|
||||
"detected_format": ".pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Pre-defined error examples for OpenAPI documentation
|
||||
ERROR_EXAMPLES = {
|
||||
"INVALID_FORMAT": {
|
||||
"summary": "Format de fichier non supporté",
|
||||
"value": {
|
||||
"error": "INVALID_FORMAT",
|
||||
"message": "Format PDF non supporté. Formats acceptés: .xlsx, .docx, .pptx",
|
||||
"details": {
|
||||
"accepted_formats": [".xlsx", ".docx", ".pptx"],
|
||||
"detected_format": ".pdf"
|
||||
}
|
||||
}
|
||||
},
|
||||
"CORRUPTED_FILE": {
|
||||
"summary": "Fichier corrompu",
|
||||
"value": {
|
||||
"error": "CORRUPTED_FILE",
|
||||
"message": "Le fichier n'est pas un document Office valide ou est corrompu.",
|
||||
"details": {
|
||||
"reason": "Invalid magic bytes"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FILE_TOO_LARGE": {
|
||||
"summary": "Fichier trop volumineux",
|
||||
"value": {
|
||||
"error": "FILE_TOO_LARGE",
|
||||
"message": "Le fichier dépasse la limite de 50 MB.",
|
||||
"details": {
|
||||
"max_size_mb": 50,
|
||||
"actual_size_mb": 65.3
|
||||
}
|
||||
}
|
||||
},
|
||||
"QUOTA_EXCEEDED": {
|
||||
"summary": "Limite quotidienne atteinte",
|
||||
"value": {
|
||||
"error": "QUOTA_EXCEEDED",
|
||||
"message": "Limite quotidienne de 5 traductions atteinte. Réessayez après minuit UTC.",
|
||||
"details": {
|
||||
"current_usage": 5,
|
||||
"limit": 5,
|
||||
"tier": "free",
|
||||
"reset_at": "2024-01-16T00:00:00Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"UNAUTHORIZED": {
|
||||
"summary": "Authentification requise",
|
||||
"value": {
|
||||
"error": "UNAUTHORIZED",
|
||||
"message": "Authentification requise.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"FORBIDDEN": {
|
||||
"summary": "Accès interdit",
|
||||
"value": {
|
||||
"error": "FORBIDDEN",
|
||||
"message": "Vous n'avez pas accès à cette ressource.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"INVALID_CREDENTIALS": {
|
||||
"summary": "Identifiants invalides",
|
||||
"value": {
|
||||
"error": "INVALID_CREDENTIALS",
|
||||
"message": "Email ou mot de passe incorrect.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"EMAIL_EXISTS": {
|
||||
"summary": "Email déjà utilisé",
|
||||
"value": {
|
||||
"error": "EMAIL_EXISTS",
|
||||
"message": "Un compte existe déjà avec cette adresse email.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"TOKEN_EXPIRED": {
|
||||
"summary": "Token expiré",
|
||||
"value": {
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token invalide ou expiré.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"PRO_FEATURE_REQUIRED": {
|
||||
"summary": "Fonctionnalité Pro requise",
|
||||
"value": {
|
||||
"error": "PRO_FEATURE_REQUIRED",
|
||||
"message": "Cette fonctionnalité nécessite un abonnement Pro.",
|
||||
"details": {
|
||||
"feature": "llm_translation",
|
||||
"current_tier": "free",
|
||||
"required_tier": "pro"
|
||||
}
|
||||
}
|
||||
},
|
||||
"API_KEY_NOT_FOUND": {
|
||||
"summary": "Clé API non trouvée",
|
||||
"value": {
|
||||
"error": "API_KEY_NOT_FOUND",
|
||||
"message": "Clé API non trouvée, n'appartient pas à l'utilisateur ou déjà révoquée.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"FILE_EXPIRED": {
|
||||
"summary": "Fichier expiré",
|
||||
"value": {
|
||||
"error": "FILE_EXPIRED",
|
||||
"message": "Le fichier traduit n'est plus disponible ou a expiré.",
|
||||
"details": {
|
||||
"job_id": "tr_abc123",
|
||||
"status": "not_found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NOT_READY": {
|
||||
"summary": "Traduction en cours",
|
||||
"value": {
|
||||
"error": "NOT_READY",
|
||||
"message": "La traduction est encore en cours.",
|
||||
"details": {
|
||||
"job_id": "tr_abc123",
|
||||
"status": "processing",
|
||||
"progress_percent": 45
|
||||
}
|
||||
}
|
||||
},
|
||||
"NOT_FOUND": {
|
||||
"summary": "Ressource non trouvée",
|
||||
"value": {
|
||||
"error": "NOT_FOUND",
|
||||
"message": "Ressource non trouvée.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"INTERNAL_ERROR": {
|
||||
"summary": "Erreur interne",
|
||||
"value": {
|
||||
"error": "INTERNAL_ERROR",
|
||||
"message": "Une erreur interne est survenue. Veuillez réessayer.",
|
||||
"details": None
|
||||
}
|
||||
},
|
||||
"INVALID_WEBHOOK_URL": {
|
||||
"summary": "URL webhook invalide",
|
||||
"value": {
|
||||
"error": "INVALID_WEBHOOK_URL",
|
||||
"message": "L'URL du webhook doit être une URL HTTP/HTTPS valide.",
|
||||
"details": {
|
||||
"field": "webhook_url",
|
||||
"allowed_schemes": ["http", "https"],
|
||||
"hint": "L'URL doit commencer par http:// ou https://"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WEBHOOK_LOCALHOST_BLOCKED": {
|
||||
"summary": "Localhost non autorisé",
|
||||
"value": {
|
||||
"error": "INVALID_WEBHOOK_URL",
|
||||
"message": "Les URLs localhost ne sont pas autorisées.",
|
||||
"details": {
|
||||
"field": "webhook_url",
|
||||
"reason": "localhost_blocked"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WEBHOOK_PRIVATE_IP_BLOCKED": {
|
||||
"summary": "IP privée non autorisée",
|
||||
"value": {
|
||||
"error": "INVALID_WEBHOOK_URL",
|
||||
"message": "Les adresses IP privées ne sont pas autorisées.",
|
||||
"details": {
|
||||
"field": "webhook_url",
|
||||
"reason": "private_ip_blocked"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WEBHOOK_CREDENTIALS_IN_URL": {
|
||||
"summary": "Credentials dans l'URL",
|
||||
"value": {
|
||||
"error": "INVALID_WEBHOOK_URL",
|
||||
"message": "L'URL ne doit pas contenir d'identifiants (credentials).",
|
||||
"details": {
|
||||
"field": "webhook_url",
|
||||
"reason": "credentials_in_url"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
schemas/glossary_schemas.py
Normal file
100
schemas/glossary_schemas.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Pydantic schemas for glossary endpoints.
|
||||
Story 3.9: Glossaires - Endpoint CRUD
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class GlossaryTermCreate(BaseModel):
|
||||
"""Schema for creating a single term."""
|
||||
|
||||
source: str = Field(..., min_length=1, max_length=500, description="Terme source")
|
||||
target: str = Field(
|
||||
..., min_length=1, max_length=500, description="Traduction cible"
|
||||
)
|
||||
|
||||
@field_validator("source", "target")
|
||||
@classmethod
|
||||
def strip_whitespace(cls, v: str) -> str:
|
||||
return v.strip()
|
||||
|
||||
|
||||
class GlossaryTermResponse(BaseModel):
|
||||
"""Schema for term in response."""
|
||||
|
||||
id: str
|
||||
source: str
|
||||
target: str
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class GlossaryCreate(BaseModel):
|
||||
"""Schema for creating a glossary."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255, description="Nom du glossaire")
|
||||
terms: list[GlossaryTermCreate] = Field(
|
||||
default_factory=list, description="Liste des termes"
|
||||
)
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def strip_name(cls, v: str) -> str:
|
||||
return v.strip()
|
||||
|
||||
|
||||
class GlossaryUpdate(BaseModel):
|
||||
"""Schema for updating a glossary (all fields optional)."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
terms: Optional[list[GlossaryTermCreate]] = Field(None)
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def strip_name(cls, v: Optional[str]) -> Optional[str]:
|
||||
return v.strip() if v else None
|
||||
|
||||
|
||||
class GlossaryResponse(BaseModel):
|
||||
"""Schema for glossary in response (with full terms)."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
terms: list[GlossaryTermResponse] = []
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class GlossaryListItem(BaseModel):
|
||||
"""Schema for glossary in list (without full terms)."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
terms_count: int = Field(
|
||||
default=0, description="Nombre de termes dans le glossaire"
|
||||
)
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class GlossaryListResponse(BaseModel):
|
||||
"""Schema for glossaries list response."""
|
||||
|
||||
data: list[GlossaryListItem] = []
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class GlossaryDetailResponse(BaseModel):
|
||||
"""Schema for single glossary response."""
|
||||
|
||||
data: GlossaryResponse
|
||||
meta: dict = Field(default_factory=dict)
|
||||
79
schemas/prompt_schemas.py
Normal file
79
schemas/prompt_schemas.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Pydantic schemas for custom prompt endpoints.
|
||||
Story 3.11: Custom Prompts - Endpoint CRUD
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class PromptCreate(BaseModel):
|
||||
"""Schema for creating a prompt."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255, description="Nom du prompt")
|
||||
content: str = Field(
|
||||
..., min_length=1, max_length=10000, description="Contenu du prompt"
|
||||
)
|
||||
|
||||
@field_validator("name", "content")
|
||||
@classmethod
|
||||
def strip_whitespace(cls, v: str) -> str:
|
||||
return v.strip()
|
||||
|
||||
|
||||
class PromptUpdate(BaseModel):
|
||||
"""Schema for updating a prompt (at least one field required)."""
|
||||
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
content: Optional[str] = Field(None, min_length=1, max_length=10000)
|
||||
|
||||
@field_validator("name", "content")
|
||||
@classmethod
|
||||
def strip_whitespace(cls, v: Optional[str]) -> Optional[str]:
|
||||
return v.strip() if v else None
|
||||
|
||||
def has_updates(self) -> bool:
|
||||
"""Check if at least one field is provided for update."""
|
||||
return self.name is not None or self.content is not None
|
||||
|
||||
|
||||
class PromptResponse(BaseModel):
|
||||
"""Schema for prompt in response."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
content: str
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class PromptListItem(BaseModel):
|
||||
"""Schema for prompt in list (lighter version)."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
content_preview: str = Field(
|
||||
..., description="First 100 chars of content for list view"
|
||||
)
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class PromptListResponse(BaseModel):
|
||||
"""Schema for prompts list response."""
|
||||
|
||||
data: list[PromptListItem] = []
|
||||
meta: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class PromptDetailResponse(BaseModel):
|
||||
"""Schema for single prompt response."""
|
||||
|
||||
data: PromptResponse
|
||||
meta: dict = Field(default_factory=dict)
|
||||
236
schemas/translation.py
Normal file
236
schemas/translation.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""
|
||||
Pydantic models for translation endpoints
|
||||
Story 3.6: Documentation OpenAPI (Swagger + ReDoc)
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Literal, List, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TranslateResponseData(BaseModel):
|
||||
"""Response data for translation request"""
|
||||
|
||||
id: str = Field(
|
||||
...,
|
||||
example="tr_abc123def456",
|
||||
description="Identifiant unique du job de traduction"
|
||||
)
|
||||
status: Literal["processing"] = Field(
|
||||
default="processing",
|
||||
description="Statut du job (toujours 'processing' à la création)"
|
||||
)
|
||||
file_name: Optional[str] = Field(
|
||||
None,
|
||||
example="rapport_financier.xlsx",
|
||||
description="Nom du fichier original"
|
||||
)
|
||||
source_lang: Optional[str] = Field(
|
||||
None,
|
||||
example="en",
|
||||
description="Code langue source (ISO 639-1)"
|
||||
)
|
||||
target_lang: str = Field(
|
||||
...,
|
||||
example="fr",
|
||||
description="Code langue cible (ISO 639-1)"
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"id": "tr_abc123def456",
|
||||
"status": "processing",
|
||||
"file_name": "rapport_financier.xlsx",
|
||||
"source_lang": "en",
|
||||
"target_lang": "fr"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TranslateResponseMeta(BaseModel):
|
||||
"""Metadata for translation response"""
|
||||
|
||||
rate_limit_remaining: int = Field(
|
||||
...,
|
||||
description="Nombre de traductions restantes aujourd'hui",
|
||||
example=4
|
||||
)
|
||||
estimated_time_seconds: Optional[int] = Field(
|
||||
None,
|
||||
description="Temps estimé pour la traduction en secondes",
|
||||
example=15
|
||||
)
|
||||
|
||||
|
||||
class TranslateResponse(BaseModel):
|
||||
"""Full response for translation request"""
|
||||
|
||||
data: TranslateResponseData
|
||||
meta: TranslateResponseMeta
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "tr_abc123def456",
|
||||
"status": "processing",
|
||||
"file_name": "rapport_financier.xlsx",
|
||||
"source_lang": "en",
|
||||
"target_lang": "fr"
|
||||
},
|
||||
"meta": {
|
||||
"rate_limit_remaining": 4,
|
||||
"estimated_time_seconds": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TranslationStatusData(BaseModel):
|
||||
"""Response data for translation status endpoint"""
|
||||
|
||||
id: str = Field(
|
||||
...,
|
||||
description="Identifiant unique du job de traduction",
|
||||
example="tr_abc123def456"
|
||||
)
|
||||
status: Literal["queued", "processing", "completed", "failed"] = Field(
|
||||
...,
|
||||
description="Statut actuel du job"
|
||||
)
|
||||
progress_percent: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
le=100,
|
||||
description="Pourcentage de progression (0-100)",
|
||||
example=65
|
||||
)
|
||||
current_step: str = Field(
|
||||
default="Initializing",
|
||||
description="Description de l'opération en cours",
|
||||
example="Traduction de la diapositive 3/5"
|
||||
)
|
||||
file_name: Optional[str] = Field(
|
||||
None,
|
||||
description="Nom du fichier original",
|
||||
example="presentation.pptx"
|
||||
)
|
||||
source_lang: Optional[str] = Field(
|
||||
None,
|
||||
description="Code langue source",
|
||||
example="en"
|
||||
)
|
||||
target_lang: Optional[str] = Field(
|
||||
None,
|
||||
description="Code langue cible",
|
||||
example="fr"
|
||||
)
|
||||
created_at: Optional[str] = Field(
|
||||
None,
|
||||
description="Date de création (ISO 8601)",
|
||||
example="2024-01-15T10:30:00Z"
|
||||
)
|
||||
completed_at: Optional[str] = Field(
|
||||
None,
|
||||
description="Date de complétion (ISO 8601)",
|
||||
example="2024-01-15T10:35:00Z"
|
||||
)
|
||||
failed_at: Optional[str] = Field(
|
||||
None,
|
||||
description="Date d'échec (ISO 8601)",
|
||||
example=None
|
||||
)
|
||||
error_message: Optional[str] = Field(
|
||||
None,
|
||||
description="Message d'erreur si status='failed'",
|
||||
example=None
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"id": "tr_abc123def456",
|
||||
"status": "processing",
|
||||
"progress_percent": 65,
|
||||
"current_step": "Traduction de la diapositive 3/5",
|
||||
"file_name": "presentation.pptx",
|
||||
"source_lang": "en",
|
||||
"target_lang": "fr",
|
||||
"created_at": "2024-01-15T10:30:00Z",
|
||||
"completed_at": None,
|
||||
"failed_at": None,
|
||||
"error_message": None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TranslationStatusMeta(BaseModel):
|
||||
"""Metadata for translation status response"""
|
||||
|
||||
estimated_remaining_seconds: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Temps restant estimé en secondes",
|
||||
example=30
|
||||
)
|
||||
|
||||
|
||||
class TranslationStatusResponse(BaseModel):
|
||||
"""Full response for translation status endpoint"""
|
||||
|
||||
data: TranslationStatusData
|
||||
meta: Optional[TranslationStatusMeta] = None
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"data": {
|
||||
"id": "tr_abc123def456",
|
||||
"status": "processing",
|
||||
"progress_percent": 65,
|
||||
"current_step": "Traduction de la diapositive 3/5",
|
||||
"file_name": "presentation.pptx",
|
||||
"source_lang": "en",
|
||||
"target_lang": "fr",
|
||||
"created_at": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
"meta": {
|
||||
"estimated_remaining_seconds": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LanguageItem(BaseModel):
|
||||
"""Single language entry"""
|
||||
|
||||
code: str = Field(
|
||||
...,
|
||||
description="Code langue ISO 639-1",
|
||||
example="fr"
|
||||
)
|
||||
name: str = Field(
|
||||
...,
|
||||
description="Nom de la langue en français",
|
||||
example="Français"
|
||||
)
|
||||
|
||||
|
||||
class LanguageResponse(BaseModel):
|
||||
"""Response model for supported languages"""
|
||||
|
||||
supported_languages: Dict[str, str] = Field(
|
||||
...,
|
||||
description="Dictionnaire des langues supportées (code -> nom)",
|
||||
example={
|
||||
"fr": "French",
|
||||
"de": "German",
|
||||
"es": "Spanish",
|
||||
"it": "Italian"
|
||||
}
|
||||
)
|
||||
note: Optional[str] = Field(
|
||||
None,
|
||||
description="Note sur la disponibilité des langues",
|
||||
example="Les langues supportées peuvent varier selon le service de traduction configuré"
|
||||
)
|
||||
Reference in New Issue
Block a user