Factories

  • pre_init is a method decorator that will always run before the instance is initialised.

  • pre_save is a method decorator that will always run before the instance is saved.

  • post_save is a method decorator that will always run after the instance is saved.

  • trait decorator runs the code if set to True in factory constructor.

  • PreInit is like the pre_init decorator of the ModelFactory, but you can pass arguments to it and have a lot of flexibility. See a working example (below) of how set a user password in Django.

  • PreSave is like the pre_save decorator of the ModelFactory, but you can pass arguments to it and have a lot of flexibility. See a working example (below) of how set a user password in Django.

  • PostSave is like the post_save decorator of the ModelFactory, but you can pass arguments to it and have a lot of flexibility. See a working example (below) of how to assign a User to a Group after User creation.

  • LazyAttribute expects a callable, will take the instance as a first argument, runs it with extra arguments specified and sets the value as an attribute name.

  • LazyFunction expects a callable, runs it (without any arguments) and sets the value as an attribute name.

  • SubFactory is for specifying relations (typically - ForeignKeys or nested objects).

Django example

Models

In the Django example, we will be using User and Group models from django.contrib.auth sub-package. The Article would be the only application specific custom model.

Filename: article/models.py

from django.conf import settings
from django.db import models
from django.utils import timezone


class Article(models.Model):
    title = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    headline = models.TextField()
    category = models.CharField(max_length=255)
    pages = models.IntegerField()
    auto_minutes_to_read = models.IntegerField()
    image = models.ImageField(null=True, blank=True)
    pub_date = models.DateField(default=timezone.now)
    safe_for_work = models.BooleanField(default=False)
    minutes_to_read = models.IntegerField(default=5)
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE
    )
    tags = models.JSONField(default=list)

See the full example here


Factories

Factory for the Django’s built-in Group model could look as simple as this:

Filename: article/factories.py

from django.contrib.auth.models import (
    Group,
)
from fake import (
    FACTORY,
    DjangoModelFactory,
)


class GroupFactory(DjangoModelFactory):
    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)

See the full example here


Factory for the Django’s built-in User model could look as this:

Filename: article/factories.py

from typing import Any, Dict

from django.contrib.auth.models import (
    User,
)
from django.utils import timezone
from django.utils.text import slugify
from fake import (
    FAKER,
    PostSave,
    PreInit,
    PreSave,
)


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    group = GroupFactory(name=name)
    user.groups.add(group)


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(DjangoModelFactory):
    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time(tzinfo=timezone.get_current_timezone())
    last_login = FACTORY.date_time(tzinfo=timezone.get_current_timezone())
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User
        get_or_create = ("username",)

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True

See the full example here

Breakdown:

  • username is a required field. We shouldn’t be using PreSave or PostSave methods here, because we need it to be available and resolved before calling the class constructor (missing required fields would fail on Pydantic and other frameworks that enforce strong type checking). That’s why PreInit, which operates on the dict level, from which the model instance is constructed, is used here to construct the username value from first_name and the last_name. The set_username helper function, which is used by PreInit, accepts a dictionary with model data as argument and all changes to that dictionary are passed further to the class constructor. It’s important to mention that functions passed to the PreInit, do hot have to return anything.

  • password is a non-required field and since Django has a well working way for setting it, use of PreSave is the best option here. It’s important to mention that functions passed to the PreSave, do hot have to return anything.

  • group is a non-required many-to-many relationship. We need a User instance to be created before we can add User to a Group. That’s why PostSave is best option here. It’s important to mention that functions passed to the PostSave, do hot have to return anything.


Factory for the the Artice model could look as this:

Filename: article/factories.py

from django.conf import settings
from fake import (
    FileSystemStorage,
    SubFactory,
    pre_init,
)

from article.models import Article

# For Django, all files shall be placed inside `MEDIA_ROOT` directory.
# That's why you need to apply this trick - define a
# custom `FileSystemStorage` class and pass it to the file factory as
# `storage` argument.
STORAGE = FileSystemStorage(root_path=settings.MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(DjangoModelFactory):
    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date(tzinfo=timezone.get_current_timezone())
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here

Breakdown:

  • headline is a required field that should be available and resolved before the class constructor is called. We already know that PreInit should be used for such cases. The headline value is constructed from content.

  • author is a foreign key relation field to the User model. For foreign key relations SubFactory is our best choice.

  • image is a file field. Files created shall be placed in the path specified in MEDIA_ROOT Django setting. That’s why we create and configure the STORAGE instance to pass it to FACTORY.png_file in a storage argument.

  • auto_minutes_to_read is a required field of the Article model. It needs to be resolved and available before the constructor class is called. That’s the @pre_init decorator is used on the set_auto_minutes_read helper method.


All together it would look as follows:

Filename: article/factories.py

from typing import Any, Dict

from django.conf import settings
from django.contrib.auth.models import (
    Group,
    User,
)
from django.utils import timezone
from django.utils.text import slugify
from fake import (
    FACTORY,
    FAKER,
    DjangoModelFactory,
    FileSystemStorage,
    PostSave,
    PreInit,
    PreSave,
    SubFactory,
    post_save,
    pre_init,
    pre_save,
    trait,
)

from article.models import Article

# For Django, all files shall be placed inside `MEDIA_ROOT` directory.
# That's why you need to apply this trick - define a
# custom `FileSystemStorage` class and pass it to the file factory as
# `storage` argument.
STORAGE = FileSystemStorage(root_path=settings.MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


class GroupFactory(DjangoModelFactory):
    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    group = GroupFactory(name=name)
    user.groups.add(group)


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(DjangoModelFactory):
    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time(tzinfo=timezone.get_current_timezone())
    last_login = FACTORY.date_time(tzinfo=timezone.get_current_timezone())
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User
        get_or_create = ("username",)

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(DjangoModelFactory):
    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date(tzinfo=timezone.get_current_timezone())
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here


Usage example

# Create one article
article = ArticleFactory()

# Create 5 articles
articles = ArticleFactory.create_batch(5)

# Create one article with authors username set to admin.
article = ArticleFactory(author__username="admin")

# Using trait
user = UserFactory(is_admin_user=True)

# Using trait in SubFactory
article = ArticleFactory(author__is_admin_user=True)

# Create a user. Created user will automatically have his password
# set to "test1234" and will be added to the group "Test group".
user = UserFactory()

# Create a user with custom password
user = UserFactory(
    password=PreSave(set_password, password="another-pass"),
)

# Add a user to another group
user = UserFactory(
    group=PostSave(add_to_group, name="Another group"),
)

# Or even add user to multiple groups at once
user = UserFactory(
    group_1=PostSave(add_to_group, name="Another group"),
    group_2=PostSave(add_to_group, name="Yet another group"),
)

Pydantic example

Models

Example Pydantic models closely resemble the earlier shown Django models.

Filename: article/models.py

from datetime import date, datetime
from typing import List, Optional, Set

from fake import xor_transform
from pydantic import BaseModel, Field


class Group(BaseModel):
    id: int
    name: str

    class Config:
        allow_mutation = False

    def __hash__(self):
        return hash((self.id, self.name))


class User(BaseModel):
    id: int
    username: str = Field(max_length=255)
    first_name: str = Field(max_length=255)
    last_name: str = Field(max_length=255)
    email: str = Field(max_length=255)
    date_joined: datetime = Field(default_factory=datetime.utcnow)
    last_login: Optional[datetime] = None
    password: Optional[str] = Field("", max_length=255)
    is_superuser: bool = Field(default=False)
    is_staff: bool = Field(default=False)
    is_active: bool = Field(default=True)
    groups: Set[Group] = Field(default_factory=set)

    class Config:
        extra = "allow"  # For testing purposes only

    def __str__(self):
        return self.username

    def set_password(self, password: str) -> None:
        self.password = xor_transform(password)


class Article(BaseModel):
    id: int
    title: str = Field(max_length=255)
    slug: str = Field(max_length=255, unique=True)
    content: str
    headline: str
    category: str = Field(max_length=255)
    pages: int
    auto_minutes_to_read: int
    author: User
    image: Optional[str] = None  # Use str to represent the image path or URL
    pub_date: date = Field(default_factory=date.today)
    safe_for_work: bool = False
    minutes_to_read: int = 5
    tags: List[str] = Field(default_factory=list)

    class Config:
        extra = "allow"  # For testing purposes only

    def __str__(self):
        return self.title

See the full example here


Factories

Example Pydantic factories are almost identical to the earlier shown Django factories.

Filename: article/factories.py

from pathlib import Path
from typing import Any, Dict

from fake import (
    FACTORY,
    FAKER,
    FileSystemStorage,
    PostSave,
    PreInit,
    PreSave,
    PydanticModelFactory,
    SubFactory,
    post_save,
    pre_init,
    pre_save,
    slugify,
    trait,
)

from article.models import Article, Group, User

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR / "media"

STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


class GroupFactory(PydanticModelFactory):
    id = FACTORY.pyint()
    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    group = GroupFactory(name=name)
    user.groups.add(group)


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(PydanticModelFactory):
    id = FACTORY.pyint()
    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time()
    last_login = FACTORY.date_time()
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True


def set_headline(data) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(PydanticModelFactory):
    id = FACTORY.pyint()
    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date()
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here

Used just like in previous example.


TortoiseORM example

Models

Example TortoiseORM models closely resemble the earlier shown Django models.

Filename: article/models.py

from datetime import date

from fake import xor_transform
from tortoise import fields
from tortoise.models import Model


class Group(Model):
    """Group model."""

    id = fields.IntField(pk=True)
    name = fields.CharField(max_length=255, unique=True)


class User(Model):
    """User model."""

    id = fields.IntField(pk=True)
    username = fields.CharField(max_length=255, unique=True)
    first_name = fields.CharField(max_length=255)
    last_name = fields.CharField(max_length=255)
    email = fields.CharField(max_length=255)
    password = fields.CharField(max_length=255, null=True, blank=True)
    last_login = fields.DatetimeField(null=True, blank=True)
    is_superuser = fields.BooleanField(default=False)
    is_staff = fields.BooleanField(default=False)
    is_active = fields.BooleanField(default=True)
    date_joined = fields.DatetimeField(null=True, blank=True)
    groups = fields.ManyToManyField("models.Group", on_delete=fields.CASCADE)

    def set_password(self, password: str) -> None:
        self.password = xor_transform(password)


class Article(Model):
    """Article model."""

    id = fields.IntField(pk=True)
    title = fields.CharField(max_length=255)
    slug = fields.CharField(max_length=255, unique=True)
    content = fields.TextField()
    headline = fields.TextField()
    category = fields.CharField(max_length=255)
    pages = fields.IntField()
    auto_minutes_to_read = fields.IntField()
    image = fields.TextField(null=True, blank=True)

See the full example here


Factories

Example TortoiseORM factories are almost identical to the earlier shown Django factories.

Filename: article/factories.py

from pathlib import Path
from typing import Any, Dict

from fake import (
    FACTORY,
    FAKER,
    FileSystemStorage,
    PostSave,
    PreInit,
    PreSave,
    SubFactory,
    TortoiseModelFactory,
    post_save,
    pre_init,
    pre_save,
    run_async_in_thread,
    slugify,
    trait,
)

from article.models import Article, Group, User

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR / "media"

STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp")
CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


class GroupFactory(TortoiseModelFactory):
    """Group factory."""

    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    group = GroupFactory(name=name)

    async def _add_to_group():
        await user.groups.add(group)
        await user.save()

    run_async_in_thread(_add_to_group())


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(TortoiseModelFactory):
    """User factory."""

    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time()
    last_login = FACTORY.date_time()
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User
        get_or_create = ("username",)

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(TortoiseModelFactory):
    """Article factory."""

    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date()
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here

Used just like in previous example.


Dataclasses example

Models

Example dataclass models closely resemble the earlier shown Django models.

Filename: article/models.py

from dataclasses import dataclass, field
from datetime import date, datetime
from typing import List, Optional, Set

from fake import xor_transform


@dataclass(frozen=True)
class Group:
    id: int
    name: str


@dataclass
class User:
    id: int
    username: str
    first_name: str
    last_name: str
    email: str
    date_joined: datetime = field(default_factory=datetime.utcnow)
    last_login: Optional[datetime] = None
    password: Optional[str] = None
    is_superuser: bool = False
    is_staff: bool = False
    is_active: bool = True
    groups: Set[Group] = field(default_factory=set)

    def __str__(self):
        return self.username

    def set_password(self, password: str) -> None:
        self.password = xor_transform(password)


@dataclass
class Article:
    id: int
    title: str
    slug: str
    content: str
    headline: str
    category: str
    pages: int
    auto_minutes_to_read: int
    author: User
    image: Optional[str] = None  # Use str to represent the image path or URL
    pub_date: date = field(default_factory=date.today)
    safe_for_work: bool = False
    minutes_to_read: int = 5
    tags: List[str] = field(default_factory=list)

    def __str__(self):
        return self.title

See the full example here


Factories

Example dataclass factories are almost identical to the earlier shown Django factories.

Filename: article/factories.py

from pathlib import Path
from typing import Any, Dict

from fake import (
    FACTORY,
    FAKER,
    FileSystemStorage,
    ModelFactory,
    PostSave,
    PreInit,
    PreSave,
    SubFactory,
    post_save,
    pre_init,
    pre_save,
    slugify,
    trait,
)

from article.models import Article, Group, User

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR / "media"

STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


class GroupFactory(ModelFactory):
    id = FACTORY.pyint()
    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    group = GroupFactory(name=name)
    user.groups.add(group)


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(ModelFactory):
    id = FACTORY.pyint()
    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time()
    last_login = FACTORY.date_time()
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(ModelFactory):
    id = FACTORY.pyint()
    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date()
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here

Used just like in previous example.


SQLAlchemy example

Configuration

Filename: config.py

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

# SQLAlchemy
DATABASE_URL = "sqlite:///test_database.db"
ENGINE = create_engine(DATABASE_URL)
SESSION = scoped_session(sessionmaker(bind=ENGINE))

See the full example here


Models

Example SQLAlchemy models closely resemble the earlier shown Django models.

Filename: article/models.py

from datetime import datetime

from fake import xor_transform
from sqlalchemy import (
    JSON,
    Boolean,
    Column,
    DateTime,
    ForeignKey,
    Integer,
    String,
    Table,
    Text,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship


Base = declarative_base()


# Association table for the many-to-many relationship
user_group_association = Table(
    "user_group",
    Base.metadata,
    Column("user_id", Integer, ForeignKey("users.id")),
    Column("group_id", Integer, ForeignKey("groups.id")),
)


class Group(Base):
    """Group model."""

    __tablename__ = "groups"

    id = Column(Integer, primary_key=True)
    name = Column(String(255), unique=True)


class User(Base):
    """User model."""

    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    username = Column(String(255), unique=True)
    first_name = Column(String(255))
    last_name = Column(String(255))
    email = Column(String(255))
    date_joined = Column(DateTime, default=datetime.utcnow)
    last_login = Column(DateTime, nullable=True)
    password = Column(String(255), nullable=True)
    is_superuser = Column(Boolean, default=False)
    is_staff = Column(Boolean, default=False)
    is_active = Column(Boolean, default=True)

    # Many-to-many relationship
    groups = relationship(
        "Group", secondary=user_group_association, backref="users"
    )

    # One-to-many relationship
    articles = relationship("Article", back_populates="author")

    def set_password(self, password: str) -> None:
        self.password = xor_transform(password)


class Article(Base):
    """Article model."""

    __tablename__ = "articles"

    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    slug = Column(String(255), unique=True)
    content = Column(Text)
    headline = Column(Text)
    category = Column(String(255))
    pages = Column(Integer)
    auto_minutes_to_read = Column(Integer)
    image = Column(Text, nullable=True)
    pub_date = Column(DateTime, default=datetime.utcnow)
    safe_for_work = Column(Boolean, default=False)
    minutes_to_read = Column(Integer, default=5)
    author_id = Column(Integer, ForeignKey("users.id"))
    tags = Column(JSON, default=list)

    # Relationships
    author = relationship("User", back_populates="articles")

See the full example here


Factories

Example SQLAlchemy factories are almost identical to the earlier shown Django factories.

Filename: article/factories.py

from pathlib import Path
from typing import Any, Dict

from fake import (
    FACTORY,
    FAKER,
    FileSystemStorage,
    PostSave,
    PreInit,
    PreSave,
    SQLAlchemyModelFactory,
    SubFactory,
    post_save,
    pre_init,
    pre_save,
    slugify,
    trait,
)

from article.models import Article, Group, User
from config import SESSION

# Storage config. Build paths inside the project like this: BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR / "media"
STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


def get_session():
    return SESSION()


class GroupFactory(SQLAlchemyModelFactory):
    """User factory."""

    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)

    class MetaSQLAlchemy:
        get_session = get_session


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    session = get_session()
    # Check if the group already exists
    group = session.query(Group).filter_by(name=name).first()

    # If the group doesn't exist, create a new one
    if not group:
        group = Group(name=name)
        session.add(group)
        session.commit()  # Commit to assign an ID to the new group

    # Add the group to the user's groups using append
    if group not in user.groups:
        user.groups.append(group)
        session.commit()  # Commit the changes


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(SQLAlchemyModelFactory):
    """User factory."""

    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time()
    last_login = FACTORY.date_time()
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User
        get_or_create = ("username",)

    class MetaSQLAlchemy:
        get_session = get_session

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(SQLAlchemyModelFactory):
    """Article factory."""

    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date()
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    class MetaSQLAlchemy:
        get_session = get_session

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

See the full example here

Used just like in previous example.


SQLModel example

Configuration

Filename: config.py

from sqlalchemy.orm import scoped_session, sessionmaker
from sqlmodel import SQLModel, create_engine

# SQLAlchemy
DATABASE_URL = "sqlite:///test_database.db"
ENGINE = create_engine(DATABASE_URL)
SESSION = scoped_session(sessionmaker(bind=ENGINE))


def create_tables():
    """Create tables."""
    SQLModel.metadata.create_all(ENGINE)


def get_db():
    """Get database connection."""
    session = SESSION()
    try:
        yield session
        session.commit()
    finally:
        session.close()


if __name__ == "__main__":
    create_tables()

See the full example here


Models

Example SQLModel models closely resemble the earlier shown Django models.

Filename: article/models.py

from datetime import datetime
from typing import List, Optional

from fake import xor_transform
from sqlmodel import JSON, Column, Field, Relationship, SQLModel


class UserGroup(SQLModel, table=True):
    user_id: Optional[int] = Field(
        default=None,
        foreign_key="user.id",
        primary_key=True,
    )
    group_id: Optional[int] = Field(
        default=None,
        foreign_key="group.id",
        primary_key=True,
    )


class Group(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(sa_column_kwargs={"unique": True})
    users: List["User"] = Relationship(
        back_populates="groups",
        link_model=UserGroup,
    )

    def __repr__(self):
        return f"<Group(group='{self.name}')>"


class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    username: str = Field(sa_column_kwargs={"unique": True})
    first_name: str
    last_name: str
    email: str
    date_joined: datetime = Field(default_factory=datetime.utcnow)
    last_login: Optional[datetime]
    password: str
    is_superuser: bool = Field(default=False)
    is_staff: bool = Field(default=False)
    is_active: bool = Field(default=True)
    groups: List[Group] = Relationship(
        back_populates="users",
        link_model=UserGroup,
    )
    articles: List["Article"] = Relationship(back_populates="author")

    def __repr__(self):
        return f"<User(username='{self.username}', email='{self.email}')>"

    def set_password(self, password: str) -> None:
        self.password = xor_transform(password)


class Article(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    slug: str = Field(sa_column_kwargs={"unique": True})
    content: str
    headline: str
    category: str
    pages: int
    auto_minutes_to_read: int
    image: Optional[str]
    pub_date: datetime = Field(default_factory=datetime.utcnow)
    safe_for_work: bool = Field(default=False)
    minutes_to_read: int = Field(default=5)
    author_id: int = Field(foreign_key="user.id")
    tags: Optional[List[str]] = Field(
        default_factory=list,
        sa_column=Column(JSON),
    )
    author: User = Relationship(back_populates="articles")

    # Needed for Column(JSON)
    class Config:
        arbitrary_types_allowed = True

    def __repr__(self):
        return (
            f"<Article(title='{self.title}', author='{self.author.username}')>"
        )

See the full example here


Factories

Example SQLModel factories are identical to the earlier shown SQLAlchemy factories.

Filename: article/factories.py

from pathlib import Path
from typing import Any, Dict

from fake import (
    FACTORY,
    FAKER,
    FileSystemStorage,
    PostSave,
    PreInit,
    PreSave,
    SQLAlchemyModelFactory,
    SubFactory,
    post_save,
    pre_init,
    pre_save,
    slugify,
    trait,
)

from article.models import Article, Group, User
from config import SESSION

# Storage config. Build paths inside the project like this: BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR / "media"
STORAGE = FileSystemStorage(root_path=MEDIA_ROOT, rel_path="tmp")

CATEGORIES = (
    "art",
    "technology",
    "literature",
)
TAGS = (
    "painting",
    "photography",
    "ai",
    "data-engineering",
    "fiction",
    "poetry",
    "manual",
)


def get_session():
    return SESSION()


class GroupFactory(SQLAlchemyModelFactory):
    """User factory."""

    name = FACTORY.word()

    class Meta:
        model = Group
        get_or_create = ("name",)

    class MetaSQLAlchemy:
        get_session = get_session


def set_password(user: User, password: str) -> None:
    user.set_password(password)


def add_to_group(user: User, name: str) -> None:
    session = get_session()
    # Check if the group already exists
    group = session.query(Group).filter_by(name=name).first()

    # If the group doesn't exist, create a new one
    if not group:
        group = Group(name=name)
        session.add(group)
        session.commit()  # Commit to assign an ID to the new group

    # Add the group to the user's groups using append
    if group not in user.groups:
        user.groups.append(group)
        session.commit()  # Commit the changes


def set_username(data: Dict[str, Any]) -> None:
    first_name = slugify(data["first_name"])
    last_name = slugify(data["last_name"])
    data["username"] = f"{first_name}_{last_name}_{FAKER.pystr().lower()}"


class UserFactory(SQLAlchemyModelFactory):
    """User factory."""

    username = PreInit(set_username)
    first_name = FACTORY.first_name()
    last_name = FACTORY.last_name()
    email = FACTORY.email()
    date_joined = FACTORY.date_time()
    last_login = FACTORY.date_time()
    is_superuser = False
    is_staff = False
    is_active = FACTORY.pybool()
    password = PreSave(set_password, password="test1234")
    group = PostSave(add_to_group, name="TestGroup1234")

    class Meta:
        model = User
        get_or_create = ("username",)

    class MetaSQLAlchemy:
        get_session = get_session

    @trait
    def is_admin_user(self, instance: User) -> None:
        instance.is_superuser = True
        instance.is_staff = True
        instance.is_active = True

    @pre_save
    def _pre_save_method(self, instance):
        # For testing purposes only
        instance._pre_save_called = True

    @post_save
    def _post_save_method(self, instance):
        # For testing purposes only
        instance._post_save_called = True


def set_headline(data: Dict[str, Any]) -> None:
    data["headline"] = data["content"][:25]


class ArticleFactory(SQLAlchemyModelFactory):
    """Article factory."""

    title = FACTORY.sentence()
    slug = FACTORY.slug()
    content = FACTORY.text()
    headline = PreInit(set_headline)
    category = FACTORY.random_choice(elements=CATEGORIES)
    pages = FACTORY.pyint(min_value=1, max_value=100)
    image = FACTORY.png_file(storage=STORAGE)
    pub_date = FACTORY.date()
    safe_for_work = FACTORY.pybool()
    minutes_to_read = FACTORY.pyint(min_value=1, max_value=10)
    author = SubFactory(UserFactory)
    tags = FACTORY.random_sample(elements=TAGS, length=3)

    class Meta:
        model = Article

    class MetaSQLAlchemy:
        get_session = get_session

    @pre_init
    def set_auto_minutes_to_read(self, data: Dict[str, Any]) -> None:
        data["auto_minutes_to_read"] = data["pages"]

    @pre_save
    def _pre_save_method(self, instance):
        # For testing purposes only
        instance._pre_save_called = True

    @post_save
    def _post_save_method(self, instance):
        # For testing purposes only
        instance._post_save_called = True

See the full example here

Used just like in previous example.