"""User domain Pydantic schemas."""
from __future__ import annotations
import datetime
from typing import Annotated
from uuid import UUID
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
from pydotorg.domains.users.models import EmailPrivacy, MembershipType, SearchVisibility, UserGroupType
MIN_PASSWORD_LENGTH = 8
[docs]
class UserBase(BaseModel):
"""Base user schema with common fields."""
username: Annotated[str, Field(min_length=1, max_length=150)]
email: EmailStr
first_name: Annotated[str, Field(max_length=150)] = ""
last_name: Annotated[str, Field(max_length=150)] = ""
bio: str = ""
search_visibility: SearchVisibility = SearchVisibility.PUBLIC
email_privacy: EmailPrivacy = EmailPrivacy.PRIVATE
public_profile: bool = True
[docs]
class UserCreate(UserBase):
"""Schema for creating a new user."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"username": "guido_van_rossum",
"email": "guido@python.org",
"password": "SecurePass123!",
"first_name": "Guido",
"last_name": "van Rossum",
"bio": "Python creator and BDFL",
"search_visibility": "public",
"email_privacy": "private",
"public_profile": True,
}
}
)
password: Annotated[str, Field(min_length=8, max_length=255)]
@field_validator("password")
@classmethod
def validate_password_strength(cls, v: str) -> str:
if len(v) < MIN_PASSWORD_LENGTH:
msg = f"Password must be at least {MIN_PASSWORD_LENGTH} characters long"
raise ValueError(msg)
return v
[docs]
class UserUpdate(BaseModel):
"""Schema for updating an existing user."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"first_name": "Guido",
"last_name": "van Rossum",
"bio": "Python creator, former BDFL, now retired from leadership but still contributing to Python development",
"search_visibility": "public",
"email_privacy": "private",
}
}
)
email: EmailStr | None = None
first_name: Annotated[str, Field(max_length=150)] | None = None
last_name: Annotated[str, Field(max_length=150)] | None = None
bio: str | None = None
search_visibility: SearchVisibility | None = None
email_privacy: EmailPrivacy | None = None
public_profile: bool | None = None
[docs]
class UserRead(UserBase):
"""Schema for reading user data."""
model_config = ConfigDict(
from_attributes=True,
json_schema_extra={
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"username": "guido_van_rossum",
"email": "guido@python.org",
"first_name": "Guido",
"last_name": "van Rossum",
"bio": "Python creator and BDFL",
"search_visibility": "public",
"email_privacy": "private",
"public_profile": True,
"is_active": True,
"is_staff": False,
"is_superuser": False,
"date_joined": "2025-01-15T10:30:00Z",
"last_login": "2025-11-29T08:45:00Z",
"created_at": "2025-01-15T10:30:00Z",
"updated_at": "2025-11-20T14:22:00Z",
}
},
)
id: UUID
is_active: bool
is_staff: bool
is_superuser: bool
date_joined: datetime.datetime
last_login: datetime.datetime | None
created_at: datetime.datetime
updated_at: datetime.datetime
@property
def full_name(self) -> str:
"""Get the user's full name."""
return f"{self.first_name} {self.last_name}".strip()
[docs]
class UserPublic(BaseModel):
"""Public user schema with limited fields."""
id: UUID
username: str
first_name: str
last_name: str
bio: str
model_config = ConfigDict(from_attributes=True)
@property
def full_name(self) -> str:
"""Get the user's full name."""
return f"{self.first_name} {self.last_name}".strip()
[docs]
class MembershipBase(BaseModel):
"""Base membership schema."""
membership_type: MembershipType = MembershipType.BASIC
legal_name: Annotated[str, Field(max_length=255)] = ""
preferred_name: Annotated[str, Field(max_length=255)] = ""
email_address: EmailStr | str = ""
city: Annotated[str, Field(max_length=100)] = ""
region: Annotated[str, Field(max_length=100)] = ""
country: Annotated[str, Field(max_length=100)] = ""
postal_code: Annotated[str, Field(max_length=20)] = ""
psf_code_of_conduct: bool = False
psf_announcements: bool = False
votes: bool = False
[docs]
class MembershipCreate(MembershipBase):
"""Schema for creating a new membership."""
user_id: UUID
[docs]
class MembershipUpdate(BaseModel):
"""Schema for updating a membership."""
membership_type: MembershipType | None = None
legal_name: Annotated[str, Field(max_length=255)] | None = None
preferred_name: Annotated[str, Field(max_length=255)] | None = None
email_address: EmailStr | str | None = None
city: Annotated[str, Field(max_length=100)] | None = None
region: Annotated[str, Field(max_length=100)] | None = None
country: Annotated[str, Field(max_length=100)] | None = None
postal_code: Annotated[str, Field(max_length=20)] | None = None
psf_code_of_conduct: bool | None = None
psf_announcements: bool | None = None
votes: bool | None = None
[docs]
class MembershipRead(MembershipBase):
"""Schema for reading membership data."""
id: UUID
user_id: UUID
last_vote_affirmation: datetime.date | None
created_at: datetime.datetime
updated_at: datetime.datetime
model_config = ConfigDict(from_attributes=True)
[docs]
class UserGroupBase(BaseModel):
"""Base user group schema."""
name: Annotated[str, Field(max_length=255)]
location: Annotated[str, Field(max_length=255)] = ""
url: Annotated[str, Field(max_length=500)] = ""
url_type: UserGroupType = UserGroupType.OTHER
approved: bool = False
trusted: bool = False
[docs]
class UserGroupCreate(UserGroupBase):
"""Schema for creating a new user group."""
start_date: datetime.date | None = None
[docs]
class UserGroupUpdate(BaseModel):
"""Schema for updating a user group."""
name: Annotated[str, Field(max_length=255)] | None = None
location: Annotated[str, Field(max_length=255)] | None = None
url: Annotated[str, Field(max_length=500)] | None = None
url_type: UserGroupType | None = None
start_date: datetime.date | None = None
approved: bool | None = None
trusted: bool | None = None
[docs]
class UserGroupRead(UserGroupBase):
"""Schema for reading user group data."""
id: UUID
start_date: datetime.date | None
created_at: datetime.datetime
updated_at: datetime.datetime
model_config = ConfigDict(from_attributes=True)
[docs]
class APIKeyCreate(BaseModel):
"""Schema for creating a new API key."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "CI/CD Pipeline Key",
"description": "API key for automated deployments",
"expires_in_days": 365,
}
}
)
name: Annotated[str, Field(min_length=1, max_length=100)]
description: str = ""
expires_in_days: Annotated[int, Field(ge=1, le=3650)] | None = None
[docs]
class APIKeyRead(BaseModel):
"""Schema for reading API key data (without the actual key)."""
model_config = ConfigDict(from_attributes=True)
id: UUID
user_id: UUID
name: str
key_prefix: str
description: str
is_active: bool
expires_at: datetime.datetime | None
last_used_at: datetime.datetime | None
created_at: datetime.datetime
updated_at: datetime.datetime
[docs]
class APIKeyCreated(APIKeyRead):
"""Schema returned when creating a new API key (includes the raw key)."""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440001",
"name": "CI/CD Pipeline Key",
"key": "pyorg_abc123xyz789...",
"key_prefix": "pyorg_abc123",
"description": "API key for automated deployments",
"is_active": True,
"expires_at": "2026-12-13T18:00:00Z",
"last_used_at": None,
"created_at": "2025-12-13T18:00:00Z",
"updated_at": "2025-12-13T18:00:00Z",
}
}
)
key: str