cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
137 lines
5.8 KiB
Python
137 lines
5.8 KiB
Python
import importlib
|
|
import inspect
|
|
import pkgutil
|
|
import re
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from logging import Logger
|
|
from types import ModuleType
|
|
from typing import Any
|
|
|
|
from invokeai.app.services.config.config_default import InvokeAIAppConfig
|
|
from invokeai.app.services.image_files.image_files_base import ImageFileStorageBase
|
|
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
|
|
|
|
DEFAULT_MIGRATIONS_PACKAGE = "invokeai.app.services.shared.sqlite_migrator.migrations"
|
|
_MIGRATION_MODULE_RE = re.compile(r"^migration_(\d+)$")
|
|
_DATED_MIGRATION_MODULE_RE = re.compile(r"^migration_(\d{4}_\d{2}_\d{2}_[a-z0-9][a-z0-9_]*)$")
|
|
|
|
|
|
class MigrationLoaderError(RuntimeError):
|
|
"""Raised when migration discovery or construction fails."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MigrationBuildContext:
|
|
app_config: InvokeAIAppConfig
|
|
logger: Logger
|
|
image_files: ImageFileStorageBase
|
|
|
|
def get_dependency(self, name: str) -> Any:
|
|
if name == "app_config":
|
|
return self.app_config
|
|
if name == "config":
|
|
return self.app_config
|
|
if name == "logger":
|
|
return self.logger
|
|
if name == "image_files":
|
|
return self.image_files
|
|
raise MigrationLoaderError(f"Migration builder requested unknown dependency '{name}'")
|
|
|
|
|
|
MigrationBuilder = Callable[..., Migration]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DiscoveredMigrationBuilder:
|
|
module_name: str
|
|
sort_key: tuple[int, int | str]
|
|
expected_migration_id: str
|
|
builder: MigrationBuilder
|
|
|
|
|
|
def discover_migration_builders(package_name: str = DEFAULT_MIGRATIONS_PACKAGE) -> list[DiscoveredMigrationBuilder]:
|
|
try:
|
|
package = importlib.import_module(package_name)
|
|
except Exception as e:
|
|
raise MigrationLoaderError(f"Unable to import migrations package '{package_name}': {e}") from e
|
|
|
|
package_path = getattr(package, "__path__", None)
|
|
if package_path is None:
|
|
raise MigrationLoaderError(f"Migrations package '{package_name}' is not a package")
|
|
|
|
discovered: list[DiscoveredMigrationBuilder] = []
|
|
for module_info in pkgutil.iter_modules(package_path):
|
|
module_short_name = module_info.name
|
|
if not module_short_name.startswith("migration_"):
|
|
continue
|
|
numeric_match = _MIGRATION_MODULE_RE.match(module_short_name)
|
|
dated_match = _DATED_MIGRATION_MODULE_RE.match(module_short_name)
|
|
if numeric_match is None and dated_match is None:
|
|
raise MigrationLoaderError(f"Malformed migration module name '{module_short_name}'")
|
|
|
|
if numeric_match is not None:
|
|
sort_key: tuple[int, int | str] = (0, int(numeric_match.group(1)))
|
|
expected_builder_name = f"build_migration_{numeric_match.group(1)}"
|
|
expected_migration_id = f"migration_{numeric_match.group(1)}"
|
|
else:
|
|
sort_key = (1, module_short_name)
|
|
expected_builder_name = "build_migration"
|
|
expected_migration_id = module_short_name.removeprefix("migration_")
|
|
|
|
full_module_name = f"{package_name}.{module_short_name}"
|
|
try:
|
|
module = importlib.import_module(full_module_name)
|
|
except Exception as e:
|
|
raise MigrationLoaderError(f"Unable to import migration module '{full_module_name}': {e}") from e
|
|
|
|
builder = _get_builder(module, expected_builder_name)
|
|
discovered.append(DiscoveredMigrationBuilder(full_module_name, sort_key, expected_migration_id, builder))
|
|
|
|
return sorted(discovered, key=lambda discovered_builder: discovered_builder.sort_key)
|
|
|
|
|
|
def build_migrations(context: MigrationBuildContext, package_name: str = DEFAULT_MIGRATIONS_PACKAGE) -> list[Migration]:
|
|
migrations: list[Migration] = []
|
|
for discovered_builder in discover_migration_builders(package_name):
|
|
kwargs = _get_builder_kwargs(discovered_builder.builder, context, discovered_builder.module_name)
|
|
migration = discovered_builder.builder(**kwargs)
|
|
if not isinstance(migration, Migration):
|
|
raise MigrationLoaderError(
|
|
f"Migration builder '{discovered_builder.builder.__name__}' in "
|
|
f"'{discovered_builder.module_name}' must return Migration"
|
|
)
|
|
if migration.id != discovered_builder.expected_migration_id:
|
|
raise MigrationLoaderError(
|
|
f"Migration builder '{discovered_builder.builder.__name__}' in "
|
|
f"'{discovered_builder.module_name}' must return migration id "
|
|
f"'{discovered_builder.expected_migration_id}', got '{migration.id}'"
|
|
)
|
|
migrations.append(migration)
|
|
return migrations
|
|
|
|
|
|
def _get_builder(module: ModuleType, builder_name: str) -> MigrationBuilder:
|
|
builder = getattr(module, builder_name, None)
|
|
if builder is None:
|
|
raise MigrationLoaderError(f"Migration module '{module.__name__}' must define '{builder_name}'")
|
|
if not callable(builder):
|
|
raise MigrationLoaderError(f"Migration builder '{module.__name__}.{builder_name}' is not callable")
|
|
return builder
|
|
|
|
|
|
def _get_builder_kwargs(builder: MigrationBuilder, context: MigrationBuildContext, module_name: str) -> dict[str, Any]:
|
|
signature = inspect.signature(builder)
|
|
kwargs: dict[str, Any] = {}
|
|
for parameter in signature.parameters.values():
|
|
if parameter.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
|
raise MigrationLoaderError(
|
|
f"Migration builder '{module_name}.{builder.__name__}' must not use *args or **kwargs"
|
|
)
|
|
if parameter.kind == inspect.Parameter.POSITIONAL_ONLY:
|
|
raise MigrationLoaderError(
|
|
f"Migration builder '{module_name}.{builder.__name__}' must not use positional-only parameters"
|
|
)
|
|
kwargs[parameter.name] = context.get_dependency(parameter.name)
|
|
return kwargs
|