Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

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