from datetime import datetime from pydantic import BaseModel, EmailStr, Field, field_validator from typing import Any, Optional import re class UserCreate(BaseModel): email: EmailStr name: str = Field(..., min_length=1, max_length=100) password: str = Field(..., min_length=8) @field_validator('name') def validate_name(cls, v): if not v.strip(): raise ValueError('Name cannot be empty or just whitespace') return v.strip() @field_validator('password') def validate_password(cls, v): if not validate_password_strength(v): raise ValueError( 'Password must be at least 8 characters long and contain ' 'at least one uppercase letter, one lowercase letter, ' 'one digit' ) return v class Token(BaseModel): access_token: str token_type: str = "bearer" expires_in: int email: EmailStr name: str class LogoutResponse(BaseModel): message: str class MessageResponse(BaseModel): message: str class EmailVerificationRequest(BaseModel): email: EmailStr class UserPublic(BaseModel): id: str email: EmailStr name: str created_at: datetime model_config = { "json_encoders": { datetime: lambda v: v.isoformat() } } def validate_password_strength(password: str) -> bool: """Validate password meets security requirements""" if len(password) < 8: return False if not re.search(r"[A-Z]", password): return False if not re.search(r"[a-z]", password): return False if not re.search(r"[0-9]", password): return False return True class UserQuery(BaseModel): query:str thread_id: Optional[str] = None create_new_thread: Optional[bool] = False class SourceItem(BaseModel): document_id: str filename: str chunk_index: Optional[int] = None score: Optional[float] = None class ChatResponse(BaseModel): result: str thread_id: str user_id: str is_new_thread: bool sources: list[SourceItem] = Field(default_factory=list) class ThreadItem(BaseModel): thread_id: str title: str created_at: Optional[datetime] = None class ThreadsResponse(BaseModel): threads: list[ThreadItem] total: int class MessageContent(BaseModel): query: Optional[str] = None messages: list[Any] = Field(default_factory=list) @field_validator("messages", mode="before") @classmethod def normalize_messages(cls, v): """Always coerce a bare string into a single-element list.""" if isinstance(v, str): return [v] if v is None: return [] return v class ThreadMessagesResponse(BaseModel): thread_id: str messages: MessageContent class DocumentItem(BaseModel): document_id: str filename: str content_type: str status: str chunk_count: int = 0 created_at: datetime updated_at: datetime error_message: Optional[str] = None class DocumentListResponse(BaseModel): documents: list[DocumentItem] total: int class DocumentUploadResponse(BaseModel): message: str document: DocumentItem class DocumentDeleteResponse(BaseModel): message: str document_id: str