Skip to content

API Reference

Core Classes

SQLCrucibleBaseModel

sqlcrucible.SQLCrucibleBaseModel

Bases: BaseModel, SQLCrucibleEntity

__sqlalchemy_params__ = {'__abstract__': True} class-attribute instance-attribute

SQLCrucibleEntity

sqlcrucible.SQLCrucibleEntity

Base class for entities that auto-generate SQLAlchemy models.

Subclasses define their schema using type annotations with SQLAlchemy markers (mapped_column, relationship, etc.). The SQLAlchemy model is automatically generated and accessible via sqlalchemy_type.

Class Attributes

sqlalchemy_base: Optional custom DeclarativeBase for the SA model. sqlalchemy_params: SQLAlchemy configuration (tablename, etc.). converter_registry: Converter registry for field type conversion. sqlalchemy_automodel: The auto-generated SQLAlchemy model class. sqlalchemy_type: The SQLAlchemy model class to use (defaults to sqlalchemy_automodel).

Example
@dataclass
class User(SQLCrucibleEntity):
    __sqlalchemy_params__ = {"__tablename__": "users"}

    id: Annotated[int, mapped_column(Integer, primary_key=True)]
    name: Annotated[str, mapped_column(String(50))]
    email: Annotated[str | None, mapped_column(String(100))]


# Use the entity
user = User(id=1, name="Alice", email="alice@example.com")
sa_model = user.to_sa_model()  # Convert to SQLAlchemy

# Convert back
user2 = User.from_sa_model(sa_model)

__sqlalchemy_type__ = SQLAlchemyBase class-attribute

__sqlalchemy_params__ = {} class-attribute

to_sa_model()

Convert this entity to a SQLAlchemy model instance.

Creates a new SQLAlchemy model populated with data from this entity, applying any configured type converters for each field. Internally splices :meth:to_columns (column-shaped pairs, including decomposed composites) with the relationship-projection chain (nested-entity attributes), so the column-projection logic is a single source of truth shared with bulk-insert consumers.

Returns:

Type Description
Any

A SQLAlchemy model instance ready to be added to a session.

from_sa_model(sa_model) classmethod

Create an entity instance from a SQLAlchemy model.

This method converts a SQLAlchemy model instance into the corresponding entity class. For polymorphic models, it automatically selects the most specific entity subclass that matches the model type — searching the whole subclass subtree, not just cls's direct children, so that a polymorphic query through a generic STI root reaches the named concrete subclasses rather than only the transparent Foo[X] parameterisations that share the root's automodel.

Parameters:

Name Type Description Default
sa_model Any

A SQLAlchemy model instance to convert.

required

Returns:

Type Description
Self

An entity instance populated with data from the SQLAlchemy model.

Raises:

Type Description
TypeError

If sa_model is None.

ValueError

If sa_model is not compatible with this entity's SQLAlchemy type.

SAType

sqlcrucible.SAType

Utility to access an entity's SQLAlchemy type.

Provides a cleaner syntax for accessing an entity's SQLAlchemy type:

# Instead of:
select(Track.__sqlalchemy_type__).where(Track.__sqlalchemy_type__.length_seconds > 180)

# Write:
select(SAType[Track]).where(SAType[Track].length_seconds > 180)

With generated stubs, type checkers know the exact return type and can provide autocompletion for column names.

Annotations

SQLAlchemyField

sqlcrucible.entity.annotations.SQLAlchemyField dataclass

Configuration for mapping an entity field to SQLAlchemy.

This annotation can be used to customize how entity fields are mapped to SQLAlchemy columns or relationships.

Attributes:

Name Type Description
name str | None

The name to use for the mapped attribute (defaults to field name)

attr ORMDescriptor[Any] | None

An ORM descriptor to use directly (e.g., hybrid_property, association_proxy)

tp Any | None

The type to use for the mapped attribute

merge_all(*fields) classmethod

Merge multiple SQLAlchemyField annotations, with later values taking precedence.

ExcludeSAField

sqlcrucible.entity.annotations.ExcludeSAField dataclass

ConvertToSAWith

sqlcrucible.entity.annotations.ConvertToSAWith dataclass

Annotation specifying a custom converter from entity to SQLAlchemy.

The callable receives (value, context). context.target_type is the resolved entity-side field type of the value being converted.

Example
from typing import Annotated


class MyEntity(SQLCrucibleEntity):
    created_at: Annotated[
        datetime,
        mapped_column(),
        ConvertToSAWith(lambda dt, ctx: dt.astimezone(timezone.utc)),
    ]

ConvertFromSAWith

sqlcrucible.entity.annotations.ConvertFromSAWith dataclass

Annotation specifying a custom converter from SQLAlchemy to entity.

The callable receives (value, context). context.target_type is the resolved entity-side field type — for a concrete specialisation of a generic entity it's the specialised type on the subclass, not the TypeVar — so a converter can validate against it without having to guess.

Example
from typing import Annotated
from pydantic import TypeAdapter


class MyEntity(SQLCrucibleEntity):
    created_at: Annotated[
        datetime,
        mapped_column(),
        ConvertFromSAWith(lambda dt, ctx: dt.astimezone(timezone.utc)),
    ]
    payload: Annotated[
        SomeModel | None,
        mapped_column(JSON),
        ConvertFromSAWith(
            lambda v, ctx: (
                None if v is None else TypeAdapter(ctx.target_type).validate_python(v)
            )
        ),
    ]

ConversionContext

sqlcrucible.conversion.context.ConversionContext dataclass

Context handed to ConvertToSAWith / ConvertFromSAWith callables.

target_type is the resolved entity-side field type — for a concrete specialisation of a generic entity it's the specialised type, not the TypeVar — so a converter can validate against it (e.g. with pydantic.TypeAdapter) without having to guess. New fields can be added here as more context is needed; the callable signature stays Callable[[Any, ConversionContext], Any].

Fields

readonly_field

sqlcrucible.entity.descriptors.readonly_field(tp, *args)

readonly_field(tp: type[_T]) -> _T
readonly_field(
    tp: type[_T], arg1: SQLAlchemyField | ORMDescriptor[Any]
) -> _T
readonly_field(
    tp: type[_T],
    arg1: SQLAlchemyField | ORMDescriptor[Any],
    arg2: SQLAlchemyField | ORMDescriptor[Any],
) -> _T
readonly_field(tp: str) -> Any
readonly_field(
    tp: str, arg1: SQLAlchemyField | ORMDescriptor[Any]
) -> Any
readonly_field(
    tp: str,
    arg1: SQLAlchemyField | ORMDescriptor[Any],
    arg2: SQLAlchemyField | ORMDescriptor[Any],
) -> Any

Create a readonly field descriptor.

Readonly fields are loaded from the SQLAlchemy model but cannot be set on the entity. They are useful for computed properties like hybrid_property and association_proxy.

Parameters:

Name Type Description Default
tp type[_T] | str

The type of the field value

required
*args SQLAlchemyField | ORMDescriptor[Any]

Optional SQLAlchemyField and/or ORMDescriptor (e.g., hybrid_property, association_proxy) in any order. If both are provided, the descriptor is merged into the SQLAlchemyField. If neither is provided, the descriptor is extracted from the field's Annotated type if present.

()

Returns:

Type Description
Any

A descriptor that loads the field value from the backing SQLAlchemy model.

Example
def _full_name(self) -> str:
    return f"{self.first_name} {self.last_name}"


class Person(SQLCrucibleBaseModel):
    first_name: Annotated[str, mapped_column()]
    last_name: Annotated[str, mapped_column()]

    # Simplest: pass descriptor directly
    full_name = readonly_field(str, hybrid_property(_full_name))

    # Alternative: use Annotated syntax (descriptor extracted automatically)
    full_name: Annotated[str, hybrid_property(_full_name)] = readonly_field(str)

    # With SQLAlchemyField (order doesn't matter)
    full_name = readonly_field(
        str,
        hybrid_property(_full_name),
        SQLAlchemyField(name="computed_full_name"),
    )

    # Or reversed order
    full_name = readonly_field(
        str,
        SQLAlchemyField(name="computed_full_name"),
        hybrid_property(_full_name),
    )
Note

Accessing a readonly_field on an entity not loaded via from_sa_model() raises RuntimeError. For Pydantic models, add ReadonlyFieldDescriptor to model_config's ignored_types or inherit from SQLCrucibleBaseModel.

Utilities

lazyproperty

sqlcrucible.entity.core.lazyproperty(func)