mirror of
https://github.com/fastapi-practices/fastapi_best_architecture.git
synced 2026-03-13 09:31:31 +08:00
Optimize the installation of plugin dependencies (#700)
* Optimize the installation of plugin dependencies * Remove enumerate * Update main params
This commit is contained in:
@@ -36,11 +36,6 @@ COPY . /fba
|
||||
|
||||
COPY --from=builder /usr/local /usr/local
|
||||
|
||||
# Install plugin dependencies
|
||||
WORKDIR /fba
|
||||
ENV PYTHONPATH=/fba
|
||||
RUN python3 backend/scripts/init_plugin.py
|
||||
|
||||
# === FastAPI server image ===
|
||||
FROM base_server AS fastapi_server
|
||||
|
||||
|
||||
@@ -56,10 +56,9 @@ class PluginService:
|
||||
if not file:
|
||||
raise errors.RequestError(msg='ZIP 压缩包不能为空')
|
||||
return await install_zip_plugin(file)
|
||||
elif type == PluginType.git:
|
||||
if not repo_url:
|
||||
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
||||
return await install_git_plugin(repo_url)
|
||||
if not repo_url:
|
||||
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
||||
return await install_git_plugin(repo_url)
|
||||
|
||||
@staticmethod
|
||||
async def uninstall(*, plugin: str):
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated
|
||||
|
||||
@@ -7,45 +9,21 @@ import cappa
|
||||
import uvicorn
|
||||
|
||||
from rich.panel import Panel
|
||||
from rich.progress import (
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TextColumn,
|
||||
TimeElapsedColumn,
|
||||
)
|
||||
from rich.text import Text
|
||||
|
||||
from backend import console, get_version
|
||||
from backend.common.exception.errors import BaseExceptionMixin
|
||||
from backend.core.conf import settings
|
||||
from backend.plugin.tools import get_plugins, install_requirements
|
||||
from backend.utils._await import run_await
|
||||
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
||||
|
||||
|
||||
def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
||||
console.print(Text('检测插件依赖...', style='bold cyan'))
|
||||
|
||||
plugins = get_plugins()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(finished_text='[bold green]插件依赖安装完成[/]'),
|
||||
TextColumn('[green]{task.completed}/{task.total}[/]'),
|
||||
TimeElapsedColumn(),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task('安装插件依赖...', total=len(plugins))
|
||||
for i, plugin in enumerate(plugins):
|
||||
install_requirements(plugin)
|
||||
progress.advance(task)
|
||||
|
||||
url = f'http://{host}:{port}'
|
||||
docs_url = url + settings.FASTAPI_DOCS_URL
|
||||
redoc_url = url + settings.FASTAPI_REDOC_URL
|
||||
openapi_url = url + settings.FASTAPI_OPENAPI_URL
|
||||
|
||||
console.print(Text('启动 fba 服务...', style='bold magenta'))
|
||||
|
||||
panel_content = Text()
|
||||
panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue')
|
||||
panel_content.append(f'📚 Redoc 文档: {redoc_url}\n', style='yellow')
|
||||
@@ -56,7 +34,14 @@ def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
||||
)
|
||||
|
||||
console.print(Panel(panel_content, title='fba 服务信息', border_style='purple', padding=(1, 2)))
|
||||
uvicorn.run(app='backend.main:app', host=host, port=port, reload=not reload, workers=workers)
|
||||
uvicorn.run(
|
||||
app='backend.main:app',
|
||||
host=host,
|
||||
port=port,
|
||||
reload=not reload,
|
||||
reload_excludes=[os.path.abspath('../.venv' if 'backend' in os.getcwd() else '.venv')],
|
||||
workers=workers,
|
||||
)
|
||||
|
||||
|
||||
def install_plugin(path: str, repo_url: str) -> None:
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
||||
from rich.text import Text
|
||||
|
||||
from backend import console
|
||||
from backend.core.registrar import register_app
|
||||
from backend.plugin.tools import get_plugins, install_requirements
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
_print_log_style = f'{timezone.to_str(timezone.now(), "%Y-%m-%d %H:%M:%S.%M0")} | fba | - | '
|
||||
console.print(Text(f'{_print_log_style}检测插件依赖...', style='bold cyan'))
|
||||
|
||||
_plugins = get_plugins()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(finished_text=f'[bold green]{_print_log_style}插件准备就绪[/]'),
|
||||
TextColumn('[bold green]{task.completed}/{task.total}[/]'),
|
||||
TimeElapsedColumn(),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task('安装插件依赖...', total=len(_plugins))
|
||||
for plugin in _plugins:
|
||||
install_requirements(plugin)
|
||||
progress.advance(task)
|
||||
|
||||
console.print(Text(f'{_print_log_style}启动服务...', style='bold magenta'))
|
||||
|
||||
app = register_app()
|
||||
|
||||
@@ -8,11 +8,13 @@ import sys
|
||||
import warnings
|
||||
|
||||
from functools import lru_cache
|
||||
from importlib.metadata import PackageNotFoundError, distribution
|
||||
from typing import Any
|
||||
|
||||
import rtoml
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from packaging.requirements import Requirement
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
from backend.common.enums import StatusType
|
||||
@@ -33,6 +35,10 @@ class PluginInjectError(Exception):
|
||||
"""插件注入错误"""
|
||||
|
||||
|
||||
class PluginInstallError(Exception):
|
||||
"""插件安装错误"""
|
||||
|
||||
|
||||
@lru_cache
|
||||
def get_plugins() -> list[str]:
|
||||
"""获取插件列表"""
|
||||
@@ -262,7 +268,24 @@ def install_requirements(plugin: str | None) -> None:
|
||||
|
||||
for plugin in plugins:
|
||||
requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt')
|
||||
missing_dependencies = False
|
||||
if os.path.exists(requirements_file):
|
||||
with open(requirements_file, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
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} 格式错误: {str(e)}') from e
|
||||
try:
|
||||
distribution(dependency)
|
||||
except PackageNotFoundError:
|
||||
missing_dependencies = True
|
||||
|
||||
if missing_dependencies:
|
||||
try:
|
||||
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
|
||||
pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file]
|
||||
@@ -271,7 +294,7 @@ def install_requirements(plugin: str | None) -> None:
|
||||
subprocess.check_call(ensurepip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise PluginInjectError(f'插件 {plugin} 依赖安装失败:{e.stderr}') from e
|
||||
raise PluginInstallError(f'插件 {plugin} 依赖安装失败:{e}') from e
|
||||
|
||||
|
||||
def uninstall_requirements(plugin: str) -> None:
|
||||
@@ -285,9 +308,9 @@ def uninstall_requirements(plugin: str) -> None:
|
||||
if os.path.exists(requirements_file):
|
||||
try:
|
||||
pip_uninstall = [sys.executable, '-m', 'pip', 'uninstall', '-r', requirements_file, '-y']
|
||||
subprocess.check_call(pip_uninstall)
|
||||
subprocess.check_call(pip_uninstall, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise PluginInjectError(f'插件 {plugin} 依赖卸载失败:{e.stderr}') from e
|
||||
raise PluginInstallError(f'插件 {plugin} 依赖卸载失败:{e}') from e
|
||||
|
||||
|
||||
async def install_requirements_async(plugin: str | None = None) -> None:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -7,8 +9,12 @@ if __name__ == '__main__':
|
||||
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
|
||||
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
|
||||
try:
|
||||
config = uvicorn.Config(app='backend.main:app', reload=True)
|
||||
server = uvicorn.Server(config)
|
||||
server.run()
|
||||
uvicorn.run(
|
||||
app='backend.main:app',
|
||||
host='127.0.0.1',
|
||||
port=8000,
|
||||
reload=True,
|
||||
reload_excludes=[os.path.abspath('../.venv')],
|
||||
)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from anyio import run
|
||||
|
||||
from backend.plugin.tools import install_requirements_async
|
||||
|
||||
|
||||
async def init() -> None:
|
||||
print('Starting initial plugin')
|
||||
await install_requirements_async()
|
||||
print('Plugin successfully installed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run(init) # type: ignore
|
||||
Reference in New Issue
Block a user