"""Pydantic schemas for User management endpoints.""" # Import re import re # Import uuid import uuid # Import datetime from datetime from datetime import datetime # Import BaseModel, ConfigDict, field_validator from pydantic from pydantic import BaseModel, ConfigDict, field_validator # ── Username policy ───────────────────────────────────────────────── _USERNAME_RE = re.compile(r"^[a-zA-Z0-9_-]{3,50}$") # Assign _RESERVED_USERNAMES = frozenset({ _RESERVED_USERNAMES = frozenset({ # Literal argument value "admin", "root", "system", "api", "null", "undefined", # Literal argument value "administrator", "superuser", "aegis", }) # Define function _validate_username def _validate_username(username: str) -> str: """Validate username format and reject reserved names. Args: username (str): The username string to validate. Returns: str: The validated username, unchanged. """ # Check: not _USERNAME_RE.match(username) if not _USERNAME_RE.match(username): # Raise ValueError raise ValueError( # Literal argument value "Username must be 3-50 characters, containing only " # Literal argument value "letters, digits, underscores, and hyphens" ) # Check: username.lower() in _RESERVED_USERNAMES if username.lower() in _RESERVED_USERNAMES: # Raise ValueError raise ValueError(f"Username '{username}' is reserved") # Return username return username # ── Password policy ───────────────────────────────────────────────── _MIN_PASSWORD_LENGTH = 12 # Assign _PASSWORD_RULES = [ _PASSWORD_RULES: list[tuple[str, str]] = [ (r"[A-Z]", "at least one uppercase letter"), (r"[a-z]", "at least one lowercase letter"), (r"[0-9]", "at least one digit"), (r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?`~]", "at least one special character"), ] # Define function _validate_password_strength def _validate_password_strength(password: str) -> str: """Check that *password* satisfies the complexity policy. Rules: - Minimum 12 characters - At least one uppercase letter - At least one lowercase letter - At least one digit - At least one special character Args: password (str): The plaintext password to validate. Returns: str: The validated password, unchanged. """ # Assign errors = [] errors: list[str] = [] # Check: len(password) < _MIN_PASSWORD_LENGTH if len(password) < _MIN_PASSWORD_LENGTH: # Call errors.append() errors.append(f"must be at least {_MIN_PASSWORD_LENGTH} characters long") # Iterate over _PASSWORD_RULES for pattern, description in _PASSWORD_RULES: # Check: not re.search(pattern, password) if not re.search(pattern, password): # Call errors.append() errors.append(description) # Check: errors if errors: # Raise ValueError raise ValueError( # Literal argument value "Password does not meet complexity requirements: " + "; ".join(errors) ) # Return password return password # ── Create ────────────────────────────────────────────────────────── class UserCreate(BaseModel): """Payload for creating a new user.""" # username: str username: str # Assign email = None email: str | None = None # password: str password: str # Assign role = "viewer" role: str = "viewer" # Apply the @field_validator decorator @field_validator("username") # Apply the @classmethod decorator @classmethod # Define function username_format def username_format(cls, v: str) -> str: """Validate the username field against the platform policy. Args: v (str): Raw username value from the request body. Returns: str: The validated username. """ # Return _validate_username(v) return _validate_username(v) # Apply the @field_validator decorator @field_validator("password") # Apply the @classmethod decorator @classmethod # Define function password_strength def password_strength(cls, v: str) -> str: """Validate the password field against the complexity policy. Args: v (str): Raw password value from the request body. Returns: str: The validated password. """ # Return _validate_password_strength(v) return _validate_password_strength(v) # ── Update ────────────────────────────────────────────────────────── class UserUpdate(BaseModel): """Payload for partially updating an existing user. Every field is optional so callers send only what changed. """ # Assign email = None email: str | None = None # Assign role = None role: str | None = None # Assign is_active = None is_active: bool | None = None # Assign password = None password: str | None = None # Apply the @field_validator decorator @field_validator("password") # Apply the @classmethod decorator @classmethod # Define function password_strength def password_strength(cls, v: str | None) -> str | None: """Validate the password field when provided. Args: v (str | None): Raw password value, or ``None`` when unchanged. Returns: str | None: The validated password, or ``None``. """ # Check: v is not None if v is not None: # Return _validate_password_strength(v) return _validate_password_strength(v) # Return v return v # ── Read (full) ───────────────────────────────────────────────────── class PasswordChange(BaseModel): """Payload for changing the current user's password.""" # current_password: str current_password: str # new_password: str new_password: str # Apply the @field_validator decorator @field_validator("new_password") # Apply the @classmethod decorator @classmethod # Define function new_password_strength def new_password_strength(cls, v: str) -> str: """Validate the new password against the complexity policy. Args: v (str): Raw new-password value from the request body. Returns: str: The validated new password. """ # Return _validate_password_strength(v) return _validate_password_strength(v) # Define class UserOut class UserOut(BaseModel): """Complete representation returned by the API.""" # id: uuid.UUID id: uuid.UUID # username: str username: str # Assign email = None email: str | None = None # role: str role: str # is_active: bool is_active: bool # Assign must_change_password = True must_change_password: bool = True # Assign created_at = None created_at: datetime | None = None # Assign last_login = None last_login: datetime | None = None # Assign model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)