User Models¶
Think of a model as a blueprint for your database table. Just like an architect's blueprint defines what a building will look like, a model defines what data you'll store and how it's structured. In Edgy, models are Python classes that automatically become database tables.
Ravyn provides pre-built User models so you don't have to reinvent the wheel for authentication.
What You'll Learn¶
- Using Ravyn's built-in User models
- Creating and managing users
- Password hashing and validation
- Leveraging settings for cleaner code
- Best practices for user authentication
Quick Start¶
from ravyn.contrib.auth.edgy.models import User
from ravyn import RavynSettings
# Create a user
user = await User.query.create_user(
email="user@example.com",
password="securepassword123",
first_name="John"
)
# Verify password
is_valid = user.check_password("securepassword123") # True
User Models Overview¶
Ravyn provides two user models for Edgy integration:
AbstractUser- Base class with all user fields and functionalityUser- Ready-to-use subclass ofAbstractUser
You can use User directly or extend AbstractUser for custom fields.
Basic Usage¶
Using the Default User Model¶
from datetime import datetime
from enum import Enum
from edgy import Database, Registry, fields
from ravyn.contrib.auth.edgy.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: datetime = fields.DateField()
is_verified: bool = fields.BooleanField(default=False)
role: UserType = fields.ChoiceField(
UserType,
max_length=255,
null=False,
default=UserType.USER,
)
class Meta:
registry = models
def __str__(self):
return f"{self.email} - {self.role}"
async with models:
# 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 gives you a complete user authentication system with minimal code.
User Model Fields¶
The User model includes these Django-inspired fields:
| Field | Type | Description |
|---|---|---|
first_name |
CharField | User's first name |
last_name |
CharField | User's last name |
username |
CharField | Unique username |
email |
EmailField | User's email address |
password |
CharField | Hashed password |
last_login |
DateTimeField | Last login timestamp |
is_active |
BooleanField | Account active status |
is_staff |
BooleanField | Staff access flag |
is_superuser |
BooleanField | Superuser access flag |
Leveraging Settings¶
Instead of repeating database configuration, use Ravyn settings to centralize your setup:
from typing import TYPE_CHECKING
from functools import cached_property
from edgy import Registry
from ravyn.conf.global_settings import RavynSettings
if TYPE_CHECKING:
from edgy import EdgySettings
class AppSettings(RavynSettings):
# this strategy works only when there is a single set of models (no clashing model names, no redefinitions)
# otherwise have a look in ravyn tests how it is solved
@cached_property
def registry(self) -> Registry:
return Registry("<YOUR-SQL-QUERY-STRING")
# optional, in case we want a centralized place
@cached_property
def edgy_settings(self) -> "EdgySettings":
from edgy import EdgySettings
return EdgySettings(preloads=["myproject.models"])
from datetime import datetime
from enum import Enum
from edgy import fields
from ravyn.conf import settings
from ravyn.contrib.auth.edgy.base_user import User as BaseUser
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: datetime = fields.DateField()
is_verified: bool = fields.BooleanField(default=False)
role: UserType = fields.ChoiceField(
UserType,
max_length=255,
null=False,
default=UserType.USER,
)
class Meta:
registry = models
def __str__(self):
return f"{self.email} - {self.role}"
import os
import sys
from ravyn import Ravyn, Include
def build_path():
"""
Builds the path of the project and project root.
""" #
SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
if SITE_ROOT not in sys.path:
sys.path.append(SITE_ROOT)
# in case of an application model with apps
sys.path.append(os.path.join(SITE_ROOT, "apps"))
def disable_edgy_settings_load():
os.environ["EDGY_SETTINGS_MODULE"] = ""
def get_application():
"""
Encapsulating in methods can be useful for controlling the import order but is optional.
"""
# first call build_path
build_path()
# this is optional, for rewiring edgy settings to ravyn settings
disable_edgy_settings_load() # disable any settings load
# import edgy now
from edgy import Instance, monkay
from ravyn.conf import settings
monkay.settings = lambda: settings.edgy_settings # rewire
monkay.evaluate_settings_once(ignore_import_errors=False) # import manually
# now the project is in the search path and we can import
registry = settings.registry
app = registry.asgi(
Ravyn(
routes=[Include(namespace="my_project.urls")],
)
)
monkay.set_instance(Instance(registry=registry, app=app))
return app
app = get_application()
This approach lets you:
- Access database configuration anywhere in your codebase
- Avoid repeating yourself
- Easily switch between environments
- Share configuration across multiple apps
User Management Functions¶
Warning
The following examples assume you're using settings as described above.
create_user¶
Create a regular user with automatic password hashing:
from pydantic import EmailStr
from edgy import monkay
from .accounts.models import User
async def create_user(
first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
"""
Creates a user in the database.
"""
registry = monkay.instance.registry
async with registry:
User = registry.get_model("User")
user = await User.query.create_user(
username=username,
password=password,
email=email,
first_name=first_name,
last_name=last_name,
)
return user
What happens behind the scenes:
- Password is automatically hashed using bcrypt
- User is created in the database
- Returns the User instance
create_superuser¶
Create an admin user with elevated privileges:
from pydantic import EmailStr
from edgy import monkay
from .accounts.models import User
async def create_superuser(
first_name: str, last_name: str, username: str, email: EmailStr, password: str
) -> User:
"""
Creates a superuser in the database.
"""
registry = monkay.instance.registry
async with registry:
User = registry.get_model("User")
user = await User.query.create_superuser(
username=username,
password=password,
email=email,
first_name=first_name,
last_name=last_name,
)
return user
Automatically sets:
- is_staff = True
- is_superuser = True
- is_active = True
check_password¶
Verify a user's password:
from pydantic import EmailStr
from edgy import monkay
# 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.
"""
registry = monkay.instance.registry
async with registry:
User = registry.get_model("User")
user: User = await User.query.get(email=email)
is_valid_password = await user.check_password(password)
return is_valid_password
Security features:
- Compares against hashed password
- Constant-time comparison (prevents timing attacks)
- Returns boolean (True/False)
set_password¶
Change a user's password:
from pydantic import EmailStr
from edgy import monkay
# Update password
async def set_password(email: EmailStr, password: str) -> None:
"""
Set the password of a user is correct.
"""
registry = monkay.instance.registry
async with registry:
User = registry.get_model("User")
user: User = await User.query.get(email=email)
await user.set_password(password)
What happens:
- New password is hashed
- Old password is replaced
- User instance is updated (remember to save!)
Password Hashing¶
Ravyn uses secure password hashing out of the box.
Default Hashers¶
@property
def password_hashers(self) -> list[str]:
return [
"ravyn.contrib.auth.hashers.BcryptPasswordHasher",
]
Ravyn uses passlib under the hood for secure password hashing.
Custom Hashers¶
Override the password_hashers property in your settings:
from typing import List
from ravyn import RavynSettings
from ravyn.contrib.auth.hashers import BcryptPasswordHasher
class CustomHasher(BcryptPasswordHasher):
"""
All the hashers inherit from BasePasswordHasher
"""
salt_entropy = 3000
class MySettings(RavynSettings):
@property
def password_hashers(self) -> List[str]:
return ["myapp.hashers.CustomHasher"]
Common Pitfalls & Fixes¶
Pitfall 1: Forgetting to Save After set_password¶
Problem: Password change doesn't persist.
# Wrong - password not saved
user.set_password("newpassword")
# User logs out, can't log back in!
Solution: Always save after setting password:
# Correct
user.set_password("newpassword")
await user.save()
Pitfall 2: Storing Plain Text Passwords¶
Problem: Manually setting password field.
# Wrong - plain text password!
user = await User.query.create(
email="user@example.com",
password="plaintext123" # Not hashed!
)
Solution: Use create_user or set_password:
# Correct
user = await User.query.create_user(
email="user@example.com",
password="plaintext123" # Automatically hashed
)
Pitfall 3: Not Using Unique Constraints¶
Problem: Duplicate emails or usernames.
Solution: Add unique constraints in your model:
class CustomUser(AbstractUser):
email = fields.EmailField(unique=True)
username = fields.CharField(max_length=150, unique=True)
class Meta:
registry = registry
Best Practices¶
1. Use create_user for User Creation¶
# Good - automatic password hashing
user = await User.query.create_user(
email="user@example.com",
password="secure123"
)
2. Validate Before Creating¶
# Good - validate input first
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
@validator('password')
def password_strength(cls, v):
if len(v) < 8:
raise ValueError('Password must be at least 8 characters')
return v
# Then create user
data = UserCreate(email="user@example.com", password="securepass123")
user = await User.query.create_user(**data.dict())
3. Use Settings for Registry¶
# Good - centralized configuration
from ravyn.conf import settings
class User(AbstractUser):
class Meta:
registry = settings.edgy_registry
Database Migrations¶
Edgy provides built-in migration commands that use Alembic under the hood:
# Install Edgy (includes Alembic)
pip install edgy
# Initialize migrations
edgy init
# Create migration
edgy makemigrations
# Apply migrations
edgy migrate
Edgy's migration system supports auto-discovery and can be configured using the --app flag or EDGY_DEFAULT_APP environment variable.
For advanced migration scenarios, you can also use Alembic directly. See Edgy Migrations for complete documentation.
Initialize Migrations¶
Auto-Discovery:
$ edgy init
Edgy automatically discovers your application in src/main.py following its search pattern.
Using --app:
$ edgy --app src.main init
Using Environment Variable:
$ export EDGY_DEFAULT_APP=src.main
$ edgy init
Create Migrations¶
Auto-Discovery:
$ edgy makemigrations
Using --app:
$ edgy --app src.main makemigrations
Using Environment Variable:
$ export EDGY_DEFAULT_APP=src.main
$ edgy makemigrations
Note
As of version 0.23.0, the import path must point to a module where the Instance object triggers automatic registration.
Using Preloads¶
Instead of --app or EDGY_DEFAULT_APP, use the preloads setting in your configuration to specify an import path. When an instance is set in a preloaded file, auto-discovery is skipped.
Edgy provides detailed migration guidance in its documentation.
Learn More¶
- Edgy Documentation - Complete Edgy guide
- Edgy Migrations - Database migrations
- Passlib Documentation - Password hashing
Next Steps¶
- JWT Middleware - Secure your APIs
- Complete Example - Full authentication tutorial
- Mongoz Documents - NoSQL alternative