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
121
122
123
124
125
126
127
128
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
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.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 = {}

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

_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
163
164
165
166
167
168
169
170
171
172
173
174
175
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
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
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
203
204
205
206
207
208
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
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
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
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
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
297
298
299
300
301
302
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
304
305
306
307
308
309
310
311
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
337
338
339
340
341
342
343
344
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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
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
402
403
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
438
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
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)
    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
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
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):
        meta = type(
            "Meta",
            (),
            {
                "registry": self,
                "tablename": "contenttypes",
            },
        )
        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"
        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
521
522
523
524
525
526
527
528
529
530
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)
Source code in saffier/core/connection/registry.py
532
533
534
535
def _handle_model_registration(self, model_class: type[Any]) -> None:
    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
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
615
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
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
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
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
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
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
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
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
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
904
905
906
907
908
909
910
911
912
913
914
915
916
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
918
919
920
921
922
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
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
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.

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
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
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.

    Args:
        model_name: Name of the model to remove.

    Returns:
        bool: `True` when a model entry was removed.
    """
    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
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
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
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
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
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
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
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
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
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
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
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
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
1124
1125
1126
1127
1128
1129
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
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
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
1203
1204
1205
1206
1207
1208
1209
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
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
@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
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
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
1262
1263
1264
1265
1266
1267
1268
1269
1270
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
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
@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()

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
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
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
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
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,
    )