mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 12:06:51 +08:00
195 lines
6.9 KiB
Python
195 lines
6.9 KiB
Python
"""Managed Resource 的声明发现与 Capability Runtime 适配器。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import importlib
|
|
import inspect
|
|
from pathlib import Path
|
|
from typing import Any, Iterable
|
|
|
|
from app.runtime.capabilities.errors import CapabilityAdapterContractError
|
|
from app.runtime.capabilities.model import (
|
|
ActivationPolicy,
|
|
AdapterExecutionMode,
|
|
CapabilitySpec,
|
|
)
|
|
from app.runtime.capabilities.registry import CapabilityRegistry
|
|
from app.runtime.managed_resources import (
|
|
MANAGED_RESOURCE_ASYNC_KIND,
|
|
MANAGED_RESOURCE_SYNC_KIND,
|
|
)
|
|
|
|
|
|
_DEFAULT_RESOURCE_ROOT = Path(__file__).resolve().parents[2] / "adapters"
|
|
_RESOURCE_KINDS = {MANAGED_RESOURCE_SYNC_KIND, MANAGED_RESOURCE_ASYNC_KIND}
|
|
|
|
|
|
def _load_entrypoint(spec: CapabilitySpec) -> Any:
|
|
"""解析声明中的 canonical 实现对象,不创建资源实例。"""
|
|
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
|
module = importlib.import_module(module_name)
|
|
try:
|
|
return getattr(module, symbol_name)
|
|
except AttributeError as error:
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 未公开 Managed Resource 实现"
|
|
) from error
|
|
|
|
|
|
def _create_candidate(spec: CapabilitySpec, implementation: Any) -> Any:
|
|
"""通过零参数工厂创建资源候选,实例在 start 成功前不可见。"""
|
|
if not callable(implementation):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 不是可调用的 Managed Resource 工厂"
|
|
)
|
|
candidate = implementation()
|
|
if candidate is None or inspect.isawaitable(candidate):
|
|
close = getattr(candidate, "close", None)
|
|
if callable(close):
|
|
close()
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 必须同步返回资源候选"
|
|
)
|
|
return candidate
|
|
|
|
|
|
def _resource_method(spec: CapabilitySpec, candidate: Any, name: str) -> Any:
|
|
"""读取必需生命周期方法并生成稳定合同错误。"""
|
|
callback = getattr(candidate, name, None)
|
|
if not callable(callback):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint} 的资源候选缺少 {name}()"
|
|
)
|
|
return callback
|
|
|
|
|
|
class SyncManagedResourceAdapter:
|
|
"""把同步 start/stop 资源接入 Capability Runtime。"""
|
|
|
|
execution_mode = AdapterExecutionMode.SYNC
|
|
|
|
@staticmethod
|
|
def materialize(spec: CapabilitySpec) -> Any:
|
|
"""解析资源工厂。"""
|
|
return _load_entrypoint(spec)
|
|
|
|
@staticmethod
|
|
def create(
|
|
spec: CapabilitySpec,
|
|
implementation: Any,
|
|
_generation: int,
|
|
_previous: Any = None,
|
|
) -> Any:
|
|
"""创建尚未发布的同步资源候选。"""
|
|
return _create_candidate(spec, implementation)
|
|
|
|
@staticmethod
|
|
def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
|
|
"""启动同步候选;同步 kind 不接受 awaitable 返回值。"""
|
|
result = _resource_method(spec, candidate, "start")()
|
|
if inspect.isawaitable(result):
|
|
close = getattr(result, "close", None)
|
|
if callable(close):
|
|
close()
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.start() 返回 awaitable,与同步 kind 不匹配"
|
|
)
|
|
|
|
@staticmethod
|
|
def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
|
|
"""停止同步资源,异常交由 Runtime 保留资源所有权并支持重试。"""
|
|
result = _resource_method(spec, instance, "stop")()
|
|
if inspect.isawaitable(result):
|
|
close = getattr(result, "close", None)
|
|
if callable(close):
|
|
close()
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.stop() 返回 awaitable,与同步 kind 不匹配"
|
|
)
|
|
|
|
@staticmethod
|
|
def cleanup(
|
|
spec: CapabilitySpec,
|
|
candidate: Any,
|
|
generation: int,
|
|
_error: BaseException,
|
|
) -> None:
|
|
"""启动失败时按同一 stop 合同清理尚未发布的候选。"""
|
|
SyncManagedResourceAdapter.stop(spec, candidate, generation)
|
|
|
|
|
|
class AsyncManagedResourceAdapter:
|
|
"""把异步 start/stop 资源接入 Capability Runtime。"""
|
|
|
|
execution_mode = AdapterExecutionMode.ASYNC
|
|
|
|
@staticmethod
|
|
async def materialize(spec: CapabilitySpec) -> Any:
|
|
"""在线程中解析资源工厂,避免第三方导入阻塞事件循环。"""
|
|
return await asyncio.to_thread(_load_entrypoint, spec)
|
|
|
|
@staticmethod
|
|
async def create(
|
|
spec: CapabilitySpec,
|
|
implementation: Any,
|
|
_generation: int,
|
|
_previous: Any = None,
|
|
) -> Any:
|
|
"""创建尚未发布的异步资源候选。"""
|
|
return _create_candidate(spec, implementation)
|
|
|
|
@staticmethod
|
|
async def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
|
|
"""等待异步候选完成启动。"""
|
|
result = _resource_method(spec, candidate, "start")()
|
|
if not inspect.isawaitable(result):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.start() 必须返回 awaitable"
|
|
)
|
|
await result
|
|
|
|
@staticmethod
|
|
async def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
|
|
"""等待异步资源完成停止。"""
|
|
result = _resource_method(spec, instance, "stop")()
|
|
if not inspect.isawaitable(result):
|
|
raise CapabilityAdapterContractError(
|
|
f"{spec.entrypoint}.stop() 必须返回 awaitable"
|
|
)
|
|
await result
|
|
|
|
@staticmethod
|
|
async def cleanup(
|
|
spec: CapabilitySpec,
|
|
candidate: Any,
|
|
generation: int,
|
|
_error: BaseException,
|
|
) -> None:
|
|
"""启动失败时等待同一 stop 合同清理候选。"""
|
|
await AsyncManagedResourceAdapter.stop(spec, candidate, generation)
|
|
|
|
|
|
def _validate_registry(registry: CapabilityRegistry) -> None:
|
|
"""固定类别级声明合同,资源只能由显式首用触发。"""
|
|
for spec in registry.list_specs():
|
|
if set(spec.metadata) != {"name"}:
|
|
raise ValueError(f"{spec.source}: Managed Resource metadata 只能包含 name")
|
|
if spec.activation is not ActivationPolicy.ON_FIRST_USE:
|
|
raise ValueError(f"{spec.source}: Managed Resource 必须使用 on_first_use")
|
|
if spec.selector is not None or spec.watch:
|
|
raise ValueError(f"{spec.source}: Managed Resource 不接受配置 selector 或 watch")
|
|
|
|
|
|
def build_managed_resource_registry(
|
|
roots: Iterable[Path | str] | None = None,
|
|
) -> CapabilityRegistry:
|
|
"""从 data-only manifest 构建不导入资源实现的注册表。"""
|
|
registry = CapabilityRegistry.discover(
|
|
tuple(roots) if roots is not None else (_DEFAULT_RESOURCE_ROOT,),
|
|
kinds=_RESOURCE_KINDS,
|
|
selector_schemas={},
|
|
)
|
|
_validate_registry(registry)
|
|
return registry
|