Skip to content

Models

In simple terms, models are a representation of a database table in the format of an object declared by the language implementing.

User models

Integrating with Saffier, Esmerald already provides some of the models that helps you with the initial configuration.

  1. AbstractUser - The base user class containing all the fields required a user.
  2. User - A subsclass of AbstractUser

User

Extenting the existing User model is as simple as this:

from enum import Enum

from saffier import Database, Registry, fields

from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database = Database("<YOUR-SQL-QUERY_STRING")
models = Registry(database=database)


class UserType(Enum):
    ADMIN = "admin"
    USER = "user"
    OTHER = "other"


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    date_of_birth = fields.DateField()
    is_verified = fields.BooleanField(default=False)
    role = fields.ChoiceField(
        UserType,
        max_length=255,
        null=False,
        default=UserType.USER,
    )

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.role}"


# Create the db and tables
# Don't use this in production! Use Alembic or any tool to manage
# The migrations for you
await models.create_all()

await User.query.create(is_active=False)

user = await User.query.get(id=1)
print(user)
# User(id=1)

This is a clean way of declaring the models and using the Saffier docs, you can easily understand why this is like the way it is.

Meta class

There are way of making the models and the registry cleaner, after all, you might want to use the same registry in different models across multiple applications in your codebase.

One way and a way Esmerald always recommend, is by leveraging the settings.

Leveraging the settings for your models

Let us use the same example but this time, we will be using the settings. Since you can access the settings anywhere in the codebase.

Check it out the example below and how by using the settings, you can literally leverage Esmerald with Saffier.

from typing import Tuple

from saffier import Database, Registry

from esmerald.conf.global_settings import EsmeraldAPISettings


class AppSettings(EsmeraldAPISettings):
    @property
    def registry(self) -> Tuple[Database, Registry]:
        database = Database("<YOUR-SQL-QUERY-STRING")
        return database, Registry(database=database)
from enum import Enum

from saffier import fields

from esmerald.conf import settings
from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database, models = settings.registry


class UserType(Enum):
    ADMIN = "admin"
    USER = "user"
    OTHER = "other"


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    date_of_birth = fields.DateField()
    is_verified = fields.BooleanField(default=False)
    role = fields.ChoiceField(
        UserType,
        max_length=255,
        null=False,
        default=UserType.USER,
    )

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.role}"

You simply isolated your common database connection and registry inside the globally accessible settings and with that you can import in any Esmerald application, ChildEsmerald or whatever you prefer without the need of repeating yourself.

User model fields

If you are familiar with Django then you are also aware of the way they have their users table and the way they have the fields declared. Esmerald has a similar approach and provides the following.

  • first_name
  • last_name
  • username
  • email
  • password
  • last_login
  • is_active
  • is_staff
  • is_superuser

The functions available

Using simply this model it does not bring too much benefits as it is something you can do easily and fast but the functionality applied to it is already something that would require some extra time to assemble.

Warning

The following examples assume that you are taking advantage of the settings as decribed before.

create_user

from pydantic import EmailStr

from esmerald.conf import settings
from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database, models = settings.registry


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.last_login}"


async def create_user(
    first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
    """
    Creates a user in the database.
    """
    user = await User.query.create_user(
        username=username,
        password=password,
        email=email,
        first_name=first_name,
        last_name=last_name,
    )
    return user

create_superuser

from pydantic import EmailStr

from esmerald.conf import settings
from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database, models = settings.registry


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.last_login}"


async def create_superuser(
    first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
    """
    Creates a superuser in the database.
    """
    user = await User.query.create_superuser(
        username=username,
        password=password,
        email=email,
        first_name=first_name,
        last_name=last_name,
    )
    return user

check_password

from pydantic import EmailStr

from esmerald.conf import settings
from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database, models = settings.registry


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.last_login}"


# Check if password is valid or correct
async def check_password(email: EmailStr, password: str) -> bool:
    """
    Check if the password of a user is correct.
    """
    user: User = await User.query.get(email=email)

    is_valid_password = await user.check_password(password)
    return is_valid_password

Because you are using the User provided by Esmerald, the same object is also prepared to validate the password against the system. If you are familiar with Django, this was based on it and has the same principle.

set_password

from pydantic import EmailStr

from esmerald.conf import settings
from esmerald.contrib.auth.saffier.base_user import User as BaseUser

database, models = settings.registry


class User(BaseUser):
    """
    Inherits from the BaseUser all the fields and adds extra unique ones.
    """

    class Meta:
        registry = models

    def __str__(self):
        return f"{self.email} - {self.last_login}"


# Update password
async def set_password(email: EmailStr, password: str) -> None:
    """
    Set the password of a user is correct.
    """
    user: User = await User.query.get(email=email)

    await user.set_password(password)

The same for setting passwords. The User already contains the functionality to set a password of a given User instance.

What happened

Although the way of using the User table was intentionally designed to be simple there is in fact a lot going on behind the scenes.

When using the create_user and create_superuser behind the scenes it is not only creating that same record and storing in the database but is also hashing the password for you, using the built-in Esmerald password hashers and this is a life saving time and implementation.

Esmerald also provides the set_password and check_password functions to make it easier to validate and change a user's password using the User instance.

Password Hashers

Esmerald already brings some pre-defined password hashers that are available in the Esmerald settings and ready to be used.

@property
def password_hashers(self) -> List[str]:
    return [
        "esmerald.contrib.auth.hashers.PBKDF2PasswordHasher",
        "esmerald.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
    ]

Esmerald uses passlib under the hood in order to facilitate the process of hashing passwords.

You can always override the property password_hashers in your custom settings and use your own.

from typing import List

from esmerald import EsmeraldAPISettings
from esmerald.contrib.auth.hashers import PBKDF2PasswordHasher


class CustomHasher(PBKDF2PasswordHasher):
    """
    All the hashers inherit from BasePasswordHasher
    """

    salt_entropy = 3000


class MySettings(EsmeraldAPISettings):
    @property
    def password_hashers(self) -> List[str]:
        return ["myapp.hashers.CustomHasher"]

Migrations

You can use any migration tool as you see fit. It is recommended Alembic.

Saffier also provides some insights in how to migrate using alembic.

General example

More examples and more thorough explanations how to use Saffier can be consulted in its own documentation.