# seedgraph > Seed your SQLAlchemy models as a referentially-consistent graph — one call, shared parents, verified links. seedgraph builds a graph of SQLAlchemy ORM objects from a declared shape (`seed(session, User, post=2, post__comment=3)`), writes it to the session, lets the database assign the keys and verifies every foreign key against the row it points at. Values come from Faker with a fixed seed, fit each column's type and stay unique against rows already in the database. Use it to create test data for SQLAlchemy 2.x models, in pytest or any test runner. # Getting started # seedgraph > Seed your SQLAlchemy models as a referentially-consistent graph — one call, shared parents, verified links. **seedgraph fills your test database with a coherent graph of objects, in a single call.** You declare what you want — "3 users, each with 2 posts, each post with 3 comments" — and the library builds the objects, links them, writes them to your session and then verifies every foreign key against the row it points at. Values are realistic and reproducible ("Jose Bishop", not "user-0"), generated by Faker with a fixed seed, valid for each column's type, and unique where the schema says so — even against rows already in the database. You can pin any column, replace how any column is generated, and attach the new graph to rows you already have. It plugs into pytest with no configuration. **Status: alpha** — the API may still change before 1.0. Every guarantee below is backed by a named test, on SQLite and on PostgreSQL. **Documentation**: [jrachid.github.io/seedgraph](https://jrachid.github.io/seedgraph/) — recipes for pytest, FastAPI and custom column types, the API reference, and [a Claude Code plugin](https://jrachid.github.io/seedgraph/agents/) that teaches coding agents to use seedgraph. ## Quick start ``` pip install seedgraph pip install "seedgraph[async]" # for seed_async and the async pytest fixtures ``` Requires Python 3.11+, SQLAlchemy 2.x and Faker 30+. The async extra adds greenlet (through `sqlalchemy[asyncio]`) and aiosqlite. Declare a **shape** from a root model; each key walks a one-to-many or many-to-many relationship, by relationship name or by target class name: ``` from sqlalchemy import ForeignKey, create_engine from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from seedgraph import seed class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] email: Mapped[str] = mapped_column(unique=True) posts: Mapped[list["Post"]] = relationship(back_populates="author") class Post(Base): __tablename__ = "posts" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] subtitle: Mapped[str | None] author_id: Mapped[int] = mapped_column(ForeignKey("users.id")) author: Mapped[User] = relationship(back_populates="posts") comments: Mapped[list["Comment"]] = relationship(back_populates="post") class Comment(Base): __tablename__ = "comments" id: Mapped[int] = mapped_column(primary_key=True) body: Mapped[str] post_id: Mapped[int] = mapped_column(ForeignKey("posts.id")) post: Mapped[Post] = relationship(back_populates="comments") engine = create_engine("sqlite://") Base.metadata.create_all(engine) session = Session(engine) graph = seed(session, User, post=2, post__comment=3) # 3 users by default assert len(graph.users) == 3 assert len(graph.posts) == 6 assert len(graph.comments) == 18 post = graph.users[0].posts[0] assert post.author_id == graph.users[0].id # real key, assigned by the database ``` `seed()` returns once the graph is flushed and verified; commit or roll back as your test needs. `seed_async(async_session, ...)` is its twin for an `AsyncSession`. The graph exposes every table of the model's metadata as an attribute, empty when the shape built none. ### Existing and missing parents ``` alice = session.get(User, 1) graph = seed(session, Post, parents=[alice]) # every post's author is alice; alice is not in graph.users graph = seed(session, Comment) # one Post and one User are generated, shared by all comments ``` A link first takes the nearest ancestor of its type in the shape, then the object of that type passed in `parents` (optional links included). A required link still empty gets **one** generated parent per type, shared by every object that needs it. Several objects of one type are accepted in `parents`; a link towards a single parent refuses to choose between them with `AmbiguousParentError`. ### Many-to-many ``` from sqlalchemy import Column, Table article_tag = Table( "article_tag", Base.metadata, Column("article_id", ForeignKey("articles.id"), primary_key=True), Column("tag_id", ForeignKey("tags.id"), primary_key=True), ) class Tag(Base): __tablename__ = "tags" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(unique=True) class Article(Base): __tablename__ = "articles" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] tags: Mapped[list[Tag]] = relationship(secondary=article_tag) Base.metadata.create_all(engine) python, sql = Tag(name="python"), Tag(name="sql") session.add_all([python, sql]) graph = seed(session, Article, article=5, tags=3) # 15 new tags, 3 per article assert len(graph.tags) == 15 graph = seed(session, Article, article=5, parents=[python, sql]) # every article tagged with both existing tags assert all(article.tags == [python, sql] for article in graph.articles) ``` A count keeps its one-to-many meaning: new objects for each parent. Objects passed in `parents` join every many-to-many collection of their type, next to the ones the shape builds. SQLAlchemy writes the association rows itself. ### Pinning and generating values ``` graph = seed( session, User, post=2, generators={User: {"name": lambda ctx: ctx.fake.first_name()}}, # replace how a column is generated overrides={Post: {"title": "Imposed", "subtitle": None}}, # pin a value, None included ) ``` `ctx.fake` is the session's seeded Faker; `ctx.column` is the column name. An override can also be a callable taking the same context. ### pytest Installing seedgraph registers four fixtures, prefixed so they never shadow your own `session` or `graph`: ``` def test_feed(seedgraph_graph): graph = seedgraph_graph(User, post=2) # fresh in-memory SQLite, FK enforced, tables created on demand assert len(graph.posts) == 6 async def test_feed_async(seedgraph_agraph): graph = await seedgraph_agraph(User, post=2) ``` `seedgraph_session` and `seedgraph_asession` expose the sessions behind them. To seed your own database, call `seed()` on your own session. ## Why Every Python team that seeds a relational test database eventually hand-rolls the same plumbing: generate rows, stage commits so primary keys exist, chase those keys into FK columns, repeat for every relationship, and hope the graph stays consistent. Here is what the alternatives give you, measured on 27 September 2026 by the scripts of [seedgraph-comparisons](https://github.com/jrachid/seedgraph-comparisons), which anyone can rerun: | Tool | What you get | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `polyfactory` 3.3 | Builds related objects, and SQLAlchemy puts the right keys in the FK columns when it writes them. **By default it draws every primary key at random between 0 and 9999**, so a test that writes a few dozen rows fails at random with `IntegrityError: UNIQUE constraint failed`: about a third of runs at 50 posts, three in four at 100, every run at 200. One line fixes it: `__set_primary_key__ = False`. A shape like 3 users × 2 posts × 5 comments comes out exact when the factory calls are nested from the top (`UserFactory.build(posts=[...])`). | | `faker-sqlalchemy` 0.10 (last release August 2022, requires SQLAlchemy < 2.0) | With `generate_related=True`, `RecursionError` on any two-way relationship (`backref`) and on a self-referential FK; a foreign key passed in `overrides` is silently replaced by a newly generated parent. | | `sqlalchemyseed` 2.6 | Writes data you already have (JSON, YAML, CSV) through your models, nested relationships included. It doesn't generate values. | | `sqlseed` 0.2 | Fills an existing SQLite or PostgreSQL database from its schema, not from your models, one row count per table. A graph shape is reachable indirectly: with the `coverage` strategy, 6 posts over 3 users gives exactly 2 each, so you work out the totals yourself. | | `sowdb` 0.3 | PostgreSQL only, from the schema, one row count per table; each foreign key draws a random parent, so 6 posts over 3 users came out as 2, 2, 2 in one run out of ten. | The failure you meet first, once a test writes enough rows: ``` import pytest from polyfactory.factories.sqlalchemy_factory import SQLAlchemyFactory from sqlalchemy.exc import IntegrityError class PostFactory(SQLAlchemyFactory[Post]): __set_relationships__ = True factory_engine = create_engine("sqlite://") Base.metadata.create_all(factory_engine) with Session(factory_engine) as factory_session, pytest.raises(IntegrityError, match="UNIQUE constraint failed: users.id"): factory_session.add_all(PostFactory.batch(1000)) # 1000 random ids between 0 and 9999 collide factory_session.flush() graph = seed(session, User, user=250, post=4) # the database assigns the keys: nothing to collide assert len(graph.posts) == 1000 ``` polyfactory avoids it with `__set_primary_key__ = False` on the factory; seedgraph needs no setting. ### "But other libraries do this too, don't they?" Some of it, yes: polyfactory builds the same graph once its primary keys are switched off and its calls are nested from the top. What seedgraph adds: **1. The shape in one call.** `seed(session, User, user=3, post=2, post__comment=5)` replaces the nested factory calls. A link that needs a parent takes it from the shape, from `parents`, or from one generated parent shared by every object that needs it. **2. A verified exit contract.** `seed()` flushes the graph, lets the database assign the keys, then walks every link and raises `IncoherentGraphError` if a foreign key disagrees with the row it points at. **3. Coexistence with a populated database, with no setting.** The database always assigns the keys, so seeding on top of existing rows never collides on ids, never desynchronises a PostgreSQL sequence, and stays safe when two sessions seed the same tables at once: a unique value the other session commits meanwhile is regenerated. Unique columns are checked against the rows already there before anything is written. **4. Determinism wired into pytest.** A new session replays the same values from the same seed; consecutive calls on one session continue the sequence instead of repeating it — inside a two-line fixture. ## Guarantees and the tests that prove them | Guarantee | Test | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Every link of the returned graph is verified after flush | `test_verification.py::test_seed_returns_a_graph_already_written_with_real_keys`, `::test_verify_graph_names_the_link_whose_foreign_key_disagrees` | | Seeding on top of existing rows keeps PostgreSQL sequences intact | `test_postgres.py::test_the_application_still_inserts_after_a_seed_on_top_of_its_rows` | | Two sessions seeding the same tables at once do not collide, unique values included | `test_postgres.py::test_two_sessions_seeding_the_same_tables_at_once_do_not_collide`, `::test_a_unique_value_another_session_commits_mid_seed_is_regenerated` | | Unique columns skip values already in the database | `test_unique.py::test_a_new_session_on_a_populated_database_skips_the_values_already_taken`, `::test_postgres_rows_from_an_earlier_run_do_not_block_a_new_seed` | | Same seed, same values; consecutive calls do not repeat | `test_generators.py::test_a_new_session_replays_the_same_values`, `::test_two_seeds_in_one_session_continue_the_same_faker_sequence` | | Generated values fit the column type (enum, length, precision, arrays) | `test_types.py` | | Many-to-many shapes build new objects per parent; existing objects are shared | `test_many_to_many.py::test_a_many_to_many_count_builds_new_objects_for_each_parent`, `::test_existing_objects_passed_as_parents_are_shared_by_every_generated_object` | | Natural and composite primary keys are generated and never collide | `test_verification.py::test_a_natural_text_key_is_generated`, `::test_natural_keys_skip_the_ones_already_in_the_database`, `::test_a_composite_integer_key_and_its_composite_foreign_key_are_generated` | | Multi-column unique constraints hold | `test_unique.py::test_many_rows_under_one_parent_keep_a_multi_column_constraint`, `::test_a_second_session_keeps_a_multi_column_constraint` | | Existing rows serve as parents; missing required parents are generated once | `test_parents.py::test_a_parent_already_in_the_database_is_linked_and_left_out_of_the_graph`, `::test_a_child_seeded_alone_gets_one_generated_parent_shared_by_all` | | Plugin fixtures live beside a project's own `session` and `graph` | `test_fixtures.py::test_the_prefixed_fixtures_live_beside_a_project_own_session_and_graph` | ## Limits - **`seed()` flushes the session.** It flushes the objects already pending before building the graph, so the unique checks see them; the keys come from the database, and the graph is no longer pending when it returns. - **A shape key towards a parent is refused**, as parents are linked or generated on their own; pass existing ones in `parents`. View-only relationships are refused too, since nothing would be written. - **Required columns of uncovered types** (JSON, custom `TypeDecorator`, arrays of those) raise `UnsupportedPlaceholderError`; declare a generator for them. Nullable ones are left empty. - **A multi-column unique constraint whose generated columns are only booleans or enums** is left to the database. For the others, one generated column is kept unique on its own, which is stricter than the constraint. - **An association class whose primary key combines its two foreign keys** holds one row per parent pair: the generated parent is shared, so two rows under the same parent collide. Seed one per parent, or use a many-to-many relationship. - **A concurrent seed can wait for the other session.** When two sessions generate the same unique value, the database holds the second one until the first commits or rolls back; seedgraph then regenerates the value if it was taken. On SQLite the write is not retried, since its drivers open no transaction before a SAVEPOINT: the second session gets the `IntegrityError`. - **A loop of required links between tables**, or a required link to its own table, cannot be generated; pass one side in `parents`. - **Determinism holds for a given Faker version.** Faker may change its data between releases. ## Design principles 1. **Model-first, not schema-first.** Works from your SQLAlchemy ORM models and relationships. 1. **Referential consistency is verified, not hoped for.** The database assigns the keys, seedgraph checks every link afterwards. 1. **Shared parents are the point.** Realistic data shares parents (one author, many posts). One object per FK is not a graph. 1. **Deterministic.** A new session with the same calls produces the same graph. 1. **Self-references and mutually referencing tables are normal.** `Category.parent` and tables pointing at each other are supported; only unsatisfiable loops of required links are refused. 1. **Stop generating at the boundary.** Existing rows are usable as parents; only missing parents get generated. ## Roadmap - [x] Shape API (`relation=n`, nesting, shared parents) - [x] Custom field generators (Faker under the hood) - [x] Overriding specific attributes on generated objects - [x] pytest fixture helpers - [x] Self-referential and cyclic FKs - [x] Async sessions support - [x] Database-assigned keys and post-flush verification, PostgreSQL in the test suite - [x] Type-valid values, uniqueness against existing rows, existing and generated parents - [x] Many-to-many shapes, generated natural keys, multi-column uniqueness, arrays - [x] Publication on PyPI ## License MIT # Recipes # Seeding your own database in pytest The four built-in fixtures (`seedgraph_graph`, `seedgraph_agraph` and their sessions) give each test a fresh in-memory SQLite. This recipe is for the other case: your tests run against your own engine — PostgreSQL in CI, SQLite locally — and each test must leave the database as it found it. ## One engine, one rolled-back session per test `seed()` flushes but never commits. A session that rolls back at the end of each test therefore undoes everything the test seeded, with no cleanup code: ``` import os import pytest from sqlalchemy import ForeignKey, create_engine, event, func, select from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from seedgraph import seed class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] posts: Mapped[list["Post"]] = relationship(back_populates="author") class Post(Base): __tablename__ = "posts" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] author_id: Mapped[int] = mapped_column(ForeignKey("users.id")) author: Mapped[User] = relationship(back_populates="posts") @pytest.fixture(scope="session") def engine(): engine = create_engine(os.environ.get("TEST_DATABASE_URL", "sqlite://")) if engine.dialect.name == "sqlite": @event.listens_for(engine, "connect") def enforce_foreign_keys(dbapi_connection, _): dbapi_connection.execute("PRAGMA foreign_keys=ON") Base.metadata.create_all(engine) yield engine engine.dispose() @pytest.fixture() def session(engine): with Session(engine) as session: yield session session.rollback() ``` SQLite ignores foreign keys unless each connection turns them on. Without the `PRAGMA`, a broken link goes unnoticed — which is exactly the bug seedgraph exists to prevent. ## Writing the tests ``` def test_each_post_belongs_to_the_user_it_was_seeded_under(session): graph = seed(session, User, post=2) for user in graph.users: assert [post.author_id for post in user.posts] == [user.id, user.id] def test_a_test_starts_on_an_empty_database(session): seed(session, User, post=2) assert session.scalar(select(func.count()).select_from(Post)) == 6 def test_the_rows_of_the_previous_test_are_gone(session): assert session.scalar(select(func.count()).select_from(Post)) == 0 ``` ## When the code under test commits A `commit()` inside the code under test ends the transaction the fixture meant to roll back. Bind the session to a connection whose outer transaction the fixture owns, as described in SQLAlchemy's [joining a session into an external transaction](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites); `seed()` works unchanged on that session. ## Same values on every run A new session replays the same generated values from the same seed, so a failing test fails the same way on the next run. Two calls to `seed()` in one session continue the sequence instead of repeating it, which keeps unique columns unique. # Testing a FastAPI endpoint Seed the graph in the test, hand the same session to the application through `dependency_overrides`, and call the endpoint: the application reads rows that were never committed, and the test rolls them back at the end. ## The application ``` from typing import Annotated from fastapi import Depends, FastAPI from sqlalchemy import ForeignKey, create_engine, select from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] posts: Mapped[list["Post"]] = relationship(back_populates="author") class Post(Base): __tablename__ = "posts" id: Mapped[int] = mapped_column(primary_key=True) title: Mapped[str] author_id: Mapped[int] = mapped_column(ForeignKey("users.id")) author: Mapped[User] = relationship(back_populates="posts") production_engine = create_engine("sqlite:///app.db") def get_session(): with Session(production_engine) as session: yield session app = FastAPI() @app.get("/users/{user_id}/posts") def list_posts(user_id: int, session: Annotated[Session, Depends(get_session)]) -> list[dict]: posts = session.scalars(select(Post).where(Post.author_id == user_id).order_by(Post.id)) return [{"id": post.id, "title": post.title} for post in posts] ``` ## The tests `TestClient` runs synchronous endpoints in a worker thread. An in-memory SQLite gives each thread its own empty database unless the engine keeps a single connection, hence `StaticPool`: ``` import pytest from fastapi.testclient import TestClient from sqlalchemy.pool import StaticPool from seedgraph import seed @pytest.fixture() def session(): engine = create_engine("sqlite://", poolclass=StaticPool, connect_args={"check_same_thread": False}) Base.metadata.create_all(engine) with Session(engine) as session: yield session session.rollback() engine.dispose() @pytest.fixture() def client(session): app.dependency_overrides[get_session] = lambda: session yield TestClient(app) app.dependency_overrides.clear() def test_a_user_sees_only_their_own_posts(session, client): graph = seed(session, User, user=2, post=3) alice, bob = graph.users response = client.get(f"/users/{alice.id}/posts") assert response.status_code == 200 assert [post["id"] for post in response.json()] == [post.id for post in alice.posts] assert not {post.id for post in bob.posts} & {post["id"] for post in response.json()} def test_a_user_without_posts_gets_an_empty_list(session, client): graph = seed(session, User) assert client.get(f"/users/{graph.users[0].id}/posts").json() == [] ``` Against PostgreSQL, drop `StaticPool` and `check_same_thread`: every thread goes through the same session object, so it sees the same transaction. # Columns seedgraph cannot fill on its own seedgraph generates strings, numbers, dates, booleans, enums, UUIDs, binary and arrays of those, fitted to each column's type. Two kinds of column have no defensible default: **JSON**, whose expected shape only your application knows, and **custom types** built on `TypeDecorator`. A nullable one is left empty; a required one stops the seed with `UnsupportedPlaceholderError` until you say how to fill it. These tests use the `seedgraph_graph` fixture, which the plugin registers when seedgraph is installed. ## The models ``` from decimal import Decimal import pytest from sqlalchemy import JSON, Integer, TypeDecorator from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column from seedgraph import UnsupportedPlaceholderError class Cents(TypeDecorator): """Store a Decimal amount as an integer number of cents.""" impl = Integer cache_ok = True def process_bind_param(self, value, dialect): return None if value is None else int(value * 100) def process_result_value(self, value, dialect): return None if value is None else Decimal(value) / 100 class Base(DeclarativeBase): pass class Order(Base): __tablename__ = "orders" id: Mapped[int] = mapped_column(primary_key=True) reference: Mapped[str] total = mapped_column(Cents(), nullable=False) details: Mapped[dict] = mapped_column(JSON) ``` ## The error names the column ``` def test_a_required_custom_column_stops_the_seed(seedgraph_graph): with pytest.raises(UnsupportedPlaceholderError, match="orders.total"): seedgraph_graph(Order) ``` ## Declaring a generator A generator receives a context: `ctx.fake` is the session's seeded [Faker](https://faker.readthedocs.io/), so values stay reproducible, and `ctx.column` is the column name. ``` def amount(ctx): return Decimal(ctx.fake.pyint(min_value=100, max_value=99_999)) / 100 def test_generators_fill_the_custom_and_json_columns(seedgraph_graph): graph = seedgraph_graph( Order, order=5, generators={ Order: { "total": amount, "details": lambda ctx: {"channel": ctx.fake.random_element(["web", "store"])}, } }, ) assert all(Decimal("1.00") <= order.total <= Decimal("999.99") for order in graph.orders) assert {order.details["channel"] for order in graph.orders} <= {"web", "store"} ``` ## Pinning one value instead When every object needs the same value, an override is shorter than a generator. A plain value pins it; a callable taking the same context computes it: ``` def test_an_override_pins_the_value(seedgraph_graph): graph = seedgraph_graph( Order, order=2, generators={Order: {"total": amount}}, overrides={Order: {"details": {"channel": "web"}, "reference": lambda ctx: ctx.fake.bothify("ORD-####")}}, ) assert [order.details for order in graph.orders] == [{"channel": "web"}, {"channel": "web"}] assert all(order.reference.startswith("ORD-") for order in graph.orders) ``` # Coding agents # Using seedgraph with coding agents A coding agent writes better test data when it knows seedgraph exists and how to call it. Pick the form your agent reads. ## Claude Code: install the plugin The seedgraph repository is also a Claude Code plugin marketplace. In a terminal: ``` claude plugin marketplace add jrachid/seedgraph claude plugin install seedgraph@seedgraph ``` Or inside a session: `/plugin marketplace add jrachid/seedgraph`, then `/plugin install seedgraph@seedgraph`. The plugin carries one skill, which Claude loads on its own when a task needs SQLAlchemy test data: the call, the shape syntax, the pytest fixtures and the fix for each error. It follows the repository's commits; `/plugin marketplace update seedgraph` fetches the latest. To keep the skill in a project without the plugin, copy [`SKILL.md`](https://github.com/jrachid/seedgraph/blob/main/plugins/seedgraph/skills/seedgraph/SKILL.md) to `.claude/skills/seedgraph/SKILL.md` and commit it. ## Any agent: a paragraph in AGENTS.md or CLAUDE.md Paste this into the instructions file your agent reads (`AGENTS.md`, `CLAUDE.md`, `.cursor/rules`, `.github/copilot-instructions.md`): ``` ## Test data Create SQLAlchemy test data with seedgraph, never by hand-building objects or setting foreign key ids: `seed(session, User, post=2, post__comment=3)` builds 3 users with 2 posts each and 3 comments per post, flushes, and verifies every foreign key. Existing rows go in `parents=[...]`; required JSON or custom-type columns need `generators={Model: {"column": lambda ctx: ...}}`. In pytest, use the `seedgraph_graph` fixture. Docs for agents: https://jrachid.github.io/seedgraph/llms-full.txt ``` ## Documentation in a form agents read - [`llms.txt`](https://jrachid.github.io/seedgraph/llms.txt): a summary with one link per page. - [`llms-full.txt`](https://jrachid.github.io/seedgraph/llms-full.txt): the whole documentation in one Markdown file. Both are rebuilt with the site on each release, so they describe the version on PyPI. # Reference # API reference Everything below is importable from `seedgraph`, except the fixtures, which the pytest plugin registers on its own. ## Seeding ## seedgraph.seed ``` seed( session: Session, model: type[DeclarativeBase], /, generators: GeneratorMap | None = None, overrides: OverrideMap | None = None, parents: Sequence[Any] = (), **shape: int, ) -> Graph ``` Seed a coherent object graph from the declared shape and return it. The graph is flushed, the database assigns its keys, and every FK column is verified against the row it points at. `generators` replaces how a column generates, `overrides` pins a value; both are keyed {Model: {"column": ...}}. `parents` are objects of the session that links of their type point at. Raises a `SeedgraphError` subclass on any bad declaration or broken link. ## seedgraph.seed_async ``` seed_async( session: AsyncSession, model: type[DeclarativeBase], /, generators: GeneratorMap | None = None, overrides: OverrideMap | None = None, parents: Sequence[Any] = (), **shape: int, ) -> Graph ``` Twin of `seed` on an AsyncSession: same contract, the flush is awaited. ## seedgraph.Graph Group the seeded objects by table name, exposed as attributes. Each table of the seeded model's metadata is an attribute holding the list of generated objects of that table, empty when the shape built none: `graph.users`. ## Custom generators `generators=` and `overrides=` are keyed by model, then by column name: `{User: {"name": ...}}`. A generator, or a callable override, receives this context: ## seedgraph.generators.GenerationContext The object handed to custom generators: the seeded fake and the column name. ## pytest fixtures pytest plugin: a fresh sqlite session and a seed callable, served without configuration. Every fixture carries the `seedgraph_` prefix, so it never shadows a project's own `session`. ## seedgraph_session ``` seedgraph_session() -> Iterator[Session] ``` Serve a fresh in-memory sqlite Session with foreign keys enforced. ## seedgraph_graph ``` seedgraph_graph( seedgraph_session: Session, ) -> Callable[..., Graph] ``` Serve a seed callable on the fresh session; tables are created on demand. ## seedgraph_asession ``` seedgraph_asession() -> AsyncIterator[AsyncSession] ``` Serve a fresh in-memory aiosqlite AsyncSession with foreign keys enforced. ## seedgraph_agraph ``` seedgraph_agraph( seedgraph_asession: AsyncSession, ) -> Callable[..., Any] ``` Serve an awaited seed callable on the fresh async session; tables on demand. ## Errors Every error derives from `SeedgraphError`, so one `except SeedgraphError` catches them all. ## seedgraph.SeedgraphError Bases: `Exception` Base class for every error seedgraph raises. Constructed with one message string naming the exact shape key, column or model at fault. ## seedgraph.UnknownShapeKeyError Bases: `SeedgraphError` A shape key matches no relationship of the model at that point of the path. ## seedgraph.AmbiguousShapeKeyError Bases: `SeedgraphError` A shape key matches several relationships of the model at that point of the path. ## seedgraph.InvalidShapeCountError Bases: `SeedgraphError` A shape count is not an integer greater than or equal to zero. ## seedgraph.UnsupportedShapeDirectionError Bases: `SeedgraphError` A shape key walks a relationship that cannot build children: towards a parent, or view-only. ## seedgraph.MissingRequiredParentError Bases: `SeedgraphError` A required link has no matching ancestor in the branch and was not declared in the shape. ## seedgraph.AmbiguousParentError Bases: `SeedgraphError` A link towards a single parent finds several objects of its type among the provided parents. ## seedgraph.UnattachedParentError Bases: `SeedgraphError` A provided parent is neither pending nor persistent in the session seeding the graph. ## seedgraph.UnsupportedPlaceholderError Bases: `SeedgraphError` A NOT NULL column without default carries a type no generator covers. ## seedgraph.UniqueValueExhaustedError Bases: `SeedgraphError` A unique column's generator kept producing values already used or already in the database. ## seedgraph.UnknownGeneratorColumnError Bases: `SeedgraphError` A column declared in generators does not exist on its model. ## seedgraph.UnknownOverrideColumnError Bases: `SeedgraphError` A column declared in overrides does not exist on its model. ## seedgraph.IncoherentGraphError Bases: `SeedgraphError` A link of the seeded graph carries FK values that differ from the linked object's key.