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 Articulate 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”smith make:factory PostFactorysmith make:factory PostFactory --model Postsmith make:model Post -m -f # model + migration + factorysmith make:model Post -mf # same flags, clusteredFactories 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. 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()CamelCase aliases (self.fake.safeEmail()) also work.
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. (for is a Python keyword, so the method is
for_.)
# 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 write past the mass-assignment guard: Almasix models are guarded by
default, so a factory fills attributes directly rather than relying on
fillable. A created parent’s relations are not loaded — has() writes the
children and stops there; read them back with await user.load("posts") when
you need them in memory.
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: await Post.factory().count(3).for_(ada, "author").create() await User.factory().count(10).has(Post.factory().count(2), "posts").create()smith db:seedsmith migrate:fresh --seedSee Seeding for the seeder API itself.