Quickstart¶
This page takes you from zero to persisting objects. We start with no backend at all, then bind to SQLAlchemy.
Without a backend (NoOpMateria)¶
The default NoOpMateria is active automatically — define transmuters and
use them like regular Pydantic models, no setup required. Great for tests and prototyping.
from arcanus.base import BaseTransmuter, Identity
from arcanus.association import (
Relation,
RelationCollection,
Relationship,
Relationships,
)
from pydantic import Field
from typing import Annotated, Optional
class Author(BaseTransmuter):
id: Annotated[Optional[int], Identity] = Field(default=None, frozen=True)
name: str
books: RelationCollection["Book"] = Relationships()
class Book(BaseTransmuter):
id: Annotated[Optional[int], Identity] = Field(default=None, frozen=True)
title: str
author_id: int | None = None
author: Relation[Author] = Relationship()
author = Author(id=1, name="Isaac Asimov")
book = Book(id=1, title="Foundation", author=Relation(author))
print(book.author.value.name) # Isaac Asimov
print(list(author.books)) # [Book(...)]
With SQLAlchemy¶
1. Define ORM models¶
Your SQLAlchemy models are plain, untouched SQLAlchemy:
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase): ...
class AuthorModel(Base):
__tablename__ = "authors"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100))
books: Mapped[list["BookModel"]] = relationship(back_populates="author")
class BookModel(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(200))
author_id: Mapped[int] = mapped_column(ForeignKey(AuthorModel.id))
author: Mapped[AuthorModel] = relationship(back_populates="books")
2. Bind transmuters with bless()¶
from arcanus.base import BaseTransmuter, Identity
from arcanus.association import (
Relation,
RelationCollection,
Relationship,
Relationships,
)
from arcanus.materia.sqlalchemy import SqlalchemyMateria
from pydantic import Field
from typing import Annotated, Optional
materia = SqlalchemyMateria()
@materia.bless(AuthorModel)
class Author(BaseTransmuter):
id: Annotated[Optional[int], Identity] = Field(default=None, frozen=True)
name: str
books: RelationCollection["Book"] = Relationships()
@materia.bless(BookModel)
class Book(BaseTransmuter):
id: Annotated[Optional[int], Identity] = Field(default=None, frozen=True)
title: str
author_id: int | None = None
author: Relation[Author] = Relationship()
3. Use the arcanus Session¶
Important
Use arcanus.materia.sqlalchemy.Session (not SQLAlchemy's native Session). The arcanus
session automatically "blesses" ORM rows into transmuters as they come out of queries.
from sqlalchemy import create_engine
from arcanus.materia.sqlalchemy import Session
engine = create_engine("sqlite://")
Base.metadata.create_all(engine)
Create¶
Adding the book cascades to its author. After a flush, server-generated ids are synced back onto the transmuters automatically.
with Session(engine) as session:
author = Author(name="Isaac Asimov")
book = Book(title="Foundation", author=Relation(author))
session.add(book) # adding the book also adds its author
session.flush() # INSERT; server-generated ids synced to the transmuters
session.commit()
print(book.id) # e.g. 1
Read¶
Every result is a transmuter, not a raw ORM row. Reads are plain SQLAlchemy — session.get and
session.execute(select(...)) — and typed column references (Author["name"]) drop straight into a
select.
from sqlalchemy import select
with Session(engine) as session:
author = session.get_one(Author, 1) # by primary key (raises if missing)
author = session.execute( # by filter
select(Author).filter_by(name="Isaac Asimov")
).scalar_one()
authors = (
session.execute( # many, with filter + ordering + paging
select(Author)
.where(Author["name"].like("Isaac%"))
.order_by(Author["name"].asc())
.limit(10)
)
.scalars()
.all()
)
books = (
session.execute(select(Book).where(Book["title"].like("Found%")))
.scalars()
.all()
)
Typed shortcuts
For common reads, Arcanus also adds typed helpers to the session — one, first, list,
count, bulk, partitions — so the above can be one-liners. See
Session Helpers.
Navigate relationships¶
Related rows load on access and keep their identity.
with Session(engine) as session:
author = session.get_one(Author, 1)
for book in author.books: # loads on access
assert book.author.value is author # same instance — identity preserved
Update & delete¶
Mutating a transmuter syncs to its ORM row in place — no model_dump() round-trip.
with Session(engine) as session:
book = session.get_one(Book, 1)
book.title = "Foundation (Revised)" # synced to the ORM row
session.commit()
session.delete(book) # follows your ORM cascade rules
session.commit()
Partial models at the boundary¶
Generated .Create / .Update models validate incomplete payloads; shell() / absorb() move
between a partial and a full transmuter.
Experimental — for API boundaries (e.g. FastAPI)
The .Create / .Update partials are experimental (the generated surface may change). They
exist for request/response boundaries — validating untrusted, incomplete bodies in a FastAPI
handler and the like. If you're just working with objects in memory, you don't need them:
construct and mutate the transmuter directly.
with Session(engine) as session:
author = Author.shell(Author.Create(name="New Author")) # Create excludes the id
session.add(author)
session.flush() # id synced back automatically
author.absorb(Author.Update(name="Renamed")) # applies only what's set
session.commit()
Async¶
Swap Session for AsyncSession and await the I/O — the API is otherwise identical. Awaiting a
relationship triggers its lazy load. See Lifecycle & Async.
from sqlalchemy.ext.asyncio import create_async_engine
from arcanus.materia.sqlalchemy import AsyncSession
async with AsyncSession(create_async_engine("sqlite+aiosqlite://")) as session:
author = await session.get_one(Author, 1)
books = await author.books # awaits the lazy load → list[Book]
session.add(Book(title="Async Book", author=Relation(author)))
await session.commit()
Next¶
- Usage — the full guide, grouped by materia: relationships, loading, querying, and partial schemas in depth.
- The Materia System — what
bless()does and the design philosophy. - API Reference.