mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Update plugin config and dependency cache (#1130)
* Update plugin config and dependency cache * Fix deps check
This commit is contained in:
@@ -25,8 +25,7 @@ class PluginService:
|
||||
async def get_all() -> list[dict[str, Any]]:
|
||||
"""获取所有插件"""
|
||||
|
||||
changed_key = f'{settings.PLUGIN_REDIS_PREFIX}:changed'
|
||||
keys = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:*') if key != changed_key]
|
||||
keys = [key async for key in redis_client.scan_iter(f'{settings.PLUGIN_REDIS_PREFIX}:config:*')]
|
||||
if not keys:
|
||||
return []
|
||||
|
||||
@@ -83,7 +82,7 @@ class PluginService:
|
||||
backup_file = PLUGIN_DIR / f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup.zip'
|
||||
await run_in_threadpool(zip_plugin, plugin_dir, backup_file)
|
||||
await run_in_threadpool(remove_plugin, plugin_dir)
|
||||
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:config:{plugin}')
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
|
||||
|
||||
@staticmethod
|
||||
@@ -94,7 +93,8 @@ class PluginService:
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
plugin_key = f'{settings.PLUGIN_REDIS_PREFIX}:config:{plugin}'
|
||||
plugin_info = await redis_client.get(plugin_key)
|
||||
if not plugin_info:
|
||||
raise errors.NotFoundError(msg='插件不存在')
|
||||
plugin_info = json.loads(plugin_info)
|
||||
@@ -106,7 +106,7 @@ class PluginService:
|
||||
else str(StatusType.disable.value)
|
||||
)
|
||||
plugin_info['plugin']['enable'] = new_status
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}', json.dumps(plugin_info, ensure_ascii=False))
|
||||
await redis_client.set(plugin_key, json.dumps(plugin_info, ensure_ascii=False))
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -144,9 +144,11 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
run_await(current_redis_client.init)()
|
||||
|
||||
# 清理未知插件信息
|
||||
exclude_keys = [f'{settings.PLUGIN_REDIS_PREFIX}:config:{key}' for key in plugins]
|
||||
exclude_keys.extend(f'{settings.PLUGIN_REDIS_PREFIX}:requirements_hash:{key}' for key in plugins)
|
||||
run_await(current_redis_client.delete_prefix)(
|
||||
settings.PLUGIN_REDIS_PREFIX,
|
||||
exclude=[f'{settings.PLUGIN_REDIS_PREFIX}:{key}' for key in plugins],
|
||||
exclude=exclude_keys,
|
||||
)
|
||||
|
||||
for plugin in plugins:
|
||||
@@ -160,7 +162,8 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
|
||||
# 补充插件信息
|
||||
data['plugin']['name'] = plugin
|
||||
plugin_cache_info = run_await(current_redis_client.get)(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}')
|
||||
plugin_cache_key = f'{settings.PLUGIN_REDIS_PREFIX}:config:{plugin}'
|
||||
plugin_cache_info = run_await(current_redis_client.get)(plugin_cache_key)
|
||||
if plugin_cache_info:
|
||||
try:
|
||||
data['plugin']['enable'] = json.loads(plugin_cache_info)['plugin']['enable']
|
||||
@@ -170,10 +173,7 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
data['plugin']['enable'] = str(StatusType.enable.value)
|
||||
|
||||
# 缓存最新插件信息
|
||||
run_await(current_redis_client.set)(
|
||||
f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}',
|
||||
json.dumps(data, ensure_ascii=False),
|
||||
)
|
||||
run_await(current_redis_client.set)(plugin_cache_key, json.dumps(data, ensure_ascii=False))
|
||||
|
||||
# 重置插件变更状态
|
||||
run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||
@@ -308,7 +308,7 @@ class PluginStatusChecker:
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:{self.plugin}')
|
||||
plugin_info = await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:config:{self.plugin}')
|
||||
if not plugin_info:
|
||||
log.error('插件状态未初始化或丢失,需重启服务自动修复')
|
||||
raise PluginInjectError('插件状态未初始化或丢失,请联系系统管理员')
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import hashlib
|
||||
import os
|
||||
import site
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from importlib import invalidate_caches
|
||||
from importlib.metadata import PackageNotFoundError, distribution
|
||||
from pathlib import Path
|
||||
|
||||
from packaging.requirements import Requirement
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.core.conf import settings
|
||||
@@ -20,6 +24,14 @@ def _is_in_virtualenv() -> bool:
|
||||
return hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
|
||||
|
||||
|
||||
def _refresh_site_packages() -> None:
|
||||
"""刷新当前进程的 site-packages 路径,确保新装依赖立即可导入。"""
|
||||
invalidate_caches()
|
||||
for site_dir in site.getsitepackages():
|
||||
if site_dir.endswith('site-packages'):
|
||||
site.addsitedir(site_dir)
|
||||
|
||||
|
||||
def install_requirements(plugin: str | None) -> None: # noqa: C901
|
||||
"""
|
||||
安装插件依赖
|
||||
@@ -31,15 +43,36 @@ def install_requirements(plugin: str | None) -> None: # noqa: C901
|
||||
|
||||
for plugin in plugins:
|
||||
requirements_file = PLUGIN_DIR / plugin / 'requirements.txt'
|
||||
hash_key = f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}:requirements_hash'
|
||||
hash_key = f'{settings.PLUGIN_REDIS_PREFIX}:requirements_hash:{plugin}'
|
||||
cached_hash = run_await(redis_client.get)(hash_key)
|
||||
|
||||
if not os.path.exists(requirements_file):
|
||||
run_await(redis_client.delete)(hash_key)
|
||||
continue
|
||||
|
||||
missing_dependencies = False
|
||||
for line in Path(requirements_file).read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
try:
|
||||
req = Requirement(line)
|
||||
dependency = req.name.lower()
|
||||
except Exception as e:
|
||||
raise PluginInstallError(f'插件 {plugin} 依赖 {line} 格式错误: {e!s}') from e
|
||||
|
||||
try:
|
||||
dist = distribution(dependency)
|
||||
except PackageNotFoundError:
|
||||
missing_dependencies = True
|
||||
break
|
||||
|
||||
if req.specifier and not req.specifier.contains(dist.version, prereleases=True):
|
||||
missing_dependencies = True
|
||||
break
|
||||
|
||||
current_hash = hashlib.sha256(Path(requirements_file).read_bytes()).hexdigest()
|
||||
cached_hash = run_await(redis_client.get)(hash_key)
|
||||
if cached_hash == current_hash:
|
||||
if cached_hash == current_hash and not missing_dependencies:
|
||||
continue
|
||||
|
||||
pip_install = ['uv', 'pip', 'install', '-r', requirements_file]
|
||||
@@ -52,6 +85,7 @@ def install_requirements(plugin: str | None) -> None: # noqa: C901
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
subprocess.check_call(pip_install)
|
||||
_refresh_site_packages()
|
||||
run_await(redis_client.set)(hash_key, current_hash)
|
||||
break
|
||||
except subprocess.TimeoutExpired:
|
||||
@@ -71,7 +105,7 @@ def uninstall_requirements(plugin: str) -> None:
|
||||
:param plugin: 插件名称
|
||||
:return:
|
||||
"""
|
||||
run_await(redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:{plugin}:requirements_hash')
|
||||
run_await(redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:requirements_hash:{plugin}')
|
||||
requirements_file = PLUGIN_DIR / plugin / 'requirements.txt'
|
||||
if os.path.exists(requirements_file):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user