Skip to content

Migrations

didactic.api.register_migration

register_migration(
    source: type[A],
    target: type[B],
    migration: Lens[A, B] | Iso[A, B] | Mapping[A, B],
) -> None

Register a migration from source to target.

Parameters:

Name Type Description Default
source type[A]

The older Model class.

required
target type[B]

The newer Model class.

required
migration Lens[A, B] | Iso[A, B] | Mapping[A, B]

A Lens, Iso, or Mapping that maps a source instance to a target instance.

required

Raises:

Type Description
TypeError

If a migration is already registered for the same (source_spec, target_spec) pair.

Notes

The registry is process-global. The lookup key is a fingerprint of the didactic-shape spec, so a structurally-identical class re-imported from a different module shares one registry entry.

Examples:

>>> import didactic.api as dx
>>> class UserV1(dx.Model):
...     id: str
...     name: str
>>> class UserV2(dx.Model):
...     id: str
...     given_name: str
...     family_name: str
>>> class V1ToV2(dx.Iso[UserV1, UserV2]):
...     def forward(self, u: UserV1) -> UserV2:
...         first, _, last = u.name.partition(" ")
...         return UserV2(id=u.id, given_name=first, family_name=last)
...
...     def backward(self, u: UserV2) -> UserV1:
...         return UserV1(
...             id=u.id,
...             name=f"{u.given_name} {u.family_name}".rstrip(),
...         )
>>> dx.register_migration(UserV1, UserV2, V1ToV2())

didactic.api.migrate

migrate(
    payload: Model | JsonObject,
    *,
    source: type[Model] | None = None,
    target: type[B],
) -> B

Migrate a payload to the target Model class.

Parameters:

Name Type Description Default
payload Model | JsonObject

Either an instance of an older Model class or a dict produced by older.model_dump().

required
source type[Model] | None

The source Model class. Required when payload is a dict; optional when it's already a Model instance.

None
target type[B]

The Model class to migrate to.

required

Returns:

Type Description
Model

A target instance produced by walking the registered migration graph from source to target and composing the lenses along the way.

Raises:

Type Description
LookupError

If no path exists in the registry. The error message includes the source and target fingerprints so the user can diagnose which migration is missing.

TypeError

If payload is a dict and source is not given.

Notes

Path search is breadth-first over fingerprints. Lens composition is associative, so any path produces the same result for round-trip-clean migrations.

didactic.api.save_registry

save_registry(path: str | PathLike[str]) -> None

Persist the registry's metadata to a JSON file.

Parameters:

Name Type Description Default
path str | PathLike[str]

Filesystem path to write. The parent directory must already exist; the file is created or truncated.

required
Notes

Lenses themselves are Python callables and do not serialise. What gets written is the metadata that lets a subsequent load_registry call reconnect Python-side lenses to their entries: source spec, target spec, source fingerprint, target fingerprint, and the lens's class __qualname__ for diagnostic purposes. After loading, the user must re-register the lens (typically by re-importing the module that called register_migration), which is a no-op because the fingerprints already match.

The on-disk shape is a JSON object with one top-level key "entries", mapping to a list of records:

.. code-block:: json

{
  "entries": [
    {
      "source_fp": "...",
      "target_fp": "...",
      "source_spec": {...},
      "target_spec": {...},
      "lens_qualname": "module.path.LensClass"
    }
  ]
}

didactic.api.load_registry

load_registry(path: str | PathLike[str]) -> int

Load registry metadata from a JSON file produced by save_registry.

Parameters:

Name Type Description Default
path str | PathLike[str]

Path to a JSON file written by save_registry.

required

Returns:

Type Description
int

The number of entries cross-checked against the in-memory registry. The disk-side metadata is informational; it does not re-bind lenses (Python callables don't survive a process boundary). Entries whose fingerprints already exist in the in-memory registry are silently confirmed; entries whose fingerprints are missing are reported via the return value being smaller than the number of records on disk.

Raises:

Type Description
FileNotFoundError

If path does not exist.

ValueError

If the file is not in the expected format.

Notes

The intended workflow is:

  1. Application starts.
  2. Each module that defines migration lenses runs its register_migration calls at import time.
  3. load_registry(path) is called for diagnostic purposes: compare the registry on disk to what's been registered in the current process. The return value is the count of confirmed entries; a smaller-than-expected number is a signal that a migration module wasn't imported.

The on-disk format is intentionally human-readable so that operators can audit a deployment's expected migrations.

didactic.migrations._migrations.lookup_migration

lookup_migration(
    source: type[A], target: type[B]
) -> Lens[A, B] | Iso[A, B] | Mapping[A, B] | None

Return the migration registered for (source, target), or None.

Parameters:

Name Type Description Default
source type[A]

The source Model class.

required
target type[B]

The target Model class.

required

Returns:

Type Description
Lens or Iso or Mapping or None

The registered migration, or None if no direct (single-hop) migration is registered. Multi-hop chains are found by migrate, not by this function.

Notes

Lookup is by fingerprint of the didactic spec; class identity is not used. A structurally-identical class re-imported from a different module finds the same migration.

didactic.migrations._migrations.registered_fingerprints

registered_fingerprints() -> list[tuple[str, str]]

Return every registered (source_fp, target_fp) pair.

Returns:

Type Description
list of tuples

One (source_fp, target_fp) tuple per registered migration. Useful for debugging "no migration path" errors.

didactic.migrations._migrations.clear_registry

clear_registry() -> None

Wipe the migration registry. Test-suite hygiene only.