hyrax.config_migrations
=======================

.. py:module:: hyrax.config_migrations

.. autoapi-nested-parse::

   Versioned migrations for Hyrax user configuration files.

   Hyrax tags its config schema with a top-level ``config_version`` scalar in
   ``hyrax_default_config.toml``. When a user loads an older config, the
   migrations registered here run before the merge step in
   :class:`hyrax.config_utils.ConfigManager`, bringing the user's document
   forward one version at a time until it matches :data:`CURRENT_CONFIG_VERSION`.

   Each migration step lives in its own descriptively-named module (e.g.
   ``001_rename_model_inputs_to_data_request.py``) and self-registers via the
   :func:`migration_step` decorator, which populates the :data:`MIGRATIONS` dict.
   :data:`CURRENT_CONFIG_VERSION` is auto-derived from the highest registered
   migration — developers do not bump it manually.

   Adding a new migration:

   1. Create ``src/hyrax/config_migrations/migrations/00N_description.py`` (e.g.
      ``002_move_learning_rate.py``). Decorate the migration function with
      ``@migration_step(from_version=N, key_renames={...})``. Import the
      decorator and helpers from ``hyrax.config_migrations.migration_utils``.
      The module is auto-discovered — no import line needed elsewhere.
      ``CURRENT_CONFIG_VERSION`` and ``config_version`` in the default TOML are
      both stamped automatically at runtime.
   2. Add a unit test in ``tests/hyrax/test_config_migrations.py``.



Submodules
----------

.. toctree::
   :maxdepth: 1

   /autoapi/hyrax/config_migrations/migration_utils/index
   /autoapi/hyrax/config_migrations/migrations/index


Attributes
----------

.. autoapisummary::

   hyrax.config_migrations.MIGRATIONS
   hyrax.config_migrations.CURRENT_CONFIG_VERSION
   hyrax.config_migrations.DEPRECATED_KEY_NAMES


Classes
-------

.. autoapisummary::

   hyrax.config_migrations.MigrationStep


Functions
---------

.. autoapisummary::

   hyrax.config_migrations.migrate_config
   hyrax.config_migrations.migration_step
   hyrax.config_migrations.move_key
   hyrax.config_migrations.rename_table


Package Contents
----------------

.. py:data:: MIGRATIONS
   :type:  dict[int, MigrationStep]

.. py:class:: MigrationStep

   A single schema migration with optional key-rename metadata.

   :param func: The function that upgrades a config document by one version.
   :type func: Callable[[TOMLDocument], TOMLDocument]
   :param key_renames: Old dotted path -> new dotted path for every key renamed by this
                       migration. Used by ``ConfigManager.set_config`` to warn callers
                       who still reference the old names at runtime.
   :type key_renames: dict[str, str]


   .. py:attribute:: func
      :type:  collections.abc.Callable[[tomlkit.toml_document.TOMLDocument], tomlkit.toml_document.TOMLDocument]


   .. py:attribute:: key_renames
      :type:  dict[str, str]


.. py:function:: migrate_config(user_config: tomlkit.toml_document.TOMLDocument, *, _migrations: dict[int, MigrationStep] | None = None, _target_version: int | None = None) -> tomlkit.toml_document.TOMLDocument

   Upgrade a user config document to the current schema version.

   The document is mutated in place (and also returned for convenience). If
   ``config_version`` is absent it is assumed to be ``1`` — older configs
   predate the versioning field. If it is greater than the current schema
   version, a :class:`RuntimeError` is raised because the installed Hyrax
   does not know how to read the schema.

   :param user_config: The parsed user config. An empty document (the "no user config" case)
                       is returned unchanged.
   :type user_config: TOMLDocument
   :param _migrations: Override the global :data:`MIGRATIONS` registry (testing only).
   :type _migrations: dict[int, MigrationStep], optional
   :param _target_version: Override the auto-derived target version (testing only).
   :type _target_version: int, optional

   :returns: The upgraded document with ``config_version`` stamped to the current
             value.
   :rtype: TOMLDocument

   :raises RuntimeError: If ``user_config`` declares a version newer than this Hyrax can
       understand, or if a migration step is missing from :data:`MIGRATIONS`.


.. py:function:: migration_step(from_version: int, key_renames: dict[str, str] | None = None)

   Decorator that registers a migration function into :data:`MIGRATIONS`.

   :param from_version: The config schema version this function upgrades FROM. The target
                        version is implicitly ``from_version + 1``.
   :type from_version: int
   :param key_renames: Old dotted path -> new dotted path for every key renamed by this
                       migration.
   :type key_renames: dict[str, str], optional


.. py:function:: move_key(cfg: tomlkit.toml_document.TOMLDocument | dict, old_path: str, new_path: str) -> bool

   Move a nested key from ``old_path`` to ``new_path``.

   Paths are dotted strings parsed by
   :func:`hyrax.config_utils.parse_dotted_key`, so quoted segments like
   ``"'torch.optim.Adam'.lr"`` are supported.

   :param cfg: The user config document to mutate in place.
   :type cfg: TOMLDocument or dict
   :param old_path: The dotted path to the existing key.
   :type old_path: str
   :param new_path: The dotted path to write the value to. Intermediate tables are
                    created as needed.
   :type new_path: str

   :returns: ``True`` iff ``old_path`` was present and the value was moved.
   :rtype: bool


.. py:function:: rename_table(cfg: tomlkit.toml_document.TOMLDocument | dict, old: str, new: str) -> bool

   Rename a top-level table from ``old`` to ``new``.

   If only ``old`` exists it is moved. If both exist, ``cfg[new]`` wins on
   leaf collisions; the two are deep-merged via `ConfigManager.merge_configs`
   and ``old`` is removed. If neither exists this is a no-op.

   :param cfg: The user config document to mutate in place.
   :type cfg: TOMLDocument or dict
   :param old: The table name that is being retired.
   :type old: str
   :param new: The new table name to adopt.
   :type new: str

   :returns: ``True`` iff ``old`` was present and something was renamed (so the
             caller can emit a deprecation warning conditionally).
   :rtype: bool


.. py:data:: CURRENT_CONFIG_VERSION
   :type:  int
   :value: 1


.. py:data:: DEPRECATED_KEY_NAMES
   :type:  dict[str, str]

