from datetime import datetime
from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship, Column, String
from pydantic import EmailStr, field_validator
import re
class UserBase(SQLModel):
"""Base user model with shared fields."""
username: str = Field(min_length=3, max_length=50, sa_column=Column(String(50), unique=True))
email: EmailStr = Field(sa_column=Column(String(255), unique=True))
full_name: Optional[str] = Field(default=None, max_length=100)
is_active: bool = Field(default=True)
@field_validator("username")
@classmethod
def validate_username(cls, v: str) -> str:
if not re.match("^[a-zA-Z0-9_-]+$", v):
raise ValueError("Username can only contain letters, numbers, underscores, and hyphens")
return v
class User(UserBase, table=True):
"""User database model."""
id: Optional[int] = Field(default=None, primary_key=True)
hashed_password: str = Field(min_length=8)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: Optional[datetime] = Field(default=None)
# Relationships
posts: List["Post"] = Relationship(back_populates="author")
class UserCreate(UserBase):
"""User creation model."""
password: str = Field(min_length=8)
@field_validator("password")
@classmethod
def validate_password(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
if not re.search(r"[A-Za-z]", v):
raise ValueError("Password must contain at least one letter")
if not re.search(r"\d", v):
raise ValueError("Password must contain at least one digit")
return v
class UserRead(UserBase):
"""User read model."""
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class UserUpdate(SQLModel):
"""User update model."""
username: Optional[str] = Field(default=None, min_length=3, max_length=50)
email: Optional[EmailStr] = None
full_name: Optional[str] = Field(default=None, max_length=100)
is_active: Optional[bool] = None
password: Optional[str] = Field(default=None, min_length=8)
@field_validator("username")
@classmethod
def validate_username(cls, v: Optional[str]) -> Optional[str]:
if v is not None and not re.match("^[a-zA-Z0-9_-]+$", v):
raise ValueError("Username can only contain letters, numbers, underscores, and hyphens")
return v
@field_validator("password")
@classmethod
def validate_password(cls, v: Optional[str]) -> Optional[str]:
if v is not None:
if len(v) < 8:
raise ValueError("Password must be at least 8 characters long")
if not re.search(r"[A-Za-z]", v):
raise ValueError("Password must contain at least one letter")
if not re.search(r"\d", v):
raise ValueError("Password must contain at least one digit")
return v