mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00: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
|
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 ===
|
# === FastAPI server image ===
|
||||||
FROM base_server AS fastapi_server
|
FROM base_server AS fastapi_server
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,9 @@ class PluginService:
|
|||||||
if not file:
|
if not file:
|
||||||
raise errors.RequestError(msg='ZIP 压缩包不能为空')
|
raise errors.RequestError(msg='ZIP 压缩包不能为空')
|
||||||
return await install_zip_plugin(file)
|
return await install_zip_plugin(file)
|
||||||
elif type == PluginType.git:
|
if not repo_url:
|
||||||
if not repo_url:
|
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
||||||
raise errors.RequestError(msg='Git 仓库地址不能为空')
|
return await install_git_plugin(repo_url)
|
||||||
return await install_git_plugin(repo_url)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def uninstall(*, plugin: str):
|
async def uninstall(*, plugin: str):
|
||||||
|
|||||||
+10
-25
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
@@ -7,45 +9,21 @@ import cappa
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.progress import (
|
|
||||||
Progress,
|
|
||||||
SpinnerColumn,
|
|
||||||
TextColumn,
|
|
||||||
TimeElapsedColumn,
|
|
||||||
)
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from backend import console, get_version
|
from backend import console, get_version
|
||||||
from backend.common.exception.errors import BaseExceptionMixin
|
from backend.common.exception.errors import BaseExceptionMixin
|
||||||
from backend.core.conf import settings
|
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._await import run_await
|
||||||
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
from backend.utils.file_ops import install_git_plugin, install_zip_plugin
|
||||||
|
|
||||||
|
|
||||||
def run(host: str, port: int, reload: bool, workers: int | None) -> None:
|
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}'
|
url = f'http://{host}:{port}'
|
||||||
docs_url = url + settings.FASTAPI_DOCS_URL
|
docs_url = url + settings.FASTAPI_DOCS_URL
|
||||||
redoc_url = url + settings.FASTAPI_REDOC_URL
|
redoc_url = url + settings.FASTAPI_REDOC_URL
|
||||||
openapi_url = url + settings.FASTAPI_OPENAPI_URL
|
openapi_url = url + settings.FASTAPI_OPENAPI_URL
|
||||||
|
|
||||||
console.print(Text('启动 fba 服务...', style='bold magenta'))
|
|
||||||
|
|
||||||
panel_content = Text()
|
panel_content = Text()
|
||||||
panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue')
|
panel_content.append(f'📝 Swagger 文档: {docs_url}\n', style='blue')
|
||||||
panel_content.append(f'📚 Redoc 文档: {redoc_url}\n', style='yellow')
|
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)))
|
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:
|
def install_plugin(path: str, repo_url: str) -> None:
|
||||||
|
|||||||
@@ -1,5 +1,29 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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.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()
|
app = register_app()
|
||||||
|
|||||||
+26
-3
@@ -8,11 +8,13 @@ import sys
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
from importlib.metadata import PackageNotFoundError, distribution
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import rtoml
|
import rtoml
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from packaging.requirements import Requirement
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
@@ -33,6 +35,10 @@ class PluginInjectError(Exception):
|
|||||||
"""插件注入错误"""
|
"""插件注入错误"""
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallError(Exception):
|
||||||
|
"""插件安装错误"""
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
def get_plugins() -> list[str]:
|
def get_plugins() -> list[str]:
|
||||||
"""获取插件列表"""
|
"""获取插件列表"""
|
||||||
@@ -262,7 +268,24 @@ def install_requirements(plugin: str | None) -> None:
|
|||||||
|
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt')
|
requirements_file = os.path.join(PLUGIN_DIR, plugin, 'requirements.txt')
|
||||||
|
missing_dependencies = False
|
||||||
if os.path.exists(requirements_file):
|
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:
|
try:
|
||||||
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
|
ensurepip_install = [sys.executable, '-m', 'ensurepip', '--upgrade']
|
||||||
pip_install = [sys.executable, '-m', 'pip', 'install', '-r', requirements_file]
|
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(ensurepip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
subprocess.check_call(pip_install, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
except subprocess.CalledProcessError as e:
|
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:
|
def uninstall_requirements(plugin: str) -> None:
|
||||||
@@ -285,9 +308,9 @@ def uninstall_requirements(plugin: str) -> None:
|
|||||||
if os.path.exists(requirements_file):
|
if os.path.exists(requirements_file):
|
||||||
try:
|
try:
|
||||||
pip_uninstall = [sys.executable, '-m', 'pip', 'uninstall', '-r', requirements_file, '-y']
|
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:
|
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:
|
async def install_requirements_async(plugin: str | None = None) -> None:
|
||||||
|
|||||||
+9
-3
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -7,8 +9,12 @@ if __name__ == '__main__':
|
|||||||
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
|
# 如果你喜欢在 IDE 中进行 DEBUG,可在 IDE 中直接右键启动此文件
|
||||||
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
|
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
|
||||||
try:
|
try:
|
||||||
config = uvicorn.Config(app='backend.main:app', reload=True)
|
uvicorn.run(
|
||||||
server = uvicorn.Server(config)
|
app='backend.main:app',
|
||||||
server.run()
|
host='127.0.0.1',
|
||||||
|
port=8000,
|
||||||
|
reload=True,
|
||||||
|
reload_excludes=[os.path.abspath('../.venv')],
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise 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