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.
AbstractUser
- The base user class containing all the fields required a user.User
- A subsclass ofAbstractUser
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.