Skip to content

Registry

Registry is the runtime hub for model registration and database ownership.

If models are the shape of your application data, the registry is the runtime environment that tells those models where they live and how metadata should be built.

Core responsibilities

  • own the primary database and any extra databases
  • register declared, reflected, and pattern-generated models
  • expose SQLAlchemy metadata for migrations and table building
  • copy model definitions into isolated registries when needed
  • coordinate content types, schema helpers, and reflection callbacks

Practical example

database = saffier.Database("postgresql+asyncpg://postgres:postgres@localhost:5432/app")
models = saffier.Registry(database=database)

Runtime patterns to know

copy.copy(registry) is a supported workflow for migration preparation and test isolation.

with_async_env() exists for synchronous scripts or CLI-style code that still needs the registry lifecycle managed correctly.

metadata_by_name and metadata_by_url matter when a project uses extra= databases and migration/runtime code needs the correct SQLAlchemy metadata container for each connection.

saffier.Registry

Registry(
    database,
    *,
    with_content_type=False,
    model_engine=None,
    **kwargs,
)

Central model registry for Saffier applications.

A registry owns the primary database connection, optional extra databases, all registered model classes, and the SQLAlchemy metadata containers used for table generation, reflection, and migrations. It is the hub where model registration, content-type integration, schema helpers, registry copying, reflection, and CLI migration preparation all meet.

Most user-facing Saffier applications create a single registry and point every model at it, but the implementation also supports copied registries, extra databases, pattern-based reflection, and delayed relation callbacks.

Source code in saffier/core/connection/registry.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def __init__(
    self,
    database: Database | str,
    *,
    with_content_type: bool | type[Any] = False,
    model_engine: Any | None = None,
    **kwargs: Any,
) -> None:
    self.db_schema = kwargs.get("schema")
    self._automigrate_config = kwargs.pop("automigrate_config", None)
    self._is_automigrated: bool = False
    extra = kwargs.pop("extra", {}) or {}
    self.database: Database = (
        database if isinstance(database, Database) else Database(database)
    )
    self.models: dict[str, Any] = {}
    self.admin_models: set[str] = set()
    self.reflected: dict[str, Any] = {}
    self.pattern_models: dict[str, Any] = {}
    self.content_type: Any | None = None
    self.model_engine = model_engine
    self.extra: dict[str, Database] = {
        name: value if isinstance(value, Database) else Database(value)
        for name, value in extra.items()
    }
    assert all(self.extra_name_check(name) for name in self.extra), (
        "Invalid name in extra detected. See logs for details."
    )
    self._pattern_reflected_dbs: set[str | None] = set()
    self._content_type_models_bound: set[str] = set()
    self._model_callbacks: dict[str, list[tuple[Callable[[type[Any]], None], bool]]] = {}

    self.schema = Schema(registry=self)
    self._metadata = self._make_metadata()
    self._metadata_by_name = MetaDataDict(self)
    self._metadata_by_name[None] = self._metadata
    for name in self.extra:
        self._metadata_by_name[name] = self._make_metadata()
    self._metadata_by_url = MetaDataByUrlDict(self)

    if with_content_type is not False:
        self._set_content_type(with_content_type)

db_schema instance-attribute

db_schema = get('schema')

_automigrate_config instance-attribute

_automigrate_config = pop('automigrate_config', None)

_is_automigrated instance-attribute

_is_automigrated = False

database instance-attribute

database = (
    database
    if isinstance(database, Database)
    else Database(database)
)

models instance-attribute

models = {}

admin_models instance-attribute

admin_models = set()

reflected instance-attribute

reflected = {}

pattern_models instance-attribute

pattern_models = {}

content_type instance-attribute

content_type = None

model_engine instance-attribute

model_engine = model_engine

extra instance-attribute

extra = {
    name: (
        value
        if isinstance(value, Database)
        else Database(value)
    )
    for name, value in (items())
}

_pattern_reflected_dbs instance-attribute

_pattern_reflected_dbs = set()

_content_type_models_bound instance-attribute

_content_type_models_bound = set()

_model_callbacks instance-attribute

_model_callbacks = {}

schema instance-attribute

schema = Schema(registry=self)

_metadata instance-attribute

_metadata = _make_metadata()

_metadata_by_name instance-attribute

_metadata_by_name = MetaDataDict(self)

_metadata_by_url instance-attribute

_metadata_by_url = MetaDataByUrlDict(self)

metadata property writable

metadata

Return the primary metadata container for the registry.

Accessing this property forces registered models to build their tables so callers such as migration tools and schema helpers observe a metadata object that already includes all declared models.

RETURNS DESCRIPTION
Any

Primary SQLAlchemy metadata object.

TYPE: Any

metadata_by_name property writable

metadata_by_name

Return metadata containers keyed by database alias.

The property eagerly builds registered models and best-effort builds reflected models so callers receive a stable mapping that includes the current table definitions for the primary database and every configured extra database.

RETURNS DESCRIPTION
MetaDataDict

Metadata mapping keyed by database alias.

TYPE: MetaDataDict

metadata_by_url property

metadata_by_url

Return metadata containers addressable by database URL string.

RETURNS DESCRIPTION
MetaDataByUrlDict

Reverse metadata mapping keyed by URL string.

TYPE: MetaDataByUrlDict

metadatas property

metadatas

declarative_base cached property

declarative_base

Create a declarative base bound to the registry schema.

This is primarily used by SQLAlchemy integration and reflection helpers that need a conventional declarative base sharing the registry schema configuration.

RETURNS DESCRIPTION
Any

SQLAlchemy declarative base class.

TYPE: Any

engine property

engine

Return the started async SQLAlchemy engine for the primary database.

RETURNS DESCRIPTION
AsyncEngine

Connected async engine for the main database.

TYPE: AsyncEngine

RAISES DESCRIPTION
AssertionError

If the database has not been connected yet.

sync_engine property

sync_engine

Return the synchronous engine wrapper for the primary database.

RETURNS DESCRIPTION
Engine

Synchronous engine used by reflection and sync-only helpers.

TYPE: Engine

apply_default_force_nullable_fields async

apply_default_force_nullable_fields(
    *,
    force_fields_nullable=None,
    model_defaults=None,
    filter_db_url=None,
    filter_db_name=None,
)

Backfill defaults for fields made temporarily nullable by migrations.

Forced-nullable migration generation is a two-step safety mechanism. Alembic first sees selected fields as nullable so it can add them to populated tables without violating constraints. During online upgrade, generated migration files call this method to find rows where those fields are still NULL and update them with the model's declared defaults or with explicit defaults supplied by the migration.

The method deliberately stays on Saffier's public ORM path. It filters registered models, selects only primary-key data for matching rows, then calls update() on each model instance with concrete default values. That preserves field normalization, SQLAlchemy execution, and model update signals while avoiding a second migration-specific persistence layer.

PARAMETER DESCRIPTION
force_fields_nullable

Iterable of (model_name, field_name) selectors. When omitted, selectors are read from the active FORCE_FIELDS_NULLABLE context.

TYPE: Iterable[tuple[str, str]] | None DEFAULT: None

model_defaults

Optional per-model default overrides keyed by model name and field name. Overrides are also treated as selectors so migrations can backfill fields that do not declare a Python default on the model.

TYPE: dict[str, dict[str, Any]] | None DEFAULT: None

filter_db_url

Optional database URL string. When provided, only models using that database are processed.

TYPE: str | None DEFAULT: None

filter_db_name

Optional registry database name. "" targets the primary database; any other value targets the matching extra database and takes precedence over filter_db_url.

TYPE: str | None DEFAULT: None

Source code in saffier/core/connection/registry.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
async def apply_default_force_nullable_fields(
    self,
    *,
    force_fields_nullable: Iterable[tuple[str, str]] | None = None,
    model_defaults: dict[str, dict[str, Any]] | None = None,
    filter_db_url: str | None = None,
    filter_db_name: str | None = None,
) -> None:
    """Backfill defaults for fields made temporarily nullable by migrations.

    Forced-nullable migration generation is a two-step safety mechanism.
    Alembic first sees selected fields as nullable so it can add them to
    populated tables without violating constraints. During online upgrade,
    generated migration files call this method to find rows where those
    fields are still ``NULL`` and update them with the model's declared
    defaults or with explicit defaults supplied by the migration.

    The method deliberately stays on Saffier's public ORM path. It filters
    registered models, selects only primary-key data for matching rows, then
    calls ``update()`` on each model instance with concrete default values.
    That preserves field normalization, SQLAlchemy execution, and model
    update signals while avoiding a second migration-specific persistence
    layer.

    Args:
        force_fields_nullable: Iterable of ``(model_name, field_name)``
            selectors. When omitted, selectors are read from the active
            ``FORCE_FIELDS_NULLABLE`` context.
        model_defaults: Optional per-model default overrides keyed by model
            name and field name. Overrides are also treated as selectors so
            migrations can backfill fields that do not declare a Python
            default on the model.
        filter_db_url: Optional database URL string. When provided, only
            models using that database are processed.
        filter_db_name: Optional registry database name. ``""`` targets the
            primary database; any other value targets the matching
            ``extra`` database and takes precedence over ``filter_db_url``.
    """
    if force_fields_nullable is None:
        selected_fields = set(FORCE_FIELDS_NULLABLE.get())
    else:
        selected_fields = set(force_fields_nullable)
    effective_model_defaults = model_defaults or {}
    for model_name, defaults in effective_model_defaults.items():
        for field_name in defaults:
            selected_fields.add((model_name, field_name))

    for model_name, field_name in tuple(selected_fields):
        if model_name:
            continue
        selected_fields.discard((model_name, field_name))
        for model_class in self.models.values():
            if field_name in model_class.fields:
                selected_fields.add((model_class.__name__, field_name))

    if not selected_fields:
        return

    if isinstance(filter_db_name, str):
        filter_db_url = (
            str(self.database.url)
            if filter_db_name == ""
            else str(self.extra[filter_db_name].url)
        )

    models_with_fields: dict[str, set[str]] = {}
    for model_name, field_name in selected_fields:
        model_class = self.models.get(model_name)
        if model_class is None:
            continue

        field = model_class.fields.get(field_name)
        if field is None:
            continue

        default_overrides = effective_model_defaults.get(model_name, {})
        if not field.has_default() and field_name not in default_overrides:
            continue

        models_with_fields.setdefault(model_name, set()).add(field_name)

    async def backfill_model(
        model_name: str,
        field_names: set[str],
    ) -> None:
        """Backfill one model's matching rows through normal ORM updates.

        The closure is scheduled by ``run_concurrently`` for each model that
        has at least one valid forced-nullable field. It keeps row selection
        narrow by loading primary keys first, then computes default values
        per object so callable defaults are evaluated for each updated row
        rather than once for the whole migration.

        Args:
            model_name: Registered model class name to process.
            field_names: Field names that were temporarily nullable and
                should now receive defaults where the database still stores
                ``NULL``.
        """
        model_class = self.models[model_name]
        if filter_db_url and str(model_class.database.url) != filter_db_url:
            return

        query = model_class.query.filter(**dict.fromkeys(field_names)).only(
            *model_class.pknames
        )
        objects = await query
        default_overrides = effective_model_defaults.get(model_name, {})
        for obj in objects:
            values = {
                field_name: (
                    default_overrides[field_name]
                    if field_name in default_overrides
                    else model_class.fields[field_name].get_default_value()
                )
                for field_name in field_names
            }
            await obj.update(**values)

    await run_concurrently(
        [
            backfill_model(model_name, field_names)
            for model_name, field_names in models_with_fields.items()
        ]
    )

_make_metadata

_make_metadata()

Create a fresh SQLAlchemy metadata container for this registry.

When the registry is pinned to a default schema, every metadata object created here is initialized with that schema so table creation, reflection, and migration preparation all operate in the same namespace.

RETURNS DESCRIPTION
MetaData

sqlalchemy.MetaData: New metadata object for the registry.

Source code in saffier/core/connection/registry.py
298
299
300
301
302
303
304
305
306
307
308
309
310
def _make_metadata(self) -> sqlalchemy.MetaData:
    """Create a fresh SQLAlchemy metadata container for this registry.

    When the registry is pinned to a default schema, every metadata object
    created here is initialized with that schema so table creation,
    reflection, and migration preparation all operate in the same namespace.

    Returns:
        sqlalchemy.MetaData: New metadata object for the registry.
    """
    if self.db_schema is not None:
        return sqlalchemy.MetaData(schema=self.db_schema)
    return sqlalchemy.MetaData()

extra_name_check

extra_name_check(name)

Validate one entry key from the registry extra mapping.

Saffier accepts arbitrary names for extra databases, but empty or non-string keys make later lookups ambiguous. This helper centralizes the validation and logs user-facing diagnostics before registry creation proceeds.

PARAMETER DESCRIPTION
name

Candidate extra-database name.

TYPE: Any

RETURNS DESCRIPTION
bool

True when the key is usable, otherwise False.

TYPE: bool

Source code in saffier/core/connection/registry.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def extra_name_check(self, name: Any) -> bool:
    """Validate one entry key from the registry `extra` mapping.

    Saffier accepts arbitrary names for extra databases, but empty or
    non-string keys make later lookups ambiguous. This helper centralizes the
    validation and logs user-facing diagnostics before registry creation
    proceeds.

    Args:
        name: Candidate extra-database name.

    Returns:
        bool: `True` when the key is usable, otherwise `False`.
    """
    if not isinstance(name, str):
        logger.error("Extra database name: %r is not a string.", name)
        return False
    if not name.strip():
        logger.error('Extra database name: "%s" is empty.', name)
        return False
    if name.strip() != name:
        logger.warning(
            'Extra database name: "%s" starts or ends with whitespace characters.', name
        )
    return True

_get_relation_target_name

_get_relation_target_name(relation)
Source code in saffier/core/connection/registry.py
338
339
340
341
342
343
def _get_relation_target_name(self, relation: Any) -> str | None:
    if isinstance(relation, str):
        return relation
    if isinstance(relation, type):
        return relation.__name__
    return None

_is_auto_through_model

_is_auto_through_model(model_class)

Detect auto-generated many-to-many through models.

Registry copying skips Saffier-generated through models during the first pass because they are recreated or patched after their owning models have been copied. This helper matches both the older and newer generated naming conventions so copied registries preserve many-to-many behavior without duplicating synthetic models.

PARAMETER DESCRIPTION
model_class

Model class to inspect.

TYPE: type[Any]

RETURNS DESCRIPTION
bool

True if the model looks like an auto-generated through model.

TYPE: bool

Source code in saffier/core/connection/registry.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
def _is_auto_through_model(self, model_class: type[Any]) -> bool:
    """Detect auto-generated many-to-many through models.

    Registry copying skips Saffier-generated through models during the first
    pass because they are recreated or patched after their owning models have
    been copied. This helper matches both the older and newer generated
    naming conventions so copied registries preserve many-to-many behavior
    without duplicating synthetic models.

    Args:
        model_class: Model class to inspect.

    Returns:
        bool: `True` if the model looks like an auto-generated through model.
    """
    from saffier.core.db.fields.base import ManyToManyField

    for owner_model in self.models.values():
        for field_name, field in owner_model.fields.items():
            if not isinstance(field, ManyToManyField):
                continue
            through_model = getattr(field, "through", None)
            if through_model is not model_class:
                continue
            target_name = self._get_relation_target_name(field.to) or field.target.__name__
            expected_old_name = f"{owner_model.__name__}{target_name}"
            expected_old_table = f"{owner_model.__name__.lower()}s_{target_name}s".lower()

            expected_new_name = f"{owner_model.__name__}{field_name.capitalize()}Through"
            expected_new_table = expected_new_name.lower()
            if owner_model.meta.table_prefix:
                expected_old_table = f"{owner_model.meta.table_prefix}_{expected_old_table}"
                expected_new_table = f"{owner_model.meta.table_prefix}_{expected_new_table}"

            return (
                model_class.__name__ == expected_old_name
                and getattr(model_class.meta, "tablename", None) == expected_old_table
            ) or (
                model_class.__name__ == expected_new_name
                and getattr(model_class.meta, "tablename", None) == expected_new_table
            )
    return False

_copy_dependencies

_copy_dependencies(model_class, skipped)
Source code in saffier/core/connection/registry.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def _copy_dependencies(self, model_class: type[Any], skipped: set[str]) -> set[str]:
    from saffier.core.db.fields.base import ForeignKey

    dependencies: set[str] = set()
    for field in model_class.fields.values():
        if isinstance(field, ForeignKey):
            target_name = self._get_relation_target_name(field.to)
            if (
                target_name
                and target_name in self.models
                and target_name != model_class.__name__
            ):
                dependencies.add(target_name)
    dependencies.difference_update(skipped)
    return dependencies

_sorted_model_names_for_copy

_sorted_model_names_for_copy()

Topologically order model names for registry copying.

Models with foreign-key dependencies should be copied after their targets whenever possible so string references and through-model patches are minimized. Auto-generated through models are skipped entirely here because they are handled separately after the base copy pass.

RETURNS DESCRIPTION
list[str]

list[str]: Model names ordered for safe copying.

Source code in saffier/core/connection/registry.py
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
def _sorted_model_names_for_copy(self) -> list[str]:
    """Topologically order model names for registry copying.

    Models with foreign-key dependencies should be copied after their
    targets whenever possible so string references and through-model patches
    are minimized. Auto-generated through models are skipped entirely here
    because they are handled separately after the base copy pass.

    Returns:
        list[str]: Model names ordered for safe copying.
    """
    skipped = {
        name
        for name, model_class in self.models.items()
        if self._is_auto_through_model(model_class)
    }
    remaining = {
        name: self._copy_dependencies(model_class, skipped)
        for name, model_class in self.models.items()
        if name not in skipped
    }
    ordered: list[str] = []
    resolved: set[str] = set()

    while remaining:
        ready = [name for name, deps in remaining.items() if deps.issubset(resolved)]
        if not ready:
            ordered.extend(remaining.keys())
            break
        for name in ready:
            ordered.append(name)
            resolved.add(name)
            remaining.pop(name)
    return ordered

_copy_model_to_registry

_copy_model_to_registry(
    model_class, registry, *, pending_m2m_patches
)

Clone one model class into a target registry.

The copied class receives copied field and manager instances so later mutations in migration preparation or isolated test registries do not leak back into the source registry. Same-registry relation targets are rewritten to string references until the target registry has all of its models in place, at which point pending many-to-many through models are patched separately.

PARAMETER DESCRIPTION
model_class

Source model to copy.

TYPE: type[Any]

registry

Target registry receiving the copied model.

TYPE: Registry

pending_m2m_patches

Collector for many-to-many fields whose through model must be rebound after all models have been copied.

TYPE: list[tuple[str, str, str]]

RETURNS DESCRIPTION
type[Any]

type[Any]: Newly created copied model class.

Source code in saffier/core/connection/registry.py
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
def _copy_model_to_registry(
    self,
    model_class: type[Any],
    registry: "Registry",
    *,
    pending_m2m_patches: list[tuple[str, str, str]],
) -> type[Any]:
    """Clone one model class into a target registry.

    The copied class receives copied field and manager instances so later
    mutations in migration preparation or isolated test registries do not
    leak back into the source registry. Same-registry relation targets are
    rewritten to string references until the target registry has all of its
    models in place, at which point pending many-to-many through models are
    patched separately.

    Args:
        model_class: Source model to copy.
        registry: Target registry receiving the copied model.
        pending_m2m_patches: Collector for many-to-many fields whose through
            model must be rebound after all models have been copied.

    Returns:
        type[Any]: Newly created copied model class.
    """
    from saffier.core.db.fields.base import ForeignKey, ManyToManyField
    from saffier.core.db.models.managers import Manager
    from saffier.core.utils.models import create_saffier_model

    definitions: dict[str, Any] = {}
    manager_annotations: dict[str, Any] = {}
    existing_annotations = dict(getattr(model_class, "__annotations__", {}))
    for field_name, field in model_class.fields.items():
        field_copy = copy.copy(field)
        if hasattr(field_copy, "_target"):
            delattr(field_copy, "_target")
        if isinstance(field_copy, ForeignKey):
            target_name = self._get_relation_target_name(field_copy.to)
            if target_name and target_name in self.models:
                field_copy.to = target_name
        elif isinstance(field_copy, ManyToManyField):
            target_name = self._get_relation_target_name(field_copy.to)
            if target_name and target_name in self.models:
                field_copy.to = target_name

            through_name = self._get_relation_target_name(field_copy.through)
            if through_name and through_name in self.models:
                if self._is_auto_through_model(self.models[through_name]):
                    field_copy.through = None
                elif through_name in registry.models:
                    field_copy.through = registry.models[through_name]
                else:
                    pending_m2m_patches.append(
                        (model_class.__name__, field_name, through_name)
                    )

        definitions[field_name] = field_copy

    for manager_name in getattr(model_class.meta, "managers", []):
        manager = getattr(model_class, manager_name, None)
        if isinstance(manager, Manager):
            definitions[manager_name] = copy.copy(manager)
            manager_annotations[manager_name] = existing_annotations.get(
                manager_name,
                ClassVar[Any],
            )

    if manager_annotations:
        definitions["__annotations__"] = {
            **existing_annotations,
            **manager_annotations,
        }

    meta = type(
        "Meta",
        (),
        {
            "registry": registry,
            "tablename": getattr(model_class.meta, "tablename", None),
            "table_prefix": getattr(model_class.meta, "table_prefix", None),
            "unique_together": list(getattr(model_class.meta, "unique_together", []) or []),
            "indexes": list(getattr(model_class.meta, "indexes", []) or []),
            "constraints": list(getattr(model_class.meta, "constraints", []) or []),
            "reflect": getattr(model_class.meta, "reflect", False),
            "abstract": getattr(model_class.meta, "abstract", False),
            "model_engine": getattr(model_class.meta, "model_engine", None),
        },
    )

    return create_saffier_model(
        model_class.__name__,
        model_class.__module__,
        __definitions__=definitions,
        __metadata__=meta,
        __qualname__=model_class.__qualname__,
        __bases__=model_class.__bases__,
    )

__copy__

__copy__()

Copy the registry and all registered models into an isolated registry instance.

The copy operation is intentionally deep for model definitions: each copied model gets copied fields, managers, metadata, and many-to-many relation descriptors so migration preparation or test isolation does not mutate the live application registry.

RETURNS DESCRIPTION
Registry

A new registry instance containing copied model classes.

TYPE: Registry

Source code in saffier/core/connection/registry.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def __copy__(self) -> "Registry":
    """Copy the registry and all registered models into an isolated registry instance.

    The copy operation is intentionally deep for model definitions: each
    copied model gets copied fields, managers, metadata, and many-to-many
    relation descriptors so migration preparation or test isolation does not
    mutate the live application registry.

    Returns:
        Registry: A new registry instance containing copied model classes.
    """
    registry_copy = type(self)(
        self.database,
        schema=self.db_schema,
        extra=self.extra,
        automigrate_config=self._automigrate_config,
        model_engine=self.model_engine,
    )
    pending_m2m_patches: list[tuple[str, str, str]] = []

    for model_name in self._sorted_model_names_for_copy():
        self._copy_model_to_registry(
            self.models[model_name],
            registry_copy,
            pending_m2m_patches=pending_m2m_patches,
        )

    for model_name, model_class in self.reflected.items():
        if model_name in registry_copy.models or model_name in registry_copy.reflected:
            continue
        self._copy_model_to_registry(
            model_class,
            registry_copy,
            pending_m2m_patches=pending_m2m_patches,
        )

    for owner_name, field_name, through_name in pending_m2m_patches:
        from saffier.core.db.fields.base import ManyToManyField
        from saffier.core.db.relationships.relation import Relation

        through_model = registry_copy.models.get(through_name) or registry_copy.reflected.get(
            through_name
        )
        if through_model is None:
            continue
        owner_model = registry_copy.models[owner_name]
        field = cast("ManyToManyField", owner_model.fields[field_name])
        field.through = through_model
        setattr(
            owner_model,
            settings.many_to_many_relation.format(key=field_name),
            Relation(through=through_model, to=field.target, owner=owner_model),
        )

    registry_copy.pattern_models = dict(self.pattern_models)
    registry_copy.admin_models = {
        name
        for name in self.admin_models
        if name in registry_copy.models or name in registry_copy.reflected
    }
    if hasattr(self, "tenant_models") and hasattr(registry_copy, "tenant_models"):
        registry_copy.tenant_models = {
            name: model
            for name in self.tenant_models
            if (model := registry_copy.models.get(name) or registry_copy.reflected.get(name))
            is not None
        }
    registry_copy._pattern_reflected_dbs = set(self._pattern_reflected_dbs)
    registry_copy._content_type_models_bound = set(self._content_type_models_bound)
    if self.content_type is not None:
        with_content_type = registry_copy.models.get(
            "ContentType"
        ) or registry_copy.reflected.get("ContentType")
        if with_content_type is None:
            with_content_type = self.content_type
        registry_copy.content_type = with_content_type
        registry_copy._attach_content_type_to_registered_models()
    return registry_copy

_set_content_type

_set_content_type(with_content_type)

Enable and normalize content-type support for the registry.

The registry accepts either True to use the built-in content type model or an explicit model class. Abstract or detached content type models are copied into this registry as needed, then all registered concrete models are updated so they expose a content-type relation and pre-save hook.

PARAMETER DESCRIPTION
with_content_type

Flag or model class describing the content-type model to use.

TYPE: bool | type[Any]

RAISES DESCRIPTION
TypeError

If the provided value is neither a boolean flag nor a model class.

Source code in saffier/core/connection/registry.py
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
def _set_content_type(self, with_content_type: bool | type[Any]) -> None:
    """Enable and normalize content-type support for the registry.

    The registry accepts either `True` to use the built-in content type
    model or an explicit model class. Abstract or detached content type
    models are copied into this registry as needed, then all registered
    concrete models are updated so they expose a content-type relation and
    pre-save hook.

    Args:
        with_content_type: Flag or model class describing the content-type
            model to use.

    Raises:
        TypeError: If the provided value is neither a boolean flag nor a
            model class.
    """
    from saffier.contrib.contenttypes.models import ContentType

    content_type_model = ContentType if with_content_type is True else with_content_type
    if not isinstance(content_type_model, type):
        raise TypeError("with_content_type must be True/False or a model type.")

    if getattr(content_type_model.meta, "abstract", False):
        in_admin = getattr(content_type_model.meta, "in_admin", None)
        no_admin_create = getattr(content_type_model.meta, "no_admin_create", None)
        meta = type(
            "Meta",
            (),
            {
                "registry": self,
                "tablename": "contenttypes",
                "in_admin": True if in_admin is None else in_admin,
                "no_admin_create": True if no_admin_create is None else no_admin_create,
            },
        )
        content_type_model = type("ContentType", (content_type_model,), {"Meta": meta})
    elif getattr(content_type_model.meta, "registry", None) in (None, False):
        if not getattr(content_type_model.meta, "tablename", None):
            content_type_model.meta.tablename = "contenttypes"
        if getattr(content_type_model.meta, "in_admin", None) is None:
            content_type_model.meta.in_admin = True
        if getattr(content_type_model.meta, "no_admin_create", None) is None:
            content_type_model.meta.no_admin_create = True
        content_type_model = content_type_model.add_to_registry(self, name="ContentType")

    registered_content_type = self.models.get("ContentType")
    if registered_content_type is not None:
        content_type_model = registered_content_type

    self.content_type = content_type_model
    self._attach_content_type_to_registered_models()

_attach_content_type_to_registered_models

_attach_content_type_to_registered_models()

Attach content-type integration to every registered concrete model.

This method is idempotent. It is called after registry creation, after registry copies, and whenever a registry gains a new content type model.

Source code in saffier/core/connection/registry.py
669
670
671
672
673
674
675
676
677
678
def _attach_content_type_to_registered_models(self) -> None:
    """Attach content-type integration to every registered concrete model.

    This method is idempotent. It is called after registry creation, after
    registry copies, and whenever a registry gains a new content type model.
    """
    if self.content_type is None:
        return
    for model in self.models.values():
        self._attach_content_type_to_model(model)

_handle_model_registration

_handle_model_registration(model_class)

Finalize registry-owned state for a newly registered model.

Model registration is responsible for more than adding the class to registry.models. The registry also owns the admin visibility index and optional content-type wiring. Updating both concerns here keeps dynamically registered, copied, and generated models consistent after their concrete registry name is known.

PARAMETER DESCRIPTION
model_class

Model class that has just been registered.

TYPE: type[Any]

Source code in saffier/core/connection/registry.py
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
def _handle_model_registration(self, model_class: type[Any]) -> None:
    """Finalize registry-owned state for a newly registered model.

    Model registration is responsible for more than adding the class to
    ``registry.models``. The registry also owns the admin visibility index
    and optional content-type wiring. Updating both concerns here keeps
    dynamically registered, copied, and generated models consistent after
    their concrete registry name is known.

    Args:
        model_class: Model class that has just been registered.
    """
    if not getattr(model_class, "is_proxy_model", False) and not getattr(
        model_class.meta,
        "abstract",
        False,
    ):
        model_name = model_class.__name__
        if getattr(model_class.meta, "in_admin", None) is not False:
            self.admin_models.add(model_name)
        else:
            self.admin_models.discard(model_name)

    if self.content_type is None:
        return
    self._attach_content_type_to_model(model_class)

_attach_content_type_to_model

_attach_content_type_to_model(model_class)

Install or normalize the content-type field on one model class.

The helper handles three cases: models that already declare a ContentTypeField, models that declare an alternate content type field name, and models that need the default synthetic content_type field injected. It also refreshes reverse descriptors, deletion behavior, and proxy-model caches when the model definition changes.

PARAMETER DESCRIPTION
model_class

Concrete model to update.

TYPE: type[Any]

Source code in saffier/core/connection/registry.py
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
def _attach_content_type_to_model(self, model_class: type[Any]) -> None:
    """Install or normalize the content-type field on one model class.

    The helper handles three cases: models that already declare a
    `ContentTypeField`, models that declare an alternate content type field
    name, and models that need the default synthetic `content_type` field
    injected. It also refreshes reverse descriptors, deletion behavior, and
    proxy-model caches when the model definition changes.

    Args:
        model_class: Concrete model to update.
    """
    if self.content_type is None:
        return
    if model_class in (self.content_type, getattr(self.content_type, "proxy_model", None)):
        return
    if getattr(model_class.meta, "abstract", False):
        return
    if getattr(model_class, "is_proxy_model", False):
        return
    if model_class.__name__ in self.reflected:
        return

    from saffier.contrib.contenttypes.fields import ContentTypeField
    from saffier.core.db.models.metaclasses import _set_related_name_for_foreign_keys

    if "content_type" in model_class.fields:
        if isinstance(model_class.fields["content_type"], ContentTypeField):
            if getattr(model_class.meta, "is_tenant", False):
                model_class.fields["content_type"].no_constraint = True
            target_registry = getattr(self.content_type.meta, "registry", None)
            if (
                getattr(model_class.meta, "registry", None) is not target_registry
                or getattr(model_class, "database", None)
                is not getattr(self.content_type, "database", None)
                or getattr(model_class.meta, "is_tenant", False)
            ):
                self.content_type.__require_model_based_deletion__ = True
            self._bind_content_type_pre_save(model_class)
        return

    has_content_type_field = any(
        isinstance(field, ContentTypeField) for field in model_class.fields.values()
    )
    if has_content_type_field:
        content_type_fields = {
            field_name: field
            for field_name, field in model_class.fields.items()
            if isinstance(field, ContentTypeField)
        }
        for field_name, field in content_type_fields.items():
            field.owner = model_class
            field.registry = self
            if getattr(model_class.meta, "is_tenant", False):
                field.no_constraint = True
            if isinstance(field.to, str) and field.to == "ContentType":
                field.to = self.content_type
                if hasattr(field, "_target"):
                    delattr(field, "_target")
            auto_related_names = {
                model_class.__name__.lower(),
                f"{model_class.__name__.lower()}s_set",
            }
            desired_related_name = (
                f"reverse_{model_class.__name__.lower()}"
                if field.related_name in auto_related_names or field.related_name is None
                else field.related_name
            )

            if desired_related_name not in model_class.meta.related_names:
                if field.related_name in model_class.meta.related_names:
                    previous_related_name = cast("str", field.related_name)
                    model_class.meta.related_names.discard(previous_related_name)
                    model_class.meta.related_fields.pop(previous_related_name, None)
                    model_class.meta.related_names_mapping.pop(previous_related_name, None)

                    target_meta = field.target.meta
                    target_meta.related_fields.pop(previous_related_name, None)
                    target_meta.related_names_mapping.pop(previous_related_name, None)
                    target_meta.fields.pop(previous_related_name, None)
                    target_meta.fields_mapping.pop(previous_related_name, None)
                    if hasattr(field.target, previous_related_name):
                        delattr(field.target, previous_related_name)
                    proxy_target = getattr(field.target, "proxy_model", None)
                    if proxy_target is not None and hasattr(
                        proxy_target, previous_related_name
                    ):
                        delattr(proxy_target, previous_related_name)

                field.related_name = desired_related_name
                related_names = _set_related_name_for_foreign_keys(
                    {field_name: field},
                    cast(Any, model_class),
                )
                model_class.meta.related_names.update(related_names)
            target_registry = getattr(self.content_type.meta, "registry", None)
            if (
                getattr(model_class.meta, "registry", None) is not target_registry
                or getattr(model_class, "database", None)
                is not getattr(self.content_type, "database", None)
                or getattr(model_class.meta, "is_tenant", False)
            ):
                self.content_type.__require_model_based_deletion__ = True
        self._bind_content_type_pre_save(model_class)
        return

    related_name = f"reverse_{model_class.__name__.lower()}"
    if hasattr(self.content_type, related_name):
        raise RuntimeError(
            f"Duplicate related content type name generated: {related_name!r} for {model_class!r}"
        )

    field = ContentTypeField(
        to=self.content_type,
        related_name=related_name,
        on_delete=CASCADE,
        no_constraint=(
            getattr(self.content_type, "no_constraint", False)
            or getattr(model_class.meta, "is_tenant", False)
        ),
    )
    # ContentType is managed by registry pre-save hooks.
    field.validator.read_only = True
    field.name = "content_type"
    field.owner = model_class
    field.registry = self
    model_class.fields["content_type"] = field
    model_class.meta.fields["content_type"] = field
    model_class.meta.fields_mapping["content_type"] = field
    model_class.meta.foreign_key_fields["content_type"] = field

    model_related_names = _set_related_name_for_foreign_keys(
        {"content_type": field},
        cast(Any, model_class),
    )
    model_class.meta.related_names.update(model_related_names)

    if (
        getattr(model_class.meta, "registry", None)
        is not getattr(self.content_type.meta, "registry", None)
        or getattr(model_class, "database", None)
        is not getattr(self.content_type, "database", None)
        or getattr(model_class.meta, "is_tenant", False)
    ):
        self.content_type.__require_model_based_deletion__ = True

    self._bind_content_type_pre_save(model_class)

    self._clear_model_table_cache(model_class)
    model_class.__proxy_model__ = None
    proxy_model = model_class.generate_proxy_model()
    model_class.__proxy_model__ = proxy_model
    model_class.__proxy_model__.parent = model_class

_clear_model_table_cache

_clear_model_table_cache(model_class)

Invalidate cached SQLAlchemy tables for one model.

Content-type injection and similar runtime mutations change the effective table definition. Rather than mutating SQLAlchemy tables in place, the registry drops any cached table objects from the primary metadata and schema-specific metadata caches so the next build() call recreates them from the updated model definition.

PARAMETER DESCRIPTION
model_class

Model whose cached tables should be removed.

TYPE: type[Any]

Source code in saffier/core/connection/registry.py
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
def _clear_model_table_cache(self, model_class: type[Any]) -> None:
    """Invalidate cached SQLAlchemy tables for one model.

    Content-type injection and similar runtime mutations change the effective
    table definition. Rather than mutating SQLAlchemy tables in place, the
    registry drops any cached table objects from the primary metadata and
    schema-specific metadata caches so the next `build()` call recreates
    them from the updated model definition.

    Args:
        model_class: Model whose cached tables should be removed.
    """
    model_class._table = None
    model_class._db_schemas = {}

    table_name = cast("str | None", getattr(model_class.meta, "tablename", None))
    if table_name is None:
        return

    metadata_pool = [self._metadata]
    metadata_pool.extend(getattr(self, "_schema_metadata_cache", {}).values())

    for metadata in metadata_pool:
        table_keys = {table_name}
        if metadata.schema:
            table_keys.add(f"{metadata.schema}.{table_name}")
        for table_key in table_keys:
            existing_table = metadata.tables.get(table_key)
            if existing_table is not None:
                metadata.remove(existing_table)

_bind_content_type_pre_save

_bind_content_type_pre_save(model_class)

Register a pre-save hook that guarantees content-type rows exist.

Saffier content types are created lazily. Before a model instance is saved, the bound hook inspects every content-type field on the sender and creates the matching ContentType row when the field is missing or still points to an unsaved object.

PARAMETER DESCRIPTION
model_class

Model class that should receive the hook.

TYPE: type[Any]

Source code in saffier/core/connection/registry.py
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
def _bind_content_type_pre_save(self, model_class: type[Any]) -> None:
    """Register a pre-save hook that guarantees content-type rows exist.

    Saffier content types are created lazily. Before a model instance is
    saved, the bound hook inspects every content-type field on the sender and
    creates the matching `ContentType` row when the field is missing or still
    points to an unsaved object.

    Args:
        model_class: Model class that should receive the hook.
    """
    if model_class.__name__ in self._content_type_models_bound:
        return
    if self.content_type is None:
        return
    from saffier.contrib.contenttypes.fields import ContentTypeField

    async def ensure_content_type(
        sender: type[Any],
        instance: Any,
        **kwargs: Any,
    ) -> None:
        if self.content_type is None:
            return
        for field_name, field in sender.fields.items():
            if not isinstance(field, ContentTypeField):
                continue
            current_content_type = instance.__dict__.get(field_name)
            if current_content_type is None and field.null:
                continue
            if (
                current_content_type is not None
                and getattr(current_content_type, "pk", None) is not None
            ):
                continue
            payload = {}
            if current_content_type is not None and hasattr(
                current_content_type, "extract_db_fields"
            ):
                payload = current_content_type.extract_db_fields()

            payload["name"] = sender.__name__
            payload["schema_name"] = instance.get_active_instance_schema()
            content_type_obj = await self.content_type.query.create(**payload)
            setattr(instance, field_name, content_type_obj)

    model_class.signals.pre_save.connect(ensure_content_type)
    self._content_type_models_bound.add(model_class.__name__)

create_all async

create_all(refresh_metadata=True, databases=(None,))

Create all tables registered in the target databases.

PARAMETER DESCRIPTION
refresh_metadata

Whether to rebuild registry metadata before creating tables.

TYPE: bool DEFAULT: True

databases

Database names to operate on. None refers to the primary database.

TYPE: Sequence[str | None] DEFAULT: (None,)

Source code in saffier/core/connection/registry.py
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
async def create_all(
    self,
    refresh_metadata: bool = True,
    databases: Sequence[str | None] = (None,),
) -> None:
    """Create all tables registered in the target databases.

    Args:
        refresh_metadata: Whether to rebuild registry metadata before
            creating tables.
        databases: Database names to operate on. `None` refers to the
            primary database.
    """
    self._attach_content_type_to_registered_models()
    if refresh_metadata:
        await self.arefresh_metadata(multi_schema=True)
    await self.schema.create_schema(
        self.db_schema,
        if_not_exists=True,
        init_models=True,
        update_cache=bool(self.db_schema),
        databases=databases,
    )

drop_all async

drop_all(databases=(None,))

Drop the registry schema and registered tables from selected databases.

PARAMETER DESCRIPTION
databases

Database aliases to operate on. None targets the primary database.

TYPE: Sequence[str | None] DEFAULT: (None,)

Source code in saffier/core/connection/registry.py
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
async def drop_all(self, databases: Sequence[str | None] = (None,)) -> None:
    """Drop the registry schema and registered tables from selected databases.

    Args:
        databases: Database aliases to operate on. `None` targets the primary
            database.
    """
    await self.schema.drop_schema(
        self.db_schema,
        cascade=True,
        if_exists=True,
        databases=databases,
    )

_iter_databases

_iter_databases()
Source code in saffier/core/connection/registry.py
1088
1089
1090
1091
1092
def _iter_databases(self) -> list[tuple[str | None, Database]]:
    databases: list[tuple[str | None, Database]] = [(None, self.database)]
    for name, db in self.extra.items():
        databases.append((name, db))
    return databases

get_model

get_model(
    model_name,
    *,
    include_content_type_attr=True,
    include_reflected=True,
    include_pattern=False,
)

Resolve a model by name from the registry.

Resolution checks normal registered models first, then reflected models, and optionally pattern-generated models. If the stored class is a proxy model created for SQLAlchemy compatibility, the parent model is returned instead so callers always receive the public ORM model class.

PARAMETER DESCRIPTION
model_name

Name of the model to resolve.

TYPE: str

include_content_type_attr

Whether the synthetic ContentType attribute should be considered.

TYPE: bool DEFAULT: True

include_reflected

Whether reflected models should be searched.

TYPE: bool DEFAULT: True

include_pattern

Whether pattern-generated reflected models should be searched.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Any

The resolved model class.

TYPE: Any

RAISES DESCRIPTION
LookupError

If no matching model exists in the configured sources.

Examples:

Resolve a model declared elsewhere in the project:

>>> registry.get_model("User")
Source code in saffier/core/connection/registry.py
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
def get_model(
    self,
    model_name: str,
    *,
    include_content_type_attr: bool = True,
    include_reflected: bool = True,
    include_pattern: bool = False,
) -> Any:
    """Resolve a model by name from the registry.

    Resolution checks normal registered models first, then reflected models,
    and optionally pattern-generated models. If the stored class is a proxy
    model created for SQLAlchemy compatibility, the parent model is returned
    instead so callers always receive the public ORM model class.

    Args:
        model_name: Name of the model to resolve.
        include_content_type_attr: Whether the synthetic `ContentType`
            attribute should be considered.
        include_reflected: Whether reflected models should be searched.
        include_pattern: Whether pattern-generated reflected models should
            be searched.

    Returns:
        Any: The resolved model class.

    Raises:
        LookupError: If no matching model exists in the configured sources.

    Examples:
        Resolve a model declared elsewhere in the project:

        >>> registry.get_model("User")
    """
    if (
        include_content_type_attr
        and model_name == "ContentType"
        and self.content_type is not None
    ):
        return self.content_type
    if model_name in self.models:
        model = self.models[model_name]
        if getattr(model, "is_proxy_model", False):
            parent = getattr(model, "parent", None)
            if (
                parent is not None
                and getattr(getattr(parent, "meta", None), "registry", None) is self
            ):
                return parent
        return model
    if include_reflected and model_name in self.reflected:
        return self.reflected[model_name]
    if include_pattern and model_name in self.pattern_models:
        return self.pattern_models[model_name]
    raise LookupError(f"Registry doesn't have a {model_name} model.")

delete_model

delete_model(model_name)

Remove a model from any registry model mapping.

The helper checks concrete, reflected, and pattern-generated model collections. It is used by conflict-resolution code when copying or re-registering models. The admin model index is updated at the same time so deleted models cannot remain visible in the built-in admin.

PARAMETER DESCRIPTION
model_name

Name of the model to remove.

TYPE: str

RETURNS DESCRIPTION
bool

True when a model entry was removed.

TYPE: bool

Source code in saffier/core/connection/registry.py
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
def delete_model(self, model_name: str) -> bool:
    """Remove a model from any registry model mapping.

    The helper checks concrete, reflected, and pattern-generated model
    collections. It is used by conflict-resolution code when copying or
    re-registering models. The admin model index is updated at the same time
    so deleted models cannot remain visible in the built-in admin.

    Args:
        model_name: Name of the model to remove.

    Returns:
        bool: `True` when a model entry was removed.
    """
    self.admin_models.discard(model_name)
    for model_dict in (self.models, self.reflected, self.pattern_models):
        if model_name in model_dict:
            del model_dict[model_name]
            return True
    return False

register_callback

register_callback(
    model_reference, callback, *, one_time=False
)

Register a callback that runs once a model with the given name is available.

This is used heavily by lazy relation wiring. A field pointing to a string model name can register a callback and finish installing reverse descriptors as soon as the target model is added to the registry.

PARAMETER DESCRIPTION
model_reference

Target model class or model name to wait for.

TYPE: str | type[Any]

callback

Function called with the resolved model class.

TYPE: Callable[[type[Any]], None]

one_time

When True, discard the callback after the first run.

TYPE: bool DEFAULT: False

Source code in saffier/core/connection/registry.py
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
def register_callback(
    self,
    model_reference: str | type[Any],
    callback: Callable[[type[Any]], None],
    *,
    one_time: bool = False,
) -> None:
    """Register a callback that runs once a model with the given name is available.

    This is used heavily by lazy relation wiring. A field pointing to a
    string model name can register a callback and finish installing reverse
    descriptors as soon as the target model is added to the registry.

    Args:
        model_reference: Target model class or model name to wait for.
        callback: Function called with the resolved model class.
        one_time: When `True`, discard the callback after the first run.
    """
    model_name = (
        model_reference if isinstance(model_reference, str) else model_reference.__name__
    )
    callbacks = self._model_callbacks.setdefault(model_name, [])
    callbacks.append((callback, one_time))

    model_class = self.models.get(model_name) or self.reflected.get(model_name)
    if model_class is None or getattr(model_class, "is_proxy_model", False):
        return
    callback(model_class)
    if one_time:
        callbacks.remove((callback, one_time))
        if not callbacks:
            self._model_callbacks.pop(model_name, None)

execute_model_callbacks

execute_model_callbacks(model_class)

Run deferred callbacks waiting for a model to become available.

Lazy relation wiring registers callbacks keyed by model name. Once the target model is registered, this helper executes the callbacks and drops one-time entries.

PARAMETER DESCRIPTION
model_class

Newly available model class.

TYPE: type[Any]

Source code in saffier/core/connection/registry.py
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
def execute_model_callbacks(self, model_class: type[Any]) -> None:
    """Run deferred callbacks waiting for a model to become available.

    Lazy relation wiring registers callbacks keyed by model name. Once the
    target model is registered, this helper executes the callbacks and drops
    one-time entries.

    Args:
        model_class: Newly available model class.
    """
    if getattr(model_class, "is_proxy_model", False):
        return
    callbacks = list(self._model_callbacks.get(model_class.__name__, ()))
    if not callbacks:
        return

    remaining: list[tuple[Callable[[type[Any]], None], bool]] = []
    for callback, one_time in callbacks:
        callback(model_class)
        if not one_time:
            remaining.append((callback, one_time))

    if remaining:
        self._model_callbacks[model_class.__name__] = remaining
    else:
        self._model_callbacks.pop(model_class.__name__, None)

init_models

init_models(
    *, init_column_mappers=True, init_class_attrs=True
)

Eagerly initialize metadata caches for registered models.

This is mainly useful for tooling and tests that want all column-mapping caches, table properties, and proxy-model attributes prepared up front.

PARAMETER DESCRIPTION
init_column_mappers

Whether to populate column-to-field mappings.

TYPE: bool DEFAULT: True

init_class_attrs

Whether to warm class-level table and proxy caches.

TYPE: bool DEFAULT: True

Source code in saffier/core/connection/registry.py
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
def init_models(
    self, *, init_column_mappers: bool = True, init_class_attrs: bool = True
) -> None:
    """Eagerly initialize metadata caches for registered models.

    This is mainly useful for tooling and tests that want all column-mapping
    caches, table properties, and proxy-model attributes prepared up front.

    Args:
        init_column_mappers: Whether to populate column-to-field mappings.
        init_class_attrs: Whether to warm class-level table and proxy caches.
    """
    for model_class in self.models.values():
        model_class.meta.full_init(
            init_column_mappers=init_column_mappers,
            init_class_attrs=init_class_attrs,
        )
    for model_class in self.reflected.values():
        model_class.meta.full_init(
            init_column_mappers=init_column_mappers,
            init_class_attrs=init_class_attrs,
        )

invalidate_models

invalidate_models(*, clear_class_attrs=True)

Invalidate metadata caches for all registered and reflected models.

PARAMETER DESCRIPTION
clear_class_attrs

Whether to also clear cached tables, PK metadata, and proxy-model caches on the model classes themselves.

TYPE: bool DEFAULT: True

Source code in saffier/core/connection/registry.py
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
def invalidate_models(self, *, clear_class_attrs: bool = True) -> None:
    """Invalidate metadata caches for all registered and reflected models.

    Args:
        clear_class_attrs: Whether to also clear cached tables, PK metadata,
            and proxy-model caches on the model classes themselves.
    """
    for model_class in self.models.values():
        model_class.meta.invalidate(clear_class_attrs=clear_class_attrs)
    for model_class in self.reflected.values():
        model_class.meta.invalidate(clear_class_attrs=clear_class_attrs)

get_tablenames

get_tablenames()

Collect table names across concrete and reflected models.

RETURNS DESCRIPTION
set[str]

set[str]: All known table names currently tracked by the registry.

Source code in saffier/core/connection/registry.py
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
def get_tablenames(self) -> set[str]:
    """Collect table names across concrete and reflected models.

    Returns:
        set[str]: All known table names currently tracked by the registry.
    """
    tables = set()
    for model_class in self.models.values():
        tables.add(model_class.meta.tablename)
    for model_class in self.reflected.values():
        tables.add(model_class.meta.tablename)
    return tables

_automigrate_update

_automigrate_update(migration_settings)
Source code in saffier/core/connection/registry.py
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
def _automigrate_update(self, migration_settings: Any) -> None:
    from saffier.cli.base import upgrade

    self._is_automigrated = True
    with _monkay.with_full_overwrite(
        extensions={},
        settings=migration_settings,
        instance=Instance(registry=self),
        apply_extensions=True,
        evaluate_settings_with={
            "on_conflict": "replace",
            "ignore_import_errors": False,
            "ignore_preload_import_errors": False,
        },
    ):
        upgrade(app=None)

_automigrate async

_automigrate()
Source code in saffier/core/connection/registry.py
1296
1297
1298
1299
1300
1301
async def _automigrate(self) -> None:
    migration_settings = self._automigrate_config
    if migration_settings is None or not _monkay.settings.allow_automigrations:
        self._is_automigrated = True
        return
    await asyncio.to_thread(self._automigrate_update, migration_settings)

reflect_pattern_models async

reflect_pattern_models(
    *, database_name=None, database=None
)

Instantiate pattern-based reflected models from live database tables.

Pattern models act as templates that decide which reflected tables should become real runtime models. This method reflects matching schemas, tests include and exclude patterns, validates field compatibility, and creates concrete reflected models for every matching table exactly once per database alias.

PARAMETER DESCRIPTION
database_name

Optional alias of the database being reflected.

TYPE: str | None DEFAULT: None

database

Explicit database object to use instead of looking it up from the alias.

TYPE: Database | None DEFAULT: None

RAISES DESCRIPTION
RuntimeError

If a reflected pattern would generate a model name that conflicts with an existing model.

Source code in saffier/core/connection/registry.py
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
async def reflect_pattern_models(
    self,
    *,
    database_name: str | None = None,
    database: Database | None = None,
) -> None:
    """Instantiate pattern-based reflected models from live database tables.

    Pattern models act as templates that decide which reflected tables should
    become real runtime models. This method reflects matching schemas, tests
    include and exclude patterns, validates field compatibility, and creates
    concrete reflected models for every matching table exactly once per
    database alias.

    Args:
        database_name: Optional alias of the database being reflected.
        database: Explicit database object to use instead of looking it up
            from the alias.

    Raises:
        RuntimeError: If a reflected pattern would generate a model name that
            conflicts with an existing model.
    """
    if not self.pattern_models:
        return
    if database_name in self._pattern_reflected_dbs:
        return

    target_db = database
    if target_db is None:
        target_db = self.database if database_name is None else self.extra[database_name]

    schemes: set[None | str] = set()
    patterns = []
    for pattern_model in self.pattern_models.values():
        meta = pattern_model.meta
        if database_name not in meta.databases:
            continue
        schemes.update(meta.schemes)
        patterns.append(pattern_model)

    if not patterns:
        self._pattern_reflected_dbs.add(database_name)
        return

    tmp_metadata = sqlalchemy.MetaData()
    for schema in schemes:
        await target_db.run_sync(self._reflect_schema_metadata, tmp_metadata, schema)

    for table in tmp_metadata.tables.values():
        for pattern_model in patterns:
            meta = pattern_model.meta
            if table.schema not in meta.schemes:
                continue
            if not meta.include_pattern.match(table.name):
                continue
            if meta.exclude_pattern and meta.exclude_pattern.match(table.name):
                continue
            if pattern_model.fields_not_supported_by_table(table):
                continue

            model_name = meta.template(table)
            try:
                self.get_model(model_name, include_pattern=False)
            except LookupError:
                ...
            else:
                raise RuntimeError(
                    f"Conflicting reflected model name generated: {model_name!r}."
                )

            pattern_model.create_reflected_model(
                table=table,
                registry=self,
                database=target_db,
                name=model_name,
            )

    self._pattern_reflected_dbs.add(database_name)

_reflect_schema_metadata staticmethod

_reflect_schema_metadata(connection, metadata, schema)

Reflect every table from one schema into a metadata object.

PARAMETER DESCRIPTION
connection

SQLAlchemy connection used for reflection.

TYPE: Any

metadata

Metadata container receiving reflected tables.

TYPE: MetaData

schema

Schema name to inspect, or None for the default schema.

TYPE: str | None

Source code in saffier/core/connection/registry.py
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
@staticmethod
def _reflect_schema_metadata(
    connection: Any,
    metadata: sqlalchemy.MetaData,
    schema: str | None,
) -> None:
    """Reflect every table from one schema into a metadata object.

    Args:
        connection: SQLAlchemy connection used for reflection.
        metadata: Metadata container receiving reflected tables.
        schema: Schema name to inspect, or `None` for the default schema.
    """
    inspector = sqlalchemy.inspect(connection)
    table_names = inspector.get_table_names(schema=schema)
    for table_name in table_names:
        try:
            sqlalchemy.Table(
                table_name,
                metadata,
                schema=schema,
                autoload_with=connection,
            )
        except NoSuchTableError:
            continue

__aenter__ async

__aenter__()

Connect the registry and prepare runtime metadata.

Entering the async context connects every configured database, runs automigrations once when enabled, and reflects any pattern-based models for each database alias.

RETURNS DESCRIPTION
Registry

Connected registry instance.

TYPE: Registry

Source code in saffier/core/connection/registry.py
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
async def __aenter__(self) -> "Registry":
    """Connect the registry and prepare runtime metadata.

    Entering the async context connects every configured database, runs
    automigrations once when enabled, and reflects any pattern-based models
    for each database alias.

    Returns:
        Registry: Connected registry instance.
    """
    connected: list[Database] = []
    try:
        for name, database in self._iter_databases():
            await database.connect()
            connected.append(database)
            if not self._is_automigrated:
                await self._automigrate()
            await self.reflect_pattern_models(database_name=name, database=database)
    except Exception:
        for database in reversed(connected):
            if database.is_connected:
                await database.disconnect()
        raise
    return self

__aexit__ async

__aexit__(*args, **kwargs)

Disconnect all configured databases in reverse registration order.

Reverse teardown mirrors connection order and helps dependent databases shut down cleanly.

Source code in saffier/core/connection/registry.py
1434
1435
1436
1437
1438
1439
1440
1441
1442
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
    """Disconnect all configured databases in reverse registration order.

    Reverse teardown mirrors connection order and helps dependent databases
    shut down cleanly.
    """
    for _, database in reversed(self._iter_databases()):
        if database.is_connected:
            await database.disconnect()

with_async_env

with_async_env(loop=None)

Run registry lifecycle management inside synchronous code.

The context manager starts the registry, binds the event loop used by run_sync(), and tears everything down when the block exits. It is the bridge that allows CLI tools, synchronous test helpers, or scripts to drive Saffier's async APIs without manually managing the registry connection lifecycle.

PARAMETER DESCRIPTION
loop

Optional event loop to reuse. A new loop is created when no running or stored loop is available.

TYPE: AbstractEventLoop | None DEFAULT: None

YIELDS DESCRIPTION
Registry

The connected registry instance.

TYPE:: Registry

Source code in saffier/core/connection/registry.py
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
@contextlib.contextmanager
def with_async_env(
    self, loop: asyncio.AbstractEventLoop | None = None
) -> Generator["Registry", None, None]:
    """Run registry lifecycle management inside synchronous code.

    The context manager starts the registry, binds the event loop used by
    `run_sync()`, and tears everything down when the block exits. It is the
    bridge that allows CLI tools, synchronous test helpers, or scripts to
    drive Saffier's async APIs without manually managing the registry
    connection lifecycle.

    Args:
        loop: Optional event loop to reuse. A new loop is created when no
            running or stored loop is available.

    Yields:
        Registry: The connected registry instance.
    """
    close = False
    if loop is None:
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            loop = current_eventloop.get()
            if loop is None:
                loop = asyncio.new_event_loop()
                close = True

    token = current_eventloop.set(loop)
    try:
        yield cast("Registry", run_sync(self.__aenter__(), loop=loop))
    finally:
        run_sync(self.__aexit__(), loop=loop)
        current_eventloop.reset(token)
        if close:
            loop.run_until_complete(loop.shutdown_asyncgens())
            loop.close()

asgi

asgi(app=None, handle_lifespan=False)

Wrap an ASGI application with registry lifecycle management.

The wrapper enters the registry async context during ASGI startup and closes it during ASGI shutdown. Because the registry owns the primary database, extra databases, automigration hooks, and reflected-pattern metadata, this is the Saffier-native integration point for applications that want the whole registry lifecycle instead of one database engine.

PARAMETER DESCRIPTION
app

Optional ASGI application to wrap. When omitted, a decorator-style wrapper factory is returned.

TYPE: ASGIApp | None DEFAULT: None

handle_lifespan

Whether the wrapper should fully answer lifespan startup/shutdown messages instead of forwarding them to the downstream application.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
ASGIApp | Callable[[ASGIApp], ASGIApp]

ASGIApp | Callable[[ASGIApp], ASGIApp]: Wrapped ASGI application or

ASGIApp | Callable[[ASGIApp], ASGIApp]

wrapper factory.

RAISES DESCRIPTION
RuntimeError

If the optional monkay.asgi integration is not installed.

Source code in saffier/core/connection/registry.py
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
def asgi(
    self,
    app: ASGIApp | None = None,
    handle_lifespan: bool = False,
) -> ASGIApp | Callable[[ASGIApp], ASGIApp]:
    """Wrap an ASGI application with registry lifecycle management.

    The wrapper enters the registry async context during ASGI startup and
    closes it during ASGI shutdown. Because the registry owns the primary
    database, extra databases, automigration hooks, and reflected-pattern
    metadata, this is the Saffier-native integration point for applications
    that want the whole registry lifecycle instead of one database engine.

    Args:
        app: Optional ASGI application to wrap. When omitted, a
            decorator-style wrapper factory is returned.
        handle_lifespan: Whether the wrapper should fully answer lifespan
            startup/shutdown messages instead of forwarding them to the
            downstream application.

    Returns:
        ASGIApp | Callable[[ASGIApp], ASGIApp]: Wrapped ASGI application or
        wrapper factory.

    Raises:
        RuntimeError: If the optional ``monkay.asgi`` integration is not
            installed.
    """
    if LifespanHook is None:
        raise RuntimeError("monkay.asgi is required for ASGI integration.")

    async def lifespan() -> contextlib.AsyncExitStack:
        """Enter the registry and return cleanup owned by the ASGI lifespan.

        Returns:
            contextlib.AsyncExitStack: Stack that exits the registry during
            ASGI shutdown.
        """
        cleanup_stack = contextlib.AsyncExitStack()
        await cleanup_stack.enter_async_context(self)
        return cleanup_stack

    return LifespanHook(app, setup=lifespan, do_forward=not handle_lifespan)

refresh_metadata

refresh_metadata(
    *,
    update_only=False,
    multi_schema=False,
    ignore_schema_pattern=None,
)

Rebuild metadata containers used by migrations and schema reflection.

Saffier keeps metadata refresh deliberately explicit. Rather than trying to patch SQLAlchemy table objects in place, this method clears registry metadata containers and table caches so subsequent build() calls recreate tables from the current model definitions.

PARAMETER DESCRIPTION
update_only

When True, keep the current top-level metadata containers and only clear model-level table caches.

TYPE: bool DEFAULT: False

multi_schema

Accepted for API compatibility with migration helpers.

TYPE: bool | str | object DEFAULT: False

ignore_schema_pattern

Accepted for API compatibility with migration helpers.

TYPE: str | object | None DEFAULT: None

RETURNS DESCRIPTION
Registry

The current registry for fluent-style usage.

TYPE: Registry

Source code in saffier/core/connection/registry.py
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
def refresh_metadata(
    self,
    *,
    update_only: bool = False,
    multi_schema: bool | str | object = False,
    ignore_schema_pattern: str | object | None = None,
) -> "Registry":
    """Rebuild metadata containers used by migrations and schema reflection.

    Saffier keeps metadata refresh deliberately explicit. Rather than trying
    to patch SQLAlchemy table objects in place, this method clears registry
    metadata containers and table caches so subsequent `build()` calls
    recreate tables from the current model definitions.

    Args:
        update_only: When `True`, keep the current top-level metadata
            containers and only clear model-level table caches.
        multi_schema: Accepted for API compatibility with migration helpers.
        ignore_schema_pattern: Accepted for API compatibility with migration
            helpers.

    Returns:
        Registry: The current registry for fluent-style usage.
    """
    del multi_schema, ignore_schema_pattern

    if not update_only:
        self._metadata = self._make_metadata()
        self._metadata_by_name = MetaDataDict(self)
        self._metadata_by_name[None] = self._metadata
        for name in self.extra:
            self._metadata_by_name[name] = self._make_metadata()
        self._metadata_by_url.process()
    self._schema_metadata_cache = {}

    for collection in (self.models, self.reflected):
        for model_class in collection.values():
            model_class._table = None
            model_class._db_schemas = {}

    return self

arefresh_metadata async

arefresh_metadata(
    *,
    update_only=False,
    multi_schema=False,
    ignore_schema_pattern=None,
)

Async wrapper around refresh_metadata() for API compatibility.

RETURNS DESCRIPTION
Registry

The refreshed registry instance.

TYPE: Registry

Source code in saffier/core/connection/registry.py
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
async def arefresh_metadata(
    self,
    *,
    update_only: bool = False,
    multi_schema: bool | str | object = False,
    ignore_schema_pattern: str | object | None = None,
) -> "Registry":
    """Async wrapper around `refresh_metadata()` for API compatibility.

    Returns:
        Registry: The refreshed registry instance.
    """
    return self.refresh_metadata(
        update_only=update_only,
        multi_schema=multi_schema,
        ignore_schema_pattern=ignore_schema_pattern,
    )