sqlalchemy-seedling
Async-native seeder and factory library for SQLAlchemy. Dependency-aware runners, declarative factories, and a full CLI — designed for the async Python ecosystem.
Why seedling?
- Async-native — built for
async/awaitand SQLAlchemy 2.0's async session from day one. No sync wrappers, no thread-pool shims. - Dependency-aware — declare
depends_onbetween seeders; the runner topologically sorts them and runs independent seeders in parallel viaasyncio.gather. - Rich factories —
Factory[T]withFaker,LazyAttribute,Sequence,SubFactory, declarativeTraitclasses,@post_generationhooks, andAutoFactory[T]for mapper-introspected defaults. - State tracking — every run is recorded in a
seedling_statetable: append-only audit log, drift detection via content hash, and a--new-onlyskip flag. - Full CLI —
seed run,seed fresh,seed status,seed validate,seed graph,seed export,seed restore, and scaffolding commands. - Framework-agnostic — works with FastAPI, Litestar, or a plain script; no framework coupling.
Installation
pip install sqlalchemy-seedling
# or
uv add sqlalchemy-seedling
Optional YAML fixture support:
pip install sqlalchemy-seedling[yaml]
Requires Python 3.11+ and SQLAlchemy 2.0+.
Quick start
Scaffold the project layout
seed init
Creates seeders/ and factories/ packages and appends [tool.seedling] to pyproject.toml.
Define seeders
# seeders/users.py
from seedling import Seeder, DEV_AND_TEST
from sqlalchemy.ext.asyncio import AsyncSession
from myapp.models import User
class UserSeeder(Seeder):
environments = DEV_AND_TEST
models = [User]
tags = {"demo"}
async def run(self, session: AsyncSession) -> None:
session.add(User(email="admin@example.com", name="Admin"))
await session.commit()
# seeders/posts.py
from seedling import Seeder, DEV_AND_TEST
from seeders.users import UserSeeder
class PostSeeder(Seeder):
depends_on = [UserSeeder] # runs after UserSeeder automatically
environments = DEV_AND_TEST
async def run(self, session: AsyncSession) -> None: ...
Create a runner factory
# seeders/__init__.py
from seedling import SeederRunner
from myapp.db import async_session_maker
from .users import UserSeeder
from .posts import PostSeeder
def create_runner(env: str = "development") -> SeederRunner:
runner = SeederRunner(session_factory=async_session_maker, env=env)
runner.register(UserSeeder, PostSeeder)
return runner
Configure the CLI
# pyproject.toml
[tool.seedling]
runner = "seeders:create_runner"
Run
seed run # run all seeders for development
seed run --tag demo # run only seeders tagged "demo"
seed fresh # truncate then re-seed
seed status # latest run per seeder + drift detection
seed list # print execution order without running
Factories
from seedling import Factory, Faker, LazyAttribute, Sequence, SubFactory, Trait
class UserFactory(Factory[User]):
model = User
email = Faker("email")
name = Sequence(lambda n: f"User {n}")
class admin(Trait):
is_superuser = True
class PostFactory(Factory[Post]):
model = Post
author = SubFactory(UserFactory)
title = Faker("sentence")
# In-memory — no DB required
user = UserFactory.build(admin=True)
# Persisted
user = await UserFactory.create(session)
posts = await PostFactory.create_batch(session, 5)
# Fast bulk insert — no hooks fired, no SubFactory resolution
rows = await UserFactory.create_batch(session, 10_000, bulk=True)
AutoFactory[T] generates sensible defaults from mapper introspection, with
name-based smart defaults (email → faker.email(), phone → faker.phone_number(), etc.):
class UserFactory(AutoFactory[User]):
model = User
Helpers
from seedling import upsert, truncate_tables, reset_sequences, deferred_constraints
await upsert(session, User, {"id": 1, "email": "a@b.com"}) # idempotent insert
await truncate_tables(session, User, Post, cascade=True) # dialect-aware TRUNCATE
await reset_sequences(session, User) # PostgreSQL: reset SERIAL
async with deferred_constraints(session): # PostgreSQL: defer FKs
...
State tracking
seed status # show last run per seeder + drift flag
seed run --new-only # skip seeders whose source hasn't changed
seed run --force # force re-run even if --new-only would skip
Disable per-project:
[tool.seedling]
state_tracking = false
pytest integration
# conftest.py
@pytest.fixture(scope="session")
def seedling_session_factory():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
return async_sessionmaker(engine, expire_on_commit=False)
from seedling.pytest_plugin import seed
@seed(UserSeeder)
async def test_with_user(seedling_transactional_session):
# UserSeeder ran before the test body (committed by the runner);
# anything done through this session is rolled back after the test
...
CLI reference
| Command | Description |
|---|---|
seed run |
Run seeders in dependency order |
seed fresh |
Truncate then re-seed |
seed list |
Print resolved execution order |
seed status |
Latest run per seeder with drift detection |
seed validate |
Static checks: cycles, missing deps, empty envs |
seed graph |
Dependency graph (Graphviz DOT or Mermaid) |
seed export |
Dump seeded rows to JSON or YAML |
seed restore |
Load a fixture file back into the database |
seed init |
Scaffold seeders/ and factories/ |
seed make:seeder <Name> |
Generate a seeder stub |
seed make:factory <module:ClassName> |
Generate a factory stub |
See the CLI reference for full options and flags.
Stability
From 1.0.0 onward, seedling follows Semantic Versioning: breaking changes only on major version bumps.
Documentation
Full docs: https://arthurvasconcelos.github.io/seedling
License
MIT — see LICENSE.
Metadata
Release files for sqlalchemy-seedling 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqlalchemy_seedling-1.0.0.tar.gz | 61.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqlalchemy_seedling-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 95.8 kB
Release files / sqlalchemy_seedling-1.0.0.tar.gz
| Download URL | sqlalchemy_seedling-1.0.0.tar.gz |
|---|---|
| Size | 61.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9d77da628c05423546a9ae3a6f7acc2d6223722fc6c1336c62586a33d62d317c
|
|
BLAKE2b-256 checksum How to use checksums |
61e9f74ae039652b04b1c0493209801872c2af5cff0d379e3456ca90f7f66f7b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / sqlalchemy_seedling-1.0.0-py3-none-any.whl
| Download URL | sqlalchemy_seedling-1.0.0-py3-none-any.whl |
|---|---|
| Size | 34.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
068a1ad7e01d09bba7b967cf7a726b642e1dd5add75f7f79c1e83c6616fd87be
|
|
BLAKE2b-256 checksum How to use checksums |
60374798afac363dd5d04b676b84ca12d61fcb801814310422a23b1188a69fc2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|