โโโโimport enum
โโโโimport hashlib
โโโโimport re
โโโโfrom collections.abc import Callable
โโโโfrom typing import Any, Self
โโโโfrom pydantic import (
โโโโ BaseModel,
โโโโ EmailStr,
โโโโ Field,
โโโโ SecretStr,
โโโโ SerializationInfo,
โโโโ field_serializer,
โโโโ field_validator,
โโโโ model_serializer,
โโโโ model_validator,
โโโโ)
โโโโVALID_PASSWORD_REGEX = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$")
โโโโVALID_NAME_REGEX = re.compile(r"^[a-zA-Z]{2,}$")
โโโโclass Role(enum.IntFlag):
โโโโ User = 0
โโโโ Author = 1
โโโโ Editor = 2
โโโโ Admin = 4
โโโโ SuperAdmin = 8
โโโโclass User(BaseModel):
โโโโ name: str = Field(examples=["Example"])
โโโโ email: EmailStr = Field(
โโโโ examples=["user@arjancodes.com"],
โโโโ description="The email address of the user",
โโโโ frozen=True,
โโโโ )
โโโโ password: SecretStr = Field(
โโโโ examples=["Password123"],
โโโโ description="The password of the user",
โโโโ exclude=True,
โโโโ )
โโโโ role: Role = Field(
โโโโ description="The role of the user",
โโโโ examples=[1, 2, 4, 8],
โโโโ default=0,
โโโโ validate_default=True,
โโโโ )
โโโโ @field_validator("name")
โโโโ def validate_name(cls, v: str) -> str:
โโโโ if not VALID_NAME_REGEX.match(v):
โโโโ raise ValueError(
โโโโ "Name is invalid, must contain only letters and be at least 2 characters long"
โโโโ )
โโโโ return v
โโโโ @field_validator("role", mode="before")
โโโโ @classmethod
โโโโ def validate_role(cls, v: int | str | Role) -> Role:
โโโโ op = {int: lambda x: Role(x), str: lambda x: Role[x], Role: lambda x: x}
โโโโ try:
โโโโ return op[type(v)](v)
โโโโ except (KeyError, ValueError) as exc:
โโโโ roles = ", ".join([x.name for x in Role])
โโโโ raise ValueError(f"Role is invalid, please use one of the following: {roles}") from exc
โโโโ @model_validator(mode="before")
โโโโ @classmethod
โโโโ def validate_user_pre(cls, v: dict[str, Any]) -> dict[str, Any]:
โโโโ if "name" not in v or "password" not in v:
โโโโ raise ValueError("Name and password are required")
โโโโ if v["name"].casefold() in v["password"].casefold():
โโโโ raise ValueError("Password cannot contain name")
โโโโ if not VALID_PASSWORD_REGEX.match(v["password"]):
โโโโ raise ValueError(
โโโโ "Password is invalid, must contain 8 characters, 1 uppercase, 1 lowercase, 1 number"
โโโโ )
โโโโ v["password"] = hashlib.sha256(v["password"].encode()).hexdigest()
โโโโ return v
โโโโ @field_serializer("role", when_used="json")
โโโโ @classmethod
โโโโ def serialize_role(cls, v: Role) -> str:
โโโโ return v.name
โโโโ @model_validator(mode="after")
โโโโ def validate_user_post(self, v: Any) -> Self:
โโโโ if self.role == Role.Admin and self.name != "Arjan":
โโโโ raise ValueError("Only Arjan can be an admin")
โโโโ return self
โโโโ @model_serializer(mode="wrap", when_used="json")
โโโโ def serialize_user(
โโโโ self,
โโโโ serializer: Callable[[BaseModel], dict[str, Any]],
โโโโ info: SerializationInfo,
โโโโ ) -> dict[str, Any]:
โโโโ if not info.include and not info.exclude:
โโโโ return {"name": self.name, "role": self.role.name}
โโโโ return serializer(self)
โโโโdef main() -> None:
โโโโ data = {
โโโโ "name": "Arjan",
โโโโ "email": "example@arjancodes.com",
โโโโ "password": "Password123",
โโโโ "role": "Admin",
โโโโ }
โโโโ user = User.model_validate(data)
โโโโ if user:
โโโโ print(
โโโโ "The serializer that returns a dict:",
โโโโ user.model_dump(),
โโโโ sep="\n",
โโโโ end="\n\n",
โโโโ )
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ print(
โโโโ "The serializer that returns a JSON string:",
โโโโ user.model_dump(mode="json"),
โโโโ sep="\n",
โโโโ end="\n\n",
โโโโ )
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ print(
โโโโ "The serializer that returns a json string, excluding the role:",
โโโโ user.model_dump(exclude=["role"], mode="json"),
โโโโ sep="\n",
โโโโ end="\n\n",
โโโโ )
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ print("The serializer that encodes all values to a dict:", dict(user), sep="\n")
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโ
โโโโif __name__ == "__main__":
โโโโ main()