Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

115 lines
4.3 KiB
Python

"""Common types and data models for user service."""
from datetime import datetime
from pydantic import BaseModel, Field, field_validator
from pydantic_core import PydanticCustomError
def validate_email_with_special_domains(email: str) -> str:
"""Validate email address, allowing special-use domains like .local for testing.
This validator first tries standard email validation using email-validator library.
If it fails due to special-use domains (like .local, .test, .localhost), it performs
a basic syntax check instead. This allows development/testing with non-routable domains
while still catching actual typos and malformed emails.
Args:
email: The email address to validate
Returns:
The validated email address (lowercased)
Raises:
PydanticCustomError: If the email format is invalid
"""
try:
# Try standard email validation using email-validator
from email_validator import EmailNotValidError, validate_email
result = validate_email(email, check_deliverability=False)
return result.normalized
except EmailNotValidError as e:
error_msg = str(e)
# Check if the error is specifically about special-use/reserved domains or localhost
if (
"special-use" in error_msg.lower()
or "reserved" in error_msg.lower()
or "should have a period" in error_msg.lower()
):
# Perform basic email syntax validation
email = email.strip().lower()
if "@" not in email:
raise PydanticCustomError(
"value_error",
"Email address must contain an @ symbol",
)
local_part, domain = email.rsplit("@", 1)
if not local_part or not domain:
raise PydanticCustomError(
"value_error",
"Email address must have both local and domain parts",
)
# Allow localhost and domains with dots
if domain == "localhost" or "." in domain:
return email
raise PydanticCustomError(
"value_error",
"Email domain must contain a dot or be 'localhost'",
)
else:
# Re-raise other validation errors
raise PydanticCustomError(
"value_error",
f"Invalid email address: {error_msg}",
)
class UserDTO(BaseModel):
"""User data transfer object."""
user_id: str = Field(description="Unique user identifier")
email: str = Field(description="User email address")
display_name: str | None = Field(default=None, description="Display name")
is_admin: bool = Field(default=False, description="Whether user has admin privileges")
is_active: bool = Field(default=True, description="Whether user account is active")
created_at: datetime = Field(description="When the user was created")
updated_at: datetime = Field(description="When the user was last updated")
last_login_at: datetime | None = Field(default=None, description="When user last logged in")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email address, allowing special-use domains."""
return validate_email_with_special_domains(v)
class UserCreateRequest(BaseModel):
"""Request to create a new user."""
email: str = Field(description="User email address")
display_name: str | None = Field(default=None, description="Display name")
password: str = Field(description="User password")
is_admin: bool = Field(default=False, description="Whether user should have admin privileges")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email address, allowing special-use domains."""
return validate_email_with_special_domains(v)
class UserUpdateRequest(BaseModel):
"""Request to update a user."""
display_name: str | None = Field(default=None, description="Display name")
password: str | None = Field(default=None, description="New password")
is_admin: bool | None = Field(default=None, description="Whether user should have admin privileges")
is_active: bool | None = Field(default=None, description="Whether user account should be active")