200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib
|
|
import logging
|
|
from importlib.metadata import entry_points
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from core.base import BaseWorker
|
|
from core.plugin_types import HYDROGEN_API_VERSION, EmptyPluginConfig
|
|
from core.schemas import ModuleManifest
|
|
from core.schemas.config import Config, ResolvedConfig
|
|
from core.schemas.modules import LoadedModule, LoadedWorker, ModuleLoadResult
|
|
from reporting.models import PluginRuntimeError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def load_module(package_name: str) -> LoadedModule | None:
|
|
module = importlib.import_module(package_name)
|
|
raw_manifest = getattr(module, "MANIFEST", None)
|
|
|
|
if raw_manifest is None:
|
|
logger.warning("module package %s does not export MANIFEST", package_name)
|
|
return None
|
|
|
|
manifest = ModuleManifest.model_validate(raw_manifest)
|
|
if manifest.api_version != HYDROGEN_API_VERSION:
|
|
raise ValueError(
|
|
f"module package {package_name} targets api_version={manifest.api_version}, "
|
|
f"expected {HYDROGEN_API_VERSION}"
|
|
)
|
|
|
|
builder = getattr(module, "build_worker", None)
|
|
|
|
if builder is None:
|
|
logger.warning("module package %s does not export build_worker", package_name)
|
|
return None
|
|
|
|
if not callable(builder):
|
|
logger.warning("build_worker in package %s is not callable", package_name)
|
|
return None
|
|
|
|
config_model = getattr(module, "CONFIG_MODEL", None)
|
|
if config_model is None:
|
|
config_model = getattr(getattr(builder, "__self__", None), "config_model", None)
|
|
if config_model is None:
|
|
config_model = EmptyPluginConfig
|
|
if not isinstance(config_model, type) or not issubclass(config_model, BaseModel):
|
|
raise TypeError(f"module package {package_name} exports invalid CONFIG_MODEL")
|
|
|
|
return LoadedModule(
|
|
package_name=package_name,
|
|
manifest=manifest,
|
|
build_worker=builder,
|
|
config_model=config_model,
|
|
)
|
|
|
|
|
|
def build_worker(
|
|
loaded_module: LoadedModule, config: BaseModel | dict[str, Any]
|
|
) -> LoadedWorker | None:
|
|
worker = loaded_module.build_worker(config)
|
|
if not isinstance(worker, BaseWorker):
|
|
logger.warning(
|
|
"build_worker in package %s did not return BaseWorker", loaded_module.package_name
|
|
)
|
|
return None
|
|
|
|
return LoadedWorker(worker=worker, manifest=loaded_module.manifest, config=config)
|
|
|
|
|
|
def discover_modules(path: Path, config: Config, package_prefix: str = "modules") -> list[str]:
|
|
package_names: list[str] = []
|
|
|
|
if path.exists():
|
|
for entry in path.iterdir():
|
|
if not entry.is_dir() or entry.name.startswith("__"):
|
|
continue
|
|
if not (entry / "__init__.py").exists():
|
|
continue
|
|
package_names.append(f"{package_prefix}.{entry.name}")
|
|
|
|
package_names.extend(config.plugin_packages.modules)
|
|
|
|
try:
|
|
module_entry_points = entry_points(group="hydrogen.modules")
|
|
except TypeError:
|
|
module_entry_points = entry_points().select(group="hydrogen.modules")
|
|
|
|
for module_entry_point in module_entry_points:
|
|
package_names.append(module_entry_point.value.partition(":")[0])
|
|
|
|
return list(dict.fromkeys(package_names))
|
|
|
|
|
|
def load_modules(path: Path, config: Config, package_prefix: str = "modules") -> ModuleLoadResult:
|
|
workers: list[LoadedWorker] = []
|
|
errors: list[PluginRuntimeError] = []
|
|
|
|
for package_name in discover_modules(path, config, package_prefix):
|
|
try:
|
|
module = load_module(package_name)
|
|
if module is None:
|
|
continue
|
|
|
|
if module.manifest.category in config.exclude_categories:
|
|
logger.info(
|
|
"module %s was skipped due to config exclusion", module.manifest.identifier
|
|
)
|
|
continue
|
|
|
|
module_config = config.modules.get(module.manifest.identifier, {})
|
|
validated_config = module.config_model.model_validate(module_config)
|
|
worker = build_worker(module, validated_config)
|
|
if worker is not None:
|
|
workers.append(worker)
|
|
logger.info("module %s has been successfully loaded!", module.manifest.identifier)
|
|
except Exception as exc:
|
|
logger.exception("failed to load module package %s", package_name)
|
|
errors.append(
|
|
PluginRuntimeError(
|
|
plugin_kind="module",
|
|
plugin_name=package_name,
|
|
stage="load",
|
|
message=str(exc),
|
|
)
|
|
)
|
|
|
|
return ModuleLoadResult(loaded_workers=workers, errors=errors)
|
|
|
|
|
|
def load_prevalidated_workers(
|
|
loaded_modules: list[LoadedModule],
|
|
resolved_config: ResolvedConfig,
|
|
) -> ModuleLoadResult:
|
|
workers: list[LoadedWorker] = []
|
|
errors: list[PluginRuntimeError] = []
|
|
|
|
for loaded_module in loaded_modules:
|
|
if loaded_module.manifest.category in resolved_config.exclude_categories:
|
|
continue
|
|
|
|
module_config = resolved_config.modules.get(loaded_module.manifest.identifier)
|
|
if module_config is None:
|
|
continue
|
|
|
|
try:
|
|
worker = build_worker(loaded_module, module_config)
|
|
if worker is None:
|
|
errors.append(
|
|
PluginRuntimeError(
|
|
plugin_kind="module",
|
|
plugin_name=loaded_module.manifest.identifier,
|
|
stage="build_worker",
|
|
message="build_worker returned an invalid worker instance",
|
|
)
|
|
)
|
|
continue
|
|
workers.append(worker)
|
|
except Exception as exc:
|
|
logger.exception("failed to build worker for module %s", loaded_module.package_name)
|
|
errors.append(
|
|
PluginRuntimeError(
|
|
plugin_kind="module",
|
|
plugin_name=loaded_module.manifest.identifier,
|
|
stage="build_worker",
|
|
message=str(exc),
|
|
)
|
|
)
|
|
|
|
return ModuleLoadResult(loaded_workers=workers, errors=errors)
|
|
|
|
|
|
def load_module_descriptors(
|
|
path: Path, config: Config, package_prefix: str = "modules"
|
|
) -> tuple[list[LoadedModule], list[PluginRuntimeError]]:
|
|
modules: list[LoadedModule] = []
|
|
errors: list[PluginRuntimeError] = []
|
|
|
|
for package_name in discover_modules(path, config, package_prefix):
|
|
try:
|
|
module = load_module(package_name)
|
|
if module is not None:
|
|
modules.append(module)
|
|
except Exception as exc:
|
|
logger.exception("failed to discover module package %s", package_name)
|
|
errors.append(
|
|
PluginRuntimeError(
|
|
plugin_kind="module",
|
|
plugin_name=package_name,
|
|
stage="discovery",
|
|
message=str(exc),
|
|
)
|
|
)
|
|
|
|
return modules, errors
|