Factories
A factory describes what one row of a table looks like when nobody cares about the exact values — a name, an email, a title, a date somewhere in the last year. Seeders and tests then ask for as many of those rows as they need, in whatever shape the case calls for.
from typing import Any
from almasix.hashing import Hashfrom almasix.orm import Factory
from app.models.user import User
class UserFactory(Factory): model = User
def definition(self) -> dict[str, Any]: return { "name": self.fake.name(), "email": self.fake.unique().safe_email(), "password": Hash.make("password"), }user = await User.factory().create()users = await User.factory().count(10).create()Because Almasix’s ORM is async, make() and create() are coroutines. The
rest of the builder — count, state, has, for_ — is ordinary chaining
and needs no await.
Generating a factory
Section titled “Generating a factory”python smith make:factory PostFactorypython smith make:factory PostFactory --model Postpython smith make:model Post -m -f # model + migration + factoryFactories live in database/factories/, one class per file, named after the
model: PostFactory in post_factory.py.
Reaching a factory from a model
Section titled “Reaching a factory from a model”Add the HasFactory mixin — models generated by make:model already have it:
from almasix.orm import HasFactory, Model
class Post(HasFactory, Model): fillable = ("title", "user_id")Post.factory() then finds PostFactory by name. Three shorthands:
Post.factory() # a builder for one postPost.factory(3) # three postsPost.factory({"title": "Fixed"}) # one post, with a statePost.factory(3, {"title": "Fixed"}) # three of themTo name the factory yourself, override new_factory():
class Post(HasFactory, Model): @classmethod def new_factory(cls): return ArchivedPostFactory.new()Fake data
Section titled “Fake data”self.fake is a small generator with the providers definitions actually
reach for. It is seedable, so a seeded run produces the same database twice:
self.fake.name() # "Ada Lovelace"self.fake.unique().safe_email() # never repeats within a runself.fake.sentence(8)self.fake.paragraph()self.fake.number_between(1, 100)self.fake.boolean(30) # true 30% of the timeself.fake.random_element(["draft", "published"])self.fake.date_time_between(start, end)self.fake.city(), self.fake.country(), self.fake.phone_number()Laravel’s camelCase spellings work too (self.fake.safeEmail()), so a
definition ported from PHP runs unchanged.
Want the full Faker catalogue? Install it and hand it to Almasix once, at boot:
import faker
from almasix.orm import Fake
Fake.resolve_using(lambda: faker.Faker("en_GB"))Every factory built afterwards reaches that generator through self.fake.
States
Section titled “States”A state is a dict, or a callable given the attributes so far. Name them as methods and they read like the model:
class PostFactory(Factory): model = Post
def definition(self) -> dict[str, Any]: return {"title": self.fake.title(), "published": True, "views": 0}
def draft(self) -> Factory: return self.state({"published": False})
def popular(self) -> Factory: return self.state(lambda attributes: {"views": attributes["views"] + 5_000})await Post.factory().draft().popular().create()await Post.factory().state({"title": "Fixed"}).create()await Post.factory().set("title", "Fixed").create()A state callable may take the attributes, or the attributes and the parent
model being built for, and it may be async.
Attributes passed straight to make() or create() are a state as well, and
they are applied last:
await Post.factory().draft().create({"title": "Overrides everything"})Sequences
Section titled “Sequences”# Alternates as it goes.await Post.factory().count(6).sequence( {"published": True}, {"published": False},).create()
# One row per step — the sequence sets the count.await Post.factory().for_each_sequence( {"title": "one"}, {"title": "two"},).create()
# Every combination: four posts here.await Post.factory().count(4).cross_join_sequence( [{"title": "a"}, {"title": "b"}], [{"published": True}, {"published": False}],).create()A step may be a callable, which is given the sequence itself — index counts
the calls so far:
await Post.factory().count(3).sequence( lambda sequence: {"title": f"Post {sequence.index + 1}"},).create()Relationships
Section titled “Relationships”has() creates children after the parent exists; for_() supplies the parent
before the child is written.
# A user with three posts.await User.factory().has(Post.factory().count(3)).create()
# The relation name is guessed from the model; name it when the guess is wrong.await User.factory().has(Post.factory().count(3), "posts").create()
# Three posts, one author between them.await Post.factory().count(3).for_(User.factory(), "author").create()
# An author that already exists.await Post.factory().count(3).for_(ada, "author").create()Many-to-many relations attach with pivot columns:
await User.factory().has_attached(Role.factory().count(2), {"level": "lead"}, "roles").create()await User.factory().has_attached(existing_roles, lambda role: {"level": role.name}, "roles").create()Nesting works the way you would write it by hand:
await User.factory().has( Post.factory().count(2).has(Comment.factory().count(3), "comments"), "posts",).create()Magic relation methods
Section titled “Magic relation methods”await User.factory().has_posts(3).create()await User.factory().has_posts(3, {"published": False}).create()await Post.factory().for_author({"name": "Ada"}).create()has_<relation> and for_<relation> build the related model’s factory for
you. They are the same two methods above, spelled shorter.
Recycling models
Section titled “Recycling models”Left alone, every for_() in a batch creates its own parent. recycle()
hands the whole graph a pool of existing models to draw from instead:
ada = await User.factory().create()
await Post.factory().count(10).recycle(ada).for_(User.factory(), "author").create()# ten posts, all of them Ada's, and no eleventh userA factory used as an attribute value resolves to that model’s key, and honors the same pool:
def definition(self) -> dict[str, Any]: return {"title": self.fake.title(), "user_id": UserFactory.new()}class UserFactory(Factory): model = User
def configure(self) -> Factory: return self.after_creating(self.send_welcome)
async def send_welcome(self, user: User) -> None: await user.notify(WelcomeNotification())after_making(callback) runs on every model the factory builds;
after_creating(callback) runs after each one is saved and may take the
parent as a second argument. Callbacks may be sync or async.
Everything else
Section titled “Everything else”await Post.factory().raw() # the attributes, no modelawait Post.factory().count(2).raw() # a list of them
await Post.factory().make_one() # one, unsavedawait Post.factory().make_many(3)await Post.factory().create_one()await Post.factory().create_many([{"title": "a"}, {"title": "b"}])
await Post.factory().create_quietly() # no model eventsawait Post.factory().trashed().create() # arrives soft deletedawait Post.factory().connection("reporting").create()
later = Post.factory().lazy({"title": "Written when called"})post = await later()Factories in seeders
Section titled “Factories in seeders”Seeders are the primary consumer. The fixed columns a demo asserts on are states; the factory invents the rest:
from almasix.orm import Seeder
from app.models.post import Postfrom app.models.user import User
class DemoSeeder(Seeder): async def run(self) -> None: ada = await User.factory().create({"email": "ada@almasix.dev", "name": "Ada"}) await Post.factory().count(3).for_(ada, "author").create() await User.factory().count(10).has(Post.factory().count(2), "posts").create()python smith db:seedpython smith migrate:fresh --seedSee Seeding for the seeder API itself.
How this differs from Laravel
Section titled “How this differs from Laravel”make()andcreate()are coroutines. Everything that touches the database in Almasix is async; the builder methods are not.for_andhas_attachedcarry a trailing underscore or an explicit relation name where PHP writesforandhasAttached.foris a Python keyword;for_is the closest legal spelling.- Factories write past the mass assignment guard. Almasix models are
guarded by default, so a factory fills attributes directly rather than
relying on
fillable. That is what Laravel’s factories effectively do too. - Fake data ships with the framework instead of requiring Faker. The
provider list is smaller;
Fake.resolve_usingswaps in the real thing. - A created parent’s relations are not loaded.
has()writes the children and stops there — read them back withawait user.load("posts")when you need them in memory.