Compare commits
4
Commits
ae662a4941
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80a71b48ce | ||
|
|
3330592fbe | ||
|
|
a8af1f542b | ||
|
|
a62803ccf0 |
@@ -18,3 +18,8 @@ __pycache__/
|
|||||||
node_modules/
|
node_modules/
|
||||||
/.venv/
|
/.venv/
|
||||||
/venv/
|
/venv/
|
||||||
|
/shierfeng/
|
||||||
|
|
||||||
|
# Python 安装与构建产物,源码以 cli/ 为准。
|
||||||
|
*.egg-info/
|
||||||
|
/shierfeng-fastapi-backend/build/
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from cli.runtime import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
|
||||||
|
|
||||||
|
|
||||||
class AppBootstrapService:
|
|
||||||
"""
|
|
||||||
应用引导服务。
|
|
||||||
|
|
||||||
该服务负责解析应用原始入口路径、构建启动命令,并以 `exec`
|
|
||||||
方式将当前 CLI 进程切换为 FastAPI 应用入口进程。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, runtime_environment: RuntimeEnvironmentService | None = None) -> None:
|
|
||||||
"""
|
|
||||||
初始化应用引导服务。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
|
|
||||||
|
|
||||||
def get_app_entry_path(self) -> str:
|
|
||||||
"""
|
|
||||||
获取应用启动入口路径。
|
|
||||||
|
|
||||||
:return: 应用启动入口绝对路径
|
|
||||||
"""
|
|
||||||
return os.path.join(self.runtime_environment.get_backend_dir(), 'app.py')
|
|
||||||
|
|
||||||
def build_app_run_command(self, env: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建应用启动命令。
|
|
||||||
|
|
||||||
:param env: 运行环境
|
|
||||||
:return: 应用启动命令参数列表
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
self.runtime_environment.get_python_executable(),
|
|
||||||
self.get_app_entry_path(),
|
|
||||||
'--env',
|
|
||||||
env,
|
|
||||||
]
|
|
||||||
|
|
||||||
def exec_app_run_command(self, env: str) -> None:
|
|
||||||
"""
|
|
||||||
以应用原始入口进程启动当前 FastAPI 应用。
|
|
||||||
|
|
||||||
:param env: 运行环境
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
command = self.build_app_run_command(env)
|
|
||||||
os.execvp(command[0], command)
|
|
||||||
|
|
||||||
|
|
||||||
APP_BOOTSTRAP = AppBootstrapService()
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.controller import CompletionCommandController
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway
|
|
||||||
from cli.context import OutputOption
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionCommandRegistration:
|
|
||||||
"""
|
|
||||||
completion 子命令注册描述。
|
|
||||||
|
|
||||||
:param name: 子命令名称
|
|
||||||
:param help_text: 子命令帮助文本
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
help_text: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CompletionSubcommandRegistrar:
|
|
||||||
"""
|
|
||||||
completion 子命令注册器。
|
|
||||||
|
|
||||||
该对象负责将 `show/install/doctor` 子命令挂载到 completion 应用,
|
|
||||||
将 Typer 命令声明细节从构建器主体中拆出。
|
|
||||||
|
|
||||||
:param completion_provider_gateway: completion 提供器对外网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
completion_provider_gateway: CompletionProviderGateway = field(default_factory=lambda: COMPLETION_PROVIDER_GATEWAY)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_registrations() -> tuple[CompletionCommandRegistration, ...]:
|
|
||||||
"""
|
|
||||||
返回 completion 子命令注册描述列表。
|
|
||||||
|
|
||||||
:return: 子命令注册描述元组
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
CompletionCommandRegistration(name='show', help_text='输出指定 shell 的 completion 脚本'),
|
|
||||||
CompletionCommandRegistration(name='install', help_text='安装指定 shell 的 completion 脚本'),
|
|
||||||
CompletionCommandRegistration(name='doctor', help_text='检查当前 completion 配置状态'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def register(self, app: typer.Typer, *, controller: CompletionCommandController) -> None:
|
|
||||||
"""
|
|
||||||
向 completion 应用注册全部子命令。
|
|
||||||
|
|
||||||
:param app: completion 子应用
|
|
||||||
:param controller: completion 命令控制器
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
registrations = {registration.name: registration for registration in self.build_registrations()}
|
|
||||||
|
|
||||||
@app.command(registrations['show'].name, help=registrations['show'].help_text)
|
|
||||||
def show(
|
|
||||||
shell: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='shell 名称,如 bash、zsh、fish',
|
|
||||||
autocompletion=self.completion_provider_gateway.complete_shell_names,
|
|
||||||
),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出指定 shell 的 completion 脚本。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
controller.show(shell)
|
|
||||||
|
|
||||||
@app.command(registrations['install'].name, help=registrations['install'].help_text)
|
|
||||||
def install(
|
|
||||||
shell: str | None = typer.Option(
|
|
||||||
None,
|
|
||||||
'--shell',
|
|
||||||
help='shell 名称,默认自动识别当前 shell',
|
|
||||||
autocompletion=self.completion_provider_gateway.complete_shell_names,
|
|
||||||
),
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
target_file: Path | None = typer.Option(None, '--target-file', help='completion 脚本目标文件路径'),
|
|
||||||
activate: bool = typer.Option(False, '--activate', help='将激活命令写入 shell rc 文件'),
|
|
||||||
rc_file: Path | None = typer.Option(None, '--rc-file', help='自定义 shell rc 文件路径'),
|
|
||||||
force: bool = typer.Option(False, '--force', help='覆盖已存在且内容不同的目标文件'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
安装指定 shell 的 completion 脚本。
|
|
||||||
|
|
||||||
:param shell: shell 名称,默认自动识别当前 shell
|
|
||||||
:param output: 输出格式
|
|
||||||
:param target_file: completion 脚本目标文件路径
|
|
||||||
:param activate: 是否写入 shell rc 文件激活命令
|
|
||||||
:param rc_file: 自定义 shell rc 文件路径
|
|
||||||
:param force: 是否强制覆盖已存在文件
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
controller.install(
|
|
||||||
output,
|
|
||||||
shell=shell,
|
|
||||||
target_file=target_file,
|
|
||||||
activate=activate,
|
|
||||||
rc_file=rc_file,
|
|
||||||
force=force,
|
|
||||||
)
|
|
||||||
|
|
||||||
@app.command(registrations['doctor'].name, help=registrations['doctor'].help_text)
|
|
||||||
def doctor(
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
检查当前 completion 配置状态。
|
|
||||||
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
controller.doctor(output)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CompletionCommandBuilder:
|
|
||||||
"""
|
|
||||||
completion 子应用构建器。
|
|
||||||
|
|
||||||
该构建器负责装配 `completion` 子应用,并将控制器实例和子命令注册
|
|
||||||
细节收口到类式对象协作中。
|
|
||||||
|
|
||||||
:param completion_subcommand_registrar: completion 子命令注册器
|
|
||||||
"""
|
|
||||||
|
|
||||||
completion_subcommand_registrar: CompletionSubcommandRegistrar = field(
|
|
||||||
default_factory=CompletionSubcommandRegistrar
|
|
||||||
)
|
|
||||||
|
|
||||||
def build(self, root_cli: typer.Typer) -> typer.Typer:
|
|
||||||
"""
|
|
||||||
构建 completion 命令组。
|
|
||||||
|
|
||||||
:param root_cli: 根 Typer 应用
|
|
||||||
:return: completion 子应用
|
|
||||||
"""
|
|
||||||
app = typer.Typer(
|
|
||||||
help='shell completion 相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
completion_command_controller = CompletionCommandController(root_cli)
|
|
||||||
self.completion_subcommand_registrar.register(app, controller=completion_command_controller)
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
COMPLETION_COMMAND_BUILDER = CompletionCommandBuilder()
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.doctor import COMPLETION_DOCTOR, CompletionDoctorService
|
|
||||||
from cli.completion.installers import COMPLETION_INSTALLER, CompletionInstallerService
|
|
||||||
from cli.completion.presenter import CompletionCommandPresenter
|
|
||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionCommandController:
|
|
||||||
"""
|
|
||||||
completion 命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `completion` 命令组的上下文准备、payload 生成、
|
|
||||||
文本渲染与脚本输出收口。
|
|
||||||
|
|
||||||
:param root_cli: 根 Typer 应用
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: completion 命令文本渲染器
|
|
||||||
:param installer_service: completion 安装与脚本生成服务
|
|
||||||
:param doctor_service: completion 诊断服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
root_cli: typer.Typer,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: CompletionCommandPresenter | None = None,
|
|
||||||
installer_service: CompletionInstallerService | None = None,
|
|
||||||
doctor_service: CompletionDoctorService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化 completion 命令控制器。
|
|
||||||
|
|
||||||
:param root_cli: 根 Typer 应用
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: completion 命令文本渲染器
|
|
||||||
:param installer_service: completion 安装与脚本生成服务
|
|
||||||
:param doctor_service: completion 诊断服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.root_cli = root_cli
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or CompletionCommandPresenter()
|
|
||||||
self.installer_service = installer_service or COMPLETION_INSTALLER
|
|
||||||
self.doctor_service = doctor_service or COMPLETION_DOCTOR
|
|
||||||
|
|
||||||
def show(self, shell: str) -> None:
|
|
||||||
"""
|
|
||||||
输出指定 shell 的 completion 脚本。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
typer.echo(self.installer_service.render_completion_script(self.root_cli, shell), nl=False)
|
|
||||||
|
|
||||||
def install(
|
|
||||||
self,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
shell: str | None,
|
|
||||||
target_file: Path | None,
|
|
||||||
activate: bool,
|
|
||||||
rc_file: Path | None,
|
|
||||||
force: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
安装指定 shell 的 completion 脚本。
|
|
||||||
|
|
||||||
:param output: 输出格式
|
|
||||||
:param shell: shell 名称
|
|
||||||
:param target_file: completion 脚本目标文件路径
|
|
||||||
:param activate: 是否写入 shell rc 文件激活命令
|
|
||||||
:param rc_file: 自定义 shell rc 文件路径
|
|
||||||
:param force: 是否强制覆盖已存在文件
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly('dev', output)
|
|
||||||
payload = self.installer_service.install_completion_script(
|
|
||||||
self.root_cli,
|
|
||||||
shell,
|
|
||||||
target_file=target_file,
|
|
||||||
activate=activate,
|
|
||||||
rc_file=rc_file,
|
|
||||||
force=force,
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload_result(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_completion_install_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def doctor(self, output: str) -> None:
|
|
||||||
"""
|
|
||||||
检查当前 completion 配置状态。
|
|
||||||
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly('dev', output)
|
|
||||||
payload = self.doctor_service.build_completion_doctor_payload()
|
|
||||||
self.execution_service.complete_payload_result(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_completion_doctor_text,
|
|
||||||
)
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.completion.installers import (
|
|
||||||
CLICK_COMPLETE_ENV_VAR,
|
|
||||||
COMPLETION_INSTALLER,
|
|
||||||
CompletionInstallerService,
|
|
||||||
)
|
|
||||||
from cli.metadata import (
|
|
||||||
COMPLETION_SHELL_SPEC_REGISTRY,
|
|
||||||
ENVIRONMENT_OPTION_SERVICE,
|
|
||||||
CompletionShellSpecRegistry,
|
|
||||||
EnvironmentOptionService,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionDoctorService:
|
|
||||||
"""
|
|
||||||
completion 诊断服务。
|
|
||||||
|
|
||||||
该服务负责汇总活跃 shell、目标脚本路径、rc 文件状态、推荐安装命令
|
|
||||||
与环境候选列表,生成 `completion doctor` 所需的结构化结果。
|
|
||||||
|
|
||||||
:param installer_service: completion 安装服务
|
|
||||||
:param shell_spec_registry: shell 元数据注册表
|
|
||||||
:param environment_option_service: 环境选项服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
installer_service: CompletionInstallerService | None = None,
|
|
||||||
shell_spec_registry: CompletionShellSpecRegistry | None = None,
|
|
||||||
environment_option_service: EnvironmentOptionService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化 completion 诊断服务。
|
|
||||||
|
|
||||||
:param installer_service: completion 安装服务
|
|
||||||
:param shell_spec_registry: shell 元数据注册表
|
|
||||||
:param environment_option_service: 环境选项服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.installer_service = installer_service or COMPLETION_INSTALLER
|
|
||||||
self.shell_spec_registry = shell_spec_registry or COMPLETION_SHELL_SPEC_REGISTRY
|
|
||||||
self.environment_option_service = environment_option_service or ENVIRONMENT_OPTION_SERVICE
|
|
||||||
|
|
||||||
def build_completion_doctor_payload(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建 completion 诊断结果。
|
|
||||||
|
|
||||||
:return: 诊断结果字典
|
|
||||||
"""
|
|
||||||
active_shell = self.installer_service.detect_active_shell()
|
|
||||||
shells: dict[str, dict[str, Any]] = {}
|
|
||||||
for shell_name, shell_spec in self.shell_spec_registry.specs.items():
|
|
||||||
target_file = self.installer_service.resolve_completion_target(shell_name)
|
|
||||||
rc_file = self.installer_service.resolve_completion_rc_file(shell_name)
|
|
||||||
source_command = None
|
|
||||||
if shell_spec.supported and shell_spec.generator in {'click', 'custom'}:
|
|
||||||
source_command = self.installer_service.build_source_command(target_file, shell_name)
|
|
||||||
recommended_install_command = f'shierfeng completion install --shell={shell_name}'
|
|
||||||
if shell_spec.supported and not shell_spec.auto_discovery:
|
|
||||||
recommended_install_command = f'{recommended_install_command} --activate'
|
|
||||||
shells[shell_name] = {
|
|
||||||
'supported': shell_spec.supported,
|
|
||||||
'detected': shell_name == active_shell,
|
|
||||||
'description': shell_spec.description,
|
|
||||||
'targetFile': str(target_file),
|
|
||||||
'targetFileExists': target_file.exists(),
|
|
||||||
'rcFile': str(rc_file) if rc_file is not None else None,
|
|
||||||
'rcFileExists': rc_file.exists() if rc_file is not None else None,
|
|
||||||
'autoDiscovery': shell_spec.auto_discovery,
|
|
||||||
'sourceCommand': source_command,
|
|
||||||
'recommendedInstallCommand': recommended_install_command,
|
|
||||||
}
|
|
||||||
|
|
||||||
recommended_shell = active_shell if self.shell_spec_registry.get_spec(active_shell) is not None else None
|
|
||||||
recommended_install_command = None
|
|
||||||
if recommended_shell is not None:
|
|
||||||
recommended_install_command = shells[recommended_shell]['recommendedInstallCommand']
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': 'completion 诊断信息已生成',
|
|
||||||
'activeShell': active_shell or None,
|
|
||||||
'projectDir': str(Path.cwd().resolve()),
|
|
||||||
'envChoices': self.environment_option_service.discover_env_names(),
|
|
||||||
'completeEnvVar': CLICK_COMPLETE_ENV_VAR,
|
|
||||||
'recommendedInstallCommand': recommended_install_command,
|
|
||||||
'shells': shells,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
COMPLETION_DOCTOR = CompletionDoctorService()
|
|
||||||
@@ -1,465 +0,0 @@
|
|||||||
import io
|
|
||||||
import os
|
|
||||||
from contextlib import redirect_stderr
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import click
|
|
||||||
import typer
|
|
||||||
import typer.main
|
|
||||||
from click.shell_completion import BashComplete, FishComplete, ZshComplete
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY, CompletionProviderGateway
|
|
||||||
from cli.completion.shells import PowerShellComplete, ensure_custom_completion_classes_registered
|
|
||||||
from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR
|
|
||||||
from cli.metadata import COMPLETION_SHELL_SPEC_REGISTRY, CompletionShellSpec, CompletionShellSpecRegistry
|
|
||||||
|
|
||||||
CLICK_COMPLETE_ENV_VAR = '_SHIERFENG_COMPLETE'
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionInstallerShellSupport:
|
|
||||||
"""
|
|
||||||
completion shell 差异支持对象。
|
|
||||||
|
|
||||||
该对象负责封装不同 shell 在脚本兼容处理和 source 命令构建上的差异,
|
|
||||||
供安装服务通过轻量 strategy 协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def keep_script_text(script_text: str) -> str:
|
|
||||||
"""
|
|
||||||
保持脚本文本原样返回。
|
|
||||||
|
|
||||||
:param script_text: 原始脚本文本
|
|
||||||
:return: 原样脚本文本
|
|
||||||
"""
|
|
||||||
return script_text
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make_bash_completion_script_compatible(script_text: str) -> str:
|
|
||||||
"""
|
|
||||||
对 Click 生成的 Bash completion 脚本做旧版本兼容处理。
|
|
||||||
|
|
||||||
兼容目标主要包括 macOS 默认 Bash 3.2:
|
|
||||||
|
|
||||||
- `complete -o nosort` 在旧 Bash 上会在 `source` 阶段直接报错
|
|
||||||
- `compopt` 在旧 Bash 上可能不存在
|
|
||||||
|
|
||||||
:param script_text: Click 原始 Bash completion 脚本文本
|
|
||||||
:return: 兼容处理后的脚本文本
|
|
||||||
"""
|
|
||||||
compatible_lines: list[str] = []
|
|
||||||
for line in script_text.splitlines():
|
|
||||||
stripped_line = line.strip()
|
|
||||||
if stripped_line == 'compopt -o dirnames':
|
|
||||||
indent = line[: len(line) - len(line.lstrip(' '))]
|
|
||||||
compatible_lines.extend(
|
|
||||||
[
|
|
||||||
f'{indent}if command -v compopt >/dev/null 2>&1; then',
|
|
||||||
f'{indent} compopt -o dirnames',
|
|
||||||
f'{indent}fi',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if stripped_line == 'compopt -o default':
|
|
||||||
indent = line[: len(line) - len(line.lstrip(' '))]
|
|
||||||
compatible_lines.extend(
|
|
||||||
[
|
|
||||||
f'{indent}if command -v compopt >/dev/null 2>&1; then',
|
|
||||||
f'{indent} compopt -o default',
|
|
||||||
f'{indent}fi',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
if stripped_line.startswith('complete -o nosort -F '):
|
|
||||||
compatibility_line = stripped_line.replace('complete -o nosort -F ', '', 1)
|
|
||||||
function_name, command_name = compatibility_line.split(' ', 1)
|
|
||||||
compatible_lines.extend(
|
|
||||||
[
|
|
||||||
f'if complete -o nosort -F {function_name} {command_name} 2>/dev/null; then',
|
|
||||||
' :',
|
|
||||||
'else',
|
|
||||||
f' complete -F {function_name} {command_name}',
|
|
||||||
'fi',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
compatible_lines.append(line)
|
|
||||||
return '\n'.join(compatible_lines) + '\n'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_posix_source_command(target_file: Path) -> str:
|
|
||||||
"""
|
|
||||||
构建 POSIX shell 使用的 source 命令。
|
|
||||||
|
|
||||||
:param target_file: completion 脚本文件路径
|
|
||||||
:return: source 命令
|
|
||||||
"""
|
|
||||||
return f'source {target_file}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_fish_source_command(target_file: Path) -> str:
|
|
||||||
"""
|
|
||||||
构建 Fish shell 使用的 source 命令。
|
|
||||||
|
|
||||||
:param target_file: completion 脚本文件路径
|
|
||||||
:return: source 命令
|
|
||||||
"""
|
|
||||||
return f'status --is-interactive; and source {target_file}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_powershell_source_command(target_file: Path) -> str:
|
|
||||||
"""
|
|
||||||
构建 PowerShell 使用的 source 命令。
|
|
||||||
|
|
||||||
:param target_file: completion 脚本文件路径
|
|
||||||
:return: source 命令
|
|
||||||
"""
|
|
||||||
return f'. "{target_file}"'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionShellRuntimePolicy:
|
|
||||||
"""
|
|
||||||
completion shell 运行时策略定义。
|
|
||||||
|
|
||||||
:param name: shell 名称
|
|
||||||
:param click_completion_class: Click completion 生成器类型
|
|
||||||
:param script_transformer: 脚本文本后处理函数
|
|
||||||
:param source_command_builder: 激活命令构建函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
click_completion_class: type[Any]
|
|
||||||
script_transformer: Any
|
|
||||||
source_command_builder: Any
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionShellRuntimePolicyRegistry:
|
|
||||||
"""
|
|
||||||
completion shell 运行时策略注册表。
|
|
||||||
|
|
||||||
:param policies: 按 shell 名称索引的运行时策略映射
|
|
||||||
"""
|
|
||||||
|
|
||||||
policies: dict[str, CompletionShellRuntimePolicy]
|
|
||||||
|
|
||||||
def get(self, shell_name: str) -> CompletionShellRuntimePolicy | None:
|
|
||||||
"""
|
|
||||||
获取指定 shell 的运行时策略。
|
|
||||||
|
|
||||||
:param shell_name: shell 名称
|
|
||||||
:return: 运行时策略,不存在时返回 None
|
|
||||||
"""
|
|
||||||
return self.policies.get(shell_name)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COMPLETION_SHELL_RUNTIME_POLICIES = CompletionShellRuntimePolicyRegistry(
|
|
||||||
policies={
|
|
||||||
'bash': CompletionShellRuntimePolicy(
|
|
||||||
name='bash',
|
|
||||||
click_completion_class=BashComplete,
|
|
||||||
script_transformer=CompletionInstallerShellSupport.make_bash_completion_script_compatible,
|
|
||||||
source_command_builder=CompletionInstallerShellSupport.build_posix_source_command,
|
|
||||||
),
|
|
||||||
'zsh': CompletionShellRuntimePolicy(
|
|
||||||
name='zsh',
|
|
||||||
click_completion_class=ZshComplete,
|
|
||||||
script_transformer=CompletionInstallerShellSupport.keep_script_text,
|
|
||||||
source_command_builder=CompletionInstallerShellSupport.build_posix_source_command,
|
|
||||||
),
|
|
||||||
'fish': CompletionShellRuntimePolicy(
|
|
||||||
name='fish',
|
|
||||||
click_completion_class=FishComplete,
|
|
||||||
script_transformer=CompletionInstallerShellSupport.keep_script_text,
|
|
||||||
source_command_builder=CompletionInstallerShellSupport.build_fish_source_command,
|
|
||||||
),
|
|
||||||
'powershell': CompletionShellRuntimePolicy(
|
|
||||||
name='powershell',
|
|
||||||
click_completion_class=PowerShellComplete,
|
|
||||||
script_transformer=CompletionInstallerShellSupport.keep_script_text,
|
|
||||||
source_command_builder=CompletionInstallerShellSupport.build_powershell_source_command,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionInstallerService:
|
|
||||||
"""
|
|
||||||
completion 安装与脚本生成服务。
|
|
||||||
|
|
||||||
该服务负责 shell 元数据解析、脚本生成、目标路径解析、激活命令构建、
|
|
||||||
rc 文件写入以及安装结果收口。
|
|
||||||
|
|
||||||
:param completion_provider_gateway: completion 提供器对外网关
|
|
||||||
:param shell_spec_registry: shell 元数据注册表
|
|
||||||
:param shell_runtime_policy_registry: shell 运行时策略注册表
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
completion_provider_gateway: CompletionProviderGateway | None = None,
|
|
||||||
shell_spec_registry: CompletionShellSpecRegistry | None = None,
|
|
||||||
shell_runtime_policy_registry: CompletionShellRuntimePolicyRegistry | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化 completion 安装服务。
|
|
||||||
|
|
||||||
:param completion_provider_gateway: completion 提供器对外网关
|
|
||||||
:param shell_spec_registry: shell 元数据注册表
|
|
||||||
:param shell_runtime_policy_registry: shell 运行时策略注册表
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.completion_provider_gateway = completion_provider_gateway or COMPLETION_PROVIDER_GATEWAY
|
|
||||||
self.shell_spec_registry = shell_spec_registry or COMPLETION_SHELL_SPEC_REGISTRY
|
|
||||||
self.shell_runtime_policy_registry = shell_runtime_policy_registry or DEFAULT_COMPLETION_SHELL_RUNTIME_POLICIES
|
|
||||||
ensure_custom_completion_classes_registered()
|
|
||||||
|
|
||||||
def resolve_completion_shell_spec(self, shell: str) -> CompletionShellSpec:
|
|
||||||
"""
|
|
||||||
获取指定 shell 的 completion 元数据。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: shell 元数据
|
|
||||||
:raises typer.BadParameter: shell 不存在时抛出异常
|
|
||||||
"""
|
|
||||||
normalized_shell = shell.strip().lower()
|
|
||||||
shell_spec = self.shell_spec_registry.get_spec(normalized_shell)
|
|
||||||
if shell_spec is None:
|
|
||||||
supported_shells = ', '.join(self.completion_provider_gateway.list_completion_shells())
|
|
||||||
raise typer.BadParameter(f'不支持的 shell:{shell},可选值为 {supported_shells}')
|
|
||||||
return shell_spec
|
|
||||||
|
|
||||||
def resolve_shell_runtime_policy(self, shell: str) -> CompletionShellRuntimePolicy:
|
|
||||||
"""
|
|
||||||
获取指定 shell 的运行时策略。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: shell 运行时策略
|
|
||||||
:raises typer.BadParameter: 当前 shell 未实现运行时策略时抛出异常
|
|
||||||
"""
|
|
||||||
shell_spec = self.resolve_completion_shell_spec(shell)
|
|
||||||
runtime_policy = self.shell_runtime_policy_registry.get(shell_spec.name)
|
|
||||||
if runtime_policy is None:
|
|
||||||
raise typer.BadParameter(f'{shell_spec.name} completion 当前版本未实现')
|
|
||||||
return runtime_policy
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_completion_click_command(root_cli: typer.Typer) -> click.Command:
|
|
||||||
"""
|
|
||||||
将 Typer 根应用转换为 Click 命令对象,供 shell completion 生成使用。
|
|
||||||
|
|
||||||
:param root_cli: Typer 根应用
|
|
||||||
:return: Click 命令对象
|
|
||||||
"""
|
|
||||||
return typer.main.get_command(root_cli)
|
|
||||||
|
|
||||||
def render_completion_script(self, root_cli: typer.Typer, shell: str) -> str:
|
|
||||||
"""
|
|
||||||
生成指定 shell 的 completion 脚本文本。
|
|
||||||
|
|
||||||
:param root_cli: Typer 根应用
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: completion 脚本文本
|
|
||||||
:raises typer.BadParameter: 当前 shell 未实现脚本生成时抛出异常
|
|
||||||
"""
|
|
||||||
shell_spec = self.resolve_completion_shell_spec(shell)
|
|
||||||
if not shell_spec.supported or shell_spec.generator not in {'click', 'custom'}:
|
|
||||||
raise typer.BadParameter(f'{shell_spec.name} completion 当前版本未实现')
|
|
||||||
|
|
||||||
runtime_policy = self.resolve_shell_runtime_policy(shell_spec.name)
|
|
||||||
click_command = self.build_completion_click_command(root_cli)
|
|
||||||
stderr_buffer = io.StringIO()
|
|
||||||
with redirect_stderr(stderr_buffer):
|
|
||||||
script_text = runtime_policy.click_completion_class(
|
|
||||||
click_command,
|
|
||||||
{},
|
|
||||||
'shierfeng',
|
|
||||||
CLICK_COMPLETE_ENV_VAR,
|
|
||||||
).source()
|
|
||||||
return runtime_policy.script_transformer(script_text)
|
|
||||||
|
|
||||||
def resolve_completion_target(self, shell: str, target_file: Path | None = None) -> Path:
|
|
||||||
"""
|
|
||||||
解析 completion 脚本目标文件路径。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:param target_file: 用户显式指定的目标文件
|
|
||||||
:return: 目标文件绝对路径
|
|
||||||
"""
|
|
||||||
if target_file is not None:
|
|
||||||
return target_file.expanduser().resolve()
|
|
||||||
|
|
||||||
shell_spec = self.resolve_completion_shell_spec(shell)
|
|
||||||
return (Path.home() / shell_spec.default_target).expanduser().resolve()
|
|
||||||
|
|
||||||
def resolve_completion_rc_file(self, shell: str, rc_file: Path | None = None) -> Path | None:
|
|
||||||
"""
|
|
||||||
解析 completion 激活所使用的 rc 文件路径。
|
|
||||||
|
|
||||||
:param shell: shell 名称
|
|
||||||
:param rc_file: 用户显式指定的 rc 文件
|
|
||||||
:return: rc 文件绝对路径,若当前 shell 无 rc 文件则返回 None
|
|
||||||
"""
|
|
||||||
if rc_file is not None:
|
|
||||||
return rc_file.expanduser().resolve()
|
|
||||||
|
|
||||||
shell_spec = self.resolve_completion_shell_spec(shell)
|
|
||||||
if shell_spec.default_rc_file is None:
|
|
||||||
return None
|
|
||||||
return (Path.home() / shell_spec.default_rc_file).expanduser().resolve()
|
|
||||||
|
|
||||||
def build_source_command(self, target_file: Path, shell: str) -> str:
|
|
||||||
"""
|
|
||||||
构建当前 shell 对应的激活命令。
|
|
||||||
|
|
||||||
:param target_file: completion 脚本文件路径
|
|
||||||
:param shell: shell 名称
|
|
||||||
:return: 激活命令文本
|
|
||||||
"""
|
|
||||||
runtime_policy = self.resolve_shell_runtime_policy(shell)
|
|
||||||
return runtime_policy.source_command_builder(target_file)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def detect_active_shell() -> str:
|
|
||||||
"""
|
|
||||||
检测当前进程环境下的活跃 shell 名称。
|
|
||||||
|
|
||||||
:return: shell 名称,未知时返回空字符串
|
|
||||||
"""
|
|
||||||
shell_path = os.environ.get('SHELL', '').strip()
|
|
||||||
if not shell_path:
|
|
||||||
return ''
|
|
||||||
return Path(shell_path).name.lower()
|
|
||||||
|
|
||||||
def resolve_install_shell(self, shell: str | None) -> str:
|
|
||||||
"""
|
|
||||||
解析安装命令实际使用的 shell。
|
|
||||||
|
|
||||||
:param shell: 用户显式指定的 shell,允许为空
|
|
||||||
:return: 实际使用的 shell 名称
|
|
||||||
:raises typer.BadParameter: 无法推断或不支持时抛出异常
|
|
||||||
"""
|
|
||||||
if shell and shell.strip():
|
|
||||||
return self.resolve_completion_shell_spec(shell).name
|
|
||||||
|
|
||||||
active_shell = self.detect_active_shell()
|
|
||||||
if not active_shell:
|
|
||||||
raise typer.BadParameter('未检测到当前 shell,请显式传入 --shell')
|
|
||||||
|
|
||||||
shell_spec = self.shell_spec_registry.get_spec(active_shell)
|
|
||||||
if shell_spec is None:
|
|
||||||
supported_shells = ', '.join(self.completion_provider_gateway.list_completion_shells())
|
|
||||||
raise typer.BadParameter(
|
|
||||||
f'当前 shell `{active_shell}` 不在支持列表中,请显式传入 --shell,可选值为 {supported_shells}'
|
|
||||||
)
|
|
||||||
return shell_spec.name
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def append_activation_line(rc_file: Path, source_command: str) -> bool:
|
|
||||||
"""
|
|
||||||
将激活命令追加到 rc 文件,若已存在则不重复写入。
|
|
||||||
|
|
||||||
:param rc_file: rc 文件路径
|
|
||||||
:param source_command: 激活命令文本
|
|
||||||
:return: 本次是否发生写入
|
|
||||||
"""
|
|
||||||
existing_text = ''
|
|
||||||
if rc_file.exists():
|
|
||||||
existing_text = rc_file.read_text(encoding='utf-8')
|
|
||||||
if source_command in existing_text:
|
|
||||||
return False
|
|
||||||
|
|
||||||
rc_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
with rc_file.open('a', encoding='utf-8') as file_object:
|
|
||||||
if existing_text and not existing_text.endswith('\n'):
|
|
||||||
file_object.write('\n')
|
|
||||||
file_object.write(f'{source_command}\n')
|
|
||||||
return True
|
|
||||||
|
|
||||||
def install_completion_script(
|
|
||||||
self,
|
|
||||||
root_cli: typer.Typer,
|
|
||||||
shell: str | None,
|
|
||||||
*,
|
|
||||||
target_file: Path | None = None,
|
|
||||||
activate: bool = False,
|
|
||||||
rc_file: Path | None = None,
|
|
||||||
force: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
安装指定 shell 的 completion 脚本文件。
|
|
||||||
|
|
||||||
:param root_cli: Typer 根应用
|
|
||||||
:param shell: shell 名称
|
|
||||||
:param target_file: completion 脚本目标文件
|
|
||||||
:param activate: 是否写入 rc 文件激活命令
|
|
||||||
:param rc_file: 自定义 rc 文件路径
|
|
||||||
:param force: 目标文件已存在时是否强制覆盖
|
|
||||||
:return: 安装结果字典
|
|
||||||
"""
|
|
||||||
resolved_shell = self.resolve_install_shell(shell)
|
|
||||||
shell_spec = self.resolve_completion_shell_spec(resolved_shell)
|
|
||||||
if not shell_spec.supported:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': f'{shell_spec.name} completion 当前版本未实现',
|
|
||||||
'shell': shell_spec.name,
|
|
||||||
'exit_code': ARGUMENT_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved_target_file = self.resolve_completion_target(shell_spec.name, target_file)
|
|
||||||
script_text = self.render_completion_script(root_cli, shell_spec.name)
|
|
||||||
existing_text = resolved_target_file.read_text(encoding='utf-8') if resolved_target_file.exists() else None
|
|
||||||
|
|
||||||
if existing_text is not None and existing_text != script_text and not force:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '目标文件已存在且内容不同,请传入 --force 覆盖',
|
|
||||||
'shell': shell_spec.name,
|
|
||||||
'targetFile': str(resolved_target_file),
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved_target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
resolved_target_file.write_text(script_text, encoding='utf-8')
|
|
||||||
|
|
||||||
source_command = self.build_source_command(resolved_target_file, shell_spec.name)
|
|
||||||
activated = shell_spec.auto_discovery
|
|
||||||
rc_file_path = self.resolve_completion_rc_file(shell_spec.name, rc_file)
|
|
||||||
rc_file_updated = False
|
|
||||||
activation_required = not shell_spec.auto_discovery
|
|
||||||
detected_shell = self.detect_active_shell() or None
|
|
||||||
|
|
||||||
if activate and rc_file_path is not None:
|
|
||||||
rc_file_updated = self.append_activation_line(rc_file_path, source_command)
|
|
||||||
activated = True
|
|
||||||
|
|
||||||
next_step = '当前 shell 会自动发现 completion 脚本,无需额外 source 命令'
|
|
||||||
if activation_required and not activate:
|
|
||||||
next_step = f'请执行 `{source_command}`,或重新运行并传入 --activate'
|
|
||||||
elif activation_required and activate:
|
|
||||||
next_step = '请重启当前 shell,或手动执行 rc 文件中的 source 命令使其立即生效'
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': 'completion 脚本已安装',
|
|
||||||
'shell': shell_spec.name,
|
|
||||||
'detectedShell': detected_shell,
|
|
||||||
'targetFile': str(resolved_target_file),
|
|
||||||
'activated': activated,
|
|
||||||
'activateRequested': activate,
|
|
||||||
'rcFile': str(rc_file_path) if rc_file_path is not None else None,
|
|
||||||
'rcFileUpdated': rc_file_updated,
|
|
||||||
'sourceCommand': source_command,
|
|
||||||
'autoDiscovery': shell_spec.auto_discovery,
|
|
||||||
'activationRequired': activation_required,
|
|
||||||
'nextStep': next_step,
|
|
||||||
'completeEnvVar': CLICK_COMPLETE_ENV_VAR,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
COMPLETION_INSTALLER = CompletionInstallerService()
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class CompletionCommandPresenter:
|
|
||||||
"""
|
|
||||||
completion 命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `completion` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_completion_doctor_text(self, payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将 completion 诊断结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: completion 诊断结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'active_shell: {payload.get("activeShell") or "-"}',
|
|
||||||
f'project_dir: {payload.get("projectDir", "-")}',
|
|
||||||
f'complete_env_var: {payload.get("completeEnvVar", "-")}',
|
|
||||||
]
|
|
||||||
if payload.get('recommendedInstallCommand'):
|
|
||||||
lines.append(f'recommended_install_command: {payload.get("recommendedInstallCommand")}')
|
|
||||||
env_choices = payload.get('envChoices')
|
|
||||||
if isinstance(env_choices, list) and env_choices:
|
|
||||||
lines.append('env_choices:')
|
|
||||||
lines.extend(f' - {env_name}' for env_name in env_choices)
|
|
||||||
shells = payload.get('shells')
|
|
||||||
if isinstance(shells, dict) and shells:
|
|
||||||
lines.append('shells:')
|
|
||||||
for shell_name, shell_payload in shells.items():
|
|
||||||
if not isinstance(shell_payload, dict):
|
|
||||||
continue
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
f' {shell_name}:',
|
|
||||||
f' supported: {str(shell_payload.get("supported", False)).lower()}',
|
|
||||||
f' detected: {str(shell_payload.get("detected", False)).lower()}',
|
|
||||||
f' target_file: {shell_payload.get("targetFile", "-")}',
|
|
||||||
f' target_file_exists: {str(shell_payload.get("targetFileExists", False)).lower()}',
|
|
||||||
f' rc_file: {shell_payload.get("rcFile", "-") or "-"}',
|
|
||||||
f' rc_file_exists: {str(shell_payload.get("rcFileExists", False)).lower()}',
|
|
||||||
f' auto_discovery: {str(shell_payload.get("autoDiscovery", False)).lower()}',
|
|
||||||
f' source_command: {shell_payload.get("sourceCommand", "-")}',
|
|
||||||
f' recommended_install_command: {shell_payload.get("recommendedInstallCommand", "-")}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_completion_install_text(payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将 completion 安装结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: completion 安装结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'shell: {payload.get("shell", "-")}',
|
|
||||||
]
|
|
||||||
field_label_mapping = {
|
|
||||||
'detectedShell': 'detected_shell',
|
|
||||||
'targetFile': 'target_file',
|
|
||||||
'activated': 'activated',
|
|
||||||
'activateRequested': 'activate_requested',
|
|
||||||
'rcFile': 'rc_file',
|
|
||||||
'rcFileUpdated': 'rc_file_updated',
|
|
||||||
'sourceCommand': 'source_command',
|
|
||||||
'autoDiscovery': 'auto_discovery',
|
|
||||||
'activationRequired': 'activation_required',
|
|
||||||
'nextStep': 'next_step',
|
|
||||||
'completeEnvVar': 'complete_env_var',
|
|
||||||
}
|
|
||||||
for field_name, field_label in field_label_mapping.items():
|
|
||||||
if field_name in payload:
|
|
||||||
lines.append(f'{field_label}: {payload.get(field_name)}')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from click.shell_completion import (
|
|
||||||
CompletionItem,
|
|
||||||
ShellComplete,
|
|
||||||
add_completion_class,
|
|
||||||
get_completion_class,
|
|
||||||
split_arg_string,
|
|
||||||
)
|
|
||||||
|
|
||||||
_SOURCE_POWERSHELL = """\
|
|
||||||
$%(complete_func)s = {
|
|
||||||
param($wordToComplete, $commandAst, $cursorPosition)
|
|
||||||
|
|
||||||
$previousCompWords = $env:COMP_WORDS
|
|
||||||
$previousCompCword = $env:COMP_CWORD
|
|
||||||
$previousCompleteInstruction = $env:%(complete_var)s
|
|
||||||
|
|
||||||
$commandLine = $commandAst.ToString()
|
|
||||||
if ($cursorPosition -lt $commandLine.Length) {
|
|
||||||
$commandLine = $commandLine.Substring(0, $cursorPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
$env:COMP_WORDS = $commandLine
|
|
||||||
$env:COMP_CWORD = $wordToComplete
|
|
||||||
$env:%(complete_var)s = "powershell_complete"
|
|
||||||
|
|
||||||
try {
|
|
||||||
%(prog_name)s | ForEach-Object {
|
|
||||||
$line = $_.ToString()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($line)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$parts = $line -split "`t", 3
|
|
||||||
$completionValue = if ($parts.Length -ge 2) { $parts[1] } else { "" }
|
|
||||||
$completionHelp = if ($parts.Length -ge 3 -and $parts[2]) { $parts[2] } else { $completionValue }
|
|
||||||
[System.Management.Automation.CompletionResult]::new(
|
|
||||||
$completionValue,
|
|
||||||
$completionValue,
|
|
||||||
[System.Management.Automation.CompletionResultType]::ParameterValue,
|
|
||||||
$completionHelp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if ($null -ne $previousCompWords) {
|
|
||||||
$env:COMP_WORDS = $previousCompWords
|
|
||||||
} else {
|
|
||||||
Remove-Item Env:\\COMP_WORDS -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if ($null -ne $previousCompCword) {
|
|
||||||
$env:COMP_CWORD = $previousCompCword
|
|
||||||
} else {
|
|
||||||
Remove-Item Env:\\COMP_CWORD -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
if ($null -ne $previousCompleteInstruction) {
|
|
||||||
$env:%(complete_var)s = $previousCompleteInstruction
|
|
||||||
} else {
|
|
||||||
Remove-Item Env:\\%(complete_var)s -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock $%(complete_func)s
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PowerShellComplete(ShellComplete):
|
|
||||||
"""
|
|
||||||
PowerShell shell completion 支持。
|
|
||||||
|
|
||||||
基于 PowerShell `Register-ArgumentCompleter -Native` 协议,将当前
|
|
||||||
命令行和光标位置传回 Click completion 分发器,再把返回的候选项转换为
|
|
||||||
`CompletionResult` 对象。
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = 'powershell'
|
|
||||||
source_template = _SOURCE_POWERSHELL
|
|
||||||
|
|
||||||
def get_completion_args(self) -> tuple[list[str], str]:
|
|
||||||
"""
|
|
||||||
从 PowerShell 注入的环境变量中恢复 CLI 上下文。
|
|
||||||
|
|
||||||
:return: 已解析的完整参数与当前不完整输入
|
|
||||||
"""
|
|
||||||
cwords = split_arg_string(os.environ.get('COMP_WORDS', ''))
|
|
||||||
incomplete = os.environ.get('COMP_CWORD', '')
|
|
||||||
if incomplete:
|
|
||||||
incomplete_parts = split_arg_string(incomplete)
|
|
||||||
incomplete = incomplete_parts[0] if incomplete_parts else incomplete
|
|
||||||
args = cwords[1:]
|
|
||||||
if incomplete and args and args[-1] == incomplete:
|
|
||||||
args.pop()
|
|
||||||
return args, incomplete
|
|
||||||
|
|
||||||
def format_completion(self, item: CompletionItem) -> str:
|
|
||||||
"""
|
|
||||||
将候选项格式化为 PowerShell 脚本可解析的文本行。
|
|
||||||
|
|
||||||
:param item: Click completion 候选项
|
|
||||||
:return: 格式化后的文本
|
|
||||||
"""
|
|
||||||
help_text = item.help or item.value
|
|
||||||
return f'{item.type}\t{item.value}\t{help_text}'
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_custom_completion_classes_registered() -> None:
|
|
||||||
"""
|
|
||||||
确保自定义 shell completion 类已注册到 Click。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if get_completion_class(PowerShellComplete.name) is None:
|
|
||||||
add_completion_class(PowerShellComplete)
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Annotated, Literal
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliContext:
|
|
||||||
"""
|
|
||||||
CLI 运行上下文。
|
|
||||||
|
|
||||||
env: 当前命令使用的环境名称
|
|
||||||
output: 结果输出格式
|
|
||||||
operator: 操作者标识,后续用于审计
|
|
||||||
allow_prod: 是否显式允许生产环境执行危险命令
|
|
||||||
yes: 是否跳过二次确认
|
|
||||||
dry_run: 是否执行演练模式
|
|
||||||
"""
|
|
||||||
|
|
||||||
env: str = 'dev'
|
|
||||||
output: Literal['text', 'json'] = 'text'
|
|
||||||
operator: str | None = None
|
|
||||||
allow_prod: bool = False
|
|
||||||
yes: bool = False
|
|
||||||
dry_run: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class CliContextBuilder:
|
|
||||||
"""
|
|
||||||
CLI 上下文构建器。
|
|
||||||
|
|
||||||
该对象负责将命令层显式参数收口为统一的 `CliContext` 数据对象,
|
|
||||||
供上下文工厂与测试场景复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build(
|
|
||||||
env: str,
|
|
||||||
output: Literal['text', 'json'],
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> CliContext:
|
|
||||||
"""
|
|
||||||
构建命令执行上下文。
|
|
||||||
|
|
||||||
:param env: 当前命令环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过二次确认
|
|
||||||
:param dry_run: 是否执行演练模式
|
|
||||||
:return: 构建完成的 CLI 上下文
|
|
||||||
"""
|
|
||||||
return CliContext(
|
|
||||||
env=env,
|
|
||||||
output=output,
|
|
||||||
allow_prod=allow_prod,
|
|
||||||
yes=yes,
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
CLI_CONTEXT_BUILDER = CliContextBuilder()
|
|
||||||
|
|
||||||
EnvOption = Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option('--env', help='运行环境名称', autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_env_values),
|
|
||||||
]
|
|
||||||
OutputOption = Annotated[Literal['text', 'json'], typer.Option('--output', help='输出格式,支持 text 或 json')]
|
|
||||||
AllowProdOption = Annotated[bool, typer.Option('--allow-prod', help='允许在生产环境中执行危险命令')]
|
|
||||||
YesOption = Annotated[bool, typer.Option('--yes', help='跳过危险命令确认')]
|
|
||||||
DryRunOption = Annotated[bool, typer.Option('--dry-run', help='仅执行预演,不落地实际变更')]
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
from cli.core.app_builder import CliApplicationBuilder, ProjectRuntimeLocator
|
|
||||||
from cli.core.completion_dispatcher import CompletionDispatcher
|
|
||||||
from cli.core.context_factory import CliContextFactory, CliRuntimeState
|
|
||||||
from cli.core.execution import CliExecutionService
|
|
||||||
from cli.output import OutputRenderer
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CoreServiceContainer:
|
|
||||||
"""
|
|
||||||
CLI 核心服务容器。
|
|
||||||
|
|
||||||
该容器负责统一装配输出渲染器、运行期状态、上下文工厂与执行服务,
|
|
||||||
作为控制器、向导和 TUI 等编排层的默认依赖入口。
|
|
||||||
|
|
||||||
:param output_renderer: 输出渲染器
|
|
||||||
:param runtime_state: CLI 运行期状态对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
output_renderer: OutputRenderer = field(default_factory=OutputRenderer)
|
|
||||||
runtime_state: CliRuntimeState = field(default_factory=CliRuntimeState)
|
|
||||||
context_factory: CliContextFactory = field(init=False)
|
|
||||||
execution_service: CliExecutionService = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
"""
|
|
||||||
初始化核心服务依赖图。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = CliContextFactory(
|
|
||||||
runtime_state=self.runtime_state,
|
|
||||||
output_renderer=self.output_renderer,
|
|
||||||
)
|
|
||||||
self.execution_service = CliExecutionService(output_renderer=self.output_renderer)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CORE_SERVICES = CoreServiceContainer()
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'DEFAULT_CORE_SERVICES',
|
|
||||||
'CliApplicationBuilder',
|
|
||||||
'CliContextFactory',
|
|
||||||
'CliExecutionService',
|
|
||||||
'CliRuntimeState',
|
|
||||||
'CompletionDispatcher',
|
|
||||||
'CoreServiceContainer',
|
|
||||||
'ProjectRuntimeLocator',
|
|
||||||
]
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import sys
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated, Literal
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.output import OutputRenderer
|
|
||||||
from cli.runtime.base import RUNTIME_ENVIRONMENT
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ProjectRuntimeLocator:
|
|
||||||
"""
|
|
||||||
CLI 项目运行时定位器。
|
|
||||||
|
|
||||||
该对象负责判断当前目录是否为后端项目根目录、注入 `sys.path`,
|
|
||||||
并为配置导入阶段提取最小化的 `argv` 参数。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def is_backend_project_dir(self, project_dir: Path) -> bool:
|
|
||||||
"""
|
|
||||||
判断给定目录是否为后端项目根目录。
|
|
||||||
|
|
||||||
:param project_dir: 待检查目录
|
|
||||||
:return: 是否为后端项目根目录
|
|
||||||
"""
|
|
||||||
return RUNTIME_ENVIRONMENT.is_backend_project_dir(project_dir)
|
|
||||||
|
|
||||||
def ensure_backend_dir_on_sys_path(self) -> Path:
|
|
||||||
"""
|
|
||||||
将当前后端项目根目录加入 `sys.path` 首位。
|
|
||||||
|
|
||||||
:return: 当前后端项目根目录
|
|
||||||
:raises typer.Exit: 当前目录不是后端项目根目录时退出命令
|
|
||||||
"""
|
|
||||||
project_dir = Path.cwd().resolve()
|
|
||||||
if not self.is_backend_project_dir(project_dir):
|
|
||||||
typer.echo('`shierfeng` 命令需在 `shierfeng-fastapi-backend` 目录下执行', err=True)
|
|
||||||
raise typer.Exit(code=2)
|
|
||||||
|
|
||||||
project_dir_str = str(project_dir)
|
|
||||||
if sys.path and sys.path[0] == project_dir_str:
|
|
||||||
return project_dir
|
|
||||||
|
|
||||||
sys.path.insert(0, project_dir_str)
|
|
||||||
return project_dir
|
|
||||||
|
|
||||||
def extract_import_argv(self, argv: list[str] | None = None) -> list[str]:
|
|
||||||
"""
|
|
||||||
提取供配置模块导入阶段使用的最小参数集。
|
|
||||||
|
|
||||||
:param argv: 原始命令行参数列表
|
|
||||||
:return: 仅保留程序名和 `--env` 的参数列表
|
|
||||||
"""
|
|
||||||
current_argv = argv or sys.argv
|
|
||||||
program_name = current_argv[0] if current_argv else 'shierfeng'
|
|
||||||
import_argv = [program_name]
|
|
||||||
env_value = ''
|
|
||||||
current_args = current_argv[1:]
|
|
||||||
for index, argument in enumerate(current_args):
|
|
||||||
if argument.startswith('--env='):
|
|
||||||
env_value = argument.split('=', 1)[1].strip()
|
|
||||||
break
|
|
||||||
if argument == '--env' and index + 1 < len(current_args):
|
|
||||||
env_value = current_args[index + 1].strip()
|
|
||||||
break
|
|
||||||
|
|
||||||
if env_value:
|
|
||||||
import_argv.extend(['--env', env_value])
|
|
||||||
return import_argv
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliCommandGroupRegistry:
|
|
||||||
"""
|
|
||||||
CLI 命令组注册表。
|
|
||||||
|
|
||||||
该注册表集中维护根 CLI 需要挂载的业务命令组模块路径,
|
|
||||||
避免命令组声明散落在应用构建器主体中。
|
|
||||||
|
|
||||||
:param command_modules: 命令组到模块路径的映射
|
|
||||||
"""
|
|
||||||
|
|
||||||
command_modules: dict[str, str]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliExtensionRegistration:
|
|
||||||
"""
|
|
||||||
CLI 扩展入口注册描述。
|
|
||||||
|
|
||||||
:param module_path: 扩展模块导入路径
|
|
||||||
:param attach: 已加载模块挂载到根 CLI 的函数
|
|
||||||
"""
|
|
||||||
|
|
||||||
module_path: str
|
|
||||||
attach: Callable[[typer.Typer, object], None]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliExtensionRegistry:
|
|
||||||
"""
|
|
||||||
CLI 扩展入口注册表。
|
|
||||||
|
|
||||||
该注册表集中维护 completion、wizard、TUI 等扩展入口的模块路径与
|
|
||||||
挂载方式,避免这些规则继续散落在注册器主体中。
|
|
||||||
|
|
||||||
:param registrations: 扩展注册描述列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
registrations: tuple[CliExtensionRegistration, ...]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliCommandGroupRegistrar:
|
|
||||||
"""
|
|
||||||
CLI 命令组注册器。
|
|
||||||
|
|
||||||
该对象负责根据命令组注册表向根 Typer 应用挂载业务命令组。
|
|
||||||
|
|
||||||
:param command_group_registry: CLI 命令组注册表
|
|
||||||
"""
|
|
||||||
|
|
||||||
command_group_registry: CliCommandGroupRegistry
|
|
||||||
module_loader: 'CliModuleLoader' = field(default_factory=lambda: DEFAULT_CLI_MODULE_LOADER)
|
|
||||||
|
|
||||||
def register(self, cli: typer.Typer) -> None:
|
|
||||||
"""
|
|
||||||
向根应用注册业务命令组。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
for command_name, module_path in self.command_group_registry.command_modules.items():
|
|
||||||
command_module = self.module_loader.load(module_path)
|
|
||||||
cli.add_typer(command_module.app, name=command_name)
|
|
||||||
|
|
||||||
|
|
||||||
class CliExtensionMountSupport:
|
|
||||||
"""
|
|
||||||
CLI 扩展入口挂载支持对象。
|
|
||||||
|
|
||||||
该对象负责封装不同扩展模块挂载到根 CLI 的差异,避免扩展注册器本体
|
|
||||||
继续堆积 `if/else` 或硬编码模块处理逻辑。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def attach_completion(cli: typer.Typer, module: object) -> None:
|
|
||||||
"""
|
|
||||||
挂载 completion 扩展入口。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:param module: 已加载模块
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
cli.add_typer(module.COMPLETION_COMMAND_BUILDER.build(cli), name='completion')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def attach_wizard(cli: typer.Typer, module: object) -> None:
|
|
||||||
"""
|
|
||||||
挂载 wizard 扩展入口。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:param module: 已加载模块
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
cli.add_typer(module.WIZARD_COMMAND_BUILDER.build(), name='wizard')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def attach_tui(cli: typer.Typer, module: object) -> None:
|
|
||||||
"""
|
|
||||||
挂载 TUI 扩展入口。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:param module: 已加载模块
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
module.TUI_COMMAND_REGISTRATION.register(cli)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliExtensionRegistrar:
|
|
||||||
"""
|
|
||||||
CLI 扩展入口注册器。
|
|
||||||
|
|
||||||
该对象负责向根 Typer 应用挂载 completion、wizard 与 TUI
|
|
||||||
等扩展入口。
|
|
||||||
"""
|
|
||||||
|
|
||||||
extension_registry: CliExtensionRegistry
|
|
||||||
module_loader: 'CliModuleLoader' = field(default_factory=lambda: DEFAULT_CLI_MODULE_LOADER)
|
|
||||||
|
|
||||||
def register(self, cli: typer.Typer) -> None:
|
|
||||||
"""
|
|
||||||
向根应用注册扩展入口。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
for registration in self.extension_registry.registrations:
|
|
||||||
extension_module = self.module_loader.load(registration.module_path)
|
|
||||||
registration.attach(cli, extension_module)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliRootOptionInitializer:
|
|
||||||
"""
|
|
||||||
CLI 根级选项初始化器。
|
|
||||||
|
|
||||||
该对象负责将根级 `--color`、`--icon` 选项映射到共享输出渲染器,
|
|
||||||
避免根回调继续内联输出配置逻辑。
|
|
||||||
|
|
||||||
:param output_renderer: 根命令共享的输出渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
output_renderer: OutputRenderer
|
|
||||||
|
|
||||||
def initialize(self, *, color: str, icon: str) -> None:
|
|
||||||
"""
|
|
||||||
初始化根级输出选项。
|
|
||||||
|
|
||||||
:param color: 文本输出颜色模式
|
|
||||||
:param icon: 文本输出图标模式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.output_renderer.set_color_mode(color)
|
|
||||||
self.output_renderer.set_icon_mode(icon)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliRootCallbackRegistrar:
|
|
||||||
"""
|
|
||||||
CLI 根回调注册器。
|
|
||||||
|
|
||||||
该对象负责把根级 `--color`、`--icon` 选项回调挂载到 Typer 根应用,
|
|
||||||
让 `CliApplicationBuilder` 更专注于整体装配流程。
|
|
||||||
|
|
||||||
:param root_option_initializer: 根级选项初始化器
|
|
||||||
"""
|
|
||||||
|
|
||||||
root_option_initializer: CliRootOptionInitializer
|
|
||||||
|
|
||||||
def register(self, cli: typer.Typer) -> None:
|
|
||||||
"""
|
|
||||||
向根应用注册回调。
|
|
||||||
|
|
||||||
:param cli: 根 Typer 应用
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
@cli.callback()
|
|
||||||
def root_callback(
|
|
||||||
color: Annotated[
|
|
||||||
Literal['auto', 'always', 'never'],
|
|
||||||
typer.Option('--color', help='文本输出颜色模式'),
|
|
||||||
] = 'always',
|
|
||||||
icon: Annotated[
|
|
||||||
Literal['emoji', 'ascii', 'none'],
|
|
||||||
typer.Option('--icon', help='文本输出图标模式'),
|
|
||||||
] = 'emoji',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化 CLI 根级运行参数。
|
|
||||||
|
|
||||||
:param color: 文本输出颜色模式
|
|
||||||
:param icon: 文本输出图标模式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.root_option_initializer.initialize(color=color, icon=icon)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliModuleLoader:
|
|
||||||
"""
|
|
||||||
CLI 模块加载服务。
|
|
||||||
|
|
||||||
该对象负责集中执行命令组与扩展入口所需的延迟导入,
|
|
||||||
让注册器本身更专注于挂载动作。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load(module_path: str) -> object:
|
|
||||||
"""
|
|
||||||
按模块路径加载模块对象。
|
|
||||||
|
|
||||||
:param module_path: 模块导入路径
|
|
||||||
:return: 已加载模块
|
|
||||||
"""
|
|
||||||
return importlib.import_module(module_path)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliApplicationBuilder:
|
|
||||||
"""
|
|
||||||
CLI 根应用构建器。
|
|
||||||
|
|
||||||
:param output_renderer: 根命令共享的输出渲染器
|
|
||||||
:param command_group_registrar: CLI 命令组注册器
|
|
||||||
:param extension_registrar: CLI 扩展入口注册器
|
|
||||||
"""
|
|
||||||
|
|
||||||
output_renderer: OutputRenderer
|
|
||||||
command_group_registrar: CliCommandGroupRegistrar = field(
|
|
||||||
default_factory=lambda: CliCommandGroupRegistrar(DEFAULT_COMMAND_GROUP_REGISTRY)
|
|
||||||
)
|
|
||||||
extension_registrar: CliExtensionRegistrar = field(
|
|
||||||
default_factory=lambda: CliExtensionRegistrar(DEFAULT_CLI_EXTENSION_REGISTRY)
|
|
||||||
)
|
|
||||||
root_option_initializer: CliRootOptionInitializer = field(init=False)
|
|
||||||
root_callback_registrar: CliRootCallbackRegistrar = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
"""
|
|
||||||
初始化根应用构建器依赖。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.root_option_initializer = CliRootOptionInitializer(self.output_renderer)
|
|
||||||
self.root_callback_registrar = CliRootCallbackRegistrar(self.root_option_initializer)
|
|
||||||
|
|
||||||
def build(self) -> typer.Typer:
|
|
||||||
"""
|
|
||||||
构建 Typer 根应用并注册全部命令组。
|
|
||||||
|
|
||||||
:return: Typer 根应用
|
|
||||||
"""
|
|
||||||
cli = typer.Typer(
|
|
||||||
name='shierfeng',
|
|
||||||
help='ShiErFeng FastAPI 后端统一命令入口',
|
|
||||||
no_args_is_help=True,
|
|
||||||
add_completion=False,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
self.root_callback_registrar.register(cli)
|
|
||||||
self.command_group_registrar.register(cli)
|
|
||||||
self.extension_registrar.register(cli)
|
|
||||||
return cli
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COMMAND_GROUP_REGISTRY = CliCommandGroupRegistry(
|
|
||||||
command_modules={
|
|
||||||
'app': 'cli.groups.app',
|
|
||||||
'db': 'cli.groups.db',
|
|
||||||
'ops': 'cli.groups.ops',
|
|
||||||
'cache': 'cli.groups.cache',
|
|
||||||
'job': 'cli.groups.job',
|
|
||||||
'config': 'cli.groups.config',
|
|
||||||
'crypto': 'cli.groups.crypto',
|
|
||||||
'gen': 'cli.groups.gen',
|
|
||||||
'dev': 'cli.groups.dev',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DEFAULT_CLI_EXTENSION_REGISTRY = CliExtensionRegistry(
|
|
||||||
registrations=(
|
|
||||||
CliExtensionRegistration('cli.completion.commands', CliExtensionMountSupport.attach_completion),
|
|
||||||
CliExtensionRegistration('cli.wizard.commands', CliExtensionMountSupport.attach_wizard),
|
|
||||||
CliExtensionRegistration('cli.tui', CliExtensionMountSupport.attach_tui),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
DEFAULT_CLI_MODULE_LOADER = CliModuleLoader()
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import os
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
import typer
|
|
||||||
from click.shell_completion import shell_complete as click_shell_complete
|
|
||||||
from typer.completion import shell_complete as typer_shell_complete
|
|
||||||
|
|
||||||
from cli.completion.shells import ensure_custom_completion_classes_registered
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionInstructionSupport:
|
|
||||||
"""
|
|
||||||
completion 指令解析支持对象。
|
|
||||||
|
|
||||||
该对象负责管理 completion 环境变量名、受支持 shell 列表,以及
|
|
||||||
Click / Typer 两种指令格式的判定逻辑。
|
|
||||||
|
|
||||||
:param complete_env_var: completion 环境变量名
|
|
||||||
:param supported_shells: 支持的 shell 名称列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
complete_env_var: str = '_SHIERFENG_COMPLETE'
|
|
||||||
supported_shells: tuple[str, ...] = ('bash', 'zsh', 'fish', 'powershell')
|
|
||||||
|
|
||||||
def is_click_style_instruction(self, instruction: str) -> bool:
|
|
||||||
"""
|
|
||||||
判断 completion 指令是否为 Click 原生格式。
|
|
||||||
|
|
||||||
Click 原生格式为 `<shell>_<instruction>`,例如 `bash_complete`。
|
|
||||||
|
|
||||||
:param instruction: completion 指令文本
|
|
||||||
:return: 是否为 Click 风格指令
|
|
||||||
"""
|
|
||||||
return any(instruction.startswith(f'{shell}_') for shell in self.supported_shells)
|
|
||||||
|
|
||||||
def is_typer_style_instruction(self, instruction: str) -> bool:
|
|
||||||
"""
|
|
||||||
判断 completion 指令是否为 Typer 兼容格式。
|
|
||||||
|
|
||||||
Typer 兼容格式为 `<instruction>_<shell>`,例如 `complete_bash`。
|
|
||||||
|
|
||||||
:param instruction: completion 指令文本
|
|
||||||
:return: 是否为 Typer 风格指令
|
|
||||||
"""
|
|
||||||
return any(instruction.endswith(f'_{shell}') for shell in self.supported_shells)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionInstructionReader:
|
|
||||||
"""
|
|
||||||
completion 指令读取器。
|
|
||||||
|
|
||||||
该对象负责从当前进程环境中读取并规范化 completion 指令。
|
|
||||||
|
|
||||||
:param support: completion 指令解析支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
support: CompletionInstructionSupport
|
|
||||||
|
|
||||||
def read_instruction(self) -> str:
|
|
||||||
"""
|
|
||||||
读取当前进程中的 completion 指令文本。
|
|
||||||
|
|
||||||
:return: 规范化后的 completion 指令文本
|
|
||||||
"""
|
|
||||||
return os.environ.get(self.support.complete_env_var, '').strip()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionDispatcher:
|
|
||||||
"""
|
|
||||||
shell completion 请求分发器。
|
|
||||||
|
|
||||||
该对象负责识别 Click 与 Typer 两种 completion 指令格式,
|
|
||||||
并在命中时直接执行补全流程后退出当前进程。
|
|
||||||
|
|
||||||
:param support: completion 指令解析支持对象
|
|
||||||
:param instruction_reader: completion 指令读取器
|
|
||||||
"""
|
|
||||||
|
|
||||||
support: CompletionInstructionSupport = field(default_factory=CompletionInstructionSupport)
|
|
||||||
instruction_reader: CompletionInstructionReader = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
"""
|
|
||||||
初始化 completion 指令读取器。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
object.__setattr__(self, 'instruction_reader', CompletionInstructionReader(self.support))
|
|
||||||
|
|
||||||
def dispatch(self, cli: typer.Typer) -> None:
|
|
||||||
"""
|
|
||||||
处理 shell completion 请求并在命中时直接退出。
|
|
||||||
|
|
||||||
这里同时兼容 Click 与 Typer 两种 completion 指令格式,避免
|
|
||||||
生成脚本格式和运行时解析格式不一致时出现 `Shell complete not supported.`。
|
|
||||||
|
|
||||||
:param cli: Typer 根应用
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ensure_custom_completion_classes_registered()
|
|
||||||
instruction = self.instruction_reader.read_instruction()
|
|
||||||
if not instruction:
|
|
||||||
return
|
|
||||||
|
|
||||||
click_command = typer.main.get_command(cli)
|
|
||||||
if self.support.is_click_style_instruction(instruction):
|
|
||||||
raise SystemExit(
|
|
||||||
click_shell_complete(click_command, {}, 'shierfeng', self.support.complete_env_var, instruction)
|
|
||||||
)
|
|
||||||
if self.support.is_typer_style_instruction(instruction):
|
|
||||||
raise SystemExit(
|
|
||||||
typer_shell_complete(click_command, {}, 'shierfeng', self.support.complete_env_var, instruction)
|
|
||||||
)
|
|
||||||
|
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COMPLETION_INSTRUCTION_SUPPORT = CompletionInstructionSupport()
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import logging
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.context import CLI_CONTEXT_BUILDER, CliContext, CliContextBuilder
|
|
||||||
from cli.guards import (
|
|
||||||
DEFAULT_DANGEROUS_COMMAND_GUARD,
|
|
||||||
DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY,
|
|
||||||
DangerousCommandGuardService,
|
|
||||||
DangerousCommandRuleRegistry,
|
|
||||||
)
|
|
||||||
from cli.output import OutputRenderer
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliRuntimeState:
|
|
||||||
"""
|
|
||||||
CLI 运行期状态。
|
|
||||||
|
|
||||||
:param logs_suppressed: 当前进程日志是否已切换为静默模式
|
|
||||||
"""
|
|
||||||
|
|
||||||
logs_suppressed: bool = False
|
|
||||||
sqlalchemy_logs_suppressed: bool = False
|
|
||||||
|
|
||||||
def get_logger(self) -> Any:
|
|
||||||
"""
|
|
||||||
获取 CLI 使用的日志对象。
|
|
||||||
|
|
||||||
:return: 日志对象
|
|
||||||
"""
|
|
||||||
return import_module('utils.log_util').logger
|
|
||||||
|
|
||||||
def suppress_logs(self) -> None:
|
|
||||||
"""
|
|
||||||
关闭 CLI 进程中的终端日志输出。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.logs_suppressed:
|
|
||||||
return
|
|
||||||
self.get_logger().remove()
|
|
||||||
self.logs_suppressed = True
|
|
||||||
|
|
||||||
def suppress_sqlalchemy_logs(self) -> None:
|
|
||||||
"""
|
|
||||||
关闭 CLI 进程中的 SQLAlchemy 终端 SQL 日志输出。
|
|
||||||
|
|
||||||
该逻辑同时处理两类来源:
|
|
||||||
|
|
||||||
1. 将 `config.env.DataBaseConfig.db_echo` 强制关闭,避免后续新建 Engine 时
|
|
||||||
继续打开 SQLAlchemy echo。
|
|
||||||
2. 将已知 SQLAlchemy logger 级别提升到 WARNING,避免已有 logger 配置把
|
|
||||||
`INFO sqlalchemy.engine.Engine ...` 继续打到标准输出。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if self.sqlalchemy_logs_suppressed:
|
|
||||||
return
|
|
||||||
env_module = import_module('config.env')
|
|
||||||
database_config = getattr(env_module, 'DataBaseConfig', None)
|
|
||||||
if database_config is not None and hasattr(database_config, 'db_echo'):
|
|
||||||
database_config.db_echo = False
|
|
||||||
for logger_name in (
|
|
||||||
'sqlalchemy',
|
|
||||||
'sqlalchemy.engine',
|
|
||||||
'sqlalchemy.engine.Engine',
|
|
||||||
'sqlalchemy.pool',
|
|
||||||
):
|
|
||||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
|
||||||
self.sqlalchemy_logs_suppressed = True
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliLogPolicy:
|
|
||||||
"""
|
|
||||||
CLI 日志策略服务。
|
|
||||||
|
|
||||||
该对象负责在上下文构建前应用统一的日志输出策略,
|
|
||||||
将“是否静默日志”这一策略从上下文工厂主体中拆出。
|
|
||||||
|
|
||||||
:param runtime_state: CLI 运行期状态对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
runtime_state: CliRuntimeState
|
|
||||||
|
|
||||||
def prepare_regular_command(self) -> None:
|
|
||||||
"""
|
|
||||||
为普通 CLI 命令应用默认日志策略。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_state.suppress_logs()
|
|
||||||
self.runtime_state.suppress_sqlalchemy_logs()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DangerousCommandContextSupport:
|
|
||||||
"""
|
|
||||||
危险命令上下文支持服务。
|
|
||||||
|
|
||||||
该对象负责危险命令规则查询、保护执行与拒绝结果收口,
|
|
||||||
让上下文工厂本体只保留装配职责。
|
|
||||||
|
|
||||||
:param dangerous_command_rule_registry: 危险命令规则注册表
|
|
||||||
:param dangerous_command_guard_service: 危险命令保护执行服务
|
|
||||||
:param output_renderer: 输出渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
dangerous_command_rule_registry: DangerousCommandRuleRegistry
|
|
||||||
dangerous_command_guard_service: DangerousCommandGuardService
|
|
||||||
output_renderer: OutputRenderer
|
|
||||||
|
|
||||||
def guard_context(self, ctx: CliContext, *, command_name: str) -> CliContext:
|
|
||||||
"""
|
|
||||||
对危险命令上下文执行统一保护。
|
|
||||||
|
|
||||||
:param ctx: 已构建的 CLI 上下文
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: 通过保护后的 CLI 上下文
|
|
||||||
"""
|
|
||||||
rule = self.dangerous_command_rule_registry.require_rule(command_name)
|
|
||||||
guard_result = self.dangerous_command_guard_service.guard(ctx, rule=rule)
|
|
||||||
if guard_result is not None:
|
|
||||||
self.output_renderer.complete_command(guard_result, ctx)
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliContextFactory:
|
|
||||||
"""
|
|
||||||
统一构建 CLI 命令上下文。
|
|
||||||
|
|
||||||
:param runtime_state: CLI 运行期状态对象
|
|
||||||
:param output_renderer: 输出渲染器
|
|
||||||
:param cli_context_builder: CLI 上下文构建器
|
|
||||||
:param dangerous_command_rule_registry: 危险命令规则注册表
|
|
||||||
:param dangerous_command_guard_service: 危险命令保护执行服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
runtime_state: CliRuntimeState = field(default_factory=CliRuntimeState)
|
|
||||||
output_renderer: OutputRenderer = field(default_factory=OutputRenderer)
|
|
||||||
cli_context_builder: CliContextBuilder = field(default_factory=lambda: CLI_CONTEXT_BUILDER)
|
|
||||||
dangerous_command_rule_registry: DangerousCommandRuleRegistry = field(
|
|
||||||
default_factory=lambda: DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY
|
|
||||||
)
|
|
||||||
dangerous_command_guard_service: DangerousCommandGuardService = field(
|
|
||||||
default_factory=lambda: DEFAULT_DANGEROUS_COMMAND_GUARD
|
|
||||||
)
|
|
||||||
_log_policy: CliLogPolicy | None = field(default=None, init=False, repr=False)
|
|
||||||
_dangerous_command_support: DangerousCommandContextSupport | None = field(
|
|
||||||
default=None,
|
|
||||||
init=False,
|
|
||||||
repr=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_log_policy(self) -> CliLogPolicy:
|
|
||||||
"""
|
|
||||||
获取当前上下文工厂使用的日志策略服务。
|
|
||||||
|
|
||||||
:return: CLI 日志策略服务
|
|
||||||
"""
|
|
||||||
if self._log_policy is None:
|
|
||||||
self._log_policy = CliLogPolicy(runtime_state=self.runtime_state)
|
|
||||||
return self._log_policy
|
|
||||||
|
|
||||||
def get_dangerous_command_support(self) -> DangerousCommandContextSupport:
|
|
||||||
"""
|
|
||||||
获取当前上下文工厂使用的危险命令上下文支持服务。
|
|
||||||
|
|
||||||
:return: 危险命令上下文支持服务
|
|
||||||
"""
|
|
||||||
if self._dangerous_command_support is None:
|
|
||||||
self._dangerous_command_support = DangerousCommandContextSupport(
|
|
||||||
dangerous_command_rule_registry=self.dangerous_command_rule_registry,
|
|
||||||
dangerous_command_guard_service=self.dangerous_command_guard_service,
|
|
||||||
output_renderer=self.output_renderer,
|
|
||||||
)
|
|
||||||
return self._dangerous_command_support
|
|
||||||
|
|
||||||
def build_regular(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> CliContext:
|
|
||||||
"""
|
|
||||||
构建普通命令上下文。
|
|
||||||
|
|
||||||
:param env: 运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:return: CLI 上下文
|
|
||||||
"""
|
|
||||||
self.get_log_policy().prepare_regular_command()
|
|
||||||
return self.cli_context_builder.build(env, output, allow_prod, yes, dry_run)
|
|
||||||
|
|
||||||
def build_readonly(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
) -> CliContext:
|
|
||||||
"""
|
|
||||||
构建只读命令上下文。
|
|
||||||
|
|
||||||
:param env: 运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: CLI 上下文
|
|
||||||
"""
|
|
||||||
return self.build_regular(env, output, False, False, False)
|
|
||||||
|
|
||||||
def build_dangerous(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
command_name: str,
|
|
||||||
) -> CliContext:
|
|
||||||
"""
|
|
||||||
构建危险命令上下文并执行统一保护。
|
|
||||||
|
|
||||||
:param env: 运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: CLI 上下文
|
|
||||||
"""
|
|
||||||
ctx = self.build_regular(env, output, allow_prod, yes, dry_run)
|
|
||||||
return self.get_dangerous_command_support().guard_context(ctx, command_name=command_name)
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.context import CliContext
|
|
||||||
from cli.exit_codes import RUNTIME_ERROR
|
|
||||||
from cli.output import CommandResult, OutputRenderer
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CliExecutionService:
|
|
||||||
"""
|
|
||||||
CLI 执行与结果收口服务。
|
|
||||||
|
|
||||||
:param output_renderer: 输出渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
output_renderer: OutputRenderer = field(default_factory=OutputRenderer)
|
|
||||||
|
|
||||||
def run_async(self, coroutine: Any) -> Any:
|
|
||||||
"""
|
|
||||||
执行异步协程并返回结果。
|
|
||||||
|
|
||||||
:param coroutine: 待执行协程
|
|
||||||
:return: 协程执行结果
|
|
||||||
"""
|
|
||||||
return asyncio.run(coroutine)
|
|
||||||
|
|
||||||
def complete_payload(
|
|
||||||
self,
|
|
||||||
ctx: CliContext,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
*,
|
|
||||||
default_exit_code: int = 0,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出标准字典负载并结束命令。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param payload: 标准结果字典
|
|
||||||
:param default_exit_code: 默认退出码
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.output_renderer.complete_command(self.build_result(payload, default_exit_code=default_exit_code), ctx)
|
|
||||||
|
|
||||||
def complete_result(
|
|
||||||
self,
|
|
||||||
ctx: CliContext,
|
|
||||||
result: CommandResult,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出命令结果对象并结束命令。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param result: 命令执行结果对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.output_renderer.complete_command(result, ctx)
|
|
||||||
|
|
||||||
def complete_payload_with_text(
|
|
||||||
self,
|
|
||||||
ctx: CliContext,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
*,
|
|
||||||
text_builder: Callable[[dict[str, Any]], Any],
|
|
||||||
default_exit_code: int = 0,
|
|
||||||
text_condition: Callable[[dict[str, Any]], bool] | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
按输出格式收口标准负载,必要时先转换为文本结果。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param payload: 标准结果字典
|
|
||||||
:param text_builder: 文本结果构建函数
|
|
||||||
:param default_exit_code: 默认退出码
|
|
||||||
:param text_condition: 文本模式下是否应用构建函数的判定函数
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if ctx.output != 'text':
|
|
||||||
self.complete_payload(ctx, payload, default_exit_code=default_exit_code)
|
|
||||||
return
|
|
||||||
|
|
||||||
result = self.build_result(payload, default_exit_code=default_exit_code)
|
|
||||||
should_build_text = True
|
|
||||||
if callable(text_condition) and isinstance(result.data, dict):
|
|
||||||
should_build_text = bool(text_condition(result.data))
|
|
||||||
if should_build_text and isinstance(result.data, dict):
|
|
||||||
result.data = text_builder(result.data)
|
|
||||||
self.complete_result(ctx, result)
|
|
||||||
|
|
||||||
def complete_payload_result(
|
|
||||||
self,
|
|
||||||
ctx: CliContext,
|
|
||||||
payload: dict[str, Any],
|
|
||||||
*,
|
|
||||||
text_builder: Callable[[dict[str, Any]], Any] | None = None,
|
|
||||||
default_exit_code: int = 0,
|
|
||||||
text_condition: Callable[[dict[str, Any]], bool] | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
统一收口标准负载,按输出格式决定是否渲染为文本结果。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param payload: 标准结果字典
|
|
||||||
:param text_builder: 文本结果构建函数
|
|
||||||
:param default_exit_code: 默认退出码
|
|
||||||
:param text_condition: 文本模式下是否应用构建函数的判定函数
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if text_builder is None:
|
|
||||||
self.complete_payload(ctx, payload, default_exit_code=default_exit_code)
|
|
||||||
return
|
|
||||||
self.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=text_builder,
|
|
||||||
default_exit_code=default_exit_code,
|
|
||||||
text_condition=text_condition,
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_result(payload: dict[str, Any], *, default_exit_code: int = 0) -> CommandResult:
|
|
||||||
"""
|
|
||||||
将标准字典负载转换为命令结果对象。
|
|
||||||
|
|
||||||
:param payload: 标准结果字典
|
|
||||||
:param default_exit_code: 默认退出码;当 payload 未显式提供 exit_code 且
|
|
||||||
该值仍为 0 时,失败结果会统一回退到 `RUNTIME_ERROR`
|
|
||||||
:return: 命令结果对象
|
|
||||||
"""
|
|
||||||
result_payload = dict(payload)
|
|
||||||
exit_code = result_payload.pop('exit_code', None)
|
|
||||||
if exit_code is None:
|
|
||||||
is_ok = bool(result_payload.get('ok', True))
|
|
||||||
exit_code = default_exit_code if is_ok or default_exit_code != 0 else RUNTIME_ERROR
|
|
||||||
return CommandResult(data=result_payload, exit_code=exit_code)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
SUCCESS = 0
|
|
||||||
ARGUMENT_ERROR = 2
|
|
||||||
DEPENDENCY_ERROR = 10
|
|
||||||
DATABASE_ERROR = 20
|
|
||||||
REDIS_ERROR = 21
|
|
||||||
SCHEDULER_ERROR = 22
|
|
||||||
GUARD_REJECTED = 30
|
|
||||||
RUNTIME_ERROR = 50
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
from typing import Annotated, Literal
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import EnvOption, OutputOption
|
|
||||||
|
|
||||||
from .controller import AppCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='应用相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_APP_COMMAND_CONTROLLER = AppCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('run', help='启动当前 FastAPI 应用')
|
|
||||||
def run_app(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
启动当前 FastAPI 应用。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_APP_COMMAND_CONTROLLER.run_app(env)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('doctor', help='执行应用启动前检查')
|
|
||||||
def doctor(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行应用启动前检查。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_APP_COMMAND_CONTROLLER.doctor(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('config', help='查看当前应用配置快照')
|
|
||||||
def app_config(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出当前应用配置快照。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_APP_COMMAND_CONTROLLER.show_config(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('env', help='查看当前 CLI 解析到的应用环境信息')
|
|
||||||
def app_env(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出当前 CLI 解析到的应用环境信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_APP_COMMAND_CONTROLLER.show_env(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('routes', help='查看当前应用注册路由清单')
|
|
||||||
def routes(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
path_prefix: Annotated[str, typer.Option('--path-prefix', help='按路由前缀过滤')] = '',
|
|
||||||
method: Annotated[str, typer.Option('--method', help='按请求方法过滤,如 GET、POST')] = '',
|
|
||||||
group_by: Annotated[Literal['none', 'tag'], typer.Option('--group-by', help='路由分组方式')] = 'none',
|
|
||||||
include_hidden: Annotated[bool, typer.Option('--include-hidden', help='包含未出现在 OpenAPI 中的路由')] = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出当前应用注册路由清单。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param path_prefix: 路由前缀过滤条件
|
|
||||||
:param method: 请求方法过滤条件
|
|
||||||
:param group_by: 路由分组方式
|
|
||||||
:param include_hidden: 是否包含隐藏路由
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_APP_COMMAND_CONTROLLER.show_routes(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
path_prefix=path_prefix,
|
|
||||||
method=method,
|
|
||||||
group_by=group_by,
|
|
||||||
include_hidden=include_hidden,
|
|
||||||
)
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
from collections import defaultdict
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from fastapi.routing import APIRoute
|
|
||||||
|
|
||||||
from cli.bootstrap import APP_BOOTSTRAP, AppBootstrapService
|
|
||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS
|
|
||||||
from cli.runtime.app import APP_RUNTIME, AppRuntimeService
|
|
||||||
from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService
|
|
||||||
from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService
|
|
||||||
from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService
|
|
||||||
|
|
||||||
from .presenter import AppCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class AppCommandController:
|
|
||||||
"""
|
|
||||||
应用命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `app` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 构建,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: AppCommandPresenter | None = None,
|
|
||||||
runtime_service: AppRuntimeService | None = None,
|
|
||||||
database_runtime: DatabaseRuntimeService | None = None,
|
|
||||||
operations_runtime: OperationsRuntimeService | None = None,
|
|
||||||
crypto_runtime: CryptoRuntimeService | None = None,
|
|
||||||
bootstrap_service: AppBootstrapService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化应用命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 应用命令文本渲染器
|
|
||||||
:param runtime_service: 应用运行时服务
|
|
||||||
:param database_runtime: 数据库运行时服务
|
|
||||||
:param operations_runtime: 运维运行时服务
|
|
||||||
:param crypto_runtime: 传输加密运行时服务
|
|
||||||
:param bootstrap_service: 应用引导服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or AppCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or APP_RUNTIME
|
|
||||||
self.database_runtime = database_runtime or DATABASE_RUNTIME
|
|
||||||
self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME
|
|
||||||
self.crypto_runtime = crypto_runtime or CRYPTO_RUNTIME
|
|
||||||
self.bootstrap_service = bootstrap_service or APP_BOOTSTRAP
|
|
||||||
|
|
||||||
def run_app(self, env: str) -> None:
|
|
||||||
"""
|
|
||||||
启动当前 FastAPI 应用。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.bootstrap_service.exec_app_run_command(env)
|
|
||||||
|
|
||||||
def doctor(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
执行应用启动前检查。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
db_status = self.execution_service.run_async(self.database_runtime.ping_database())
|
|
||||||
redis_status = self.execution_service.run_async(self.operations_runtime.ping_redis())
|
|
||||||
crypto_status = self.crypto_runtime.validate_crypto_config()
|
|
||||||
payload = {
|
|
||||||
'env': ctx.env,
|
|
||||||
'database': db_status,
|
|
||||||
'redis': redis_status,
|
|
||||||
'crypto': crypto_status,
|
|
||||||
}
|
|
||||||
payload['ok'] = all(item.get('ok', False) for item in (db_status, redis_status, crypto_status))
|
|
||||||
exit_code = SUCCESS if payload['ok'] else DEPENDENCY_ERROR
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_doctor_text,
|
|
||||||
default_exit_code=exit_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_config(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
输出当前应用配置快照。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload_result(
|
|
||||||
ctx,
|
|
||||||
{'ok': True, 'env': ctx.env, 'config': self.runtime_service.get_app_config_snapshot()},
|
|
||||||
text_builder=self.presenter.build_app_config_text,
|
|
||||||
default_exit_code=SUCCESS,
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_env(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
输出当前 CLI 解析到的应用环境信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload_result(
|
|
||||||
ctx,
|
|
||||||
{'ok': True, 'env': ctx.env, 'runtime': self.runtime_service.get_app_env_snapshot()},
|
|
||||||
text_builder=self.presenter.build_app_env_text,
|
|
||||||
default_exit_code=SUCCESS,
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_routes(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
path_prefix: str,
|
|
||||||
method: str,
|
|
||||||
group_by: str,
|
|
||||||
include_hidden: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出当前应用注册路由清单。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param path_prefix: 路由前缀过滤条件
|
|
||||||
:param method: 请求方法过滤条件
|
|
||||||
:param group_by: 路由分组方式
|
|
||||||
:param include_hidden: 是否包含隐藏路由
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
routes_payload = self._serialize_routes(
|
|
||||||
self.runtime_service.build_app_instance(),
|
|
||||||
path_prefix=path_prefix,
|
|
||||||
method=method,
|
|
||||||
include_hidden=include_hidden,
|
|
||||||
)
|
|
||||||
grouped_routes = self._group_routes_by_tag(routes_payload) if group_by == 'tag' else None
|
|
||||||
payload = {
|
|
||||||
'ok': True,
|
|
||||||
'env': ctx.env,
|
|
||||||
'count': len(routes_payload),
|
|
||||||
'filters': {
|
|
||||||
'pathPrefix': path_prefix,
|
|
||||||
'method': method.upper().strip(),
|
|
||||||
'groupBy': group_by,
|
|
||||||
'includeHidden': include_hidden,
|
|
||||||
},
|
|
||||||
'routes': routes_payload,
|
|
||||||
'groupedRoutes': grouped_routes,
|
|
||||||
}
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_routes_text,
|
|
||||||
default_exit_code=SUCCESS,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _serialize_routes(
|
|
||||||
app_instance: Any,
|
|
||||||
*,
|
|
||||||
path_prefix: str = '',
|
|
||||||
method: str = '',
|
|
||||||
include_hidden: bool = False,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
序列化 FastAPI 路由信息。
|
|
||||||
|
|
||||||
:param app_instance: FastAPI 应用实例
|
|
||||||
:param path_prefix: 路径前缀过滤条件
|
|
||||||
:param method: 请求方法过滤条件
|
|
||||||
:param include_hidden: 是否包含未出现在 OpenAPI 中的路由
|
|
||||||
:return: 序列化后的路由列表
|
|
||||||
"""
|
|
||||||
normalized_method = method.upper().strip()
|
|
||||||
routes = []
|
|
||||||
for route in app_instance.routes:
|
|
||||||
if not isinstance(route, APIRoute):
|
|
||||||
continue
|
|
||||||
if not include_hidden and not route.include_in_schema:
|
|
||||||
continue
|
|
||||||
if path_prefix and not route.path.startswith(path_prefix):
|
|
||||||
continue
|
|
||||||
route_methods = sorted(item for item in route.methods if item not in {'HEAD', 'OPTIONS'})
|
|
||||||
if normalized_method and normalized_method not in route_methods:
|
|
||||||
continue
|
|
||||||
routes.append(
|
|
||||||
{
|
|
||||||
'path': route.path,
|
|
||||||
'methods': route_methods,
|
|
||||||
'name': route.name,
|
|
||||||
'summary': route.summary or '',
|
|
||||||
'operationId': route.operation_id or '',
|
|
||||||
'tags': route.tags or [],
|
|
||||||
'includeInSchema': route.include_in_schema,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return sorted(routes, key=lambda item: (item['path'], ','.join(item['methods'])))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _group_routes_by_tag(routes_payload: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
|
|
||||||
"""
|
|
||||||
按标签对路由信息分组。
|
|
||||||
|
|
||||||
:param routes_payload: 原始路由列表
|
|
||||||
:return: 按标签分组后的路由映射
|
|
||||||
"""
|
|
||||||
grouped_routes: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
||||||
for route in routes_payload:
|
|
||||||
tags = route.get('tags') or ['__untagged__']
|
|
||||||
for tag in tags:
|
|
||||||
grouped_routes[tag].append(route)
|
|
||||||
return dict(sorted(grouped_routes.items(), key=lambda item: item[0]))
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class AppCommandPresenter:
|
|
||||||
"""
|
|
||||||
应用命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `app` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_routes_text(self, payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将路由列表结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 路由列表结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'count: {payload.get("count", 0)}',
|
|
||||||
]
|
|
||||||
filters = payload.get('filters')
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
lines.extend(self._build_route_filter_lines(filters))
|
|
||||||
|
|
||||||
grouped_routes = payload.get('groupedRoutes')
|
|
||||||
if filters and filters.get('groupBy') == 'tag' and isinstance(grouped_routes, dict):
|
|
||||||
lines.extend(self._build_grouped_routes_text(grouped_routes))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
routes = payload.get('routes')
|
|
||||||
if not isinstance(routes, list) or not routes:
|
|
||||||
lines.append('routes: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('routes:')
|
|
||||||
for route in routes:
|
|
||||||
if isinstance(route, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_route_item_lines(route)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_app_config_text(self, payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将应用配置快照渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 应用配置结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
config = payload.get('config')
|
|
||||||
if not isinstance(config, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'config: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'application:',
|
|
||||||
f' name: {config.get("name", "-")}',
|
|
||||||
f' host: {config.get("host", "-")}:{config.get("port", "-")}',
|
|
||||||
f' root_path: {config.get("rootPath", "-") or "/"}',
|
|
||||||
f' reload: {str(config.get("reload", False)).lower()}',
|
|
||||||
f' workers: {config.get("workers", "-")}',
|
|
||||||
f' disable_swagger: {str(config.get("disableSwagger", False)).lower()}',
|
|
||||||
f' disable_redoc: {str(config.get("disableRedoc", False)).lower()}',
|
|
||||||
'database:',
|
|
||||||
f' type: {config.get("dbType", "-")}',
|
|
||||||
f' host: {config.get("dbHost", "-")}:{config.get("dbPort", "-")}',
|
|
||||||
f' name: {config.get("dbDatabase", "-")}',
|
|
||||||
'redis:',
|
|
||||||
f' host: {config.get("redisHost", "-")}:{config.get("redisPort", "-")}',
|
|
||||||
'logging:',
|
|
||||||
f' level: {config.get("logLevel", "-")}',
|
|
||||||
'transport_crypto:',
|
|
||||||
f' enabled: {str(config.get("transportCryptoEnabled", False)).lower()}',
|
|
||||||
f' mode: {config.get("transportCryptoMode", "-")}',
|
|
||||||
]
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_app_env_text(self, payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将应用环境解析结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 应用环境结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
runtime = payload.get('runtime')
|
|
||||||
if not isinstance(runtime, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'runtime: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'runtime:',
|
|
||||||
f' cli_env: {runtime.get("cliEnv", "-")}',
|
|
||||||
f' config_env: {runtime.get("configEnv", "-")}',
|
|
||||||
f' app_env: {runtime.get("appEnv", "-")}',
|
|
||||||
f' env_file: {runtime.get("envFile", "-")}',
|
|
||||||
f' env_file_exists: {str(runtime.get("envFileExists", False)).lower()}',
|
|
||||||
f' backend_dir: {runtime.get("backendDir", "-")}',
|
|
||||||
f' python_executable: {runtime.get("pythonExecutable", "-")}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_doctor_text(self, payload: dict[str, Any]) -> str:
|
|
||||||
"""
|
|
||||||
将应用启动前检查结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 启动前检查结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'checks:',
|
|
||||||
self._build_check_status_line('database', payload.get('database')),
|
|
||||||
self._build_check_status_line('redis', payload.get('redis')),
|
|
||||||
self._build_check_status_line('crypto', payload.get('crypto')),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_route_filter_lines(filters: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建路由过滤条件文本行。
|
|
||||||
|
|
||||||
:param filters: 过滤条件字典
|
|
||||||
:return: 过滤条件文本行列表
|
|
||||||
"""
|
|
||||||
active_filters = []
|
|
||||||
for key, value in filters.items():
|
|
||||||
if value in (None, '', False, 'none'):
|
|
||||||
continue
|
|
||||||
active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}')
|
|
||||||
if not active_filters:
|
|
||||||
return ['filters: none']
|
|
||||||
return ['filters:', *[f' - {item}' for item in active_filters]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_route_item_lines(route_item: dict[str, Any]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条路由记录的文本行。
|
|
||||||
|
|
||||||
:param route_item: 单条路由记录
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
methods = ','.join(route_item.get('methods', [])) or '-'
|
|
||||||
path = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('path', ''), 72)
|
|
||||||
tags = ','.join(route_item.get('tags', [])) or '-'
|
|
||||||
summary = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('summary', ''), 60)
|
|
||||||
name = SHELL_TEXT_FORMATTER.truncate_text(route_item.get('name', ''), 40)
|
|
||||||
return [
|
|
||||||
f'- [{methods}] {path}',
|
|
||||||
f' name: {name or "-"} | tags: {tags}',
|
|
||||||
f' summary: {summary or "-"}',
|
|
||||||
]
|
|
||||||
|
|
||||||
def _build_grouped_routes_text(self, grouped_routes: dict[str, Any]) -> list[str]:
|
|
||||||
"""
|
|
||||||
将按标签分组后的路由结果渲染为文本行。
|
|
||||||
|
|
||||||
:param grouped_routes: 按标签分组后的路由映射
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
if not grouped_routes:
|
|
||||||
return ['groups: none']
|
|
||||||
|
|
||||||
lines = [f'groups: {len(grouped_routes)}']
|
|
||||||
for tag, routes in grouped_routes.items():
|
|
||||||
route_items = routes if isinstance(routes, list) else []
|
|
||||||
lines.append(f' {tag}: {len(route_items)}')
|
|
||||||
for route in route_items:
|
|
||||||
if isinstance(route, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_route_item_lines(route)])
|
|
||||||
return lines
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_check_status_line(name: str, status_payload: dict[str, Any] | None) -> str:
|
|
||||||
"""
|
|
||||||
构建单项检查结果摘要行。
|
|
||||||
|
|
||||||
:param name: 检查项名称
|
|
||||||
:param status_payload: 检查结果字典
|
|
||||||
:return: 摘要行文本
|
|
||||||
"""
|
|
||||||
if not isinstance(status_payload, dict):
|
|
||||||
return f' {name}: unknown'
|
|
||||||
|
|
||||||
ok = str(status_payload.get('ok', False)).lower()
|
|
||||||
message = status_payload.get('message', '-') or '-'
|
|
||||||
error = status_payload.get('error')
|
|
||||||
if error:
|
|
||||||
return f' {name}: {ok} | {message} | error: {SHELL_TEXT_FORMATTER.truncate_text(error, 120)}'
|
|
||||||
return f' {name}: {ok} | {message}'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import CacheCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='缓存相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_CACHE_COMMAND_CONTROLLER = CacheCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('stats', help='查看缓存统计信息')
|
|
||||||
def stats(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看缓存统计信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.stats(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('keys', help='查看指定缓存名称下的键名列表')
|
|
||||||
def keys(
|
|
||||||
cache_name: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='缓存名称',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存名称下的键名列表。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.keys(cache_name, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('get', help='查看指定缓存内容')
|
|
||||||
def get(
|
|
||||||
cache_name: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='缓存名称',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names,
|
|
||||||
),
|
|
||||||
cache_key: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='缓存键名',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_keys,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存内容。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.get(cache_name, cache_key, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('clear', help='执行缓存清理')
|
|
||||||
def clear(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
cache_name: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--cache-name',
|
|
||||||
help='按缓存名称前缀清理',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names,
|
|
||||||
),
|
|
||||||
cache_key: str = typer.Option('', '--cache-key', help='按缓存键名模糊清理'),
|
|
||||||
clear_all: bool = typer.Option(False, '--all', help='清理全部缓存并重建系统基础缓存'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
清理缓存。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param cache_name: 缓存名称前缀
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param clear_all: 是否清理全部缓存
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.clear(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
cache_name=cache_name,
|
|
||||||
cache_key=cache_key,
|
|
||||||
clear_all=clear_all,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('ttl', help='查看指定缓存键的剩余过期时间')
|
|
||||||
def ttl(
|
|
||||||
cache_name: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='缓存名称',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_names,
|
|
||||||
),
|
|
||||||
cache_key: str = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='缓存键名',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_cache_keys,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存键的剩余过期时间。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.ttl(cache_name, cache_key, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('warmup', help='执行系统缓存预热')
|
|
||||||
def warmup(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行系统缓存预热。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CACHE_COMMAND_CONTROLLER.warmup(env, output, allow_prod, yes)
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.cache import CACHE_RUNTIME, CacheRuntimeService
|
|
||||||
|
|
||||||
from .presenter import CacheCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class CacheCommandController:
|
|
||||||
"""
|
|
||||||
缓存命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `cache` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 缓存命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: CacheCommandPresenter | None = None,
|
|
||||||
runtime_service: CacheRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化缓存命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 缓存命令文本渲染器
|
|
||||||
:param runtime_service: 缓存运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or CacheCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or CACHE_RUNTIME
|
|
||||||
|
|
||||||
def stats(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看缓存统计信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.get_cache_stats())
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_cache_stats_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def keys(self, cache_name: str, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存名称下的键名列表。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.list_cache_keys(cache_name))
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_cache_keys_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get(self, cache_name: str, cache_key: str, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存内容。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.get_cache_value(cache_name, cache_key))
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_cache_value_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def clear(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
cache_name: str,
|
|
||||||
cache_key: str,
|
|
||||||
clear_all: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
清理缓存。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param cache_name: 缓存名称前缀
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param clear_all: 是否清理全部缓存
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='cache clear',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx,
|
|
||||||
self.execution_service.run_async(
|
|
||||||
self.runtime_service.clear_cache(
|
|
||||||
cache_name=cache_name,
|
|
||||||
cache_key=cache_key,
|
|
||||||
clear_all=clear_all,
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def ttl(self, cache_name: str, cache_key: str, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看指定缓存键的剩余过期时间。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.get_cache_ttl(cache_name, cache_key))
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_cache_ttl_text,
|
|
||||||
text_condition=lambda result_data: 'error' not in result_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def warmup(self, env: str, output: str, allow_prod: bool, yes: bool) -> None:
|
|
||||||
"""
|
|
||||||
执行系统缓存预热。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
False,
|
|
||||||
command_name='cache warmup',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.runtime_service.warmup_cache())
|
|
||||||
)
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class CacheCommandPresenter:
|
|
||||||
"""
|
|
||||||
缓存命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `cache` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_cache_stats_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将缓存统计结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 缓存统计结果
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
info = payload.get('info')
|
|
||||||
command_stats = payload.get('commandStats')
|
|
||||||
cache_names = payload.get('cacheNames')
|
|
||||||
|
|
||||||
redis_version = '-'
|
|
||||||
connected_clients = '-'
|
|
||||||
used_memory = '-'
|
|
||||||
uptime_seconds = '-'
|
|
||||||
keyspace_hits = '-'
|
|
||||||
keyspace_misses = '-'
|
|
||||||
if isinstance(info, dict):
|
|
||||||
redis_version = info.get('redis_version', '-')
|
|
||||||
connected_clients = info.get('connected_clients', '-')
|
|
||||||
used_memory = info.get('used_memory_human', info.get('used_memory', '-'))
|
|
||||||
uptime_seconds = info.get('uptime_in_seconds', '-')
|
|
||||||
keyspace_hits = info.get('keyspace_hits', '-')
|
|
||||||
keyspace_misses = info.get('keyspace_misses', '-')
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'db_size: {payload.get("dbSize", 0)}',
|
|
||||||
'redis:',
|
|
||||||
f' version: {redis_version}',
|
|
||||||
f' connected_clients: {connected_clients}',
|
|
||||||
f' used_memory: {used_memory}',
|
|
||||||
f' uptime_seconds: {uptime_seconds}',
|
|
||||||
f' keyspace_hits: {keyspace_hits}',
|
|
||||||
f' keyspace_misses: {keyspace_misses}',
|
|
||||||
]
|
|
||||||
|
|
||||||
if isinstance(command_stats, list) and command_stats:
|
|
||||||
lines.append('command_stats_top10:')
|
|
||||||
lines.extend(
|
|
||||||
f' - {item.get("name", "-")}: {item.get("value", 0)}'
|
|
||||||
for item in command_stats[:10]
|
|
||||||
if isinstance(item, dict)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append('command_stats_top10: none')
|
|
||||||
|
|
||||||
if isinstance(cache_names, list) and cache_names:
|
|
||||||
lines.append(f'cache_names: {len(cache_names)}')
|
|
||||||
lines.append('cache_name_samples:')
|
|
||||||
lines.extend(
|
|
||||||
f' - {item.get("cacheName", "-")}: {self._build_cache_name_remark(item.get("remark", ""))}'
|
|
||||||
for item in cache_names[:10]
|
|
||||||
if isinstance(item, dict)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
lines.append('cache_names: 0')
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_cache_keys_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将缓存键列表结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 缓存键列表结果
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
keys = payload.get('keys')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'cache_name: {payload.get("cacheName", "")}',
|
|
||||||
f'count: {payload.get("count", 0)}',
|
|
||||||
]
|
|
||||||
if not isinstance(keys, list) or not keys:
|
|
||||||
lines.append('keys: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('keys:')
|
|
||||||
lines.extend(f' - {key}' for key in keys)
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_cache_value_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将缓存值读取结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 缓存值读取结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
cache_value = '' if payload.get('cacheValue') is None else str(payload.get('cacheValue'))
|
|
||||||
cache_value_lines = cache_value.splitlines() if cache_value else []
|
|
||||||
if not cache_value_lines:
|
|
||||||
rendered_value_lines = [' -']
|
|
||||||
elif len(cache_value_lines) == 1:
|
|
||||||
rendered_value_lines = [f' {SHELL_TEXT_FORMATTER.truncate_text(cache_value_lines[0], 200)}']
|
|
||||||
else:
|
|
||||||
rendered_value_lines = [' |', *[f' {line}' for line in cache_value_lines]]
|
|
||||||
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'cache_name: {payload.get("cacheName", "")}',
|
|
||||||
f'cache_key: {payload.get("cacheKey", "")}',
|
|
||||||
f'full_cache_key: {payload.get("fullCacheKey", "")}',
|
|
||||||
'cache_value:',
|
|
||||||
*rendered_value_lines,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_cache_ttl_text(payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将缓存 TTL 结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 缓存 TTL 结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'cache_name: {payload.get("cacheName", "")}',
|
|
||||||
f'cache_key: {payload.get("cacheKey", "")}',
|
|
||||||
f'full_cache_key: {payload.get("fullCacheKey", "")}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'ttl_seconds: {payload.get("ttlSeconds", "-")}',
|
|
||||||
f'persistent: {str(payload.get("persistent", False)).lower()}',
|
|
||||||
f'expires: {str(payload.get("expires", False)).lower()}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_cache_name_remark(remark: object) -> str:
|
|
||||||
"""
|
|
||||||
规范化缓存名称备注文本。
|
|
||||||
|
|
||||||
:param remark: 原始备注
|
|
||||||
:return: 规范化后的备注
|
|
||||||
"""
|
|
||||||
text = '' if remark is None else str(remark).strip()
|
|
||||||
return text or '-'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
from typing import Annotated, Literal
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import ConfigCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='参数配置相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_CONFIG_COMMAND_CONTROLLER = ConfigCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('list', help='查看参数配置列表')
|
|
||||||
def list_command(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
config_name: Annotated[str, typer.Option('--config-name', help='按参数名称过滤')] = '',
|
|
||||||
config_key: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option(
|
|
||||||
'--config-key',
|
|
||||||
help='按参数键名过滤',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys,
|
|
||||||
),
|
|
||||||
] = '',
|
|
||||||
config_type: Annotated[Literal['Y', 'N'] | None, typer.Option('--config-type', help='按系统内置标记过滤')] = None,
|
|
||||||
begin_date: Annotated[str, typer.Option('--begin-date', help='按创建时间开始日期过滤,格式 YYYY-MM-DD')] = '',
|
|
||||||
end_date: Annotated[str, typer.Option('--end-date', help='按创建时间结束日期过滤,格式 YYYY-MM-DD')] = '',
|
|
||||||
paged: Annotated[bool, typer.Option('--paged/--no-paged', help='是否启用分页结果')] = False,
|
|
||||||
page_num: Annotated[int, typer.Option('--page-num', min=1, help='分页页码')] = 1,
|
|
||||||
page_size: Annotated[int, typer.Option('--page-size', min=1, help='分页每页数量')] = 20,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看参数配置列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param config_name: 参数名称过滤条件
|
|
||||||
:param config_key: 参数键名过滤条件
|
|
||||||
:param config_type: 参数类型过滤条件
|
|
||||||
:param begin_date: 查询开始日期
|
|
||||||
:param end_date: 查询结束日期
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CONFIG_COMMAND_CONTROLLER.list_configs(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
config_name=config_name,
|
|
||||||
config_key=config_key,
|
|
||||||
config_type=config_type,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('get', help='查看单个参数配置详情')
|
|
||||||
def get_command(
|
|
||||||
config_key: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Argument(
|
|
||||||
help='需要查询的参数键名',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
source: Annotated[Literal['db', 'cache', 'both'], typer.Option('--source', help='读取来源')] = 'both',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看单个参数配置详情。
|
|
||||||
|
|
||||||
:param config_key: 需要查询的参数键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param source: 配置读取来源
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CONFIG_COMMAND_CONTROLLER.get_config(config_key, env, output, source=source)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('set', help='新增或更新单个参数配置')
|
|
||||||
def set_command(
|
|
||||||
config_key: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Argument(
|
|
||||||
help='需要写入的参数键名',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_config_keys,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
value: Annotated[str, typer.Option('--value', help='参数键值')] = ...,
|
|
||||||
name: Annotated[str | None, typer.Option('--name', help='参数名称,新增时必填')] = None,
|
|
||||||
config_type: Annotated[Literal['Y', 'N'] | None, typer.Option('--config-type', help='系统内置标记')] = None,
|
|
||||||
remark: Annotated[str | None, typer.Option('--remark', help='参数备注')] = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
新增或更新单个参数配置。
|
|
||||||
|
|
||||||
:param config_key: 需要写入的参数键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param value: 参数键值
|
|
||||||
:param name: 参数名称
|
|
||||||
:param config_type: 参数类型
|
|
||||||
:param remark: 参数备注
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CONFIG_COMMAND_CONTROLLER.set_config(
|
|
||||||
config_key,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
value=value,
|
|
||||||
name=name,
|
|
||||||
config_type=config_type,
|
|
||||||
remark=remark,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('sync-cache', help='刷新参数配置缓存')
|
|
||||||
def sync_cache(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
刷新参数配置缓存。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CONFIG_COMMAND_CONTROLLER.sync_cache(env, output, allow_prod, yes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('doctor', help='诊断参数配置数据库与缓存是否一致')
|
|
||||||
def doctor(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
sample_limit: Annotated[int, typer.Option('--sample-limit', min=1, help='问题示例键名输出上限')] = 10,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
诊断参数配置数据库与缓存是否一致。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param sample_limit: 问题示例键名输出上限
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CONFIG_COMMAND_CONTROLLER.doctor(env, output, sample_limit=sample_limit)
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
from typing import Literal
|
|
||||||
|
|
||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.config import CONFIG_RUNTIME, ConfigRuntimeService
|
|
||||||
|
|
||||||
from .presenter import ConfigCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommandController:
|
|
||||||
"""
|
|
||||||
参数配置命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `config` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 参数配置命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: ConfigCommandPresenter | None = None,
|
|
||||||
runtime_service: ConfigRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化参数配置命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 参数配置命令文本渲染器
|
|
||||||
:param runtime_service: 参数配置运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or ConfigCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or CONFIG_RUNTIME
|
|
||||||
|
|
||||||
def list_configs(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
config_name: str,
|
|
||||||
config_key: str,
|
|
||||||
config_type: Literal['Y', 'N'] | None,
|
|
||||||
begin_date: str,
|
|
||||||
end_date: str,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看参数配置列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param config_name: 参数名称过滤条件
|
|
||||||
:param config_key: 参数键名过滤条件
|
|
||||||
:param config_type: 参数类型过滤条件
|
|
||||||
:param begin_date: 查询开始日期
|
|
||||||
:param end_date: 查询结束日期
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.runtime_service.list_configs(
|
|
||||||
config_name=config_name,
|
|
||||||
config_key=config_key,
|
|
||||||
config_type=config_type,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_config_list_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_config(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
source: Literal['db', 'cache', 'both'],
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看单个参数配置详情。
|
|
||||||
|
|
||||||
:param config_key: 需要查询的参数键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param source: 配置读取来源
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.get_config(config_key, source=source))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_config_get_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_config(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
value: str,
|
|
||||||
name: str | None,
|
|
||||||
config_type: Literal['Y', 'N'] | None,
|
|
||||||
remark: str | None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
新增或更新单个参数配置。
|
|
||||||
|
|
||||||
:param config_key: 需要写入的参数键名
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param value: 参数键值
|
|
||||||
:param name: 参数名称
|
|
||||||
:param config_type: 参数类型
|
|
||||||
:param remark: 参数备注
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='config set',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.runtime_service.set_config(
|
|
||||||
config_key,
|
|
||||||
value,
|
|
||||||
config_name=name,
|
|
||||||
config_type=config_type,
|
|
||||||
remark=remark,
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload(ctx, payload)
|
|
||||||
|
|
||||||
def sync_cache(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
刷新参数配置缓存。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
False,
|
|
||||||
command_name='config sync-cache',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.sync_config_cache())
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload(ctx, payload)
|
|
||||||
|
|
||||||
def doctor(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
sample_limit: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
诊断参数配置数据库与缓存是否一致。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param sample_limit: 问题示例键名输出上限
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.diagnose_config(sample_limit=sample_limit))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_config_doctor_text,
|
|
||||||
text_condition=lambda data: 'error' not in data,
|
|
||||||
)
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigCommandPresenter:
|
|
||||||
"""
|
|
||||||
参数配置命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `config` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_config_list_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将参数配置列表结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 参数配置列表结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
]
|
|
||||||
|
|
||||||
filters = payload.get('filters')
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
lines.extend(self._build_config_filter_lines(filters))
|
|
||||||
|
|
||||||
page_payload = payload.get('page')
|
|
||||||
if isinstance(page_payload, dict):
|
|
||||||
rows = page_payload.get('rows')
|
|
||||||
lines.append(
|
|
||||||
'page: '
|
|
||||||
f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} '
|
|
||||||
f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})'
|
|
||||||
)
|
|
||||||
lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}')
|
|
||||||
if not isinstance(rows, list) or not rows:
|
|
||||||
lines.append('configs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
lines.append('configs:')
|
|
||||||
for row in rows:
|
|
||||||
if isinstance(row, dict):
|
|
||||||
lines.extend([f' {item}' for item in self._build_config_item_lines(row)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
items = payload.get('items')
|
|
||||||
lines.append(f'count: {payload.get("count", 0)}')
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
lines.append('configs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('configs:')
|
|
||||||
for item in items:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_config_item_lines(item)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_config_get_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将单个参数配置详情结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 参数配置详情结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'key: {payload.get("key", "")}',
|
|
||||||
f'source: {payload.get("source", "")}',
|
|
||||||
]
|
|
||||||
|
|
||||||
if payload.get('source') == 'both':
|
|
||||||
lines.append(f'in_sync: {str(payload.get("inSync", False)).lower()}')
|
|
||||||
|
|
||||||
if payload.get('source') in {'db', 'both'}:
|
|
||||||
lines.extend(self._build_config_detail_section('database', payload.get('database')))
|
|
||||||
if payload.get('source') in {'cache', 'both'}:
|
|
||||||
lines.extend(self._build_config_detail_section('cache', payload.get('cache')))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_config_doctor_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将参数配置诊断结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 参数配置诊断结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'database_count: {payload.get("databaseCount", 0)}',
|
|
||||||
f'cache_count: {payload.get("cacheCount", 0)}',
|
|
||||||
f'missing_in_cache_count: {payload.get("missingInCacheCount", 0)}',
|
|
||||||
f'orphan_in_cache_count: {payload.get("orphanInCacheCount", 0)}',
|
|
||||||
f'mismatch_count: {payload.get("mismatchCount", 0)}',
|
|
||||||
f'sample_limit: {payload.get("sampleLimit", 0)}',
|
|
||||||
]
|
|
||||||
lines.extend(self._build_config_doctor_items('missing_in_cache', payload.get('missingInCache')))
|
|
||||||
lines.extend(self._build_config_doctor_items('orphan_in_cache', payload.get('orphanInCache')))
|
|
||||||
lines.extend(self._build_config_doctor_items('mismatch_keys', payload.get('mismatchKeys')))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_config_filter_lines(filters: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建参数配置过滤条件文本行。
|
|
||||||
|
|
||||||
:param filters: 过滤条件字典
|
|
||||||
:return: 过滤条件文本行列表
|
|
||||||
"""
|
|
||||||
active_filters = []
|
|
||||||
for key, value in filters.items():
|
|
||||||
if value in (None, '', False):
|
|
||||||
continue
|
|
||||||
active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}')
|
|
||||||
if not active_filters:
|
|
||||||
return ['filters: none']
|
|
||||||
return ['filters:', *[f' - {item}' for item in active_filters]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_config_item_lines(config_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条参数配置记录的文本行。
|
|
||||||
|
|
||||||
:param config_item: 单条参数配置记录
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
config_id = config_item.get('configId', '-')
|
|
||||||
config_key = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configKey', ''), 50)
|
|
||||||
config_name = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configName', ''), 24)
|
|
||||||
config_type = config_item.get('configType', '-')
|
|
||||||
config_value = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configValue', ''), 80)
|
|
||||||
remark = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('remark', ''), 60)
|
|
||||||
return [
|
|
||||||
f'- [{config_id}] {config_key} | 名称: {config_name} | 内置: {config_type}',
|
|
||||||
f' value: {config_value or "-"}',
|
|
||||||
f' remark: {remark or "-"}',
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_config_detail_section(title: str, config_item: dict[str, object] | None) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单个配置来源的详情文本段落。
|
|
||||||
|
|
||||||
:param title: 段落标题
|
|
||||||
:param config_item: 配置详情字典
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
if not isinstance(config_item, dict):
|
|
||||||
return [f'{title}: none']
|
|
||||||
|
|
||||||
config_key = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configKey', ''), 60)
|
|
||||||
config_name = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configName', ''), 30)
|
|
||||||
config_value = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('configValue', ''), 120)
|
|
||||||
config_type = config_item.get('configType', '-')
|
|
||||||
remark = SHELL_TEXT_FORMATTER.truncate_text(config_item.get('remark', ''), 80)
|
|
||||||
config_id = config_item.get('configId', '-')
|
|
||||||
return [
|
|
||||||
f'{title}:',
|
|
||||||
f' id: {config_id}',
|
|
||||||
f' key: {config_key or "-"}',
|
|
||||||
f' name: {config_name or "-"}',
|
|
||||||
f' value: {config_value or "-"}',
|
|
||||||
f' type: {config_type}',
|
|
||||||
f' remark: {remark or "-"}',
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_config_doctor_items(title: str, items: list[object] | object) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建配置诊断问题示例段落。
|
|
||||||
|
|
||||||
:param title: 段落标题
|
|
||||||
:param items: 问题键名列表
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
return [f'{title}: none']
|
|
||||||
return [f'{title}:', *[f' - {item}' for item in items]]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import CryptoCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='传输加密相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_CRYPTO_COMMAND_CONTROLLER = CryptoCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('validate', help='校验传输加密配置')
|
|
||||||
def validate(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
校验传输加密配置。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CRYPTO_COMMAND_CONTROLLER.validate(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('keygen', help='生成新的传输加密密钥对')
|
|
||||||
def keygen(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
kid: str = typer.Option('default', '--kid', help='生成的密钥版本标识'),
|
|
||||||
key_size: int = typer.Option(2048, '--key-size', help='RSA 密钥长度'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
生成新的传输加密密钥对。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param kid: 生成的密钥版本标识
|
|
||||||
:param key_size: RSA 密钥长度
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CRYPTO_COMMAND_CONTROLLER.keygen(env, output, kid=kid, key_size=key_size)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('rotate', help='生成传输加密密钥轮换辅助结果')
|
|
||||||
def rotate(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
next_kid: str = typer.Option('rotated', '--next-kid', help='轮换后的新密钥版本标识'),
|
|
||||||
key_size: int = typer.Option(2048, '--key-size', help='新密钥的 RSA 长度'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
生成传输加密密钥轮换辅助结果。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param next_kid: 轮换后的新密钥版本标识
|
|
||||||
:param key_size: 新密钥的 RSA 长度
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CRYPTO_COMMAND_CONTROLLER.rotate(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
next_kid=next_kid,
|
|
||||||
key_size=key_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('export-public', help='查看当前运行环境的公钥信息')
|
|
||||||
def export_public(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
导出当前运行环境的公钥信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_CRYPTO_COMMAND_CONTROLLER.export_public(env, output)
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService
|
|
||||||
|
|
||||||
from .presenter import CryptoCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoCommandController:
|
|
||||||
"""
|
|
||||||
传输加密命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `crypto` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 传输加密命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: CryptoCommandPresenter | None = None,
|
|
||||||
runtime_service: CryptoRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化传输加密命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 传输加密命令文本渲染器
|
|
||||||
:param runtime_service: 传输加密运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or CryptoCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or CRYPTO_RUNTIME
|
|
||||||
|
|
||||||
def validate(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
校验传输加密配置。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload(ctx, self.runtime_service.validate_crypto_config())
|
|
||||||
|
|
||||||
def keygen(self, env: str, output: str, *, kid: str, key_size: int) -> None:
|
|
||||||
"""
|
|
||||||
生成新的传输加密密钥对。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param kid: 生成的密钥版本标识
|
|
||||||
:param key_size: RSA 密钥长度
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.generate_crypto_key_pair(kid, key_size)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_crypto_keygen_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def rotate(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
next_kid: str,
|
|
||||||
key_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
生成传输加密密钥轮换辅助结果。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param next_kid: 轮换后的新密钥版本标识
|
|
||||||
:param key_size: 新密钥的 RSA 长度
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='crypto rotate',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(ctx, self.runtime_service.build_rotation_payload(next_kid, key_size))
|
|
||||||
|
|
||||||
def export_public(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
导出当前运行环境的公钥信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.export_public_key()
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_export_public_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
class CryptoCommandPresenter:
|
|
||||||
"""
|
|
||||||
传输加密命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `crypto` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_crypto_keygen_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将密钥生成结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 密钥生成结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
env_patch = payload.get('envPatch')
|
|
||||||
env_patch_keys = sorted(env_patch.keys()) if isinstance(env_patch, dict) else []
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'kid: {payload.get("kid", "-")}',
|
|
||||||
f'key_size: {payload.get("keySize", "-")}',
|
|
||||||
]
|
|
||||||
lines.extend(self._build_multiline_block('public_key', payload.get('publicKey')))
|
|
||||||
lines.extend(self._build_multiline_block('private_key', payload.get('privateKey')))
|
|
||||||
if env_patch_keys:
|
|
||||||
lines.append('env_patch_keys:')
|
|
||||||
lines.extend(f' - {key}' for key in env_patch_keys)
|
|
||||||
else:
|
|
||||||
lines.append('env_patch_keys: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_export_public_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将公钥导出结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 公钥导出结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
public_key_payload = payload.get('publicKey')
|
|
||||||
if not isinstance(public_key_payload, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'public_key: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
supported_kids = public_key_payload.get('supportedKids')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'kid: {public_key_payload.get("kid", "-")}',
|
|
||||||
f'alg: {public_key_payload.get("alg", "-")}',
|
|
||||||
f'envelope_version: {public_key_payload.get("envelopeVersion", "-")}',
|
|
||||||
f'expire_at: {public_key_payload.get("expireAt", "-")}',
|
|
||||||
]
|
|
||||||
if isinstance(supported_kids, list) and supported_kids:
|
|
||||||
lines.append('supported_kids:')
|
|
||||||
lines.extend(f' - {kid}' for kid in supported_kids)
|
|
||||||
else:
|
|
||||||
lines.append('supported_kids: none')
|
|
||||||
lines.extend(self._build_multiline_block('public_key_pem', public_key_payload.get('publicKey')))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_multiline_block(title: str, value: object) -> list[str]:
|
|
||||||
"""
|
|
||||||
将多行文本构建为带标题的块级文本。
|
|
||||||
|
|
||||||
:param title: 块标题
|
|
||||||
:param value: 原始文本值
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
text = '' if value is None else str(value).strip()
|
|
||||||
if not text:
|
|
||||||
return [f'{title}: -']
|
|
||||||
return [f'{title}:', ' |', *[f' {line}' for line in text.splitlines()]]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import DbCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='数据库相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_DB_COMMAND_CONTROLLER = DbCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('check', help='检查数据库连接状态')
|
|
||||||
def check(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
检查数据库连接状态。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.check(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('current', help='查看当前数据库迁移版本')
|
|
||||||
def current(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看数据库当前迁移版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.current(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('upgrade', help='执行数据库升级')
|
|
||||||
def upgrade(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
revision: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option(
|
|
||||||
'--revision',
|
|
||||||
help='目标迁移版本',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_alembic_revisions,
|
|
||||||
),
|
|
||||||
] = 'head',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行数据库升级。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param revision: 目标迁移版本
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.upgrade(env, output, allow_prod, yes, dry_run, revision=revision)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('init', help='初始化数据库到最新迁移版本')
|
|
||||||
def init(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库到最新迁移版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.init(env, output, allow_prod, yes, dry_run)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('downgrade', help='执行数据库回退')
|
|
||||||
def downgrade(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
revision: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Option(
|
|
||||||
'--revision',
|
|
||||||
help='目标回退版本',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_alembic_revisions,
|
|
||||||
),
|
|
||||||
] = '-1',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行数据库回退。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param revision: 目标回退版本
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.downgrade(env, output, allow_prod, yes, dry_run, revision=revision)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('revision', help='创建新的数据库迁移版本文件')
|
|
||||||
def revision(
|
|
||||||
message: Annotated[str, typer.Option('--message', '-m', help='迁移说明')],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
autogenerate: bool = typer.Option(False, '--autogenerate/--no-autogenerate', help='是否自动生成迁移内容'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
创建新的数据库迁移版本文件。
|
|
||||||
|
|
||||||
:param message: 迁移说明
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param autogenerate: 是否自动生成迁移内容
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.revision(
|
|
||||||
message,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
autogenerate=autogenerate,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('heads', help='查看当前代码仓库中的 Alembic heads')
|
|
||||||
def heads(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看当前代码仓库中的 Alembic heads。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.heads(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('history', help='查看当前代码仓库中的 Alembic 历史版本')
|
|
||||||
def history(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
limit: Annotated[int, typer.Option('--limit', min=1, help='输出的最大历史记录数量')] = 20,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看当前代码仓库中的 Alembic 历史版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param limit: 输出的最大历史记录数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DB_COMMAND_CONTROLLER.history(env, output, limit=limit)
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService
|
|
||||||
|
|
||||||
from .presenter import DbCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class DbCommandController:
|
|
||||||
"""
|
|
||||||
数据库命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `db` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 数据库命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: DbCommandPresenter | None = None,
|
|
||||||
runtime_service: DatabaseRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 数据库命令文本渲染器
|
|
||||||
:param runtime_service: 数据库运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or DbCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or DATABASE_RUNTIME
|
|
||||||
|
|
||||||
def check(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
检查数据库连接状态。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.runtime_service.ping_database())
|
|
||||||
)
|
|
||||||
|
|
||||||
def current(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看数据库当前迁移版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.get_current_revision()
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_current_revision_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def upgrade(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
revision: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行数据库升级。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param revision: 目标迁移版本
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='db upgrade',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(ctx, self.runtime_service.upgrade_database(revision, dry_run=dry_run))
|
|
||||||
|
|
||||||
def init(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库到最新迁移版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='db init',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(ctx, self.runtime_service.init_database(dry_run=dry_run))
|
|
||||||
|
|
||||||
def downgrade(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
revision: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行数据库回退。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param revision: 目标回退版本
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='db downgrade',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(ctx, self.runtime_service.downgrade_database(revision, dry_run=dry_run))
|
|
||||||
|
|
||||||
def revision(
|
|
||||||
self,
|
|
||||||
message: str,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
autogenerate: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
创建新的数据库迁移版本文件。
|
|
||||||
|
|
||||||
:param message: 迁移说明
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param autogenerate: 是否自动生成迁移内容
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='db revision',
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx,
|
|
||||||
self.runtime_service.create_revision(message, autogenerate=autogenerate, dry_run=dry_run),
|
|
||||||
)
|
|
||||||
|
|
||||||
def heads(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看当前代码仓库中的 Alembic heads。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.get_alembic_heads()
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_alembic_revisions_text,
|
|
||||||
text_condition=lambda result_data: 'error' not in result_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def history(self, env: str, output: str, *, limit: int) -> None:
|
|
||||||
"""
|
|
||||||
查看当前代码仓库中的 Alembic 历史版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param limit: 输出的最大历史记录数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.get_alembic_history(limit=limit)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_alembic_revisions_text,
|
|
||||||
text_condition=lambda result_data: 'error' not in result_data,
|
|
||||||
)
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
class DbCommandPresenter:
|
|
||||||
"""
|
|
||||||
数据库命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `db` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_current_revision_text(payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将数据库当前迁移版本结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 当前迁移版本结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'current_revision: {payload.get("currentRevision", "-")}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_alembic_revisions_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将 Alembic 修订版本结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 修订版本结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
items = payload.get('items')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'count: {payload.get("count", 0)}',
|
|
||||||
]
|
|
||||||
if 'totalCount' in payload:
|
|
||||||
lines.append(f'total_count: {payload.get("totalCount", 0)}')
|
|
||||||
if 'limit' in payload:
|
|
||||||
lines.append(f'limit: {payload.get("limit", 0)}')
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
lines.append('items: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('items:')
|
|
||||||
for item in items:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_alembic_revision_item_lines(item)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_alembic_revision_item_lines(revision_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单个 Alembic 修订版本的文本行。
|
|
||||||
|
|
||||||
:param revision_item: 修订版本结果字典
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
down_revisions = revision_item.get('downRevisions')
|
|
||||||
branch_labels = revision_item.get('branchLabels')
|
|
||||||
depends_on = revision_item.get('dependsOn')
|
|
||||||
return [
|
|
||||||
f'- revision: {revision_item.get("revision", "-")}',
|
|
||||||
f' down_revisions: {",".join(down_revisions) if isinstance(down_revisions, list) and down_revisions else "-"}',
|
|
||||||
f' branch_labels: {",".join(branch_labels) if isinstance(branch_labels, list) and branch_labels else "-"}',
|
|
||||||
f' depends_on: {",".join(depends_on) if isinstance(depends_on, list) and depends_on else "-"}',
|
|
||||||
f' doc: {revision_item.get("doc", "") or "-"}',
|
|
||||||
f' path: {revision_item.get("path", "-")}',
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
from typing import Annotated
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import EnvOption, OutputOption
|
|
||||||
|
|
||||||
from .controller import DevCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='开发相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_DEV_COMMAND_CONTROLLER = DevCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('lint', help='执行 Ruff 格式化与静态检查')
|
|
||||||
def lint(
|
|
||||||
targets: Annotated[list[str] | None, typer.Argument(help='待检查路径,可传多个')] = None,
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
check_only: bool = typer.Option(False, '--check-only', help='仅检查,不写回格式化结果'),
|
|
||||||
fix: bool = typer.Option(False, '--fix', help='执行 ruff check --fix'),
|
|
||||||
unsafe_fixes: bool = typer.Option(False, '--unsafe-fixes', help='允许 Ruff 不安全修复'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行 Ruff 格式化与静态检查。
|
|
||||||
|
|
||||||
:param targets: 待检查路径列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param check_only: 是否仅检查不写回
|
|
||||||
:param fix: 是否自动修复可修复问题
|
|
||||||
:param unsafe_fixes: 是否允许不安全修复
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DEV_COMMAND_CONTROLLER.lint(
|
|
||||||
targets,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
check_only=check_only,
|
|
||||||
fix=fix,
|
|
||||||
unsafe_fixes=unsafe_fixes,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('test', help='执行项目测试')
|
|
||||||
def test(
|
|
||||||
targets: Annotated[list[str] | None, typer.Argument(help='待执行的测试路径,可传多个')] = None,
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
keyword: str = typer.Option('', '--keyword', '-k', help='pytest -k 过滤表达式'),
|
|
||||||
maxfail: int = typer.Option(0, '--maxfail', min=0, help='最大失败数,0 表示不限制'),
|
|
||||||
quiet: bool = typer.Option(False, '--quiet', '-q', help='启用简洁输出'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行项目测试。
|
|
||||||
|
|
||||||
:param targets: 测试目标路径列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param keyword: pytest 关键字过滤表达式
|
|
||||||
:param maxfail: 最大失败数
|
|
||||||
:param quiet: 是否启用简洁输出
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_DEV_COMMAND_CONTROLLER.test(
|
|
||||||
targets,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
keyword=keyword,
|
|
||||||
maxfail=maxfail,
|
|
||||||
quiet=quiet,
|
|
||||||
)
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.dev import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService
|
|
||||||
|
|
||||||
from .presenter import DevCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class DevCommandController:
|
|
||||||
"""
|
|
||||||
开发命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `dev` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 开发命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: DevCommandPresenter | None = None,
|
|
||||||
runtime_service: DevelopmentRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化开发命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 开发命令文本渲染器
|
|
||||||
:param runtime_service: 开发运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or DevCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or DEVELOPMENT_RUNTIME
|
|
||||||
|
|
||||||
def lint(
|
|
||||||
self,
|
|
||||||
targets: list[str] | None,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
check_only: bool,
|
|
||||||
fix: bool,
|
|
||||||
unsafe_fixes: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行 Ruff 格式化与静态检查。
|
|
||||||
|
|
||||||
:param targets: 待检查路径列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param check_only: 是否仅检查不写回
|
|
||||||
:param fix: 是否自动修复可修复问题
|
|
||||||
:param unsafe_fixes: 是否允许不安全修复
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.run_lint(targets, check_only=check_only, fix=fix, unsafe_fixes=unsafe_fixes)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_dev_lint_text,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test(
|
|
||||||
self,
|
|
||||||
targets: list[str] | None,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
keyword: str,
|
|
||||||
maxfail: int,
|
|
||||||
quiet: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行项目测试。
|
|
||||||
|
|
||||||
:param targets: 测试目标路径列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param keyword: pytest 关键字过滤表达式
|
|
||||||
:param maxfail: 最大失败数
|
|
||||||
:param quiet: 是否启用简洁输出
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.runtime_service.run_tests(targets, keyword=keyword, maxfail=maxfail, quiet=quiet)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_dev_test_text,
|
|
||||||
)
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
class DevCommandPresenter:
|
|
||||||
"""
|
|
||||||
开发命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `dev` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_dev_lint_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将 lint 执行结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: lint 执行结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
targets = payload.get('targets')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'check_only: {str(payload.get("checkOnly", False)).lower()}',
|
|
||||||
f'fix: {str(payload.get("fix", False)).lower()}',
|
|
||||||
f'unsafe_fixes: {str(payload.get("unsafeFixes", False)).lower()}',
|
|
||||||
]
|
|
||||||
if isinstance(targets, list) and targets:
|
|
||||||
lines.append('targets:')
|
|
||||||
lines.extend(f' - {target}' for target in targets)
|
|
||||||
else:
|
|
||||||
lines.append('targets: none')
|
|
||||||
lines.extend(self._build_command_result_section('format', payload.get('format')))
|
|
||||||
lines.extend(self._build_command_result_section('check', payload.get('check')))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_dev_test_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将测试执行结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 测试执行结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
targets = payload.get('targets')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'keyword: {payload.get("keyword", "") or "-"}',
|
|
||||||
f'maxfail: {payload.get("maxfail", 0)}',
|
|
||||||
f'quiet: {str(payload.get("quiet", False)).lower()}',
|
|
||||||
]
|
|
||||||
if isinstance(targets, list) and targets:
|
|
||||||
lines.append('targets:')
|
|
||||||
lines.extend(f' - {target}' for target in targets)
|
|
||||||
else:
|
|
||||||
lines.append('targets: none')
|
|
||||||
lines.extend(self._build_command_result_section('test', payload.get('test')))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_command_result_section(title: str, payload: dict[str, object] | None) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单个子命令执行结果段落。
|
|
||||||
|
|
||||||
:param title: 段落标题
|
|
||||||
:param payload: 子命令执行结果
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
if not isinstance(payload, dict):
|
|
||||||
return [f'{title}: none']
|
|
||||||
|
|
||||||
lines = [
|
|
||||||
f'{title}:',
|
|
||||||
f' ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f' return_code: {payload.get("returnCode", "-")}',
|
|
||||||
f' command: {DevCommandPresenter._format_command_text(payload.get("command"))}',
|
|
||||||
]
|
|
||||||
stdout = payload.get('stdout')
|
|
||||||
stderr = payload.get('stderr')
|
|
||||||
if stdout:
|
|
||||||
lines.append(' stdout:')
|
|
||||||
lines.append(' |')
|
|
||||||
lines.extend(f' {line}' for line in str(stdout).splitlines())
|
|
||||||
if stderr:
|
|
||||||
lines.append(' stderr:')
|
|
||||||
lines.append(' |')
|
|
||||||
lines.extend(f' {line}' for line in str(stderr).splitlines())
|
|
||||||
return lines
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_command_text(command: object) -> str:
|
|
||||||
"""
|
|
||||||
将命令参数列表格式化为可读文本。
|
|
||||||
|
|
||||||
:param command: 命令参数列表
|
|
||||||
:return: 格式化后的命令文本
|
|
||||||
"""
|
|
||||||
if not isinstance(command, list):
|
|
||||||
return '-'
|
|
||||||
return ' '.join(str(item) for item in command) or '-'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
from typing import Annotated, Literal
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
from cli.context import AllowProdOption, DryRunOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import GenCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='代码生成相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_GEN_COMMAND_CONTROLLER = GenCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('import-table', help='导入数据库表到代码生成业务表')
|
|
||||||
def import_table(
|
|
||||||
table_names: Annotated[
|
|
||||||
list[str],
|
|
||||||
typer.Argument(
|
|
||||||
help='待导入的数据库表名,可传多个',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_db_table_names,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
导入数据库表到代码生成业务表。
|
|
||||||
|
|
||||||
:param table_names: 待导入表名列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.import_table(
|
|
||||||
table_names,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('list', help='查看代码生成业务表列表')
|
|
||||||
def list_command(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
table_name: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--table-name',
|
|
||||||
help='按表名称过滤',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names,
|
|
||||||
),
|
|
||||||
table_comment: str = typer.Option('', '--table-comment', help='按表描述过滤'),
|
|
||||||
paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'),
|
|
||||||
page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'),
|
|
||||||
page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看代码生成业务表列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.list_tables(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
table_name=table_name,
|
|
||||||
table_comment=table_comment,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('db-list', help='查看数据库中可导入的物理表列表')
|
|
||||||
def db_list(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
table_name: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--table-name',
|
|
||||||
help='按表名称过滤',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_db_table_names,
|
|
||||||
),
|
|
||||||
table_comment: str = typer.Option('', '--table-comment', help='按表描述过滤'),
|
|
||||||
paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'),
|
|
||||||
page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'),
|
|
||||||
page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看数据库中可导入的物理表列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.list_db_tables(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
table_name=table_name,
|
|
||||||
table_comment=table_comment,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('detail', help='查看单个代码生成业务表详情')
|
|
||||||
def detail(
|
|
||||||
table_id: Annotated[int, typer.Argument(help='业务表 ID')],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看单个代码生成业务表详情。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.show_detail(table_id, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('create-table', help='根据建表 SQL 创建表结构并导入代码生成业务表')
|
|
||||||
def create_table(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
sql: str = typer.Option('', '--sql', help='直接传入建表 SQL 文本'),
|
|
||||||
sql_file: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--sql-file',
|
|
||||||
help='从文件读取建表 SQL',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_sql_files,
|
|
||||||
),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
根据建表 SQL 创建表结构并导入代码生成业务表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param sql: 直接传入的 SQL 文本
|
|
||||||
:param sql_file: SQL 文件路径
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.create_table(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
sql=sql,
|
|
||||||
sql_file=sql_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('preview', help='查看指定业务表的代码生成结果预览')
|
|
||||||
def preview(
|
|
||||||
table_id: Annotated[int, typer.Argument(help='业务表 ID')],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
预览指定业务表的代码生成结果。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.preview(table_id, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('export', help='导出代码生成结果')
|
|
||||||
def export(
|
|
||||||
table_names: Annotated[
|
|
||||||
list[str],
|
|
||||||
typer.Argument(
|
|
||||||
help='待导出的业务表名,可传多个',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
dry_run: DryRunOption = False,
|
|
||||||
mode: Annotated[Literal['zip', 'local'], typer.Option('--mode', help='导出模式')] = 'zip',
|
|
||||||
output_file: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--output-file',
|
|
||||||
help='zip 导出目标文件路径',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_output_paths,
|
|
||||||
),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
导出代码生成结果。
|
|
||||||
|
|
||||||
:param table_names: 业务表名称列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param mode: 导出模式
|
|
||||||
:param output_file: zip 导出目标文件路径
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.export(
|
|
||||||
table_names,
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
mode=mode,
|
|
||||||
output_file=output_file,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('sync-db', help='同步指定业务表的数据库表结构')
|
|
||||||
def sync_db(
|
|
||||||
table_name: Annotated[
|
|
||||||
str,
|
|
||||||
typer.Argument(
|
|
||||||
help='业务表名称',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_gen_table_names,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
同步指定业务表的数据库表结构。
|
|
||||||
|
|
||||||
:param table_name: 业务表名称
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_GEN_COMMAND_CONTROLLER.sync_db(table_name, env, output, allow_prod, yes)
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.gen import GEN_RUNTIME, GenRuntimeService
|
|
||||||
|
|
||||||
from .presenter import GenCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class GenCommandController:
|
|
||||||
"""
|
|
||||||
代码生成命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `gen` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 代码生成命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: GenCommandPresenter | None = None,
|
|
||||||
runtime_service: GenRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化代码生成命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 代码生成命令文本渲染器
|
|
||||||
:param runtime_service: 代码生成运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or GenCommandPresenter()
|
|
||||||
self.runtime_service = runtime_service or GEN_RUNTIME
|
|
||||||
|
|
||||||
def import_table(
|
|
||||||
self,
|
|
||||||
table_names: list[str],
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
导入数据库表到代码生成业务表。
|
|
||||||
|
|
||||||
:param table_names: 待导入表名列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='gen import-table',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.import_tables(table_names, dry_run=dry_run))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload(ctx, payload)
|
|
||||||
|
|
||||||
def list_tables(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
table_name: str,
|
|
||||||
table_comment: str,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看代码生成业务表列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.runtime_service.list_gen_tables(
|
|
||||||
table_name=table_name,
|
|
||||||
table_comment=table_comment,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=lambda data: self.presenter.build_gen_table_list_text(data, db_mode=False),
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def list_db_tables(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
table_name: str,
|
|
||||||
table_comment: str,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看数据库中可导入的物理表列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.runtime_service.list_gen_db_tables(
|
|
||||||
table_name=table_name,
|
|
||||||
table_comment=table_comment,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=lambda data: self.presenter.build_gen_table_list_text(data, db_mode=True),
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def show_detail(self, table_id: int, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看单个代码生成业务表详情。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.get_gen_table_detail(table_id))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_gen_detail_text,
|
|
||||||
text_condition=lambda data: 'error' not in data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_table(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
sql: str,
|
|
||||||
sql_file: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
根据建表 SQL 创建表结构并导入代码生成业务表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param sql: 直接传入的 SQL 文本
|
|
||||||
:param sql_file: SQL 文件路径
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='gen create-table',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.create_tables(sql, sql_file, dry_run=dry_run))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload(ctx, payload)
|
|
||||||
|
|
||||||
def preview(self, table_id: int, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
预览指定业务表的代码生成结果。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.preview_code(table_id))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_gen_preview_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def export(
|
|
||||||
self,
|
|
||||||
table_names: list[str],
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
*,
|
|
||||||
mode: str,
|
|
||||||
output_file: str,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
导出代码生成结果。
|
|
||||||
|
|
||||||
:param table_names: 业务表名称列表
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:param dry_run: 是否演练执行
|
|
||||||
:param mode: 导出模式
|
|
||||||
:param output_file: zip 导出目标文件路径
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
dry_run,
|
|
||||||
command_name='gen export',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.runtime_service.export_code(table_names, mode=mode, output_file=output_file, dry_run=dry_run)
|
|
||||||
)
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_gen_export_text,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync_db(
|
|
||||||
self,
|
|
||||||
table_name: str,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
allow_prod: bool,
|
|
||||||
yes: bool,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
同步指定业务表的数据库表结构。
|
|
||||||
|
|
||||||
:param table_name: 业务表名称
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
allow_prod,
|
|
||||||
yes,
|
|
||||||
False,
|
|
||||||
command_name='gen sync-db',
|
|
||||||
)
|
|
||||||
payload = self.execution_service.run_async(self.runtime_service.sync_gen_table_from_db(table_name))
|
|
||||||
payload['env'] = ctx.env
|
|
||||||
self.execution_service.complete_payload(ctx, payload)
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class GenCommandPresenter:
|
|
||||||
"""
|
|
||||||
代码生成命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `gen` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_gen_preview_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将代码预览结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 代码预览结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
preview_payload = payload.get('preview')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'table_id: {payload.get("tableId", "-")}',
|
|
||||||
f'template_count: {payload.get("templateCount", 0)}',
|
|
||||||
]
|
|
||||||
if not isinstance(preview_payload, dict) or not preview_payload:
|
|
||||||
lines.append('templates: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('templates:')
|
|
||||||
for template_name, template_content in preview_payload.items():
|
|
||||||
lines.extend([f' {line}' for line in self._build_text_block(str(template_name), template_content)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_gen_export_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将代码导出结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 代码导出结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
table_names = payload.get('tableNames')
|
|
||||||
results = payload.get('results')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
f'mode: {payload.get("mode", "-")}',
|
|
||||||
f'dry_run: {str(payload.get("dryRun", False)).lower()}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
]
|
|
||||||
if isinstance(table_names, list) and table_names:
|
|
||||||
lines.append('table_names:')
|
|
||||||
lines.extend(f' - {table_name}' for table_name in table_names)
|
|
||||||
else:
|
|
||||||
lines.append('table_names: none')
|
|
||||||
|
|
||||||
if payload.get('outputFile'):
|
|
||||||
lines.append(f'output_file: {payload.get("outputFile")}')
|
|
||||||
if payload.get('genPath'):
|
|
||||||
lines.append(f'gen_path: {payload.get("genPath")}')
|
|
||||||
if payload.get('size') is not None:
|
|
||||||
lines.append(f'size: {payload.get("size")}')
|
|
||||||
|
|
||||||
if isinstance(results, list) and results:
|
|
||||||
lines.append('results:')
|
|
||||||
lines.extend(
|
|
||||||
f' - {item.get("tableName", "-")}: {str(item.get("ok", False)).lower()} | {item.get("message", "-")}'
|
|
||||||
for item in results
|
|
||||||
if isinstance(item, dict)
|
|
||||||
)
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_gen_table_list_text(self, payload: dict[str, object], *, db_mode: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
将代码生成表列表或数据库表列表渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 列表结果字典
|
|
||||||
:param db_mode: 是否为数据库物理表列表模式
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [f'ok: {str(payload.get("ok", False)).lower()}']
|
|
||||||
filters = payload.get('filters')
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
lines.extend(self._build_gen_filter_lines(filters))
|
|
||||||
|
|
||||||
page_payload = payload.get('page')
|
|
||||||
if isinstance(page_payload, dict):
|
|
||||||
rows = page_payload.get('rows', [])
|
|
||||||
lines.append(
|
|
||||||
'page: '
|
|
||||||
f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} '
|
|
||||||
f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})'
|
|
||||||
)
|
|
||||||
lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}')
|
|
||||||
if not isinstance(rows, list) or not rows:
|
|
||||||
lines.append('items: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
lines.append('items:')
|
|
||||||
for row in rows:
|
|
||||||
if isinstance(row, dict):
|
|
||||||
item_lines = (
|
|
||||||
self._build_gen_db_table_item_lines(row) if db_mode else self._build_gen_table_item_lines(row)
|
|
||||||
)
|
|
||||||
lines.extend([f' {item}' for item in item_lines])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
items = payload.get('items')
|
|
||||||
lines.append(f'count: {payload.get("count", 0)}')
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
lines.append('items: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('items:')
|
|
||||||
for item in items:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
item_lines = (
|
|
||||||
self._build_gen_db_table_item_lines(item) if db_mode else self._build_gen_table_item_lines(item)
|
|
||||||
)
|
|
||||||
lines.extend([f' {line}' for line in item_lines])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_gen_detail_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将代码生成业务表详情渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 详情结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
detail = payload.get('detail')
|
|
||||||
if not isinstance(detail, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'table_id: {payload.get("tableId", "-")}',
|
|
||||||
'detail: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
info = detail.get('info')
|
|
||||||
if not isinstance(info, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'table_id: {payload.get("tableId", "-")}',
|
|
||||||
'info: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'table_id: {payload.get("tableId", "-")}',
|
|
||||||
f'table_name: {payload.get("tableName", "-")}',
|
|
||||||
f'column_count: {payload.get("columnCount", 0)}',
|
|
||||||
f'table_count: {payload.get("tableCount", 0)}',
|
|
||||||
f'table_comment: {SHELL_TEXT_FORMATTER.truncate_text(info.get("tableComment", ""), 80) or "-"}',
|
|
||||||
f'class_name: {info.get("className", "-")}',
|
|
||||||
f'tpl_category: {info.get("tplCategory", "-")}',
|
|
||||||
f'tpl_web_type: {info.get("tplWebType", "-")}',
|
|
||||||
f'package_name: {SHELL_TEXT_FORMATTER.truncate_text(info.get("packageName", ""), 80) or "-"}',
|
|
||||||
f'module_name: {info.get("moduleName", "-")}',
|
|
||||||
f'business_name: {info.get("businessName", "-")}',
|
|
||||||
f'function_name: {info.get("functionName", "-")}',
|
|
||||||
f'function_author: {info.get("functionAuthor", "-")}',
|
|
||||||
f'gen_type: {info.get("genType", "-")}',
|
|
||||||
f'gen_path: {SHELL_TEXT_FORMATTER.truncate_text(info.get("genPath", ""), 120) or "-"}',
|
|
||||||
f'remark: {SHELL_TEXT_FORMATTER.truncate_text(info.get("remark", ""), 160) or "-"}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_text_block(title: str, value: object) -> list[str]:
|
|
||||||
"""
|
|
||||||
将多行文本构建为带标题的块级文本。
|
|
||||||
|
|
||||||
:param title: 块标题
|
|
||||||
:param value: 原始文本值
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
text = '' if value is None else str(value)
|
|
||||||
if not text.strip():
|
|
||||||
return [f'{title}: -']
|
|
||||||
return [f'{title}:', ' |', *[f' {line}' for line in text.splitlines()]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_gen_filter_lines(filters: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建代码生成表过滤条件文本行。
|
|
||||||
|
|
||||||
:param filters: 过滤条件字典
|
|
||||||
:return: 过滤条件文本行列表
|
|
||||||
"""
|
|
||||||
active_filters = []
|
|
||||||
for key, value in filters.items():
|
|
||||||
if value in (None, '', False):
|
|
||||||
continue
|
|
||||||
active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}')
|
|
||||||
if not active_filters:
|
|
||||||
return ['filters: none']
|
|
||||||
return ['filters:', *[f' - {item}' for item in active_filters]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_gen_table_item_lines(table_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条代码生成表记录的文本行。
|
|
||||||
|
|
||||||
:param table_item: 代码生成表记录
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
f'- [{table_item.get("tableId", "-")}] {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableName", ""), 40) or "-"}',
|
|
||||||
f' comment: {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableComment", ""), 60) or "-"}',
|
|
||||||
f' class_name: {table_item.get("className", "-")}',
|
|
||||||
f' tpl_category: {table_item.get("tplCategory", "-")} | module_name: {table_item.get("moduleName", "-")}',
|
|
||||||
f' business_name: {table_item.get("businessName", "-")} | function_name: {table_item.get("functionName", "-")}',
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_gen_db_table_item_lines(table_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条数据库物理表记录的文本行。
|
|
||||||
|
|
||||||
:param table_item: 数据库物理表记录
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
f'- {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableName", ""), 48) or "-"}',
|
|
||||||
f' comment: {SHELL_TEXT_FORMATTER.truncate_text(table_item.get("tableComment", ""), 80) or "-"}',
|
|
||||||
f' create_time: {table_item.get("createTime", "-")}',
|
|
||||||
f' update_time: {table_item.get("updateTime", "-")}',
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
import typer
|
|
||||||
|
|
||||||
from cli.completion.providers import COMPLETION_PROVIDER_GATEWAY
|
|
||||||
from cli.context import AllowProdOption, EnvOption, OutputOption, YesOption
|
|
||||||
|
|
||||||
from .controller import JobCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='定时任务相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_JOB_COMMAND_CONTROLLER = JobCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('list', help='查看定时任务列表')
|
|
||||||
def list_command(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
job_name: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--job-name',
|
|
||||||
help='按任务名称过滤',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_names,
|
|
||||||
),
|
|
||||||
job_group: str = typer.Option('', '--job-group', help='按任务组过滤'),
|
|
||||||
status: str | None = typer.Option(None, '--status', help='按任务状态过滤,0正常 1暂停'),
|
|
||||||
paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'),
|
|
||||||
page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'),
|
|
||||||
page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看定时任务列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 状态过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.list(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('run-once', help='执行一次定时任务')
|
|
||||||
def run_once(
|
|
||||||
job_id: int = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='任务ID',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
执行一次定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.run_once(job_id, env, output, allow_prod, yes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('detail', help='查看单个定时任务详情')
|
|
||||||
def detail(
|
|
||||||
job_id: int = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='任务ID',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看单个定时任务详情。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.detail(job_id, env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('logs', help='查看定时任务执行日志列表')
|
|
||||||
def logs(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
job_name: str = typer.Option(
|
|
||||||
'',
|
|
||||||
'--job-name',
|
|
||||||
help='按任务名称过滤',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_names,
|
|
||||||
),
|
|
||||||
job_group: str = typer.Option('', '--job-group', help='按任务组过滤'),
|
|
||||||
status: str | None = typer.Option(None, '--status', help='按执行状态过滤,0成功 1失败'),
|
|
||||||
begin_date: str = typer.Option('', '--begin-date', help='按创建时间开始日期过滤,格式 YYYY-MM-DD'),
|
|
||||||
end_date: str = typer.Option('', '--end-date', help='按创建时间结束日期过滤,格式 YYYY-MM-DD'),
|
|
||||||
paged: bool = typer.Option(False, '--paged/--no-paged', help='是否启用分页结果'),
|
|
||||||
page_num: int = typer.Option(1, '--page-num', min=1, help='分页页码'),
|
|
||||||
page_size: int = typer.Option(20, '--page-size', min=1, help='分页每页数量'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看定时任务执行日志列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 执行状态过滤条件
|
|
||||||
:param begin_date: 查询开始日期
|
|
||||||
:param end_date: 查询结束日期
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.logs(
|
|
||||||
env,
|
|
||||||
output,
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('pause', help='暂停定时任务')
|
|
||||||
def pause(
|
|
||||||
job_id: int = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='任务ID',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
暂停定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.pause(job_id, env, output, allow_prod, yes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('resume', help='恢复定时任务')
|
|
||||||
def resume(
|
|
||||||
job_id: int = typer.Argument(
|
|
||||||
...,
|
|
||||||
help='任务ID',
|
|
||||||
autocompletion=COMPLETION_PROVIDER_GATEWAY.complete_job_ids,
|
|
||||||
),
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
恢复定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.resume(job_id, env, output, allow_prod, yes)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('sync', help='同步调度任务配置')
|
|
||||||
def sync(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
allow_prod: AllowProdOption = False,
|
|
||||||
yes: YesOption = False,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
同步调度任务配置。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_JOB_COMMAND_CONTROLLER.sync(env, output, allow_prod, yes)
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.runtime.job import JOB_RUNTIME, JobRuntimeService
|
|
||||||
from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService
|
|
||||||
|
|
||||||
from .presenter import JobCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class JobCommandController:
|
|
||||||
"""
|
|
||||||
定时任务命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `job` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 定时任务命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: JobCommandPresenter | None = None,
|
|
||||||
operations_runtime: OperationsRuntimeService | None = None,
|
|
||||||
job_runtime: JobRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化定时任务命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 定时任务命令文本渲染器
|
|
||||||
:param operations_runtime: 运维运行时服务
|
|
||||||
:param job_runtime: 定时任务运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or JobCommandPresenter()
|
|
||||||
self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME
|
|
||||||
self.job_runtime = job_runtime or JOB_RUNTIME
|
|
||||||
|
|
||||||
def list(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
job_name: str,
|
|
||||||
job_group: str,
|
|
||||||
status: str | None,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看定时任务列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 状态过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.job_runtime.list_jobs(
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_job_list_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def run_once(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None:
|
|
||||||
"""
|
|
||||||
执行一次定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job run-once')
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.job_runtime.run_job_once(job_id))
|
|
||||||
)
|
|
||||||
|
|
||||||
def detail(self, job_id: int, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
查看单个定时任务详情。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.job_runtime.get_job_detail(job_id))
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_job_detail_text,
|
|
||||||
text_condition=lambda result_data: 'error' not in result_data,
|
|
||||||
)
|
|
||||||
|
|
||||||
def logs(
|
|
||||||
self,
|
|
||||||
env: str,
|
|
||||||
output: str,
|
|
||||||
*,
|
|
||||||
job_name: str,
|
|
||||||
job_group: str,
|
|
||||||
status: str | None,
|
|
||||||
begin_date: str,
|
|
||||||
end_date: str,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
查看定时任务执行日志列表。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 执行状态过滤条件
|
|
||||||
:param begin_date: 查询开始日期
|
|
||||||
:param end_date: 查询结束日期
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(
|
|
||||||
self.job_runtime.list_job_logs(
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_job_logs_text,
|
|
||||||
text_condition=lambda result_data: bool(result_data.get('ok', False)),
|
|
||||||
)
|
|
||||||
|
|
||||||
def pause(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None:
|
|
||||||
"""
|
|
||||||
暂停定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job pause')
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.job_runtime.pause_job(job_id))
|
|
||||||
)
|
|
||||||
|
|
||||||
def resume(self, job_id: int, env: str, output: str, allow_prod: bool, yes: bool) -> None:
|
|
||||||
"""
|
|
||||||
恢复定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job resume')
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.job_runtime.resume_job(job_id))
|
|
||||||
)
|
|
||||||
|
|
||||||
def sync(self, env: str, output: str, allow_prod: bool, yes: bool) -> None:
|
|
||||||
"""
|
|
||||||
同步调度任务配置。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param allow_prod: 是否允许生产环境危险命令
|
|
||||||
:param yes: 是否跳过确认
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, False, command_name='job sync')
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx,
|
|
||||||
self.execution_service.run_async(self.operations_runtime.sync_jobs()),
|
|
||||||
)
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class JobCommandPresenter:
|
|
||||||
"""
|
|
||||||
定时任务命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `job` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_job_list_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将任务列表结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 任务列表结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [f'ok: {str(payload.get("ok", False)).lower()}']
|
|
||||||
|
|
||||||
filters = payload.get('filters')
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
lines.extend(self._build_job_filter_lines(filters))
|
|
||||||
|
|
||||||
page_payload = payload.get('page')
|
|
||||||
if isinstance(page_payload, dict):
|
|
||||||
rows = page_payload.get('rows', [])
|
|
||||||
lines.append(
|
|
||||||
'page: '
|
|
||||||
f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} '
|
|
||||||
f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})'
|
|
||||||
)
|
|
||||||
lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}')
|
|
||||||
if not isinstance(rows, list) or not rows:
|
|
||||||
lines.append('jobs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
lines.append('jobs:')
|
|
||||||
for row in rows:
|
|
||||||
if isinstance(row, dict):
|
|
||||||
lines.extend([f' {item}' for item in self._build_job_item_lines(row)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
items = payload.get('items')
|
|
||||||
lines.append(f'count: {payload.get("count", 0)}')
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
lines.append('jobs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('jobs:')
|
|
||||||
for item in items:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_job_item_lines(item)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_job_detail_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将单个定时任务详情渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 定时任务详情结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
job = payload.get('job')
|
|
||||||
if not isinstance(job, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'job_id: {payload.get("jobId", "-")}',
|
|
||||||
'job: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'job_id: {job.get("jobId", "-")}',
|
|
||||||
f'job_name: {job.get("jobName", "-")}',
|
|
||||||
f'job_group: {job.get("jobGroup", "-")}',
|
|
||||||
f'job_executor: {job.get("jobExecutor", "-")}',
|
|
||||||
f'status: {job.get("status", "-")}',
|
|
||||||
f'cron_expression: {job.get("cronExpression", "-")}',
|
|
||||||
f'invoke_target: {SHELL_TEXT_FORMATTER.truncate_text(job.get("invokeTarget", ""), 160) or "-"}',
|
|
||||||
f'job_args: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobArgs", ""), 120) or "-"}',
|
|
||||||
f'job_kwargs: {SHELL_TEXT_FORMATTER.truncate_text(job.get("jobKwargs", ""), 120) or "-"}',
|
|
||||||
f'misfire_policy: {job.get("misfirePolicy", "-")}',
|
|
||||||
f'concurrent: {job.get("concurrent", "-")}',
|
|
||||||
f'remark: {SHELL_TEXT_FORMATTER.truncate_text(job.get("remark", ""), 160) or "-"}',
|
|
||||||
f'create_by: {job.get("createBy", "-")}',
|
|
||||||
f'create_time: {job.get("createTime", "-")}',
|
|
||||||
f'update_by: {job.get("updateBy", "-")}',
|
|
||||||
f'update_time: {job.get("updateTime", "-")}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_job_logs_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将定时任务日志列表结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 定时任务日志列表结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
lines = [f'ok: {str(payload.get("ok", False)).lower()}']
|
|
||||||
|
|
||||||
filters = payload.get('filters')
|
|
||||||
if isinstance(filters, dict):
|
|
||||||
lines.extend(self._build_job_filter_lines(filters))
|
|
||||||
|
|
||||||
page_payload = payload.get('page')
|
|
||||||
if isinstance(page_payload, dict):
|
|
||||||
rows = page_payload.get('rows', [])
|
|
||||||
lines.append(
|
|
||||||
'page: '
|
|
||||||
f'{page_payload.get("pageNum", "-")}/{page_payload.get("pages", "-")} '
|
|
||||||
f'(page_size={page_payload.get("pageSize", "-")}, total={page_payload.get("total", "-")})'
|
|
||||||
)
|
|
||||||
lines.append(f'count: {len(rows) if isinstance(rows, list) else 0}')
|
|
||||||
if not isinstance(rows, list) or not rows:
|
|
||||||
lines.append('logs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
lines.append('logs:')
|
|
||||||
for row in rows:
|
|
||||||
if isinstance(row, dict):
|
|
||||||
lines.extend([f' {item}' for item in self._build_job_log_item_lines(row)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
items = payload.get('items')
|
|
||||||
lines.append(f'count: {payload.get("count", 0)}')
|
|
||||||
if not isinstance(items, list) or not items:
|
|
||||||
lines.append('logs: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('logs:')
|
|
||||||
for item in items:
|
|
||||||
if isinstance(item, dict):
|
|
||||||
lines.extend([f' {line}' for line in self._build_job_log_item_lines(item)])
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_job_filter_lines(filters: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建任务列表过滤条件文本行。
|
|
||||||
|
|
||||||
:param filters: 过滤条件字典
|
|
||||||
:return: 过滤条件文本行列表
|
|
||||||
"""
|
|
||||||
active_filters = []
|
|
||||||
for key, value in filters.items():
|
|
||||||
if value in (None, '', False):
|
|
||||||
continue
|
|
||||||
active_filters.append(f'{SHELL_TEXT_FORMATTER.to_snake_case(key)}={value}')
|
|
||||||
if not active_filters:
|
|
||||||
return ['filters: none']
|
|
||||||
return ['filters:', *[f' - {item}' for item in active_filters]]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_job_item_lines(job_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条任务记录的文本行。
|
|
||||||
|
|
||||||
:param job_item: 单条任务记录
|
|
||||||
:return: 任务记录文本行列表
|
|
||||||
"""
|
|
||||||
job_id = job_item.get('jobId', '-')
|
|
||||||
job_name = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobName', ''), 30)
|
|
||||||
job_group = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobGroup', ''), 16)
|
|
||||||
status = job_item.get('status', '-')
|
|
||||||
cron_expression = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('cronExpression', ''), 40)
|
|
||||||
invoke_target = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('invokeTarget', ''), 80)
|
|
||||||
executor = SHELL_TEXT_FORMATTER.truncate_text(job_item.get('jobExecutor', ''), 24)
|
|
||||||
return [
|
|
||||||
f'- [{job_id}] {job_name} | 组: {job_group} | 状态: {status}',
|
|
||||||
f' cron: {cron_expression or "-"}',
|
|
||||||
f' invoke: {invoke_target or "-"}',
|
|
||||||
f' executor: {executor or "-"}',
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_job_log_item_lines(job_log_item: dict[str, object]) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建单条定时任务日志记录的文本行。
|
|
||||||
|
|
||||||
:param job_log_item: 单条定时任务日志记录
|
|
||||||
:return: 文本行列表
|
|
||||||
"""
|
|
||||||
job_log_id = job_log_item.get('jobLogId', '-')
|
|
||||||
job_name = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobName', ''), 30)
|
|
||||||
job_group = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobGroup', ''), 16)
|
|
||||||
status = job_log_item.get('status', '-')
|
|
||||||
job_trigger = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobTrigger', ''), 40)
|
|
||||||
job_message = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('jobMessage', ''), 80)
|
|
||||||
exception_info = SHELL_TEXT_FORMATTER.truncate_text(job_log_item.get('exceptionInfo', ''), 120)
|
|
||||||
return [
|
|
||||||
f'- [{job_log_id}] {job_name} | 组: {job_group} | 状态: {status}',
|
|
||||||
f' trigger: {job_trigger or "-"}',
|
|
||||||
f' message: {job_message or "-"}',
|
|
||||||
f' exception: {exception_info or "-"}',
|
|
||||||
f' create_time: {job_log_item.get("createTime", "-")}',
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .command import app
|
|
||||||
|
|
||||||
__all__ = ['app']
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import EnvOption, OutputOption
|
|
||||||
|
|
||||||
from .controller import OpsCommandController
|
|
||||||
|
|
||||||
app = typer.Typer(
|
|
||||||
help='运维相关命令',
|
|
||||||
no_args_is_help=True,
|
|
||||||
context_settings={'help_option_names': ['-h', '--help']},
|
|
||||||
)
|
|
||||||
_OPS_COMMAND_CONTROLLER = OpsCommandController()
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('ping-db', help='检查数据库连接')
|
|
||||||
def ping_db(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
检查数据库连接。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_OPS_COMMAND_CONTROLLER.ping_db(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('ping-redis', help='检查 Redis 连接')
|
|
||||||
def ping_redis_command(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
检查 Redis 连接。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_OPS_COMMAND_CONTROLLER.ping_redis(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('health', help='查看基础健康检查结果')
|
|
||||||
def health(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出基础健康检查结果。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_OPS_COMMAND_CONTROLLER.health(env, output)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('deps', help='查看当前 CLI 和后端运行依赖版本')
|
|
||||||
def deps(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
include_dev: bool = typer.Option(False, '--include-dev', help='附带输出开发依赖'),
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
检查当前 CLI 和后端运行依赖版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param include_dev: 是否附带输出开发依赖
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_OPS_COMMAND_CONTROLLER.deps(env, output, include_dev=include_dev)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command('server-info', help='查看服务器运行时信息')
|
|
||||||
def server_info(
|
|
||||||
env: EnvOption = 'dev',
|
|
||||||
output: OutputOption = 'text',
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
输出服务器运行时信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
_OPS_COMMAND_CONTROLLER.server_info(env, output)
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliContextFactory,
|
|
||||||
CliExecutionService,
|
|
||||||
)
|
|
||||||
from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS
|
|
||||||
from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService
|
|
||||||
from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService
|
|
||||||
|
|
||||||
from .presenter import OpsCommandPresenter
|
|
||||||
|
|
||||||
|
|
||||||
class OpsCommandController:
|
|
||||||
"""
|
|
||||||
运维命令控制器。
|
|
||||||
|
|
||||||
该控制器负责组织 `ops` 命令组的上下文准备、runtime 调用、
|
|
||||||
payload 注入,以及基于输出格式选择 presenter 或直接返回 JSON。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 运维命令文本渲染器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
context_factory: CliContextFactory | None = None,
|
|
||||||
execution_service: CliExecutionService | None = None,
|
|
||||||
presenter: OpsCommandPresenter | None = None,
|
|
||||||
database_runtime: DatabaseRuntimeService | None = None,
|
|
||||||
operations_runtime: OperationsRuntimeService | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化运维命令控制器。
|
|
||||||
|
|
||||||
:param context_factory: CLI 上下文工厂
|
|
||||||
:param execution_service: CLI 执行服务
|
|
||||||
:param presenter: 运维命令文本渲染器
|
|
||||||
:param database_runtime: 数据库运行时服务
|
|
||||||
:param operations_runtime: 运维运行时服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
||||||
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
||||||
self.presenter = presenter or OpsCommandPresenter()
|
|
||||||
self.database_runtime = database_runtime or DATABASE_RUNTIME
|
|
||||||
self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME
|
|
||||||
|
|
||||||
def ping_db(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
检查数据库连接。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx, self.execution_service.run_async(self.database_runtime.ping_database())
|
|
||||||
)
|
|
||||||
|
|
||||||
def ping_redis(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
检查 Redis 连接。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
self.execution_service.complete_payload(
|
|
||||||
ctx,
|
|
||||||
self.execution_service.run_async(self.operations_runtime.ping_redis()),
|
|
||||||
)
|
|
||||||
|
|
||||||
def health(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
输出基础健康检查结果。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
db_status = self.execution_service.run_async(self.database_runtime.ping_database())
|
|
||||||
redis_status = self.execution_service.run_async(self.operations_runtime.ping_redis())
|
|
||||||
payload = {
|
|
||||||
'env': ctx.env,
|
|
||||||
'database': db_status,
|
|
||||||
'redis': redis_status,
|
|
||||||
'ok': db_status.get('ok', False) and redis_status.get('ok', False),
|
|
||||||
}
|
|
||||||
exit_code = SUCCESS if payload['ok'] else DEPENDENCY_ERROR
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_health_text,
|
|
||||||
default_exit_code=exit_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
def deps(self, env: str, output: str, *, include_dev: bool) -> None:
|
|
||||||
"""
|
|
||||||
检查当前 CLI 和后端运行依赖版本。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:param include_dev: 是否附带输出开发依赖
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.operations_runtime.get_dependency_versions(include_dev=include_dev)
|
|
||||||
exit_code = SUCCESS if payload.get('ok', False) else DEPENDENCY_ERROR
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_dependencies_text,
|
|
||||||
default_exit_code=exit_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
def server_info(self, env: str, output: str) -> None:
|
|
||||||
"""
|
|
||||||
输出服务器运行时信息。
|
|
||||||
|
|
||||||
:param env: 当前命令运行环境
|
|
||||||
:param output: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
ctx = self.context_factory.build_readonly(env, output)
|
|
||||||
payload = self.execution_service.run_async(self.operations_runtime.get_server_info())
|
|
||||||
self.execution_service.complete_payload_with_text(
|
|
||||||
ctx,
|
|
||||||
payload,
|
|
||||||
text_builder=self.presenter.build_server_info_text,
|
|
||||||
default_exit_code=SUCCESS,
|
|
||||||
text_condition=lambda data: data.get('ok', False),
|
|
||||||
)
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
from cli.utils import SHELL_TEXT_FORMATTER
|
|
||||||
|
|
||||||
|
|
||||||
class OpsCommandPresenter:
|
|
||||||
"""
|
|
||||||
运维命令文本渲染器。
|
|
||||||
|
|
||||||
该渲染器负责将 `ops` 命令组产生的结构化 payload 转换为稳定的文本摘要,
|
|
||||||
同时保持 JSON 输出仍由控制器直接返回,不在此处做契约变形。
|
|
||||||
"""
|
|
||||||
|
|
||||||
def build_health_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将基础健康检查结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 健康检查结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'env: {payload.get("env", "")}',
|
|
||||||
'checks:',
|
|
||||||
self._build_check_status_line('database', payload.get('database')),
|
|
||||||
self._build_check_status_line('redis', payload.get('redis')),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_server_info_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将服务器运行时信息渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 服务器运行时信息结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
server = payload.get('server')
|
|
||||||
if not isinstance(server, dict):
|
|
||||||
return '\n'.join(
|
|
||||||
[
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
'server: none',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
sys_info = server.get('sys')
|
|
||||||
cpu_info = server.get('cpu')
|
|
||||||
mem_info = server.get('mem')
|
|
||||||
py_info = server.get('py')
|
|
||||||
sys_files = server.get('sysFiles')
|
|
||||||
|
|
||||||
lines = [f'ok: {str(payload.get("ok", False)).lower()}']
|
|
||||||
if isinstance(sys_info, dict):
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
'host:',
|
|
||||||
f' name: {sys_info.get("computerName", "-")}',
|
|
||||||
f' ip: {sys_info.get("computerIp", "-")}',
|
|
||||||
f' os: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("osName", "-"), 90)}',
|
|
||||||
f' arch: {sys_info.get("osArch", "-")}',
|
|
||||||
f' user_dir: {SHELL_TEXT_FORMATTER.truncate_text(sys_info.get("userDir", "-"), 100)}',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(cpu_info, dict):
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
'cpu:',
|
|
||||||
f' cores: {cpu_info.get("cpuNum", "-")}',
|
|
||||||
f' used: {cpu_info.get("used", "-")}%',
|
|
||||||
f' sys: {cpu_info.get("sys", "-")}%',
|
|
||||||
f' free: {cpu_info.get("free", "-")}%',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(mem_info, dict):
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
'memory:',
|
|
||||||
f' total: {mem_info.get("total", "-")}',
|
|
||||||
f' used: {mem_info.get("used", "-")}',
|
|
||||||
f' free: {mem_info.get("free", "-")}',
|
|
||||||
f' usage: {mem_info.get("usage", "-")}%',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(py_info, dict):
|
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
'python:',
|
|
||||||
f' name: {py_info.get("name", "-")}',
|
|
||||||
f' version: {py_info.get("version", "-")}',
|
|
||||||
f' start_time: {py_info.get("startTime", "-")}',
|
|
||||||
f' run_time: {py_info.get("runTime", "-")}',
|
|
||||||
f' home: {SHELL_TEXT_FORMATTER.truncate_text(py_info.get("home", "-"), 100)}',
|
|
||||||
f' process_memory: {py_info.get("used", "-")} / {py_info.get("total", "-")} ({py_info.get("usage", "-")}%)',
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(sys_files, list):
|
|
||||||
lines.append(f'disks: {len(sys_files)}')
|
|
||||||
if sys_files:
|
|
||||||
lines.append('disk_samples:')
|
|
||||||
lines.extend(
|
|
||||||
f' - {item.get("dirName", "-")} | used: {item.get("used", "-")} / {item.get("total", "-")} | usage: {item.get("usage", "-")}'
|
|
||||||
for item in sys_files[:10]
|
|
||||||
if isinstance(item, dict)
|
|
||||||
)
|
|
||||||
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
def build_dependencies_text(self, payload: dict[str, object]) -> str:
|
|
||||||
"""
|
|
||||||
将依赖版本检查结果渲染为文本摘要。
|
|
||||||
|
|
||||||
:param payload: 依赖版本检查结果字典
|
|
||||||
:return: 文本摘要
|
|
||||||
"""
|
|
||||||
packages = payload.get('packages')
|
|
||||||
lines = [
|
|
||||||
f'ok: {str(payload.get("ok", False)).lower()}',
|
|
||||||
f'message: {payload.get("message", "-")}',
|
|
||||||
f'include_dev: {str(payload.get("includeDev", False)).lower()}',
|
|
||||||
]
|
|
||||||
missing_required = payload.get('missingRequired')
|
|
||||||
if isinstance(missing_required, list) and missing_required:
|
|
||||||
lines.append('missing_required:')
|
|
||||||
lines.extend(f' - {item}' for item in missing_required)
|
|
||||||
else:
|
|
||||||
lines.append('missing_required: none')
|
|
||||||
|
|
||||||
if not isinstance(packages, dict) or not packages:
|
|
||||||
lines.append('packages: none')
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
lines.append('packages:')
|
|
||||||
for package_name in sorted(packages):
|
|
||||||
dependency_payload = packages.get(package_name)
|
|
||||||
if isinstance(dependency_payload, dict):
|
|
||||||
lines.append(self._build_dependency_line(package_name, dependency_payload))
|
|
||||||
return '\n'.join(lines)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_check_status_line(name: str, status_payload: dict[str, object] | None) -> str:
|
|
||||||
"""
|
|
||||||
构建单项检查结果摘要行。
|
|
||||||
|
|
||||||
:param name: 检查项名称
|
|
||||||
:param status_payload: 检查结果字典
|
|
||||||
:return: 摘要行文本
|
|
||||||
"""
|
|
||||||
if not isinstance(status_payload, dict):
|
|
||||||
return f' {name}: unknown'
|
|
||||||
|
|
||||||
ok = str(status_payload.get('ok', False)).lower()
|
|
||||||
message = status_payload.get('message', '-') or '-'
|
|
||||||
error = status_payload.get('error')
|
|
||||||
if error:
|
|
||||||
return f' {name}: {ok} | {message} | error: {SHELL_TEXT_FORMATTER.truncate_text(error, 120)}'
|
|
||||||
return f' {name}: {ok} | {message}'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _build_dependency_line(name: str, dependency_payload: dict[str, object] | None) -> str:
|
|
||||||
"""
|
|
||||||
构建单个依赖项的文本摘要行。
|
|
||||||
|
|
||||||
:param name: 依赖项名称
|
|
||||||
:param dependency_payload: 依赖项结果字典
|
|
||||||
:return: 文本摘要行
|
|
||||||
"""
|
|
||||||
if not isinstance(dependency_payload, dict):
|
|
||||||
return f' {name}: not-installed'
|
|
||||||
|
|
||||||
installed = str(dependency_payload.get('installed', False)).lower()
|
|
||||||
version = dependency_payload.get('version', '-') or '-'
|
|
||||||
required = str(dependency_payload.get('required', False)).lower()
|
|
||||||
distribution = dependency_payload.get('distribution', '') or name
|
|
||||||
return f' {name}: {installed} | version: {version} | required: {required} | dist: {distribution}'
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
import sys
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import click
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import CliContext
|
|
||||||
from cli.exit_codes import GUARD_REJECTED
|
|
||||||
from cli.output import CommandResult
|
|
||||||
|
|
||||||
DangerousCommandRiskLevel = Literal['high', 'normal']
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DangerousCommandRule:
|
|
||||||
"""
|
|
||||||
危险命令保护规则。
|
|
||||||
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:param risk_level: 风险级别
|
|
||||||
:param supports_dry_run: 是否支持演练执行
|
|
||||||
"""
|
|
||||||
|
|
||||||
command_name: str
|
|
||||||
risk_level: DangerousCommandRiskLevel
|
|
||||||
supports_dry_run: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DangerousCommandRuleRegistry:
|
|
||||||
"""
|
|
||||||
危险命令规则注册表。
|
|
||||||
|
|
||||||
该注册表负责维护 CLI 内所有危险命令的风险元数据,并提供查询与
|
|
||||||
强制获取能力,作为上下文工厂、风险元数据与测试的统一入口。
|
|
||||||
|
|
||||||
:param rules: 按命令名索引的危险命令规则表
|
|
||||||
"""
|
|
||||||
|
|
||||||
rules: dict[str, DangerousCommandRule]
|
|
||||||
|
|
||||||
def get_rule(self, command_name: str) -> DangerousCommandRule | None:
|
|
||||||
"""
|
|
||||||
获取指定命令的危险命令规则。
|
|
||||||
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: 命令规则,不存在时返回 None
|
|
||||||
"""
|
|
||||||
return self.rules.get(command_name)
|
|
||||||
|
|
||||||
def require_rule(self, command_name: str) -> DangerousCommandRule:
|
|
||||||
"""
|
|
||||||
获取指定命令的危险命令规则,不存在时抛出异常。
|
|
||||||
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: 命令规则
|
|
||||||
:raises ValueError: 命令未注册危险命令规则时抛出
|
|
||||||
"""
|
|
||||||
rule = self.get_rule(command_name)
|
|
||||||
if rule is None:
|
|
||||||
raise ValueError(f'危险命令未注册保护规则:{command_name}')
|
|
||||||
return rule
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DangerousCommandResultBuilder:
|
|
||||||
"""
|
|
||||||
危险命令结果构建器。
|
|
||||||
|
|
||||||
该对象负责统一构建危险命令拒绝结果,避免保护服务内部继续拼装
|
|
||||||
结构化负载细节。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_guard_reject_result(message: str, hint: str) -> CommandResult:
|
|
||||||
"""
|
|
||||||
构建危险命令拒绝结果。
|
|
||||||
|
|
||||||
:param message: 拒绝原因
|
|
||||||
:param hint: 补充提示
|
|
||||||
:return: 命令拒绝结果
|
|
||||||
"""
|
|
||||||
return CommandResult(
|
|
||||||
data={
|
|
||||||
'ok': False,
|
|
||||||
'message': message,
|
|
||||||
'hint': hint,
|
|
||||||
},
|
|
||||||
exit_code=GUARD_REJECTED,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DangerousCommandConfirmationService:
|
|
||||||
"""
|
|
||||||
危险命令确认服务。
|
|
||||||
|
|
||||||
该对象负责处理 TTY 检测、确认提示与交互取消异常收口。
|
|
||||||
|
|
||||||
:param result_builder: 危险命令结果构建器
|
|
||||||
"""
|
|
||||||
|
|
||||||
result_builder: DangerousCommandResultBuilder
|
|
||||||
|
|
||||||
def confirm(self, ctx: CliContext, *, command_name: str) -> CommandResult | None:
|
|
||||||
"""
|
|
||||||
执行危险命令交互确认。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: 拒绝结果或 None
|
|
||||||
"""
|
|
||||||
if ctx.yes:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not sys.stdin.isatty():
|
|
||||||
return self.result_builder.build_guard_reject_result(
|
|
||||||
f'已取消危险命令执行:{command_name}',
|
|
||||||
'当前命令需要交互确认;如需非交互执行,请传入 --yes',
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
confirmed = typer.confirm(
|
|
||||||
f'确认执行危险命令 `{command_name}` 吗? 当前环境:{ctx.env}{"(dry-run)" if ctx.dry_run else ""}',
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
except (click.Abort, EOFError, KeyboardInterrupt):
|
|
||||||
return self.result_builder.build_guard_reject_result(
|
|
||||||
f'已取消危险命令执行:{command_name}',
|
|
||||||
'当前命令需要交互确认;如需非交互执行,请传入 --yes',
|
|
||||||
)
|
|
||||||
if confirmed:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return self.result_builder.build_guard_reject_result(
|
|
||||||
f'已取消危险命令执行:{command_name}',
|
|
||||||
'如需跳过确认,请传入 --yes',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DangerousCommandGuardService:
|
|
||||||
"""
|
|
||||||
危险命令保护执行服务。
|
|
||||||
|
|
||||||
该服务负责根据命令规则和 CLI 上下文执行生产环境保护、交互确认
|
|
||||||
与拒绝结果收口。
|
|
||||||
|
|
||||||
:param rule_registry: 危险命令规则注册表
|
|
||||||
:param result_builder: 危险命令结果构建器
|
|
||||||
:param confirmation_service: 危险命令确认服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
rule_registry: DangerousCommandRuleRegistry
|
|
||||||
result_builder: DangerousCommandResultBuilder = field(default_factory=DangerousCommandResultBuilder)
|
|
||||||
confirmation_service: DangerousCommandConfirmationService = field(init=False)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
|
||||||
"""
|
|
||||||
初始化危险命令保护服务依赖。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.confirmation_service = DangerousCommandConfirmationService(self.result_builder)
|
|
||||||
|
|
||||||
def guard(self, ctx: CliContext, *, rule: DangerousCommandRule) -> CommandResult | None:
|
|
||||||
"""
|
|
||||||
执行危险命令保护与确认。
|
|
||||||
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:param rule: 危险命令规则
|
|
||||||
:return: 拒绝结果或 None
|
|
||||||
"""
|
|
||||||
command_name = rule.command_name
|
|
||||||
if ctx.env == 'prod' and not ctx.allow_prod:
|
|
||||||
return self.result_builder.build_guard_reject_result(
|
|
||||||
f'生产环境默认禁止直接执行危险命令:{command_name}',
|
|
||||||
'如确认执行,请传入 --allow-prod;如需跳过确认,请同时传入 --yes',
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.confirmation_service.confirm(ctx, command_name=command_name)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_DANGEROUS_COMMAND_RULES: dict[str, DangerousCommandRule] = {
|
|
||||||
'cache clear': DangerousCommandRule(command_name='cache clear', risk_level='high', supports_dry_run=True),
|
|
||||||
'cache warmup': DangerousCommandRule(command_name='cache warmup', risk_level='normal', supports_dry_run=False),
|
|
||||||
'db upgrade': DangerousCommandRule(command_name='db upgrade', risk_level='high', supports_dry_run=True),
|
|
||||||
'db init': DangerousCommandRule(command_name='db init', risk_level='high', supports_dry_run=True),
|
|
||||||
'db downgrade': DangerousCommandRule(command_name='db downgrade', risk_level='high', supports_dry_run=True),
|
|
||||||
'db revision': DangerousCommandRule(command_name='db revision', risk_level='high', supports_dry_run=True),
|
|
||||||
'config set': DangerousCommandRule(command_name='config set', risk_level='high', supports_dry_run=True),
|
|
||||||
'config sync-cache': DangerousCommandRule(
|
|
||||||
command_name='config sync-cache',
|
|
||||||
risk_level='normal',
|
|
||||||
supports_dry_run=False,
|
|
||||||
),
|
|
||||||
'crypto rotate': DangerousCommandRule(command_name='crypto rotate', risk_level='high', supports_dry_run=True),
|
|
||||||
'job run-once': DangerousCommandRule(command_name='job run-once', risk_level='normal', supports_dry_run=False),
|
|
||||||
'job pause': DangerousCommandRule(command_name='job pause', risk_level='normal', supports_dry_run=False),
|
|
||||||
'job resume': DangerousCommandRule(command_name='job resume', risk_level='normal', supports_dry_run=False),
|
|
||||||
'job sync': DangerousCommandRule(command_name='job sync', risk_level='normal', supports_dry_run=False),
|
|
||||||
'gen import-table': DangerousCommandRule(command_name='gen import-table', risk_level='high', supports_dry_run=True),
|
|
||||||
'gen create-table': DangerousCommandRule(command_name='gen create-table', risk_level='high', supports_dry_run=True),
|
|
||||||
'gen export': DangerousCommandRule(command_name='gen export', risk_level='high', supports_dry_run=True),
|
|
||||||
'gen sync-db': DangerousCommandRule(command_name='gen sync-db', risk_level='normal', supports_dry_run=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY = DangerousCommandRuleRegistry(rules=DEFAULT_DANGEROUS_COMMAND_RULES)
|
|
||||||
DEFAULT_DANGEROUS_COMMAND_GUARD = DangerousCommandGuardService(rule_registry=DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from cli.core import (
|
|
||||||
DEFAULT_CORE_SERVICES,
|
|
||||||
CliApplicationBuilder,
|
|
||||||
CompletionDispatcher,
|
|
||||||
ProjectRuntimeLocator,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliImportArgvScope:
|
|
||||||
"""
|
|
||||||
CLI 导入期参数作用域。
|
|
||||||
|
|
||||||
该对象负责在根应用构建阶段暂时切换 `sys.argv`,只保留导入期
|
|
||||||
需要的最小参数集,避免该过程继续以内联 `try/finally` 形式散落
|
|
||||||
在入口运行器中。
|
|
||||||
|
|
||||||
:param project_runtime_locator: CLI 项目运行时定位器
|
|
||||||
"""
|
|
||||||
|
|
||||||
project_runtime_locator: ProjectRuntimeLocator
|
|
||||||
|
|
||||||
def run(self, callback: object) -> object:
|
|
||||||
"""
|
|
||||||
在导入期参数作用域内执行回调。
|
|
||||||
|
|
||||||
:param callback: 待执行回调
|
|
||||||
:return: 回调执行结果
|
|
||||||
"""
|
|
||||||
original_argv = list(sys.argv)
|
|
||||||
try:
|
|
||||||
sys.argv = self.project_runtime_locator.extract_import_argv(original_argv)
|
|
||||||
return callback()
|
|
||||||
finally:
|
|
||||||
sys.argv = original_argv
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CliMainRunner:
|
|
||||||
"""
|
|
||||||
CLI 根入口运行器。
|
|
||||||
|
|
||||||
该运行器负责串联项目目录定位、导入期参数裁剪、根应用构建、
|
|
||||||
completion 分发与 Typer 应用启动,避免 `main.py` 继续停留在
|
|
||||||
模块级散乱编排状态。
|
|
||||||
|
|
||||||
:param project_runtime_locator: CLI 项目运行时定位器
|
|
||||||
:param completion_dispatcher: shell completion 分发器
|
|
||||||
:param application_builder: 根应用构建器
|
|
||||||
"""
|
|
||||||
|
|
||||||
project_runtime_locator: ProjectRuntimeLocator
|
|
||||||
completion_dispatcher: CompletionDispatcher
|
|
||||||
application_builder: CliApplicationBuilder
|
|
||||||
import_argv_scope: CliImportArgvScope
|
|
||||||
|
|
||||||
def build_cli(self) -> object:
|
|
||||||
"""
|
|
||||||
构建 CLI 根应用,并在导入期仅保留最小化参数集。
|
|
||||||
|
|
||||||
:return: Typer 根应用
|
|
||||||
"""
|
|
||||||
self.project_runtime_locator.ensure_backend_dir_on_sys_path()
|
|
||||||
return self.import_argv_scope.run(self.application_builder.build)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
"""
|
|
||||||
执行 Typer CLI 根应用。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
cli = self.build_cli()
|
|
||||||
self.completion_dispatcher.dispatch(cli)
|
|
||||||
cli(prog_name='shierfeng')
|
|
||||||
|
|
||||||
|
|
||||||
CLI_APPLICATION_BUILDER = CliApplicationBuilder(output_renderer=DEFAULT_CORE_SERVICES.output_renderer)
|
|
||||||
PROJECT_RUNTIME_LOCATOR = ProjectRuntimeLocator()
|
|
||||||
CLI_MAIN_RUNNER = CliMainRunner(
|
|
||||||
project_runtime_locator=PROJECT_RUNTIME_LOCATOR,
|
|
||||||
completion_dispatcher=CompletionDispatcher(),
|
|
||||||
application_builder=CLI_APPLICATION_BUILDER,
|
|
||||||
import_argv_scope=CliImportArgvScope(project_runtime_locator=PROJECT_RUNTIME_LOCATOR),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""
|
|
||||||
执行 CLI 根入口运行器。
|
|
||||||
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
CLI_MAIN_RUNNER.run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from cli.metadata.command_specs import COMPLETION_SHELL_SPEC_REGISTRY, CompletionShellSpec, CompletionShellSpecRegistry
|
|
||||||
from cli.metadata.option_specs import ENVIRONMENT_OPTION_SERVICE, EnvironmentOptionService
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'COMPLETION_SHELL_SPEC_REGISTRY',
|
|
||||||
'ENVIRONMENT_OPTION_SERVICE',
|
|
||||||
'CompletionShellSpec',
|
|
||||||
'CompletionShellSpecRegistry',
|
|
||||||
'EnvironmentOptionService',
|
|
||||||
]
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionShellSpec:
|
|
||||||
"""
|
|
||||||
Shell completion 元数据定义。
|
|
||||||
|
|
||||||
name: shell 名称
|
|
||||||
description: shell 描述文本
|
|
||||||
generator: 脚本生成方式
|
|
||||||
default_target: 默认脚本落盘路径,相对于用户 home 目录
|
|
||||||
default_rc_file: 默认 rc 文件路径,相对于用户 home 目录
|
|
||||||
auto_discovery: 是否会被 shell 自动发现
|
|
||||||
supported: 当前版本是否已支持
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
generator: Literal['click', 'custom', 'unsupported']
|
|
||||||
default_target: str
|
|
||||||
default_rc_file: str | None
|
|
||||||
auto_discovery: bool
|
|
||||||
supported: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CompletionShellSpecRegistry:
|
|
||||||
"""
|
|
||||||
completion shell 元数据注册表。
|
|
||||||
|
|
||||||
该注册表集中维护 CLI 当前支持的 shell completion 元数据,
|
|
||||||
供 installer、doctor 和 provider 等场景统一查询。
|
|
||||||
|
|
||||||
:param specs: 按 shell 名称索引的元数据表
|
|
||||||
"""
|
|
||||||
|
|
||||||
specs: dict[str, CompletionShellSpec]
|
|
||||||
|
|
||||||
def get_spec(self, shell_name: str) -> CompletionShellSpec | None:
|
|
||||||
"""
|
|
||||||
获取指定 shell 的 completion 元数据。
|
|
||||||
|
|
||||||
:param shell_name: shell 名称
|
|
||||||
:return: shell 元数据,不存在时返回 None
|
|
||||||
"""
|
|
||||||
return self.specs.get(shell_name)
|
|
||||||
|
|
||||||
def list_shell_names(self) -> list[str]:
|
|
||||||
"""
|
|
||||||
获取已注册的 shell 名称列表。
|
|
||||||
|
|
||||||
:return: shell 名称列表
|
|
||||||
"""
|
|
||||||
return list(self.specs)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COMPLETION_SHELL_SPECS: dict[str, CompletionShellSpec] = {
|
|
||||||
'bash': CompletionShellSpec(
|
|
||||||
name='bash',
|
|
||||||
description='GNU Bash shell completion',
|
|
||||||
generator='click',
|
|
||||||
default_target='.local/share/shierfeng/completion/shierfeng.bash',
|
|
||||||
default_rc_file='.bashrc',
|
|
||||||
auto_discovery=False,
|
|
||||||
supported=True,
|
|
||||||
),
|
|
||||||
'zsh': CompletionShellSpec(
|
|
||||||
name='zsh',
|
|
||||||
description='Zsh shell completion',
|
|
||||||
generator='click',
|
|
||||||
default_target='.local/share/shierfeng/completion/shierfeng.zsh',
|
|
||||||
default_rc_file='.zshrc',
|
|
||||||
auto_discovery=False,
|
|
||||||
supported=True,
|
|
||||||
),
|
|
||||||
'fish': CompletionShellSpec(
|
|
||||||
name='fish',
|
|
||||||
description='Fish shell completion',
|
|
||||||
generator='click',
|
|
||||||
default_target='.config/fish/completions/shierfeng.fish',
|
|
||||||
default_rc_file=None,
|
|
||||||
auto_discovery=True,
|
|
||||||
supported=True,
|
|
||||||
),
|
|
||||||
'powershell': CompletionShellSpec(
|
|
||||||
name='powershell',
|
|
||||||
description='Windows PowerShell / PowerShell 7 completion',
|
|
||||||
generator='custom',
|
|
||||||
default_target='Documents/PowerShell/Profile/shierfeng.ps1',
|
|
||||||
default_rc_file='Documents/PowerShell/Microsoft.PowerShell_profile.ps1',
|
|
||||||
auto_discovery=False,
|
|
||||||
supported=True,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
COMPLETION_SHELL_SPEC_REGISTRY = CompletionShellSpecRegistry(specs=DEFAULT_COMPLETION_SHELL_SPECS)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
DEFAULT_ENVIRONMENTS = ('dev', 'prod', 'dockermy')
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class EnvironmentOptionService:
|
|
||||||
"""
|
|
||||||
CLI 环境选项服务。
|
|
||||||
|
|
||||||
该服务负责发现当前项目下可用的环境名称集合,供 completion、
|
|
||||||
wizard 和诊断信息等场景复用。
|
|
||||||
|
|
||||||
:param default_environments: 默认内置环境名称列表
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_environments: tuple[str, ...] = DEFAULT_ENVIRONMENTS
|
|
||||||
|
|
||||||
def discover_env_names(self, project_dir: Path | None = None) -> list[str]:
|
|
||||||
"""
|
|
||||||
发现当前项目可用的环境名称列表。
|
|
||||||
|
|
||||||
:param project_dir: 后端项目目录,默认使用当前工作目录
|
|
||||||
:return: 去重且排序后的环境名称列表
|
|
||||||
"""
|
|
||||||
resolved_project_dir = (project_dir or Path.cwd()).resolve()
|
|
||||||
env_names = set(self.default_environments)
|
|
||||||
for env_file in resolved_project_dir.glob('.env.*'):
|
|
||||||
suffix = env_file.name.removeprefix('.env.').strip()
|
|
||||||
if suffix:
|
|
||||||
env_names.add(suffix)
|
|
||||||
return sorted(env_names)
|
|
||||||
|
|
||||||
|
|
||||||
ENVIRONMENT_OPTION_SERVICE = EnvironmentOptionService()
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from cli.guards import DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY, DangerousCommandRuleRegistry
|
|
||||||
|
|
||||||
CommandRiskLevel = Literal['readonly', 'normal', 'high']
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CommandRiskSpec:
|
|
||||||
"""
|
|
||||||
CLI 命令风险元数据定义。
|
|
||||||
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:param risk_level: 风险级别
|
|
||||||
:param supports_dry_run: 是否支持演练执行
|
|
||||||
"""
|
|
||||||
|
|
||||||
command_name: str
|
|
||||||
risk_level: CommandRiskLevel
|
|
||||||
supports_dry_run: bool
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class CommandRiskSpecRegistry:
|
|
||||||
"""
|
|
||||||
CLI 命令风险元数据注册表。
|
|
||||||
|
|
||||||
该注册表负责从危险命令规则注册表派生风险元数据,供 TUI、文档、
|
|
||||||
诊断提示等只读场景统一查询。
|
|
||||||
|
|
||||||
:param specs: 按命令名索引的风险元数据表
|
|
||||||
"""
|
|
||||||
|
|
||||||
specs: dict[str, CommandRiskSpec]
|
|
||||||
|
|
||||||
def get_spec(self, command_name: str) -> CommandRiskSpec | None:
|
|
||||||
"""
|
|
||||||
获取指定命令的风险元数据。
|
|
||||||
|
|
||||||
:param command_name: 命令唯一标识
|
|
||||||
:return: 风险元数据,不存在时返回 None
|
|
||||||
"""
|
|
||||||
return self.specs.get(command_name)
|
|
||||||
|
|
||||||
|
|
||||||
class CommandRiskSpecRegistryBuilder:
|
|
||||||
"""
|
|
||||||
CLI 命令风险元数据注册表构建器。
|
|
||||||
|
|
||||||
该构建器负责基于危险命令规则注册表生成命令风险元数据注册表,
|
|
||||||
避免模块导入阶段散落重复的字典推导逻辑。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build(rule_registry: DangerousCommandRuleRegistry) -> CommandRiskSpecRegistry:
|
|
||||||
"""
|
|
||||||
基于危险命令规则注册表构建风险元数据注册表。
|
|
||||||
|
|
||||||
:param rule_registry: 危险命令规则注册表
|
|
||||||
:return: 风险元数据注册表
|
|
||||||
"""
|
|
||||||
return CommandRiskSpecRegistry(
|
|
||||||
specs={
|
|
||||||
command_name: CommandRiskSpec(
|
|
||||||
command_name=rule.command_name,
|
|
||||||
risk_level=rule.risk_level,
|
|
||||||
supports_dry_run=rule.supports_dry_run,
|
|
||||||
)
|
|
||||||
for command_name, rule in rule_registry.rules.items()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
COMMAND_RISK_SPEC_REGISTRY = CommandRiskSpecRegistryBuilder.build(DEFAULT_DANGEROUS_COMMAND_RULE_REGISTRY)
|
|
||||||
@@ -1,873 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import typer
|
|
||||||
|
|
||||||
from cli.context import CliContext
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OutputSettings:
|
|
||||||
"""
|
|
||||||
输出渲染配置。
|
|
||||||
|
|
||||||
:param color_mode: 颜色模式
|
|
||||||
:param icon_mode: 图标模式
|
|
||||||
"""
|
|
||||||
|
|
||||||
color_mode: str = 'always'
|
|
||||||
icon_mode: str = 'emoji'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class StatusTokenDefinition:
|
|
||||||
"""
|
|
||||||
状态标签定义。
|
|
||||||
|
|
||||||
:param emoji: emoji 模式文案
|
|
||||||
:param ascii: ASCII 模式文案
|
|
||||||
:param none: 无图标模式文案
|
|
||||||
:param color: 默认前景色
|
|
||||||
"""
|
|
||||||
|
|
||||||
emoji: str
|
|
||||||
ascii: str
|
|
||||||
none: str
|
|
||||||
color: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CommandResult:
|
|
||||||
"""
|
|
||||||
统一命令执行结果。
|
|
||||||
|
|
||||||
:param data: 命令结果负载
|
|
||||||
:param exit_code: 命令退出码
|
|
||||||
:param already_printed: 是否已由上层提前输出
|
|
||||||
"""
|
|
||||||
|
|
||||||
data: Any = None
|
|
||||||
exit_code: int = 0
|
|
||||||
already_printed: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class OutputStatusStyler:
|
|
||||||
"""
|
|
||||||
统一负责状态标签与单行文本样式。
|
|
||||||
|
|
||||||
:param settings: 输出配置
|
|
||||||
"""
|
|
||||||
|
|
||||||
_STATUS_DEFINITIONS = {
|
|
||||||
'ok': StatusTokenDefinition(emoji='✅ OK', ascii='[OK]', none='OK', color='green'),
|
|
||||||
'fail': StatusTokenDefinition(emoji='❌ FAIL', ascii='[FAIL]', none='FAIL', color='red'),
|
|
||||||
'warn': StatusTokenDefinition(emoji='💡 HINT', ascii='[HINT]', none='HINT', color='yellow'),
|
|
||||||
'error': StatusTokenDefinition(emoji='🚨 ERROR', ascii='[ERROR]', none='ERROR', color='red'),
|
|
||||||
'info': StatusTokenDefinition(emoji='💬 INFO', ascii='[INFO]', none='INFO', color='blue'),
|
|
||||||
}
|
|
||||||
_STATUS_FIELD_KIND_MAPPING = {
|
|
||||||
'message': 'info',
|
|
||||||
'info': 'info',
|
|
||||||
'hint': 'warn',
|
|
||||||
'warn': 'warn',
|
|
||||||
'warning': 'warn',
|
|
||||||
'error': 'error',
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, settings: OutputSettings) -> None:
|
|
||||||
"""
|
|
||||||
初始化状态样式器。
|
|
||||||
|
|
||||||
:param settings: 输出配置
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
def supports_color(self, stream: Any) -> bool:
|
|
||||||
"""
|
|
||||||
判断当前输出流是否应启用颜色。
|
|
||||||
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 是否启用颜色
|
|
||||||
"""
|
|
||||||
if self.settings.color_mode == 'always':
|
|
||||||
return True
|
|
||||||
if self.settings.color_mode == 'never':
|
|
||||||
return False
|
|
||||||
if os.environ.get('NO_COLOR'):
|
|
||||||
return False
|
|
||||||
if os.environ.get('TERM') == 'dumb':
|
|
||||||
return False
|
|
||||||
return bool(getattr(stream, 'isatty', lambda: False)())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def style_text(text: str, *, fg: str | None = None, bold: bool = False, dim: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
为文本附加终端样式。
|
|
||||||
|
|
||||||
:param text: 原始文本
|
|
||||||
:param fg: 前景色
|
|
||||||
:param bold: 是否加粗
|
|
||||||
:param dim: 是否弱化显示
|
|
||||||
:return: 样式化后的文本
|
|
||||||
"""
|
|
||||||
return typer.style(text, fg=fg, bold=bold, dim=dim)
|
|
||||||
|
|
||||||
def get_status_definition(self, kind: str) -> StatusTokenDefinition:
|
|
||||||
"""
|
|
||||||
获取状态标签定义。
|
|
||||||
|
|
||||||
:param kind: 状态类型
|
|
||||||
:return: 状态标签定义
|
|
||||||
"""
|
|
||||||
return self._STATUS_DEFINITIONS.get(kind, self._STATUS_DEFINITIONS['info'])
|
|
||||||
|
|
||||||
def build_status_token(self, kind: str) -> str:
|
|
||||||
"""
|
|
||||||
根据当前图标模式构建状态标签文本。
|
|
||||||
|
|
||||||
:param kind: 标签类型
|
|
||||||
:return: 标签文本
|
|
||||||
"""
|
|
||||||
definition = self.get_status_definition(kind)
|
|
||||||
return getattr(definition, self.settings.icon_mode, definition.none)
|
|
||||||
|
|
||||||
def style_status_token(self, kind: str, *, fg: str | None = None) -> str:
|
|
||||||
"""
|
|
||||||
将状态标签渲染为高亮样式。
|
|
||||||
|
|
||||||
:param kind: 标签类型
|
|
||||||
:param fg: 前景色
|
|
||||||
:return: 样式化后的标签文本
|
|
||||||
"""
|
|
||||||
definition = self.get_status_definition(kind)
|
|
||||||
return self.style_text(self.build_status_token(kind), fg=fg or definition.color, bold=True)
|
|
||||||
|
|
||||||
def style_status_message(self, kind: str, text: str) -> str:
|
|
||||||
"""
|
|
||||||
使用统一状态图标和颜色渲染消息文本。
|
|
||||||
|
|
||||||
:param kind: 状态类型
|
|
||||||
:param text: 原始消息文本
|
|
||||||
:return: 样式化后的消息文本
|
|
||||||
"""
|
|
||||||
color = self.get_status_definition(kind).color
|
|
||||||
return f'{self.style_status_token(kind, fg=color)} {self.style_text(text, fg=color, bold=kind == "error")}'
|
|
||||||
|
|
||||||
def build_result_header(self, is_ok: bool) -> str:
|
|
||||||
"""
|
|
||||||
构建统一的结果头部文本。
|
|
||||||
|
|
||||||
:param is_ok: 是否成功
|
|
||||||
:return: 结果头部文本
|
|
||||||
"""
|
|
||||||
if is_ok:
|
|
||||||
return f'{self.build_status_token("ok")} SUCCESS'
|
|
||||||
return f'{self.build_status_token("fail")} FAILED'
|
|
||||||
|
|
||||||
def style_scalar_text(self, value: str) -> str:
|
|
||||||
"""
|
|
||||||
为标量文本值应用颜色样式。
|
|
||||||
|
|
||||||
:param value: 原始标量文本
|
|
||||||
:return: 样式化后的文本
|
|
||||||
"""
|
|
||||||
normalized_value = value.strip().lower()
|
|
||||||
if normalized_value == 'true':
|
|
||||||
return self.style_text(value, fg='green', bold=True)
|
|
||||||
if normalized_value == 'false':
|
|
||||||
return self.style_text(value, fg='red', bold=True)
|
|
||||||
if normalized_value in {'null', 'none', '-'}:
|
|
||||||
return self.style_text(value, fg='bright_black')
|
|
||||||
return value
|
|
||||||
|
|
||||||
def style_inline_status_segment(self, segment: str) -> str:
|
|
||||||
"""
|
|
||||||
为行内状态片段应用样式。
|
|
||||||
|
|
||||||
:param segment: 原始片段文本
|
|
||||||
:return: 样式化后的片段文本
|
|
||||||
"""
|
|
||||||
normalized_segment = segment.strip().lower()
|
|
||||||
if normalized_segment == 'true':
|
|
||||||
return self.style_status_token('ok', fg='green')
|
|
||||||
if normalized_segment == 'false':
|
|
||||||
return self.style_status_token('fail', fg='red')
|
|
||||||
for prefix, kind in self._STATUS_FIELD_KIND_MAPPING.items():
|
|
||||||
if normalized_segment.startswith(f'{prefix}:'):
|
|
||||||
status_message = segment.split(':', 1)[1].strip()
|
|
||||||
return self.style_status_message(kind, status_message)
|
|
||||||
if ': ' in segment:
|
|
||||||
segment_key, segment_value = segment.split(': ', 1)
|
|
||||||
styled_segment_value = self.style_inline_status_value(segment_value)
|
|
||||||
if styled_segment_value != segment_value:
|
|
||||||
return f'{segment_key}: {styled_segment_value}'
|
|
||||||
return segment
|
|
||||||
|
|
||||||
def style_inline_status_value(self, value: str) -> str:
|
|
||||||
"""
|
|
||||||
为带分隔符的行内状态值应用样式。
|
|
||||||
|
|
||||||
:param value: 原始字段值
|
|
||||||
:return: 样式化后的字段值
|
|
||||||
"""
|
|
||||||
if ' | ' not in value:
|
|
||||||
styled_value = self.style_inline_status_segment(value)
|
|
||||||
if styled_value != value:
|
|
||||||
return styled_value
|
|
||||||
return self.style_scalar_text(value)
|
|
||||||
return ' | '.join(self.style_inline_status_segment(segment) for segment in value.split(' | '))
|
|
||||||
|
|
||||||
def style_named_value(self, key: str, value: str) -> str:
|
|
||||||
"""
|
|
||||||
按字段语义为值应用样式。
|
|
||||||
|
|
||||||
:param key: 字段名
|
|
||||||
:param value: 原始字段值
|
|
||||||
:return: 样式化后的字段值
|
|
||||||
"""
|
|
||||||
normalized_key = key.strip().lower()
|
|
||||||
normalized_value = value.strip().lower()
|
|
||||||
|
|
||||||
if normalized_key == 'ok':
|
|
||||||
if normalized_value == 'true':
|
|
||||||
return self.style_status_token('ok', fg='green')
|
|
||||||
if normalized_value == 'false':
|
|
||||||
return self.style_status_token('fail', fg='red')
|
|
||||||
|
|
||||||
status_kind = self._STATUS_FIELD_KIND_MAPPING.get(normalized_key)
|
|
||||||
if status_kind:
|
|
||||||
return self.style_status_message(status_kind, value)
|
|
||||||
|
|
||||||
return self.style_inline_status_value(value)
|
|
||||||
|
|
||||||
def style_line(self, line: str) -> str:
|
|
||||||
"""
|
|
||||||
对单行文本输出应用可读性增强样式。
|
|
||||||
|
|
||||||
:param line: 原始输出行
|
|
||||||
:return: 样式化后的输出行
|
|
||||||
"""
|
|
||||||
if not line.strip():
|
|
||||||
return line
|
|
||||||
|
|
||||||
leading_spaces = len(line) - len(line.lstrip(' '))
|
|
||||||
indent = line[:leading_spaces]
|
|
||||||
content = line[leading_spaces:]
|
|
||||||
|
|
||||||
if content == self.build_result_header(True):
|
|
||||||
return (
|
|
||||||
f'{indent}{self.style_status_token("ok", fg="green")} '
|
|
||||||
f'{self.style_text("SUCCESS", fg="green", bold=True)}'
|
|
||||||
)
|
|
||||||
if content == self.build_result_header(False):
|
|
||||||
return (
|
|
||||||
f'{indent}{self.style_status_token("fail", fg="red")} {self.style_text("FAILED", fg="red", bold=True)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
if content.startswith('- '):
|
|
||||||
return (
|
|
||||||
f'{indent}{self.style_text("-", fg="bright_black", bold=True)} '
|
|
||||||
f'{self.style_inline_status_value(content[2:])}'
|
|
||||||
)
|
|
||||||
|
|
||||||
if ': ' in content:
|
|
||||||
key, value = content.split(': ', 1)
|
|
||||||
styled_key = self.style_text(f'{key}:', fg='cyan', bold=leading_spaces == 0)
|
|
||||||
return f'{indent}{styled_key} {self.style_named_value(key, value)}'
|
|
||||||
|
|
||||||
if content.endswith(':'):
|
|
||||||
return f'{indent}{self.style_text(content, fg="blue", bold=True)}'
|
|
||||||
|
|
||||||
if content == '|':
|
|
||||||
return f'{indent}{self.style_text(content, fg="bright_black")}'
|
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
def render_error_text(self, message: str, exit_code: int, stream: Any) -> str:
|
|
||||||
"""
|
|
||||||
渲染标准错误输出文本。
|
|
||||||
|
|
||||||
:param message: 错误消息
|
|
||||||
:param exit_code: 退出码
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 渲染后的错误文本
|
|
||||||
"""
|
|
||||||
error_prefix = f'{self.build_status_token("error")}[{exit_code}]:'
|
|
||||||
if not self.supports_color(stream):
|
|
||||||
return f'{error_prefix} {message}'
|
|
||||||
return (
|
|
||||||
f'{self.style_status_token("error", fg="red")}[{exit_code}]: '
|
|
||||||
f'{self.style_text(message, fg="red", bold=True)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StructuredTextRenderer:
|
|
||||||
"""
|
|
||||||
负责结构化数据到文本的转换与装饰。
|
|
||||||
|
|
||||||
:param status_styler: 状态样式器
|
|
||||||
"""
|
|
||||||
|
|
||||||
_SNAKE_CASE_BOUNDARY_PATTERN = re.compile(r'(?<!^)(?=[A-Z])')
|
|
||||||
|
|
||||||
def __init__(self, status_styler: OutputStatusStyler) -> None:
|
|
||||||
"""
|
|
||||||
初始化结构化文本渲染器。
|
|
||||||
|
|
||||||
:param status_styler: 状态样式器
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.status_styler = status_styler
|
|
||||||
|
|
||||||
def render_text_output(self, data: Any) -> str:
|
|
||||||
"""
|
|
||||||
将任意数据渲染为纯文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染数据
|
|
||||||
:return: 文本输出结果
|
|
||||||
"""
|
|
||||||
if isinstance(data, str):
|
|
||||||
return data
|
|
||||||
return '\n'.join(self.render_text_lines(data))
|
|
||||||
|
|
||||||
def colorize_text_output(self, text: str, stream: Any) -> str:
|
|
||||||
"""
|
|
||||||
按终端能力为文本输出增加颜色。
|
|
||||||
|
|
||||||
:param text: 原始文本输出
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 样式化后的文本输出
|
|
||||||
"""
|
|
||||||
if not self.status_styler.supports_color(stream):
|
|
||||||
return text
|
|
||||||
return '\n'.join(self.status_styler.style_line(line) for line in text.splitlines())
|
|
||||||
|
|
||||||
def decorate_text_output(self, text: str) -> str:
|
|
||||||
"""
|
|
||||||
对文本输出进行结构化装饰。
|
|
||||||
|
|
||||||
:param text: 原始文本输出
|
|
||||||
:return: 装饰后的文本输出
|
|
||||||
"""
|
|
||||||
lines = text.splitlines()
|
|
||||||
if not lines:
|
|
||||||
return text
|
|
||||||
|
|
||||||
first_non_empty_index = next((index for index, line in enumerate(lines) if line.strip()), None)
|
|
||||||
if first_non_empty_index is None:
|
|
||||||
return text
|
|
||||||
|
|
||||||
first_line = lines[first_non_empty_index].strip().lower()
|
|
||||||
if first_line == 'ok: true':
|
|
||||||
decorated_lines = list(lines)
|
|
||||||
decorated_lines[first_non_empty_index] = self.status_styler.build_result_header(True)
|
|
||||||
return '\n'.join(decorated_lines)
|
|
||||||
if first_line == 'ok: false':
|
|
||||||
decorated_lines = list(lines)
|
|
||||||
decorated_lines[first_non_empty_index] = self.status_styler.build_result_header(False)
|
|
||||||
return '\n'.join(decorated_lines)
|
|
||||||
return text
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_scalar(value: Any) -> str:
|
|
||||||
"""
|
|
||||||
将标量值格式化为文本输出。
|
|
||||||
|
|
||||||
:param value: 待格式化的值
|
|
||||||
:return: 文本格式化结果
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return 'null'
|
|
||||||
if isinstance(value, bool):
|
|
||||||
return 'true' if value else 'false'
|
|
||||||
return str(value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def format_field_name(cls, key: object) -> str:
|
|
||||||
"""
|
|
||||||
将字段名标准化为文本输出使用的 `snake_case` 形式。
|
|
||||||
|
|
||||||
:param key: 原始字段名
|
|
||||||
:return: 标准化后的字段名
|
|
||||||
"""
|
|
||||||
text = str(key).strip()
|
|
||||||
if not text:
|
|
||||||
return ''
|
|
||||||
normalized_text = text.replace('-', '_').replace(' ', '_')
|
|
||||||
return cls._SNAKE_CASE_BOUNDARY_PATTERN.sub('_', normalized_text).lower()
|
|
||||||
|
|
||||||
def append_multiline_text(self, lines: list[str], prefix: str, value: str, indent: str) -> None:
|
|
||||||
"""
|
|
||||||
将多行文本追加到输出行列表中。
|
|
||||||
|
|
||||||
:param lines: 输出行列表
|
|
||||||
:param prefix: 当前字段前缀
|
|
||||||
:param value: 多行文本内容
|
|
||||||
:param indent: 子级缩进
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
lines.append(f'{prefix} |')
|
|
||||||
lines.extend(f'{indent}{line}' for line in value.splitlines())
|
|
||||||
|
|
||||||
def render_nested_lines(self, prefix: str, nested_data: dict[str, Any] | list[Any], indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
渲染嵌套字典或列表字段。
|
|
||||||
|
|
||||||
:param prefix: 当前字段前缀
|
|
||||||
:param nested_data: 嵌套数据
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 渲染后的文本行列表
|
|
||||||
"""
|
|
||||||
nested_lines = self.render_text_lines(nested_data, indent_level=indent_level + 1)
|
|
||||||
if len(nested_lines) == 1 and nested_lines[0].strip() in {'{}', '[]'}:
|
|
||||||
return [f'{prefix} {nested_lines[0].strip()}']
|
|
||||||
return [prefix, *nested_lines]
|
|
||||||
|
|
||||||
def render_mapping_lines(self, data: dict[str, Any], *, indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
将字典渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染字典
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
indent = ' ' * indent_level
|
|
||||||
child_indent = ' ' * (indent_level + 1)
|
|
||||||
if not data:
|
|
||||||
return [f'{indent}{{}}']
|
|
||||||
|
|
||||||
lines: list[str] = []
|
|
||||||
for key, value in data.items():
|
|
||||||
field_name = self.format_field_name(key)
|
|
||||||
prefix = f'{indent}{field_name}:'
|
|
||||||
if isinstance(value, str) and '\n' in value:
|
|
||||||
self.append_multiline_text(lines, prefix, value, child_indent)
|
|
||||||
continue
|
|
||||||
if isinstance(value, dict | list):
|
|
||||||
lines.extend(self.render_nested_lines(prefix, value, indent_level))
|
|
||||||
continue
|
|
||||||
lines.append(f'{prefix} {self.format_scalar(value)}')
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def render_list_lines(self, data: list[Any], *, indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
将列表渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染列表
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
indent = ' ' * indent_level
|
|
||||||
child_indent = ' ' * (indent_level + 1)
|
|
||||||
if not data:
|
|
||||||
return [f'{indent}[]']
|
|
||||||
|
|
||||||
lines: list[str] = []
|
|
||||||
for item in data:
|
|
||||||
item_prefix = f'{indent}-'
|
|
||||||
if isinstance(item, str) and '\n' in item:
|
|
||||||
self.append_multiline_text(lines, item_prefix, item, child_indent)
|
|
||||||
continue
|
|
||||||
if isinstance(item, dict | list):
|
|
||||||
lines.extend(self.render_nested_lines(item_prefix, item, indent_level))
|
|
||||||
continue
|
|
||||||
lines.append(f'{item_prefix} {self.format_scalar(item)}')
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def render_text_lines(self, data: Any, *, indent_level: int = 0) -> list[str]:
|
|
||||||
"""
|
|
||||||
将任意数据结构渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染数据
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
if isinstance(data, dict):
|
|
||||||
return self.render_mapping_lines(data, indent_level=indent_level)
|
|
||||||
|
|
||||||
if isinstance(data, list):
|
|
||||||
return self.render_list_lines(data, indent_level=indent_level)
|
|
||||||
|
|
||||||
indent = ' ' * indent_level
|
|
||||||
if isinstance(data, str) and '\n' in data:
|
|
||||||
return [f'{indent}{line}' for line in data.splitlines()]
|
|
||||||
|
|
||||||
return [f'{indent}{self.format_scalar(data)}']
|
|
||||||
|
|
||||||
|
|
||||||
class OutputEmitter:
|
|
||||||
"""
|
|
||||||
负责标准输出、标准错误与命令退出收口。
|
|
||||||
|
|
||||||
:param text_renderer: 结构化文本渲染器
|
|
||||||
:param status_styler: 状态样式器
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, text_renderer: StructuredTextRenderer, status_styler: OutputStatusStyler) -> None:
|
|
||||||
"""
|
|
||||||
初始化输出发射器。
|
|
||||||
|
|
||||||
:param text_renderer: 结构化文本渲染器
|
|
||||||
:param status_styler: 状态样式器
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.text_renderer = text_renderer
|
|
||||||
self.status_styler = status_styler
|
|
||||||
|
|
||||||
def emit_output(self, data: Any, output_format: str) -> None:
|
|
||||||
"""
|
|
||||||
输出命令结果。
|
|
||||||
|
|
||||||
:param data: 输出数据
|
|
||||||
:param output_format: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if data is None:
|
|
||||||
return
|
|
||||||
if output_format == 'json':
|
|
||||||
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
|
|
||||||
return
|
|
||||||
rendered_text = self.text_renderer.decorate_text_output(self.text_renderer.render_text_output(data))
|
|
||||||
print(self.text_renderer.colorize_text_output(rendered_text, sys.stdout))
|
|
||||||
|
|
||||||
def emit_error(self, message: str, output_format: str, *, exit_code: int) -> None:
|
|
||||||
"""
|
|
||||||
输出错误信息。
|
|
||||||
|
|
||||||
:param message: 错误消息
|
|
||||||
:param output_format: 输出格式
|
|
||||||
:param exit_code: 退出码
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if output_format == 'json':
|
|
||||||
print(
|
|
||||||
json.dumps({'ok': False, 'error': message, 'exitCode': exit_code}, ensure_ascii=False),
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
print(self.status_styler.render_error_text(message, exit_code, sys.stderr), file=sys.stderr)
|
|
||||||
|
|
||||||
def complete_command(self, result: CommandResult, ctx: CliContext) -> None:
|
|
||||||
"""
|
|
||||||
输出命令结果并结束当前命令。
|
|
||||||
|
|
||||||
:param result: 命令执行结果
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:return: None
|
|
||||||
:raises typer.Exit: 始终通过 Typer 退出并返回对应退出码
|
|
||||||
"""
|
|
||||||
if not result.already_printed:
|
|
||||||
self.emit_output(result.data, ctx.output)
|
|
||||||
raise typer.Exit(code=result.exit_code)
|
|
||||||
|
|
||||||
|
|
||||||
class OutputRenderer:
|
|
||||||
"""
|
|
||||||
CLI 输出渲染门面。
|
|
||||||
|
|
||||||
:param color_mode: 颜色模式
|
|
||||||
:param icon_mode: 图标模式
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *, color_mode: str = 'always', icon_mode: str = 'emoji') -> None:
|
|
||||||
"""
|
|
||||||
初始化输出渲染器。
|
|
||||||
|
|
||||||
:param color_mode: 颜色模式,支持 `auto`、`always`、`never`
|
|
||||||
:param icon_mode: 图标模式,支持 `emoji`、`ascii`、`none`
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.settings = OutputSettings(color_mode=color_mode, icon_mode=icon_mode)
|
|
||||||
self.status_styler = OutputStatusStyler(self.settings)
|
|
||||||
self.text_renderer = StructuredTextRenderer(self.status_styler)
|
|
||||||
self.emitter = OutputEmitter(self.text_renderer, self.status_styler)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color_mode(self) -> str:
|
|
||||||
"""
|
|
||||||
返回当前颜色模式。
|
|
||||||
|
|
||||||
:return: 颜色模式
|
|
||||||
"""
|
|
||||||
return self.settings.color_mode
|
|
||||||
|
|
||||||
@property
|
|
||||||
def icon_mode(self) -> str:
|
|
||||||
"""
|
|
||||||
返回当前图标模式。
|
|
||||||
|
|
||||||
:return: 图标模式
|
|
||||||
"""
|
|
||||||
return self.settings.icon_mode
|
|
||||||
|
|
||||||
def set_color_mode(self, color_mode: str) -> None:
|
|
||||||
"""
|
|
||||||
设置颜色模式。
|
|
||||||
|
|
||||||
:param color_mode: 颜色模式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.settings.color_mode = color_mode
|
|
||||||
|
|
||||||
def set_icon_mode(self, icon_mode: str) -> None:
|
|
||||||
"""
|
|
||||||
设置图标模式。
|
|
||||||
|
|
||||||
:param icon_mode: 图标模式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.settings.icon_mode = icon_mode
|
|
||||||
|
|
||||||
def supports_color(self, stream: Any) -> bool:
|
|
||||||
"""
|
|
||||||
判断当前输出流是否应启用颜色。
|
|
||||||
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 是否启用颜色
|
|
||||||
"""
|
|
||||||
return self.status_styler.supports_color(stream)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def style_text(text: str, *, fg: str | None = None, bold: bool = False, dim: bool = False) -> str:
|
|
||||||
"""
|
|
||||||
为文本附加终端样式。
|
|
||||||
|
|
||||||
:param text: 原始文本
|
|
||||||
:param fg: 前景色
|
|
||||||
:param bold: 是否加粗
|
|
||||||
:param dim: 是否弱化显示
|
|
||||||
:return: 样式化后的文本
|
|
||||||
"""
|
|
||||||
return OutputStatusStyler.style_text(text, fg=fg, bold=bold, dim=dim)
|
|
||||||
|
|
||||||
def build_status_token(self, kind: str) -> str:
|
|
||||||
"""
|
|
||||||
根据当前图标模式构建状态标签文本。
|
|
||||||
|
|
||||||
:param kind: 标签类型
|
|
||||||
:return: 标签文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.build_status_token(kind)
|
|
||||||
|
|
||||||
def style_status_token(self, kind: str, *, fg: str) -> str:
|
|
||||||
"""
|
|
||||||
将状态标签渲染为高亮样式。
|
|
||||||
|
|
||||||
:param kind: 标签类型
|
|
||||||
:param fg: 前景色
|
|
||||||
:return: 样式化后的标签文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_status_token(kind, fg=fg)
|
|
||||||
|
|
||||||
def style_status_message(self, kind: str, text: str) -> str:
|
|
||||||
"""
|
|
||||||
使用统一状态图标和颜色渲染消息文本。
|
|
||||||
|
|
||||||
:param kind: 状态类型
|
|
||||||
:param text: 原始消息文本
|
|
||||||
:return: 样式化后的消息文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_status_message(kind, text)
|
|
||||||
|
|
||||||
def build_result_header(self, is_ok: bool) -> str:
|
|
||||||
"""
|
|
||||||
构建统一的结果头部文本。
|
|
||||||
|
|
||||||
:param is_ok: 是否成功
|
|
||||||
:return: 结果头部文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.build_result_header(is_ok)
|
|
||||||
|
|
||||||
def style_scalar_text(self, value: str) -> str:
|
|
||||||
"""
|
|
||||||
为标量文本值应用颜色样式。
|
|
||||||
|
|
||||||
:param value: 原始标量文本
|
|
||||||
:return: 样式化后的文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_scalar_text(value)
|
|
||||||
|
|
||||||
def style_inline_status_segment(self, segment: str) -> str:
|
|
||||||
"""
|
|
||||||
为行内状态片段应用样式。
|
|
||||||
|
|
||||||
:param segment: 原始片段文本
|
|
||||||
:return: 样式化后的片段文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_inline_status_segment(segment)
|
|
||||||
|
|
||||||
def style_inline_status_value(self, value: str) -> str:
|
|
||||||
"""
|
|
||||||
为带分隔符的行内状态值应用样式。
|
|
||||||
|
|
||||||
:param value: 原始字段值
|
|
||||||
:return: 样式化后的字段值
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_inline_status_value(value)
|
|
||||||
|
|
||||||
def style_named_value(self, key: str, value: str) -> str:
|
|
||||||
"""
|
|
||||||
按字段语义为值应用样式。
|
|
||||||
|
|
||||||
:param key: 字段名
|
|
||||||
:param value: 原始字段值
|
|
||||||
:return: 样式化后的字段值
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_named_value(key, value)
|
|
||||||
|
|
||||||
def style_line(self, line: str) -> str:
|
|
||||||
"""
|
|
||||||
对单行文本输出应用可读性增强样式。
|
|
||||||
|
|
||||||
:param line: 原始输出行
|
|
||||||
:return: 样式化后的输出行
|
|
||||||
"""
|
|
||||||
return self.status_styler.style_line(line)
|
|
||||||
|
|
||||||
def render_text_output(self, data: Any) -> str:
|
|
||||||
"""
|
|
||||||
将任意数据渲染为纯文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染数据
|
|
||||||
:return: 文本输出结果
|
|
||||||
"""
|
|
||||||
return self.text_renderer.render_text_output(data)
|
|
||||||
|
|
||||||
def colorize_text_output(self, text: str, stream: Any) -> str:
|
|
||||||
"""
|
|
||||||
按终端能力为文本输出增加颜色。
|
|
||||||
|
|
||||||
:param text: 原始文本输出
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 样式化后的文本输出
|
|
||||||
"""
|
|
||||||
return self.text_renderer.colorize_text_output(text, stream)
|
|
||||||
|
|
||||||
def decorate_text_output(self, text: str) -> str:
|
|
||||||
"""
|
|
||||||
对文本输出进行结构化装饰。
|
|
||||||
|
|
||||||
:param text: 原始文本输出
|
|
||||||
:return: 装饰后的文本输出
|
|
||||||
"""
|
|
||||||
return self.text_renderer.decorate_text_output(text)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_scalar(value: Any) -> str:
|
|
||||||
"""
|
|
||||||
将标量值格式化为文本输出。
|
|
||||||
|
|
||||||
:param value: 待格式化的值
|
|
||||||
:return: 文本格式化结果
|
|
||||||
"""
|
|
||||||
return StructuredTextRenderer.format_scalar(value)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_field_name(key: object) -> str:
|
|
||||||
"""
|
|
||||||
将字段名标准化为文本输出使用的 `snake_case` 形式。
|
|
||||||
|
|
||||||
:param key: 原始字段名
|
|
||||||
:return: 标准化后的字段名
|
|
||||||
"""
|
|
||||||
return StructuredTextRenderer.format_field_name(key)
|
|
||||||
|
|
||||||
def append_multiline_text(self, lines: list[str], prefix: str, value: str, indent: str) -> None:
|
|
||||||
"""
|
|
||||||
将多行文本追加到输出行列表中。
|
|
||||||
|
|
||||||
:param lines: 输出行列表
|
|
||||||
:param prefix: 当前字段前缀
|
|
||||||
:param value: 多行文本内容
|
|
||||||
:param indent: 子级缩进
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.text_renderer.append_multiline_text(lines, prefix, value, indent)
|
|
||||||
|
|
||||||
def render_nested_lines(self, prefix: str, nested_data: dict[str, Any] | list[Any], indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
渲染嵌套字典或列表字段。
|
|
||||||
|
|
||||||
:param prefix: 当前字段前缀
|
|
||||||
:param nested_data: 嵌套数据
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 渲染后的文本行列表
|
|
||||||
"""
|
|
||||||
return self.text_renderer.render_nested_lines(prefix, nested_data, indent_level)
|
|
||||||
|
|
||||||
def render_mapping_lines(self, data: dict[str, Any], *, indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
将字典渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染字典
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
return self.text_renderer.render_mapping_lines(data, indent_level=indent_level)
|
|
||||||
|
|
||||||
def render_list_lines(self, data: list[Any], *, indent_level: int) -> list[str]:
|
|
||||||
"""
|
|
||||||
将列表渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染列表
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
return self.text_renderer.render_list_lines(data, indent_level=indent_level)
|
|
||||||
|
|
||||||
def render_text_lines(self, data: Any, *, indent_level: int = 0) -> list[str]:
|
|
||||||
"""
|
|
||||||
将任意数据结构渲染为层级化文本输出。
|
|
||||||
|
|
||||||
:param data: 待渲染数据
|
|
||||||
:param indent_level: 当前缩进层级
|
|
||||||
:return: 文本输出行列表
|
|
||||||
"""
|
|
||||||
return self.text_renderer.render_text_lines(data, indent_level=indent_level)
|
|
||||||
|
|
||||||
def emit_output(self, data: Any, output_format: str) -> None:
|
|
||||||
"""
|
|
||||||
输出命令结果。
|
|
||||||
|
|
||||||
:param data: 输出数据
|
|
||||||
:param output_format: 输出格式
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.emitter.emit_output(data, output_format)
|
|
||||||
|
|
||||||
def render_error_text(self, message: str, exit_code: int, stream: Any) -> str:
|
|
||||||
"""
|
|
||||||
渲染标准错误输出文本。
|
|
||||||
|
|
||||||
:param message: 错误消息
|
|
||||||
:param exit_code: 退出码
|
|
||||||
:param stream: 输出流对象
|
|
||||||
:return: 渲染后的错误文本
|
|
||||||
"""
|
|
||||||
return self.status_styler.render_error_text(message, exit_code, stream)
|
|
||||||
|
|
||||||
def emit_error(self, message: str, output_format: str, *, exit_code: int) -> None:
|
|
||||||
"""
|
|
||||||
输出错误信息。
|
|
||||||
|
|
||||||
:param message: 错误消息
|
|
||||||
:param output_format: 输出格式
|
|
||||||
:param exit_code: 退出码
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.emitter.emit_error(message, output_format, exit_code=exit_code)
|
|
||||||
|
|
||||||
def complete_command(self, result: CommandResult, ctx: CliContext) -> None:
|
|
||||||
"""
|
|
||||||
输出命令结果并结束当前命令。
|
|
||||||
|
|
||||||
:param result: 命令执行结果
|
|
||||||
:param ctx: CLI 上下文
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.emitter.complete_command(result, ctx)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
|
||||||
|
|
||||||
__all__ = ['RUNTIME_ENVIRONMENT', 'RuntimeEnvironmentService']
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import APP_RUNTIME, AppRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['APP_RUNTIME', 'AppRuntimeService']
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from types import ModuleType
|
|
||||||
|
|
||||||
|
|
||||||
class AppInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
应用基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载应用实例构建和环境配置模块,
|
|
||||||
供应用运行时 facade 与其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_server_module() -> ModuleType:
|
|
||||||
"""
|
|
||||||
获取应用 server 模块。
|
|
||||||
|
|
||||||
:return: server 模块
|
|
||||||
"""
|
|
||||||
return import_module('server')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_env_module() -> ModuleType:
|
|
||||||
"""
|
|
||||||
获取环境配置模块。
|
|
||||||
|
|
||||||
:return: 环境配置模块
|
|
||||||
"""
|
|
||||||
return import_module('config.env')
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
|
||||||
|
|
||||||
from .gateway import AppInfrastructureGateway
|
|
||||||
from .support import AppSnapshotSupport
|
|
||||||
|
|
||||||
|
|
||||||
class AppRuntimeService:
|
|
||||||
"""
|
|
||||||
应用运行时服务。
|
|
||||||
|
|
||||||
该服务作为应用运行时 facade,对外统一暴露应用实例构建、
|
|
||||||
应用配置快照与环境信息快照入口。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param infrastructure_gateway: 应用基础设施网关
|
|
||||||
:param snapshot_support: 应用快照支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
runtime_environment: RuntimeEnvironmentService | None = None,
|
|
||||||
infrastructure_gateway: AppInfrastructureGateway | None = None,
|
|
||||||
snapshot_support: AppSnapshotSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化应用运行时服务。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param infrastructure_gateway: 应用基础设施网关
|
|
||||||
:param snapshot_support: 应用快照支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or AppInfrastructureGateway()
|
|
||||||
self.snapshot_support = snapshot_support or AppSnapshotSupport(
|
|
||||||
self.infrastructure_gateway,
|
|
||||||
self.runtime_environment,
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_app_instance(self) -> Any:
|
|
||||||
"""
|
|
||||||
构建当前环境下的 FastAPI 应用实例。
|
|
||||||
|
|
||||||
:return: FastAPI 应用实例
|
|
||||||
"""
|
|
||||||
server_module = self.infrastructure_gateway.get_server_module()
|
|
||||||
return server_module.create_app()
|
|
||||||
|
|
||||||
def get_app_config_snapshot(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前运行环境的应用配置快照。
|
|
||||||
|
|
||||||
:return: 应用配置快照
|
|
||||||
"""
|
|
||||||
return self.snapshot_support.build_app_config_snapshot()
|
|
||||||
|
|
||||||
def get_app_env_snapshot(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前 CLI 进程的环境解析结果快照。
|
|
||||||
|
|
||||||
:return: 环境解析结果快照
|
|
||||||
"""
|
|
||||||
return self.snapshot_support.build_app_env_snapshot()
|
|
||||||
|
|
||||||
|
|
||||||
APP_RUNTIME = AppRuntimeService()
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.runtime.base import RuntimeEnvironmentService
|
|
||||||
|
|
||||||
from .gateway import AppInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class AppSnapshotSupport:
|
|
||||||
"""
|
|
||||||
应用快照支持对象。
|
|
||||||
|
|
||||||
该对象负责应用配置快照和环境快照的构建,
|
|
||||||
避免主运行时服务继续承载快照字段拼装细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 应用基础设施网关
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
infrastructure_gateway: AppInfrastructureGateway,
|
|
||||||
runtime_environment: RuntimeEnvironmentService,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化应用快照支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 应用基础设施网关
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
self.runtime_environment = runtime_environment
|
|
||||||
|
|
||||||
def build_app_config_snapshot(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前运行环境的应用配置快照。
|
|
||||||
|
|
||||||
:return: 应用配置快照
|
|
||||||
"""
|
|
||||||
env_module = self.infrastructure_gateway.get_env_module()
|
|
||||||
app_config = env_module.AppConfig
|
|
||||||
database_config = env_module.DataBaseConfig
|
|
||||||
log_config = env_module.LogConfig
|
|
||||||
redis_config = env_module.RedisConfig
|
|
||||||
transport_crypto_config = env_module.TransportCryptoConfig
|
|
||||||
return {
|
|
||||||
'env': app_config.app_env,
|
|
||||||
'name': app_config.app_name,
|
|
||||||
'host': app_config.app_host,
|
|
||||||
'port': app_config.app_port,
|
|
||||||
'rootPath': app_config.app_root_path,
|
|
||||||
'reload': app_config.app_reload,
|
|
||||||
'workers': app_config.app_workers,
|
|
||||||
'disableSwagger': app_config.app_disable_swagger,
|
|
||||||
'disableRedoc': app_config.app_disable_redoc,
|
|
||||||
'dbType': database_config.db_type,
|
|
||||||
'dbHost': database_config.db_host,
|
|
||||||
'dbPort': database_config.db_port,
|
|
||||||
'dbDatabase': database_config.db_database,
|
|
||||||
'redisHost': redis_config.redis_host,
|
|
||||||
'redisPort': redis_config.redis_port,
|
|
||||||
'logLevel': log_config.loguru_level,
|
|
||||||
'transportCryptoEnabled': transport_crypto_config.transport_crypto_enabled,
|
|
||||||
'transportCryptoMode': transport_crypto_config.transport_crypto_mode,
|
|
||||||
}
|
|
||||||
|
|
||||||
def build_app_env_snapshot(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前 CLI 进程的环境解析结果快照。
|
|
||||||
|
|
||||||
:return: 环境解析结果快照
|
|
||||||
"""
|
|
||||||
env_module = self.infrastructure_gateway.get_env_module()
|
|
||||||
app_config = env_module.AppConfig
|
|
||||||
backend_dir = Path(self.runtime_environment.get_backend_dir())
|
|
||||||
resolved_env = os.environ.get('APP_ENV', '') or 'dev'
|
|
||||||
env_file_name = f'.env.{resolved_env}'
|
|
||||||
env_file_path = backend_dir / env_file_name
|
|
||||||
return {
|
|
||||||
'cliEnv': resolved_env,
|
|
||||||
'configEnv': app_config.app_env,
|
|
||||||
'appEnv': os.environ.get('APP_ENV', ''),
|
|
||||||
'envFile': env_file_name,
|
|
||||||
'envFilePath': str(env_file_path),
|
|
||||||
'envFileExists': env_file_path.exists(),
|
|
||||||
'backendDir': str(backend_dir),
|
|
||||||
'pythonExecutable': self.runtime_environment.get_python_executable(),
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import getpass
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeEnvironmentService:
|
|
||||||
"""
|
|
||||||
CLI 运行时环境服务。
|
|
||||||
|
|
||||||
该服务集中管理后端根目录解析、Python 解释器定位等稳定运行时能力,
|
|
||||||
避免调用方继续依赖零散模块级函数。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_backend_project_dir(project_dir: Path) -> bool:
|
|
||||||
"""
|
|
||||||
判断目录是否为后端项目根目录。
|
|
||||||
|
|
||||||
:param project_dir: 待检查目录
|
|
||||||
:return: 是否为后端项目根目录
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
(project_dir / 'app.py').exists()
|
|
||||||
and (project_dir / 'config' / 'env.py').exists()
|
|
||||||
and (project_dir / 'cli').is_dir()
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_backend_dir(self) -> str:
|
|
||||||
"""
|
|
||||||
获取后端根目录绝对路径。
|
|
||||||
|
|
||||||
:return: 后端根目录绝对路径
|
|
||||||
"""
|
|
||||||
current_dir = Path.cwd().resolve()
|
|
||||||
if self.is_backend_project_dir(current_dir):
|
|
||||||
return str(current_dir)
|
|
||||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_python_executable() -> str:
|
|
||||||
"""
|
|
||||||
获取当前 CLI 进程使用的 Python 解释器。
|
|
||||||
|
|
||||||
:return: Python 解释器路径或命令名
|
|
||||||
"""
|
|
||||||
return sys.executable or 'python'
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeOperatorService:
|
|
||||||
"""
|
|
||||||
CLI 运行时操作者解析服务。
|
|
||||||
|
|
||||||
该服务集中提供当前 CLI 进程的操作者名称解析能力,
|
|
||||||
避免各领域运行时对象继续复制相同的 `getpass.getuser()` 回退逻辑。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_operator() -> str:
|
|
||||||
"""
|
|
||||||
解析当前 CLI 操作者名称。
|
|
||||||
|
|
||||||
:return: 操作者名称
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return getpass.getuser() or 'shierfeng-cli'
|
|
||||||
except Exception:
|
|
||||||
return 'shierfeng-cli'
|
|
||||||
|
|
||||||
|
|
||||||
RUNTIME_ENVIRONMENT = RuntimeEnvironmentService()
|
|
||||||
RUNTIME_OPERATOR = RuntimeOperatorService()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import CACHE_RUNTIME, CacheRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['CACHE_RUNTIME', 'CacheRuntimeService']
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
REDIS_TTL_KEY_NOT_FOUND = -2
|
|
||||||
REDIS_TTL_PERSISTENT = -1
|
|
||||||
|
|
||||||
|
|
||||||
class CacheInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
缓存基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载 Redis 相关基础设施依赖,
|
|
||||||
供缓存运行时 facade 和其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_error_class() -> type[Exception]:
|
|
||||||
"""
|
|
||||||
获取 Redis 异常类型。
|
|
||||||
|
|
||||||
:return: Redis 异常类型
|
|
||||||
"""
|
|
||||||
return import_module('redis.exceptions').RedisError
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_init_key_config() -> Any:
|
|
||||||
"""
|
|
||||||
获取系统缓存初始化键枚举。
|
|
||||||
|
|
||||||
:return: 缓存初始化键枚举
|
|
||||||
"""
|
|
||||||
return import_module('common.enums').RedisInitKeyConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Redis 工具类。
|
|
||||||
|
|
||||||
:return: Redis 工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_redis').RedisUtil
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import REDIS_TTL_KEY_NOT_FOUND, REDIS_TTL_PERSISTENT, CacheInfrastructureGateway
|
|
||||||
from .support import CacheDomainSupport, CacheRedisSupport
|
|
||||||
|
|
||||||
|
|
||||||
class CacheRuntimeService:
|
|
||||||
"""
|
|
||||||
缓存运行时服务。
|
|
||||||
|
|
||||||
该服务作为缓存运行时 facade,对外统一暴露缓存统计、键列表、键值、
|
|
||||||
TTL 读取,以及缓存清理和预热入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
:param domain_support: 缓存领域支持对象
|
|
||||||
:param redis_support: 缓存 Redis 访问支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: CacheInfrastructureGateway | None = None,
|
|
||||||
domain_support: CacheDomainSupport | None = None,
|
|
||||||
redis_support: CacheRedisSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化缓存运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
:param domain_support: 缓存领域支持对象
|
|
||||||
:param redis_support: 缓存 Redis 访问支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or CacheInfrastructureGateway()
|
|
||||||
self.domain_support = domain_support or CacheDomainSupport(self.infrastructure_gateway)
|
|
||||||
self.redis_support = redis_support or CacheRedisSupport(self.infrastructure_gateway, self.domain_support)
|
|
||||||
|
|
||||||
async def get_cache_stats(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取缓存统计信息。
|
|
||||||
|
|
||||||
:return: 缓存统计结果
|
|
||||||
"""
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, _redis_util):
|
|
||||||
info = await redis.info()
|
|
||||||
db_size = await redis.dbsize()
|
|
||||||
command_stats_dict = await redis.info('commandstats')
|
|
||||||
command_stats = [
|
|
||||||
{'name': key.split('_', 1)[1], 'value': int(value.get('calls', 0))}
|
|
||||||
for key, value in sorted(command_stats_dict.items(), key=lambda item: item[0])
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'dbSize': db_size,
|
|
||||||
'commandStats': command_stats,
|
|
||||||
'cacheNames': self.domain_support.build_cache_name_items(),
|
|
||||||
'info': info,
|
|
||||||
}
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('读取缓存统计失败', exc)
|
|
||||||
|
|
||||||
async def list_cache_keys(self, cache_name: str) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
列出指定缓存名称下的键名。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:return: 缓存键名列表结果
|
|
||||||
"""
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, _redis_util):
|
|
||||||
cache_keys: list[str] = await redis.keys(self.redis_support.build_cache_name_keys_pattern(cache_name))
|
|
||||||
key_items = self.domain_support.extract_cache_key_items(cache_name, cache_keys)
|
|
||||||
return {'ok': True, 'cacheName': cache_name, 'count': len(key_items), 'keys': key_items}
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('读取缓存键名失败', exc)
|
|
||||||
|
|
||||||
async def get_cache_value(self, cache_name: str, cache_key: str) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取指定缓存键值。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:return: 缓存值结果
|
|
||||||
"""
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key)
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, _redis_util):
|
|
||||||
cache_value = await redis.get(full_cache_key)
|
|
||||||
if cache_value is None:
|
|
||||||
return self.domain_support.build_missing_cache_result(cache_name, cache_key)
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'cacheName': cache_name,
|
|
||||||
'cacheKey': cache_key,
|
|
||||||
'fullCacheKey': full_cache_key,
|
|
||||||
'cacheValue': cache_value,
|
|
||||||
}
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('读取缓存内容失败', exc)
|
|
||||||
|
|
||||||
async def get_cache_ttl(self, cache_name: str, cache_key: str) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取指定缓存键的剩余过期时间。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:return: 缓存 TTL 结果
|
|
||||||
"""
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key)
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, _redis_util):
|
|
||||||
ttl_seconds = await redis.ttl(full_cache_key)
|
|
||||||
if ttl_seconds == REDIS_TTL_KEY_NOT_FOUND:
|
|
||||||
return self.domain_support.build_missing_cache_result(cache_name, cache_key)
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '缓存剩余过期时间读取成功' if ttl_seconds >= 0 else '缓存存在且未设置过期时间',
|
|
||||||
'cacheName': cache_name,
|
|
||||||
'cacheKey': cache_key,
|
|
||||||
'fullCacheKey': full_cache_key,
|
|
||||||
'ttlSeconds': ttl_seconds,
|
|
||||||
'persistent': ttl_seconds == REDIS_TTL_PERSISTENT,
|
|
||||||
'expires': ttl_seconds >= 0,
|
|
||||||
}
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('读取缓存剩余过期时间失败', exc)
|
|
||||||
|
|
||||||
async def clear_cache(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
cache_name: str = '',
|
|
||||||
cache_key: str = '',
|
|
||||||
clear_all: bool = False,
|
|
||||||
dry_run: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
清理缓存。
|
|
||||||
|
|
||||||
:param cache_name: 需要按缓存名称清理的前缀
|
|
||||||
:param cache_key: 需要按缓存键名模糊清理的键
|
|
||||||
:param clear_all: 是否清理全部缓存
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 缓存清理结果
|
|
||||||
"""
|
|
||||||
selected_modes = [bool(cache_name), bool(cache_key), clear_all]
|
|
||||||
if sum(selected_modes) != 1:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '必须且只能指定一种清理方式:--cache-name、--cache-key 或 --all',
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, redis_util):
|
|
||||||
target_pattern = self.redis_support.build_clear_target_pattern(
|
|
||||||
cache_name=cache_name,
|
|
||||||
cache_key=cache_key,
|
|
||||||
clear_all=clear_all,
|
|
||||||
)
|
|
||||||
if target_pattern is None:
|
|
||||||
target_keys = sorted(await redis.keys())
|
|
||||||
else:
|
|
||||||
target_keys = sorted(await redis.keys(target_pattern))
|
|
||||||
|
|
||||||
result = {
|
|
||||||
'ok': True,
|
|
||||||
'dryRun': dry_run,
|
|
||||||
'matchedCount': len(target_keys),
|
|
||||||
'matchedKeys': target_keys,
|
|
||||||
'scope': self.domain_support.build_clear_scope(
|
|
||||||
cache_name=cache_name,
|
|
||||||
cache_key=cache_key,
|
|
||||||
clear_all=clear_all,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
if dry_run:
|
|
||||||
result['message'] = '缓存清理演练完成,未执行实际删除'
|
|
||||||
return result
|
|
||||||
|
|
||||||
if target_keys:
|
|
||||||
await redis.delete(*target_keys)
|
|
||||||
|
|
||||||
if clear_all:
|
|
||||||
await redis_util.init_sys_dict(redis)
|
|
||||||
await redis_util.init_sys_config(redis)
|
|
||||||
|
|
||||||
result['message'] = '缓存清理完成'
|
|
||||||
return result
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('清理缓存失败', exc)
|
|
||||||
|
|
||||||
async def warmup_cache(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
预热系统缓存。
|
|
||||||
|
|
||||||
:return: 缓存预热执行结果
|
|
||||||
"""
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with self.redis_support.redis_session() as (redis, redis_util):
|
|
||||||
await redis_util.init_sys_dict(redis)
|
|
||||||
await redis_util.init_sys_config(redis)
|
|
||||||
return {'ok': True, 'message': '缓存预热完成'}
|
|
||||||
except redis_error as exc:
|
|
||||||
return self.redis_support.build_redis_error_result('缓存预热失败', exc)
|
|
||||||
|
|
||||||
|
|
||||||
CACHE_RUNTIME = CacheRuntimeService()
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
from contextlib import asynccontextmanager
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import CacheInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class CacheDomainSupport:
|
|
||||||
"""
|
|
||||||
缓存领域支持对象。
|
|
||||||
|
|
||||||
该对象负责缓存名称列表构建、键范围解析和 TTL 结果判定等
|
|
||||||
局部缓存规则,避免主运行时服务继续承载细碎领域判断。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, infrastructure_gateway: CacheInfrastructureGateway) -> None:
|
|
||||||
"""
|
|
||||||
初始化缓存领域支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
|
|
||||||
def build_cache_name_items(self) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
构建系统内置缓存名称列表。
|
|
||||||
|
|
||||||
:return: 缓存名称列表
|
|
||||||
"""
|
|
||||||
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'cacheName': key_config.key,
|
|
||||||
'remark': key_config.remark,
|
|
||||||
}
|
|
||||||
for key_config in redis_init_key_config
|
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_full_cache_key(cache_name: str, cache_key: str) -> str:
|
|
||||||
"""
|
|
||||||
构建缓存完整键名。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:return: 完整缓存键名
|
|
||||||
"""
|
|
||||||
return f'{cache_name}:{cache_key}'
|
|
||||||
|
|
||||||
def build_missing_cache_result(self, cache_name: str, cache_key: str) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建缓存不存在的统一结果。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名
|
|
||||||
:return: 缓存不存在结果
|
|
||||||
"""
|
|
||||||
full_cache_key = self.build_full_cache_key(cache_name, cache_key)
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': f'缓存不存在:{full_cache_key}',
|
|
||||||
'cacheName': cache_name,
|
|
||||||
'cacheKey': cache_key,
|
|
||||||
'fullCacheKey': full_cache_key,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_cache_name_pattern(cache_name: str) -> str:
|
|
||||||
"""
|
|
||||||
构建按缓存名称扫描时使用的 Redis 键模式。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:return: Redis 键匹配模式
|
|
||||||
"""
|
|
||||||
return f'{cache_name}:*'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_cache_key_items(cache_name: str, cache_keys: list[str]) -> list[str]:
|
|
||||||
"""
|
|
||||||
从缓存完整键名列表中提取相对键名。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_keys: 完整缓存键名列表
|
|
||||||
:return: 相对键名列表
|
|
||||||
"""
|
|
||||||
return [key.split(':', 1)[1] for key in sorted(cache_keys) if key.startswith(f'{cache_name}:')]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_clear_scope(
|
|
||||||
*,
|
|
||||||
cache_name: str,
|
|
||||||
cache_key: str,
|
|
||||||
clear_all: bool,
|
|
||||||
) -> dict[str, str]:
|
|
||||||
"""
|
|
||||||
构建缓存清理范围定义。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名片段
|
|
||||||
:param clear_all: 是否清理全部缓存
|
|
||||||
:return: 清理范围定义
|
|
||||||
"""
|
|
||||||
if clear_all:
|
|
||||||
return {'mode': 'all'}
|
|
||||||
if cache_name:
|
|
||||||
return {'mode': 'cacheName', 'cacheName': cache_name}
|
|
||||||
return {'mode': 'cacheKey', 'cacheKey': cache_key}
|
|
||||||
|
|
||||||
|
|
||||||
class CacheRedisSupport:
|
|
||||||
"""
|
|
||||||
缓存 Redis 访问支持对象。
|
|
||||||
|
|
||||||
该对象负责 Redis 连接生命周期、错误结果规整和缓存键模式拼装,
|
|
||||||
避免主运行时服务继续承载基础设施桥接细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
:param domain_support: 缓存领域支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
infrastructure_gateway: CacheInfrastructureGateway,
|
|
||||||
domain_support: CacheDomainSupport,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化缓存 Redis 访问支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 缓存基础设施网关
|
|
||||||
:param domain_support: 缓存领域支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
self.domain_support = domain_support
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def redis_session(self) -> Any:
|
|
||||||
"""
|
|
||||||
创建并托管 Redis 会话。
|
|
||||||
|
|
||||||
:return: `(redis, redis_util)` 元组
|
|
||||||
"""
|
|
||||||
redis = None
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
try:
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
yield redis, redis_util
|
|
||||||
finally:
|
|
||||||
if redis is not None:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_redis_error_result(message: str, exc: Exception) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建统一 Redis 异常结果。
|
|
||||||
|
|
||||||
:param message: 失败消息
|
|
||||||
:param exc: 原始异常
|
|
||||||
:return: 标准失败结果
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': message,
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': REDIS_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
def build_cache_name_keys_pattern(self, cache_name: str) -> str:
|
|
||||||
"""
|
|
||||||
构建按缓存名称读取键列表时使用的模式。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:return: Redis 键匹配模式
|
|
||||||
"""
|
|
||||||
return self.domain_support.build_cache_name_pattern(cache_name)
|
|
||||||
|
|
||||||
def build_clear_target_pattern(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
cache_name: str,
|
|
||||||
cache_key: str,
|
|
||||||
clear_all: bool,
|
|
||||||
) -> str | None:
|
|
||||||
"""
|
|
||||||
构建缓存清理目标的 Redis 键匹配模式。
|
|
||||||
|
|
||||||
:param cache_name: 缓存名称
|
|
||||||
:param cache_key: 缓存键名片段
|
|
||||||
:param clear_all: 是否清理全部缓存
|
|
||||||
:return: Redis 键匹配模式;全部清理时返回 `None`
|
|
||||||
"""
|
|
||||||
if clear_all:
|
|
||||||
return None
|
|
||||||
if cache_name:
|
|
||||||
return self.domain_support.build_cache_name_pattern(cache_name)
|
|
||||||
return f'*{cache_key}'
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import CONFIG_RUNTIME, ConfigRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['CONFIG_RUNTIME', 'ConfigRuntimeService']
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
参数配置基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载数据库会话、Redis、DAO、Service 与脱敏工具依赖,
|
|
||||||
供参数配置运行时 facade 和其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_error_class() -> type[Exception]:
|
|
||||||
"""
|
|
||||||
获取 Redis 异常类型。
|
|
||||||
|
|
||||||
:return: Redis 异常类型
|
|
||||||
"""
|
|
||||||
return import_module('redis.exceptions').RedisError
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_common_constant() -> Any:
|
|
||||||
"""
|
|
||||||
获取公共常量定义。
|
|
||||||
|
|
||||||
:return: 公共常量定义模块
|
|
||||||
"""
|
|
||||||
return import_module('common.constant').CommonConstant
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_init_key_config() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Redis 初始化键枚举。
|
|
||||||
|
|
||||||
:return: Redis 初始化键枚举
|
|
||||||
"""
|
|
||||||
return import_module('common.enums').RedisInitKeyConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_page_model() -> Any:
|
|
||||||
"""
|
|
||||||
获取分页模型类型。
|
|
||||||
|
|
||||||
:return: 分页模型类型
|
|
||||||
"""
|
|
||||||
return import_module('common.vo').PageModel
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_async_session_local() -> Any:
|
|
||||||
"""
|
|
||||||
获取异步数据库会话工厂。
|
|
||||||
|
|
||||||
:return: 异步数据库会话工厂
|
|
||||||
"""
|
|
||||||
return import_module('config.database').AsyncSessionLocal
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Redis 工具类。
|
|
||||||
|
|
||||||
:return: Redis 工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_redis').RedisUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_config_dao() -> Any:
|
|
||||||
"""
|
|
||||||
获取参数配置 DAO。
|
|
||||||
|
|
||||||
:return: 参数配置 DAO
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.dao.config_dao').ConfigDao
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_config_vo_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取参数配置 VO 模块。
|
|
||||||
|
|
||||||
:return: 参数配置 VO 模块
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.entity.vo.config_vo')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_config_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取参数配置服务类。
|
|
||||||
|
|
||||||
:return: 参数配置服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.service.config_service').ConfigService
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_log_sanitizer() -> Any:
|
|
||||||
"""
|
|
||||||
获取日志脱敏工具类。
|
|
||||||
|
|
||||||
:return: 日志脱敏工具类
|
|
||||||
"""
|
|
||||||
return import_module('utils.log_util').LogSanitizer
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, REDIS_ERROR
|
|
||||||
|
|
||||||
from .gateway import ConfigInfrastructureGateway
|
|
||||||
from .support import ConfigDomainSupport
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigRuntimeService:
|
|
||||||
"""
|
|
||||||
参数配置运行时服务。
|
|
||||||
|
|
||||||
该服务作为参数配置运行时 facade,对外统一暴露参数配置列表、详情、
|
|
||||||
写入、缓存刷新与一致性诊断入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 参数配置基础设施网关
|
|
||||||
:param domain_support: 参数配置领域支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: ConfigInfrastructureGateway | None = None,
|
|
||||||
domain_support: ConfigDomainSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化参数配置运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 参数配置基础设施网关
|
|
||||||
:param domain_support: 参数配置领域支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or ConfigInfrastructureGateway()
|
|
||||||
self.domain_support = domain_support or ConfigDomainSupport(self.infrastructure_gateway)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_cli_config_model(config_vo_module: Any, config_record: Any) -> Any | None:
|
|
||||||
"""
|
|
||||||
将 ORM 配置记录显式转换为 CLI 可用的配置模型。
|
|
||||||
|
|
||||||
`ConfigModel` 当前未统一开启 `populate_by_name`,因此 CLI 侧在处理
|
|
||||||
ORM 蛇形字段对象时,需要主动映射为驼峰 alias 输入,避免详情查询结果
|
|
||||||
被静默序列化为空。
|
|
||||||
|
|
||||||
:param config_vo_module: 配置 VO 模块
|
|
||||||
:param config_record: ORM 配置记录
|
|
||||||
:return: CLI 可用配置模型
|
|
||||||
"""
|
|
||||||
if config_record is None:
|
|
||||||
return None
|
|
||||||
return config_vo_module.ConfigModel(
|
|
||||||
configId=getattr(config_record, 'config_id', None),
|
|
||||||
configName=getattr(config_record, 'config_name', None),
|
|
||||||
configKey=getattr(config_record, 'config_key', None),
|
|
||||||
configValue=getattr(config_record, 'config_value', None),
|
|
||||||
configType=getattr(config_record, 'config_type', None),
|
|
||||||
createBy=getattr(config_record, 'create_by', None),
|
|
||||||
createTime=getattr(config_record, 'create_time', None),
|
|
||||||
updateBy=getattr(config_record, 'update_by', None),
|
|
||||||
updateTime=getattr(config_record, 'update_time', None),
|
|
||||||
remark=getattr(config_record, 'remark', None),
|
|
||||||
)
|
|
||||||
|
|
||||||
async def load_config_from_database(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
) -> tuple[Any | None, dict[str, Any] | None] | dict[str, Any]:
|
|
||||||
"""
|
|
||||||
从数据库加载参数配置。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:return: 成功时返回配置模型与序列化结果,失败时返回错误结果字典
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
config_dao = self.infrastructure_gateway.get_config_dao()
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
config_record = await config_dao.get_config_detail_by_info(
|
|
||||||
session,
|
|
||||||
config_vo_module.ConfigModel(configKey=config_key),
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
database_model = self.build_cli_config_model(config_vo_module, config_record)
|
|
||||||
return database_model, self.domain_support.serialize_config_model(database_model)
|
|
||||||
|
|
||||||
async def load_config_from_cache(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
) -> tuple[str | None, dict[str, Any] | None] | dict[str, Any]:
|
|
||||||
"""
|
|
||||||
从缓存加载参数配置。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:return: 成功时返回原始缓存值与序列化结果,失败时返回错误结果字典
|
|
||||||
"""
|
|
||||||
redis = None
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
config_service = self.infrastructure_gateway.get_config_service()
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
cache_value = await config_service.query_config_list_from_cache_services(redis, config_key)
|
|
||||||
return cache_value, self.domain_support.serialize_cache_payload(config_key, cache_value)
|
|
||||||
except redis_error as exc:
|
|
||||||
return {'ok': False, 'message': '读取参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
|
|
||||||
finally:
|
|
||||||
if redis is not None:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
async def list_configs(
|
|
||||||
self,
|
|
||||||
config_name: str = '',
|
|
||||||
config_key: str = '',
|
|
||||||
config_type: Literal['Y', 'N'] | None = None,
|
|
||||||
begin_date: str = '',
|
|
||||||
end_date: str = '',
|
|
||||||
*,
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
查询参数配置列表。
|
|
||||||
|
|
||||||
:param config_name: 参数名称过滤条件
|
|
||||||
:param config_key: 参数键名过滤条件
|
|
||||||
:param config_type: 参数类型过滤条件
|
|
||||||
:param begin_date: 开始日期
|
|
||||||
:param end_date: 结束日期
|
|
||||||
:param paged: 是否开启分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: CLI 标准结果字典
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
config_service = self.infrastructure_gateway.get_config_service()
|
|
||||||
query_model = self.domain_support.build_config_query(
|
|
||||||
config_name,
|
|
||||||
config_key,
|
|
||||||
config_type,
|
|
||||||
begin_date,
|
|
||||||
end_date,
|
|
||||||
page_num,
|
|
||||||
page_size,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await config_service.get_config_list_services(session, query_model, is_page=paged)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取参数配置列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
filters = self.domain_support.build_list_filters(
|
|
||||||
config_name=config_name,
|
|
||||||
config_key=config_key,
|
|
||||||
config_type=config_type,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
|
|
||||||
page_payload = result.model_dump(by_alias=True)
|
|
||||||
page_payload['rows'] = [
|
|
||||||
self.domain_support.sanitize_config_mapping(item) for item in page_payload.get('rows', [])
|
|
||||||
]
|
|
||||||
return {'ok': True, 'filters': filters, 'page': page_payload}
|
|
||||||
|
|
||||||
items = [self.domain_support.sanitize_config_mapping(item) for item in result]
|
|
||||||
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
|
|
||||||
|
|
||||||
async def get_config(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
*,
|
|
||||||
source: Literal['db', 'cache', 'both'] = 'both',
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
按参数键读取配置详情。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param source: 读取来源
|
|
||||||
:return: CLI 标准结果字典
|
|
||||||
"""
|
|
||||||
database_model: Any | None = None
|
|
||||||
database_payload: dict[str, Any] | None = None
|
|
||||||
cache_value: str | None = None
|
|
||||||
cache_payload: dict[str, Any] | None = None
|
|
||||||
|
|
||||||
if source in {'db', 'both'}:
|
|
||||||
database_result = await self.load_config_from_database(config_key)
|
|
||||||
if isinstance(database_result, dict):
|
|
||||||
return database_result
|
|
||||||
database_model, database_payload = database_result
|
|
||||||
|
|
||||||
if source in {'cache', 'both'}:
|
|
||||||
cache_result = await self.load_config_from_cache(config_key)
|
|
||||||
if isinstance(cache_result, dict):
|
|
||||||
error_payload = {**cache_result, 'source': source}
|
|
||||||
if database_payload is not None:
|
|
||||||
error_payload['database'] = database_payload
|
|
||||||
return error_payload
|
|
||||||
cache_value, cache_payload = cache_result
|
|
||||||
|
|
||||||
if source == 'db' and database_payload is None:
|
|
||||||
return self.domain_support.build_missing_config_result(config_key, source)
|
|
||||||
if source == 'cache' and cache_payload is None:
|
|
||||||
return self.domain_support.build_missing_config_result(config_key, source)
|
|
||||||
if source == 'both' and database_payload is None and cache_payload is None:
|
|
||||||
return self.domain_support.build_missing_config_result(config_key, source)
|
|
||||||
|
|
||||||
payload = {'ok': True, 'key': config_key, 'source': source}
|
|
||||||
if source in {'db', 'both'}:
|
|
||||||
payload['database'] = database_payload
|
|
||||||
if source in {'cache', 'both'}:
|
|
||||||
payload['cache'] = cache_payload
|
|
||||||
if source == 'both':
|
|
||||||
payload['inSync'] = (database_model.config_value if database_model else None) == cache_value
|
|
||||||
return payload
|
|
||||||
|
|
||||||
async def set_config(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
config_value: str,
|
|
||||||
*,
|
|
||||||
config_name: str | None = None,
|
|
||||||
config_type: Literal['Y', 'N'] | None = None,
|
|
||||||
remark: str | None = None,
|
|
||||||
dry_run: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
新增或更新参数配置,并同步缓存。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param config_value: 参数键值
|
|
||||||
:param config_name: 参数名称
|
|
||||||
:param config_type: 参数类型
|
|
||||||
:param remark: 备注
|
|
||||||
:param dry_run: 是否仅执行演练
|
|
||||||
:return: CLI 标准结果字典
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
config_dao = self.infrastructure_gateway.get_config_dao()
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
existing_config = None
|
|
||||||
target_config = None
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
config_record = await config_dao.get_config_detail_by_info(
|
|
||||||
session,
|
|
||||||
config_vo_module.ConfigModel(configKey=config_key),
|
|
||||||
)
|
|
||||||
existing_config = self.build_cli_config_model(config_vo_module, config_record)
|
|
||||||
if existing_config is None and not config_name:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '新增参数配置时必须传入 --name',
|
|
||||||
'exit_code': ARGUMENT_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
target_config = self.domain_support.build_target_config_model(
|
|
||||||
config_key,
|
|
||||||
config_value,
|
|
||||||
config_name,
|
|
||||||
config_type,
|
|
||||||
remark,
|
|
||||||
existing_config,
|
|
||||||
)
|
|
||||||
if dry_run:
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '参数配置演练完成,未执行实际写入',
|
|
||||||
'dryRun': True,
|
|
||||||
'action': 'update' if existing_config else 'create',
|
|
||||||
'config': self.domain_support.serialize_config_model(target_config),
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing_config:
|
|
||||||
await config_dao.edit_config_dao(session, target_config.model_dump(exclude_none=True))
|
|
||||||
else:
|
|
||||||
await config_dao.add_config_dao(session, target_config)
|
|
||||||
await session.commit()
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '写入参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
redis = None
|
|
||||||
try:
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
await redis.set(f'{redis_init_key_config.SYS_CONFIG.key}:{config_key}', config_value)
|
|
||||||
except redis_error as exc:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '参数配置已写入数据库,但同步缓存失败',
|
|
||||||
'error': str(exc),
|
|
||||||
'config': self.domain_support.serialize_config_model(target_config),
|
|
||||||
'databaseCommitted': True,
|
|
||||||
'exit_code': REDIS_ERROR,
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
if redis is not None:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '参数配置已同步到数据库和缓存',
|
|
||||||
'action': 'update' if existing_config else 'create',
|
|
||||||
'config': self.domain_support.serialize_config_model(target_config),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def sync_config_cache(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
刷新参数配置缓存。
|
|
||||||
|
|
||||||
:return: CLI 标准结果字典
|
|
||||||
"""
|
|
||||||
redis = None
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
config_service = self.infrastructure_gateway.get_config_service()
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
config_list = await config_service.get_config_list_services(
|
|
||||||
session,
|
|
||||||
config_vo_module.ConfigPageQueryModel(),
|
|
||||||
is_page=False,
|
|
||||||
)
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
await config_service.init_cache_sys_config_services(session, redis)
|
|
||||||
except redis_error as exc:
|
|
||||||
return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
finally:
|
|
||||||
if redis is not None:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
return {'ok': True, 'message': '参数缓存刷新成功', 'count': len(config_list)}
|
|
||||||
|
|
||||||
async def diagnose_config(self, *, sample_limit: int = 10) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
诊断数据库参数配置与 Redis 缓存的一致性状态。
|
|
||||||
|
|
||||||
:param sample_limit: 示例键名输出上限
|
|
||||||
:return: CLI 标准结果字典
|
|
||||||
"""
|
|
||||||
redis = None
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
config_service = self.infrastructure_gateway.get_config_service()
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
config_list = await config_service.get_config_list_services(
|
|
||||||
session,
|
|
||||||
config_vo_module.ConfigPageQueryModel(),
|
|
||||||
is_page=False,
|
|
||||||
)
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
cache_prefix = f'{redis_init_key_config.SYS_CONFIG.key}:'
|
|
||||||
cache_keys = sorted(await redis.keys(f'{cache_prefix}*'))
|
|
||||||
cache_values = await redis.mget(*cache_keys) if cache_keys else []
|
|
||||||
except redis_error as exc:
|
|
||||||
return {'ok': False, 'message': '读取参数缓存诊断信息失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取参数配置诊断信息失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
finally:
|
|
||||||
if redis is not None:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
database_map = {str(item.get('configKey')): str(item.get('configValue', '')) for item in config_list}
|
|
||||||
cache_map = {
|
|
||||||
cache_key.replace(cache_prefix, '', 1): '' if cache_value is None else str(cache_value)
|
|
||||||
for cache_key, cache_value in zip(cache_keys, cache_values, strict=False)
|
|
||||||
}
|
|
||||||
missing_in_cache = sorted(key for key in database_map if key not in cache_map)
|
|
||||||
orphan_in_cache = sorted(key for key in cache_map if key not in database_map)
|
|
||||||
mismatch_keys = sorted(
|
|
||||||
key for key, database_value in database_map.items() if key in cache_map and database_value != cache_map[key]
|
|
||||||
)
|
|
||||||
in_sync = not missing_in_cache and not orphan_in_cache and not mismatch_keys
|
|
||||||
return {
|
|
||||||
'ok': in_sync,
|
|
||||||
'message': '参数配置数据库与缓存一致' if in_sync else '参数配置存在数据库与缓存不一致项',
|
|
||||||
'databaseCount': len(database_map),
|
|
||||||
'cacheCount': len(cache_map),
|
|
||||||
'missingInCacheCount': len(missing_in_cache),
|
|
||||||
'orphanInCacheCount': len(orphan_in_cache),
|
|
||||||
'mismatchCount': len(mismatch_keys),
|
|
||||||
'sampleLimit': sample_limit,
|
|
||||||
'missingInCache': missing_in_cache[:sample_limit],
|
|
||||||
'orphanInCache': orphan_in_cache[:sample_limit],
|
|
||||||
'mismatchKeys': mismatch_keys[:sample_limit],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_RUNTIME = ConfigRuntimeService()
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import Any, Literal
|
|
||||||
|
|
||||||
from cli.exit_codes import RUNTIME_ERROR
|
|
||||||
from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService
|
|
||||||
|
|
||||||
from .gateway import ConfigInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigDomainSupport:
|
|
||||||
"""
|
|
||||||
参数配置领域支持对象。
|
|
||||||
|
|
||||||
该对象负责操作者解析、配置脱敏、序列化、查询模型构建与缺失结果构建,
|
|
||||||
避免主运行时服务继续承载大量领域规则。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 参数配置基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
infrastructure_gateway: ConfigInfrastructureGateway,
|
|
||||||
operator_service: RuntimeOperatorService = RUNTIME_OPERATOR,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化参数配置领域支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 参数配置基础设施网关
|
|
||||||
:param operator_service: 运行时操作者解析服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
self.operator_service = operator_service
|
|
||||||
|
|
||||||
def sanitize_config_mapping(self, config_mapping: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
对配置字典进行脱敏处理。
|
|
||||||
|
|
||||||
:param config_mapping: 原始配置字典
|
|
||||||
:return: 脱敏后的配置字典
|
|
||||||
"""
|
|
||||||
return self.infrastructure_gateway.get_log_sanitizer().sanitize_data(config_mapping)
|
|
||||||
|
|
||||||
def serialize_config_model(self, config_model: Any | None) -> dict[str, Any] | None:
|
|
||||||
"""
|
|
||||||
将配置模型序列化为 CLI 输出字典。
|
|
||||||
|
|
||||||
:param config_model: 配置模型对象
|
|
||||||
:return: 序列化后的配置字典
|
|
||||||
"""
|
|
||||||
if config_model is None or config_model.config_id is None:
|
|
||||||
return None
|
|
||||||
return self.sanitize_config_mapping(config_model.model_dump(by_alias=True, exclude_none=True))
|
|
||||||
|
|
||||||
def serialize_cache_payload(self, config_key: str, config_value: str | None) -> dict[str, Any] | None:
|
|
||||||
"""
|
|
||||||
将缓存中的配置值序列化为 CLI 输出字典。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param config_value: 参数键值
|
|
||||||
:return: 序列化后的缓存配置字典
|
|
||||||
"""
|
|
||||||
if config_value is None:
|
|
||||||
return None
|
|
||||||
return self.sanitize_config_mapping({'configKey': config_key, 'configValue': config_value})
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_missing_config_result(
|
|
||||||
config_key: str,
|
|
||||||
source: Literal['db', 'cache', 'both'],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建配置不存在的统一结果。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param source: 读取来源
|
|
||||||
:return: 配置不存在的结果字典
|
|
||||||
"""
|
|
||||||
message = f'参数缓存不存在:{config_key}' if source == 'cache' else f'参数配置不存在:{config_key}'
|
|
||||||
return {'ok': False, 'message': message, 'source': source, 'exit_code': RUNTIME_ERROR}
|
|
||||||
|
|
||||||
def build_config_query(
|
|
||||||
self,
|
|
||||||
config_name: str,
|
|
||||||
config_key: str,
|
|
||||||
config_type: Literal['Y', 'N'] | None,
|
|
||||||
begin_date: str,
|
|
||||||
end_date: str,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
构建配置列表查询对象。
|
|
||||||
|
|
||||||
:param config_name: 参数名称过滤条件
|
|
||||||
:param config_key: 参数键名过滤条件
|
|
||||||
:param config_type: 参数类型过滤条件
|
|
||||||
:param begin_date: 开始日期
|
|
||||||
:param end_date: 结束日期
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 配置分页查询模型
|
|
||||||
"""
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
return config_vo_module.ConfigPageQueryModel(
|
|
||||||
configName=config_name or None,
|
|
||||||
configKey=config_key or None,
|
|
||||||
configType=config_type,
|
|
||||||
beginTime=begin_date or None,
|
|
||||||
endTime=end_date or None,
|
|
||||||
pageNum=page_num,
|
|
||||||
pageSize=page_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
def build_target_config_model(
|
|
||||||
self,
|
|
||||||
config_key: str,
|
|
||||||
config_value: str,
|
|
||||||
config_name: str | None,
|
|
||||||
config_type: Literal['Y', 'N'] | None,
|
|
||||||
remark: str | None,
|
|
||||||
existing_config: Any | None,
|
|
||||||
) -> Any:
|
|
||||||
"""
|
|
||||||
基于当前状态构建目标配置模型。
|
|
||||||
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param config_value: 参数键值
|
|
||||||
:param config_name: 参数名称
|
|
||||||
:param config_type: 参数类型
|
|
||||||
:param remark: 参数备注
|
|
||||||
:param existing_config: 已存在的配置模型
|
|
||||||
:return: 目标配置模型
|
|
||||||
"""
|
|
||||||
operator = self.operator_service.resolve_operator()
|
|
||||||
current_time = datetime.now()
|
|
||||||
common_constant = self.infrastructure_gateway.get_common_constant()
|
|
||||||
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
|
|
||||||
default_config_type = existing_config.config_type if existing_config else common_constant.NO
|
|
||||||
target_config = config_vo_module.ConfigModel(
|
|
||||||
configId=existing_config.config_id if existing_config else None,
|
|
||||||
configName=config_name
|
|
||||||
if config_name is not None
|
|
||||||
else (existing_config.config_name if existing_config else None),
|
|
||||||
configKey=config_key,
|
|
||||||
configValue=config_value,
|
|
||||||
configType=config_type if config_type is not None else default_config_type,
|
|
||||||
createBy=existing_config.create_by if existing_config else operator,
|
|
||||||
createTime=existing_config.create_time if existing_config else current_time,
|
|
||||||
updateBy=operator,
|
|
||||||
updateTime=current_time,
|
|
||||||
remark=remark if remark is not None else (existing_config.remark if existing_config else None),
|
|
||||||
)
|
|
||||||
target_config.validate_fields()
|
|
||||||
return target_config
|
|
||||||
|
|
||||||
def build_list_filters(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
config_name: str,
|
|
||||||
config_key: str,
|
|
||||||
config_type: Literal['Y', 'N'] | None,
|
|
||||||
begin_date: str,
|
|
||||||
end_date: str,
|
|
||||||
paged: bool,
|
|
||||||
page_num: int,
|
|
||||||
page_size: int,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建配置列表过滤条件。
|
|
||||||
|
|
||||||
:param config_name: 参数名称
|
|
||||||
:param config_key: 参数键名
|
|
||||||
:param config_type: 参数类型
|
|
||||||
:param begin_date: 开始日期
|
|
||||||
:param end_date: 结束日期
|
|
||||||
:param paged: 是否分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 过滤条件字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'configName': config_name,
|
|
||||||
'configKey': config_key,
|
|
||||||
'configType': config_type,
|
|
||||||
'beginDate': begin_date,
|
|
||||||
'endDate': end_date,
|
|
||||||
'paged': paged,
|
|
||||||
'pageNum': page_num,
|
|
||||||
'pageSize': page_size,
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import CRYPTO_RUNTIME, CryptoRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['CRYPTO_RUNTIME', 'CryptoRuntimeService']
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
传输加密基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载 cryptography、环境配置和传输加密工具依赖,
|
|
||||||
供传输加密运行时 facade 与其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_serialization_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取 cryptography 序列化模块。
|
|
||||||
|
|
||||||
:return: 序列化模块
|
|
||||||
"""
|
|
||||||
return import_module('cryptography.hazmat.primitives.serialization')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_rsa_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取 RSA 非对称算法模块。
|
|
||||||
|
|
||||||
:return: RSA 模块
|
|
||||||
"""
|
|
||||||
return import_module('cryptography.hazmat.primitives.asymmetric.rsa')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_transport_crypto_config() -> Any:
|
|
||||||
"""
|
|
||||||
获取传输加密配置对象。
|
|
||||||
|
|
||||||
:return: 传输加密配置对象
|
|
||||||
"""
|
|
||||||
return import_module('config.env').TransportCryptoConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_transport_crypto_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取传输加密工具类。
|
|
||||||
|
|
||||||
:return: 传输加密工具类
|
|
||||||
"""
|
|
||||||
return import_module('utils.transport_crypto_util').TransportCryptoUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_transport_key_provider() -> Any:
|
|
||||||
"""
|
|
||||||
获取传输加密密钥提供者。
|
|
||||||
|
|
||||||
:return: 传输加密密钥提供者
|
|
||||||
"""
|
|
||||||
return import_module('utils.transport_crypto_util').TransportKeyProvider
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from .gateway import CryptoInfrastructureGateway
|
|
||||||
from .support import CryptoDomainSupport, CryptoResultSupport
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoRuntimeService:
|
|
||||||
"""
|
|
||||||
传输加密运行时服务。
|
|
||||||
|
|
||||||
该服务作为传输加密运行时 facade,对外统一暴露配置校验、
|
|
||||||
公钥导出、密钥生成和轮换辅助方案构建入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 传输加密基础设施网关
|
|
||||||
:param domain_support: 传输加密领域支持对象
|
|
||||||
:param result_support: 传输加密结果支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: CryptoInfrastructureGateway | None = None,
|
|
||||||
domain_support: CryptoDomainSupport | None = None,
|
|
||||||
result_support: CryptoResultSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化传输加密运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 传输加密基础设施网关
|
|
||||||
:param domain_support: 传输加密领域支持对象
|
|
||||||
:param result_support: 传输加密结果支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or CryptoInfrastructureGateway()
|
|
||||||
self.domain_support = domain_support or CryptoDomainSupport(self.infrastructure_gateway)
|
|
||||||
self.result_support = result_support or CryptoResultSupport()
|
|
||||||
|
|
||||||
def generate_crypto_key_pair(self, kid: str, key_size: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
生成新的传输加密密钥对。
|
|
||||||
|
|
||||||
:param kid: 目标密钥版本标识
|
|
||||||
:param key_size: RSA 密钥长度
|
|
||||||
:return: 密钥生成结果
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _operation() -> dict[str, Any]:
|
|
||||||
self.domain_support.validate_rsa_key_size(key_size)
|
|
||||||
private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size)
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '传输加密密钥生成完成',
|
|
||||||
'kid': kid,
|
|
||||||
'keySize': key_size,
|
|
||||||
'privateKey': private_key_pem,
|
|
||||||
'publicKey': public_key_pem,
|
|
||||||
'envPatch': self.domain_support.build_key_pair_env_patch(kid, private_key_pem, public_key_pem),
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.result_support.run_argument_guarded(_operation, failure_message='生成传输加密密钥失败')
|
|
||||||
|
|
||||||
def validate_crypto_config(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
校验传输加密运行配置。
|
|
||||||
|
|
||||||
:return: 传输加密配置校验结果
|
|
||||||
"""
|
|
||||||
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
|
|
||||||
|
|
||||||
def _operation() -> dict[str, Any]:
|
|
||||||
transport_key_provider.validate_runtime_configuration()
|
|
||||||
return {'ok': True, 'message': '传输加密配置校验通过'}
|
|
||||||
|
|
||||||
return self.result_support.run_runtime_guarded(_operation, failure_message='传输加密配置校验失败')
|
|
||||||
|
|
||||||
def export_public_key(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
导出当前运行环境的传输加密公钥信息。
|
|
||||||
|
|
||||||
:return: 公钥导出结果
|
|
||||||
"""
|
|
||||||
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
|
|
||||||
transport_crypto_util = self.infrastructure_gateway.get_transport_crypto_util()
|
|
||||||
|
|
||||||
def _operation() -> dict[str, Any]:
|
|
||||||
transport_key_provider.validate_runtime_configuration()
|
|
||||||
return {'ok': True, 'publicKey': transport_crypto_util.build_public_key_payload()}
|
|
||||||
|
|
||||||
return self.result_support.run_runtime_guarded(_operation, failure_message='导出传输加密公钥失败')
|
|
||||||
|
|
||||||
def build_rotation_payload(self, next_kid: str, key_size: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
生成传输加密密钥轮换辅助结果。
|
|
||||||
|
|
||||||
:param next_kid: 新密钥版本标识
|
|
||||||
:param key_size: 新密钥的 RSA 长度
|
|
||||||
:return: 轮换辅助结果
|
|
||||||
"""
|
|
||||||
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
|
|
||||||
|
|
||||||
def _operation() -> dict[str, Any]:
|
|
||||||
self.domain_support.validate_rsa_key_size(key_size)
|
|
||||||
transport_key_provider.validate_runtime_configuration()
|
|
||||||
current_key_pair = transport_key_provider.get_current_key_pair()
|
|
||||||
if next_kid == current_key_pair.kid:
|
|
||||||
raise ValueError('新密钥版本不能与当前版本相同')
|
|
||||||
|
|
||||||
private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size)
|
|
||||||
legacy_key_pairs = self.domain_support.load_existing_legacy_key_pairs()
|
|
||||||
merged_legacy_key_pairs = self.domain_support.merge_rotation_legacy_key_pairs(
|
|
||||||
legacy_key_pairs,
|
|
||||||
current_key_pair,
|
|
||||||
next_kid,
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '传输加密密钥轮换方案已生成',
|
|
||||||
'currentKid': current_key_pair.kid,
|
|
||||||
'nextKid': next_kid,
|
|
||||||
'keySize': key_size,
|
|
||||||
'generatedAt': datetime.now().isoformat(),
|
|
||||||
'nextKeyPair': {
|
|
||||||
'kid': next_kid,
|
|
||||||
'privateKey': private_key_pem,
|
|
||||||
'publicKey': public_key_pem,
|
|
||||||
},
|
|
||||||
'legacyKeyPairs': merged_legacy_key_pairs,
|
|
||||||
'envPatch': self.domain_support.build_rotation_env_patch(
|
|
||||||
next_kid,
|
|
||||||
private_key_pem,
|
|
||||||
public_key_pem,
|
|
||||||
merged_legacy_key_pairs,
|
|
||||||
),
|
|
||||||
'applySteps': self.result_support.build_rotation_apply_steps(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.result_support.run_argument_guarded(_operation, failure_message='生成密钥轮换方案失败')
|
|
||||||
|
|
||||||
|
|
||||||
CRYPTO_RUNTIME = CryptoRuntimeService()
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import json
|
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import CryptoInfrastructureGateway
|
|
||||||
|
|
||||||
MIN_RSA_KEY_SIZE = 2048
|
|
||||||
RSA_KEY_SIZE_STEP = 256
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoDomainSupport:
|
|
||||||
"""
|
|
||||||
传输加密领域支持对象。
|
|
||||||
|
|
||||||
该对象负责 RSA 密钥规则校验、密钥对生成和历史密钥规整,
|
|
||||||
避免主运行时服务继续承载局部安全规则和数据拼装细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 传输加密基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, infrastructure_gateway: CryptoInfrastructureGateway) -> None:
|
|
||||||
"""
|
|
||||||
初始化传输加密领域支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 传输加密基础设施网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate_rsa_key_size(key_size: int) -> None:
|
|
||||||
"""
|
|
||||||
校验 RSA 密钥长度是否合法。
|
|
||||||
|
|
||||||
:param key_size: RSA 密钥长度
|
|
||||||
:return: None
|
|
||||||
:raises ValueError: 密钥长度不满足约束时抛出异常
|
|
||||||
"""
|
|
||||||
if key_size < MIN_RSA_KEY_SIZE or key_size % RSA_KEY_SIZE_STEP != 0:
|
|
||||||
raise ValueError('RSA 密钥长度必须大于等于 2048,且为 256 的整数倍')
|
|
||||||
|
|
||||||
def generate_rsa_key_pair(self, key_size: int) -> tuple[str, str]:
|
|
||||||
"""
|
|
||||||
生成 PEM 格式的 RSA 密钥对。
|
|
||||||
|
|
||||||
:param key_size: RSA 密钥长度
|
|
||||||
:return: 私钥与公钥 PEM 字符串
|
|
||||||
"""
|
|
||||||
rsa_module = self.infrastructure_gateway.get_rsa_module()
|
|
||||||
serialization_module = self.infrastructure_gateway.get_serialization_module()
|
|
||||||
private_key = rsa_module.generate_private_key(public_exponent=65537, key_size=key_size)
|
|
||||||
private_key_pem = private_key.private_bytes(
|
|
||||||
encoding=serialization_module.Encoding.PEM,
|
|
||||||
format=serialization_module.PrivateFormat.PKCS8,
|
|
||||||
encryption_algorithm=serialization_module.NoEncryption(),
|
|
||||||
).decode('utf-8')
|
|
||||||
public_key_pem = (
|
|
||||||
private_key.public_key()
|
|
||||||
.public_bytes(
|
|
||||||
encoding=serialization_module.Encoding.PEM,
|
|
||||||
format=serialization_module.PublicFormat.SubjectPublicKeyInfo,
|
|
||||||
)
|
|
||||||
.decode('utf-8')
|
|
||||||
)
|
|
||||||
return private_key_pem, public_key_pem
|
|
||||||
|
|
||||||
def load_existing_legacy_key_pairs(self) -> list[dict[str, str]]:
|
|
||||||
"""
|
|
||||||
解析当前配置中的历史密钥列表。
|
|
||||||
|
|
||||||
:return: 历史密钥列表
|
|
||||||
:raises ValueError: 历史密钥配置非法时抛出异常
|
|
||||||
"""
|
|
||||||
transport_crypto_config = self.infrastructure_gateway.get_transport_crypto_config()
|
|
||||||
raw_legacy_key_pairs = transport_crypto_config.transport_crypto_legacy_key_pairs or '[]'
|
|
||||||
parsed_legacy_key_pairs = json.loads(raw_legacy_key_pairs)
|
|
||||||
if not isinstance(parsed_legacy_key_pairs, list):
|
|
||||||
raise ValueError('TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS 必须为 JSON 数组')
|
|
||||||
return [item for item in parsed_legacy_key_pairs if isinstance(item, dict)]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def merge_rotation_legacy_key_pairs(
|
|
||||||
legacy_key_pairs: list[dict[str, str]],
|
|
||||||
current_key_pair: Any,
|
|
||||||
next_kid: str,
|
|
||||||
) -> list[dict[str, str]]:
|
|
||||||
"""
|
|
||||||
合并轮换后的历史密钥列表。
|
|
||||||
|
|
||||||
:param legacy_key_pairs: 当前历史密钥列表
|
|
||||||
:param current_key_pair: 当前生效密钥对对象
|
|
||||||
:param next_kid: 即将切换的新密钥版本
|
|
||||||
:return: 合并后的历史密钥列表
|
|
||||||
"""
|
|
||||||
normalized_legacy_key_pairs: dict[str, dict[str, str]] = {
|
|
||||||
str(item.get('kid')): item for item in legacy_key_pairs if item.get('kid')
|
|
||||||
}
|
|
||||||
normalized_legacy_key_pairs[current_key_pair.kid] = {
|
|
||||||
'kid': current_key_pair.kid,
|
|
||||||
'privateKey': current_key_pair.private_key_pem,
|
|
||||||
'publicKey': current_key_pair.public_key_pem,
|
|
||||||
}
|
|
||||||
normalized_legacy_key_pairs.pop(next_kid, None)
|
|
||||||
return list(normalized_legacy_key_pairs.values())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_key_pair_env_patch(kid: str, private_key_pem: str, public_key_pem: str) -> dict[str, str]:
|
|
||||||
"""
|
|
||||||
构建密钥对写回环境变量所需的补丁数据。
|
|
||||||
|
|
||||||
:param kid: 密钥版本标识
|
|
||||||
:param private_key_pem: 私钥 PEM 文本
|
|
||||||
:param public_key_pem: 公钥 PEM 文本
|
|
||||||
:return: 环境变量补丁数据
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'TRANSPORT_CRYPTO_KID': kid,
|
|
||||||
'TRANSPORT_CRYPTO_PRIVATE_KEY': private_key_pem,
|
|
||||||
'TRANSPORT_CRYPTO_PUBLIC_KEY': public_key_pem,
|
|
||||||
}
|
|
||||||
|
|
||||||
def build_rotation_env_patch(
|
|
||||||
self,
|
|
||||||
next_kid: str,
|
|
||||||
private_key_pem: str,
|
|
||||||
public_key_pem: str,
|
|
||||||
merged_legacy_key_pairs: list[dict[str, str]],
|
|
||||||
) -> dict[str, str]:
|
|
||||||
"""
|
|
||||||
构建密钥轮换写回环境变量所需的补丁数据。
|
|
||||||
|
|
||||||
:param next_kid: 新密钥版本标识
|
|
||||||
:param private_key_pem: 新私钥 PEM 文本
|
|
||||||
:param public_key_pem: 新公钥 PEM 文本
|
|
||||||
:param merged_legacy_key_pairs: 合并后的历史密钥列表
|
|
||||||
:return: 环境变量补丁数据
|
|
||||||
"""
|
|
||||||
env_patch = self.build_key_pair_env_patch(next_kid, private_key_pem, public_key_pem)
|
|
||||||
env_patch['TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS'] = json.dumps(merged_legacy_key_pairs, ensure_ascii=False)
|
|
||||||
return env_patch
|
|
||||||
|
|
||||||
|
|
||||||
class CryptoResultSupport:
|
|
||||||
"""
|
|
||||||
传输加密结果支持对象。
|
|
||||||
|
|
||||||
该对象负责参数错误/运行时错误结果翻译,以及固定轮换提示文案构建,
|
|
||||||
避免主运行时服务继续承载重复异常映射与静态 payload 片段。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_argument_error_result(message: str, exc: Exception) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建参数错误结果。
|
|
||||||
|
|
||||||
:param message: 失败消息
|
|
||||||
:param exc: 原始异常
|
|
||||||
:return: 标准参数错误结果
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': message,
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': ARGUMENT_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_runtime_error_result(message: str, exc: Exception) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建运行时错误结果。
|
|
||||||
|
|
||||||
:param message: 失败消息
|
|
||||||
:param exc: 原始异常
|
|
||||||
:return: 标准运行时错误结果
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': message,
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run_argument_guarded(
|
|
||||||
self,
|
|
||||||
operation: Callable[[], dict[str, Any]],
|
|
||||||
*,
|
|
||||||
failure_message: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
以统一参数/运行时错误翻译执行操作。
|
|
||||||
|
|
||||||
:param operation: 待执行操作
|
|
||||||
:param failure_message: 失败消息
|
|
||||||
:return: 操作结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return operation()
|
|
||||||
except ValueError as exc:
|
|
||||||
return self.build_argument_error_result(failure_message, exc)
|
|
||||||
except Exception as exc:
|
|
||||||
return self.build_runtime_error_result(failure_message, exc)
|
|
||||||
|
|
||||||
def run_runtime_guarded(
|
|
||||||
self,
|
|
||||||
operation: Callable[[], dict[str, Any]],
|
|
||||||
*,
|
|
||||||
failure_message: str,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
以统一运行时错误翻译执行操作。
|
|
||||||
|
|
||||||
:param operation: 待执行操作
|
|
||||||
:param failure_message: 失败消息
|
|
||||||
:return: 操作结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return operation()
|
|
||||||
except Exception as exc:
|
|
||||||
return self.build_runtime_error_result(failure_message, exc)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_rotation_apply_steps() -> list[str]:
|
|
||||||
"""
|
|
||||||
构建密钥轮换后的应用步骤说明。
|
|
||||||
|
|
||||||
:return: 轮换应用步骤列表
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
'将 envPatch 中的新密钥写入目标环境配置',
|
|
||||||
'发布后保留 legacyKeyPairs,确保旧请求在轮换窗口内可解密',
|
|
||||||
'确认前端已刷新到 nextKid 后,再移除不再需要的历史密钥',
|
|
||||||
]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import DATABASE_RUNTIME, DatabaseRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['DATABASE_RUNTIME', 'DatabaseRuntimeService']
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
数据库基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载数据库引擎、SQLAlchemy 和 Alembic 相关依赖,
|
|
||||||
供数据库运行时 facade 与其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_async_db_engine_factory() -> Any:
|
|
||||||
"""
|
|
||||||
获取异步数据库引擎工厂函数。
|
|
||||||
|
|
||||||
:return: 异步数据库引擎工厂函数
|
|
||||||
"""
|
|
||||||
return import_module('config.database').create_async_db_engine
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_sync_db_engine_factory() -> Any:
|
|
||||||
"""
|
|
||||||
获取同步数据库引擎工厂函数。
|
|
||||||
|
|
||||||
:return: 同步数据库引擎工厂函数
|
|
||||||
"""
|
|
||||||
return import_module('config.database').create_sync_db_engine
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_sqlalchemy_text() -> Any:
|
|
||||||
"""
|
|
||||||
获取 SQLAlchemy `text` 构造函数。
|
|
||||||
|
|
||||||
:return: SQLAlchemy `text` 函数
|
|
||||||
"""
|
|
||||||
return import_module('sqlalchemy').text
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_alembic_config_class() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Alembic 配置类。
|
|
||||||
|
|
||||||
:return: Alembic 配置类
|
|
||||||
"""
|
|
||||||
return import_module('alembic.config').Config
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_alembic_script_directory_class() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Alembic 脚本目录类。
|
|
||||||
|
|
||||||
:return: Alembic 脚本目录类
|
|
||||||
"""
|
|
||||||
return import_module('alembic.script').ScriptDirectory
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import DATABASE_ERROR
|
|
||||||
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
|
||||||
|
|
||||||
from .gateway import DatabaseInfrastructureGateway
|
|
||||||
from .support import DatabaseAlembicCommandSupport, DatabaseRevisionSupport
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseRuntimeService:
|
|
||||||
"""
|
|
||||||
数据库运行时服务。
|
|
||||||
|
|
||||||
该服务作为数据库运行时 facade,对外统一暴露数据库连通性检查、
|
|
||||||
Alembic 版本读取与迁移命令执行入口。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param infrastructure_gateway: 数据库基础设施网关
|
|
||||||
:param revision_support: 数据库迁移版本支持对象
|
|
||||||
:param alembic_command_support: 数据库 Alembic 命令支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
runtime_environment: RuntimeEnvironmentService | None = None,
|
|
||||||
infrastructure_gateway: DatabaseInfrastructureGateway | None = None,
|
|
||||||
revision_support: DatabaseRevisionSupport | None = None,
|
|
||||||
alembic_command_support: DatabaseAlembicCommandSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库运行时服务。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param infrastructure_gateway: 数据库基础设施网关
|
|
||||||
:param revision_support: 数据库迁移版本支持对象
|
|
||||||
:param alembic_command_support: 数据库 Alembic 命令支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or DatabaseInfrastructureGateway()
|
|
||||||
self.revision_support = revision_support or DatabaseRevisionSupport(
|
|
||||||
self.infrastructure_gateway,
|
|
||||||
self.runtime_environment,
|
|
||||||
)
|
|
||||||
self.alembic_command_support = alembic_command_support or DatabaseAlembicCommandSupport(
|
|
||||||
self.runtime_environment
|
|
||||||
)
|
|
||||||
|
|
||||||
async def ping_database(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
检查数据库连通性。
|
|
||||||
|
|
||||||
:return: 数据库检查结果
|
|
||||||
"""
|
|
||||||
create_async_db_engine = self.infrastructure_gateway.get_async_db_engine_factory()
|
|
||||||
text = self.infrastructure_gateway.get_sqlalchemy_text()
|
|
||||||
engine = create_async_db_engine(echo=False)
|
|
||||||
try:
|
|
||||||
async with engine.connect() as connection:
|
|
||||||
await connection.execute(text('SELECT 1'))
|
|
||||||
return {'ok': True, 'message': '数据库连接成功'}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '数据库连接失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
finally:
|
|
||||||
await engine.dispose()
|
|
||||||
|
|
||||||
def get_current_revision(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取数据库当前迁移版本。
|
|
||||||
|
|
||||||
:return: 当前迁移版本信息
|
|
||||||
"""
|
|
||||||
create_sync_db_engine = self.infrastructure_gateway.get_sync_db_engine_factory()
|
|
||||||
text = self.infrastructure_gateway.get_sqlalchemy_text()
|
|
||||||
engine = create_sync_db_engine(echo=False)
|
|
||||||
try:
|
|
||||||
with engine.connect() as connection:
|
|
||||||
revision = connection.execute(text('SELECT version_num FROM alembic_version')).scalar()
|
|
||||||
return {'ok': True, 'currentRevision': revision}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取数据库迁移版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
finally:
|
|
||||||
engine.dispose()
|
|
||||||
|
|
||||||
def upgrade_database(self, revision: str = 'head', *, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行数据库迁移升级。
|
|
||||||
|
|
||||||
:param revision: 目标迁移版本,默认为 `head`
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 数据库迁移执行结果
|
|
||||||
"""
|
|
||||||
return self.alembic_command_support.run_alembic_command(
|
|
||||||
'upgrade',
|
|
||||||
revision,
|
|
||||||
success_message=f'数据库已升级到 {revision}',
|
|
||||||
failure_message='数据库升级失败',
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
def init_database(self, *, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
初始化数据库到最新迁移版本。
|
|
||||||
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 数据库初始化结果
|
|
||||||
"""
|
|
||||||
return self.alembic_command_support.run_alembic_command(
|
|
||||||
'upgrade',
|
|
||||||
'head',
|
|
||||||
success_message='数据库初始化完成,当前版本已同步到 head',
|
|
||||||
failure_message='数据库初始化失败',
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
def downgrade_database(self, revision: str = '-1', *, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行数据库回退。
|
|
||||||
|
|
||||||
:param revision: 目标回退版本,默认为 `-1`
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 数据库回退结果
|
|
||||||
"""
|
|
||||||
return self.alembic_command_support.run_alembic_command(
|
|
||||||
'downgrade',
|
|
||||||
revision,
|
|
||||||
success_message=f'数据库已回退到 {revision}',
|
|
||||||
failure_message='数据库回退失败',
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
def create_revision(self, message: str, *, autogenerate: bool = False, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
创建新的 Alembic 迁移版本文件。
|
|
||||||
|
|
||||||
:param message: 迁移说明
|
|
||||||
:param autogenerate: 是否自动生成变更
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 迁移版本创建结果
|
|
||||||
"""
|
|
||||||
arguments: list[str] = ['-m', message]
|
|
||||||
if autogenerate:
|
|
||||||
arguments.append('--autogenerate')
|
|
||||||
return self.alembic_command_support.run_alembic_command(
|
|
||||||
'revision',
|
|
||||||
*arguments,
|
|
||||||
success_message='数据库迁移版本文件创建完成',
|
|
||||||
failure_message='数据库迁移版本文件创建失败',
|
|
||||||
dry_run=dry_run,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_alembic_heads(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前代码仓库中的 Alembic heads 信息。
|
|
||||||
|
|
||||||
:return: Alembic heads 结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
script_directory = self.revision_support.build_alembic_script_directory()
|
|
||||||
items = [
|
|
||||||
self.revision_support.serialize_revision(revision)
|
|
||||||
for revision in script_directory.get_revisions('heads')
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '已读取 Alembic heads',
|
|
||||||
'count': len(items),
|
|
||||||
'items': items,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取 Alembic heads 失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
def get_alembic_history(self, *, limit: int = 20) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取当前代码仓库中的 Alembic 历史版本信息。
|
|
||||||
|
|
||||||
:param limit: 返回的最大历史记录数量
|
|
||||||
:return: Alembic 历史版本结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
script_directory = self.revision_support.build_alembic_script_directory()
|
|
||||||
history_items = [
|
|
||||||
self.revision_support.serialize_revision(revision) for revision in script_directory.walk_revisions()
|
|
||||||
]
|
|
||||||
limited_items = history_items[:limit]
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '已读取 Alembic 历史版本',
|
|
||||||
'count': len(limited_items),
|
|
||||||
'totalCount': len(history_items),
|
|
||||||
'limit': limit,
|
|
||||||
'items': limited_items,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取 Alembic 历史版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
|
|
||||||
DATABASE_RUNTIME = DatabaseRuntimeService()
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import DATABASE_ERROR
|
|
||||||
from cli.runtime.base import RuntimeEnvironmentService
|
|
||||||
|
|
||||||
from .gateway import DatabaseInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseRevisionSupport:
|
|
||||||
"""
|
|
||||||
数据库迁移版本支持对象。
|
|
||||||
|
|
||||||
该对象负责 Alembic script directory 构建、修订版本规整和
|
|
||||||
版本对象序列化,避免主运行时服务继续承载迁移元数据细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 数据库基础设施网关
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
infrastructure_gateway: DatabaseInfrastructureGateway,
|
|
||||||
runtime_environment: RuntimeEnvironmentService,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库迁移版本支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 数据库基础设施网关
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
self.runtime_environment = runtime_environment
|
|
||||||
|
|
||||||
def build_alembic_script_directory(self) -> Any:
|
|
||||||
"""
|
|
||||||
基于当前后端目录构建 Alembic 脚本目录对象。
|
|
||||||
|
|
||||||
:return: Alembic 脚本目录对象
|
|
||||||
"""
|
|
||||||
config_class = self.infrastructure_gateway.get_alembic_config_class()
|
|
||||||
script_directory_class = self.infrastructure_gateway.get_alembic_script_directory_class()
|
|
||||||
config = config_class(os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini'))
|
|
||||||
return script_directory_class.from_config(config)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def normalize_revision_value(value: Any) -> list[str]:
|
|
||||||
"""
|
|
||||||
规范化 Alembic 修订版本引用值。
|
|
||||||
|
|
||||||
:param value: 原始修订版本引用
|
|
||||||
:return: 规范化后的修订版本列表
|
|
||||||
"""
|
|
||||||
if value is None:
|
|
||||||
return []
|
|
||||||
if isinstance(value, str):
|
|
||||||
return [value]
|
|
||||||
return [str(item) for item in value]
|
|
||||||
|
|
||||||
def serialize_revision(self, script_revision: Any) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
序列化 Alembic 修订版本对象。
|
|
||||||
|
|
||||||
:param script_revision: Alembic 修订版本对象
|
|
||||||
:return: 序列化后的修订版本字典
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
'revision': str(script_revision.revision),
|
|
||||||
'downRevisions': self.normalize_revision_value(script_revision.down_revision),
|
|
||||||
'branchLabels': sorted(str(item) for item in script_revision.branch_labels or []),
|
|
||||||
'dependsOn': self.normalize_revision_value(script_revision.dependencies),
|
|
||||||
'doc': (script_revision.doc or '').strip(),
|
|
||||||
'path': str(script_revision.path),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseAlembicCommandSupport:
|
|
||||||
"""
|
|
||||||
数据库 Alembic 命令支持对象。
|
|
||||||
|
|
||||||
该对象负责 Alembic 命令组装与子进程执行,
|
|
||||||
避免主运行时服务继续承载命令行桥接细节。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
|
|
||||||
"""
|
|
||||||
初始化数据库 Alembic 命令支持对象。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment
|
|
||||||
|
|
||||||
def build_alembic_command(self, command: str, *arguments: str) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建 Alembic 命令。
|
|
||||||
|
|
||||||
:param command: Alembic 子命令名称
|
|
||||||
:param arguments: Alembic 子命令参数列表
|
|
||||||
:return: Alembic 命令参数列表
|
|
||||||
"""
|
|
||||||
alembic_ini_path = os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini')
|
|
||||||
return ['alembic', '-c', alembic_ini_path, command, *arguments]
|
|
||||||
|
|
||||||
def run_alembic_command(
|
|
||||||
self,
|
|
||||||
command: str,
|
|
||||||
*arguments: str,
|
|
||||||
success_message: str,
|
|
||||||
failure_message: str,
|
|
||||||
dry_run: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行 Alembic 命令并返回统一结果。
|
|
||||||
|
|
||||||
:param command: Alembic 子命令名称
|
|
||||||
:param arguments: Alembic 子命令参数列表
|
|
||||||
:param success_message: 成功提示信息
|
|
||||||
:param failure_message: 失败提示信息
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: Alembic 执行结果
|
|
||||||
"""
|
|
||||||
command_arguments = self.build_alembic_command(command, *arguments)
|
|
||||||
if dry_run:
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': f'{success_message}(dry-run)',
|
|
||||||
'dryRun': True,
|
|
||||||
'command': command_arguments,
|
|
||||||
'cwd': self.runtime_environment.get_backend_dir(),
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
completed = subprocess.run(
|
|
||||||
command_arguments,
|
|
||||||
cwd=self.runtime_environment.get_backend_dir(),
|
|
||||||
env={**os.environ, 'APP_ENV': os.environ.get('APP_ENV', 'dev')},
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
if completed.returncode != 0:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': failure_message,
|
|
||||||
'error': completed.stderr.strip() or completed.stdout.strip(),
|
|
||||||
'exit_code': DATABASE_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload: dict[str, Any] = {'ok': True, 'message': success_message}
|
|
||||||
stdout = completed.stdout.strip()
|
|
||||||
if stdout:
|
|
||||||
payload['stdout'] = stdout
|
|
||||||
return payload
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': failure_message, 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['DEVELOPMENT_RUNTIME', 'DevelopmentRuntimeService']
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import RUNTIME_ERROR
|
|
||||||
from cli.runtime.base import RuntimeEnvironmentService
|
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentProcessGateway:
|
|
||||||
"""
|
|
||||||
开发子进程执行网关。
|
|
||||||
|
|
||||||
该对象负责统一执行开发工具子进程命令,
|
|
||||||
避免支持对象直接依赖 `subprocess` 细节。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
|
|
||||||
"""
|
|
||||||
初始化开发子进程执行网关。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment
|
|
||||||
|
|
||||||
def run_command(self, command: list[str]) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行子进程命令并返回统一结果。
|
|
||||||
|
|
||||||
:param command: 待执行命令
|
|
||||||
:return: 命令执行结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
completed = subprocess.run(
|
|
||||||
command,
|
|
||||||
cwd=self.runtime_environment.get_backend_dir(),
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=False,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '开发命令执行失败',
|
|
||||||
'error': str(exc),
|
|
||||||
'command': command,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
payload: dict[str, Any] = {
|
|
||||||
'ok': completed.returncode == 0,
|
|
||||||
'command': command,
|
|
||||||
'returnCode': completed.returncode,
|
|
||||||
}
|
|
||||||
if completed.stdout.strip():
|
|
||||||
payload['stdout'] = completed.stdout.strip()
|
|
||||||
if completed.stderr.strip():
|
|
||||||
payload['stderr'] = completed.stderr.strip()
|
|
||||||
return payload
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import DEPENDENCY_ERROR, RUNTIME_ERROR
|
|
||||||
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
|
|
||||||
|
|
||||||
from .gateway import DevelopmentProcessGateway
|
|
||||||
from .support import DevelopmentCommandSupport, DevelopmentToolingSupport
|
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentRuntimeService:
|
|
||||||
"""
|
|
||||||
开发运行时服务。
|
|
||||||
|
|
||||||
该服务作为开发运行时 facade,对外统一暴露 lint/format 和 pytest
|
|
||||||
执行入口。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param tooling_support: 开发工具支持对象
|
|
||||||
:param command_support: 开发命令构建支持对象
|
|
||||||
:param process_gateway: 开发子进程执行网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
runtime_environment: RuntimeEnvironmentService | None = None,
|
|
||||||
tooling_support: DevelopmentToolingSupport | None = None,
|
|
||||||
command_support: DevelopmentCommandSupport | None = None,
|
|
||||||
process_gateway: DevelopmentProcessGateway | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化开发运行时服务。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:param tooling_support: 开发工具支持对象
|
|
||||||
:param command_support: 开发命令构建支持对象
|
|
||||||
:param process_gateway: 开发子进程执行网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
|
|
||||||
self.tooling_support = tooling_support or DevelopmentToolingSupport()
|
|
||||||
self.command_support = command_support or DevelopmentCommandSupport(self.runtime_environment)
|
|
||||||
self.process_gateway = process_gateway or DevelopmentProcessGateway(self.runtime_environment)
|
|
||||||
|
|
||||||
def run_lint(
|
|
||||||
self,
|
|
||||||
targets: list[str] | None,
|
|
||||||
*,
|
|
||||||
check_only: bool = False,
|
|
||||||
fix: bool = False,
|
|
||||||
unsafe_fixes: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行仓库 lint/format 检查。
|
|
||||||
|
|
||||||
:param targets: 目标路径列表
|
|
||||||
:param check_only: 是否仅检查不写回
|
|
||||||
:param fix: 是否允许 `ruff check --fix`
|
|
||||||
:param unsafe_fixes: 是否允许不安全修复
|
|
||||||
:return: lint 执行结果
|
|
||||||
"""
|
|
||||||
normalized_targets = self.tooling_support.resolve_targets(targets)
|
|
||||||
format_command = self.command_support.build_format_command(normalized_targets, check_only=check_only)
|
|
||||||
format_result = self.process_gateway.run_command(format_command)
|
|
||||||
if not format_result.get('ok', False):
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': 'Ruff format 阶段失败',
|
|
||||||
'targets': normalized_targets,
|
|
||||||
'format': format_result,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
check_command = self.command_support.build_check_command(
|
|
||||||
normalized_targets,
|
|
||||||
check_only=check_only,
|
|
||||||
fix=fix,
|
|
||||||
unsafe_fixes=unsafe_fixes,
|
|
||||||
)
|
|
||||||
check_result = self.process_gateway.run_command(check_command)
|
|
||||||
if not check_result.get('ok', False):
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': 'Ruff check 阶段失败',
|
|
||||||
'targets': normalized_targets,
|
|
||||||
'format': format_result,
|
|
||||||
'check': check_result,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '开发检查已完成',
|
|
||||||
'targets': normalized_targets,
|
|
||||||
'checkOnly': check_only,
|
|
||||||
'fix': fix and not check_only,
|
|
||||||
'unsafeFixes': unsafe_fixes and fix and not check_only,
|
|
||||||
'format': format_result,
|
|
||||||
'check': check_result,
|
|
||||||
}
|
|
||||||
|
|
||||||
def run_tests(
|
|
||||||
self,
|
|
||||||
targets: list[str] | None,
|
|
||||||
*,
|
|
||||||
keyword: str = '',
|
|
||||||
maxfail: int = 0,
|
|
||||||
quiet: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行项目测试。
|
|
||||||
|
|
||||||
:param targets: 目标测试路径列表
|
|
||||||
:param keyword: pytest `-k` 过滤表达式
|
|
||||||
:param maxfail: 最大失败数,0 表示不限制
|
|
||||||
:param quiet: 是否启用简洁输出
|
|
||||||
:return: 测试执行结果
|
|
||||||
"""
|
|
||||||
if not self.tooling_support.is_pytest_available():
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': 'pytest 未安装,无法执行 dev test',
|
|
||||||
'hint': '请在当前环境安装 pytest 后重试',
|
|
||||||
'exit_code': DEPENDENCY_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized_targets = self.tooling_support.resolve_targets(targets)
|
|
||||||
command = self.command_support.build_pytest_command(
|
|
||||||
normalized_targets,
|
|
||||||
keyword=keyword,
|
|
||||||
maxfail=maxfail,
|
|
||||||
quiet=quiet,
|
|
||||||
)
|
|
||||||
test_result = self.process_gateway.run_command(command)
|
|
||||||
if not test_result.get('ok', False):
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '测试执行失败',
|
|
||||||
'targets': normalized_targets,
|
|
||||||
'keyword': keyword,
|
|
||||||
'maxfail': maxfail,
|
|
||||||
'quiet': quiet,
|
|
||||||
'test': test_result,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '测试执行完成',
|
|
||||||
'targets': normalized_targets,
|
|
||||||
'keyword': keyword,
|
|
||||||
'maxfail': maxfail,
|
|
||||||
'quiet': quiet,
|
|
||||||
'test': test_result,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
DEVELOPMENT_RUNTIME = DevelopmentRuntimeService()
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import importlib.util
|
|
||||||
|
|
||||||
from cli.runtime.base import RuntimeEnvironmentService
|
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentToolingSupport:
|
|
||||||
"""
|
|
||||||
开发工具支持对象。
|
|
||||||
|
|
||||||
该对象负责目标列表规整与开发依赖可用性检查,
|
|
||||||
避免主运行时服务继续承载轻量工具规则。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_targets(targets: list[str] | None) -> list[str]:
|
|
||||||
"""
|
|
||||||
规范化命令目标列表。
|
|
||||||
|
|
||||||
:param targets: 原始目标列表
|
|
||||||
:return: 规范化后的目标列表
|
|
||||||
"""
|
|
||||||
normalized_targets = [target.strip() for target in (targets or []) if target.strip()]
|
|
||||||
return normalized_targets or ['.']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_pytest_available() -> bool:
|
|
||||||
"""
|
|
||||||
检查 pytest 是否已安装。
|
|
||||||
|
|
||||||
:return: pytest 是否可用
|
|
||||||
"""
|
|
||||||
return importlib.util.find_spec('pytest') is not None
|
|
||||||
|
|
||||||
|
|
||||||
class DevelopmentCommandSupport:
|
|
||||||
"""
|
|
||||||
开发命令构建支持对象。
|
|
||||||
|
|
||||||
该对象负责开发工具命令参数拼装,
|
|
||||||
避免主运行时服务继续承载命令行桥接细节。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
|
|
||||||
"""
|
|
||||||
初始化开发命令构建支持对象。
|
|
||||||
|
|
||||||
:param runtime_environment: 运行时环境服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.runtime_environment = runtime_environment
|
|
||||||
|
|
||||||
def build_format_command(self, normalized_targets: list[str], *, check_only: bool) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建 Ruff format 命令。
|
|
||||||
|
|
||||||
:param normalized_targets: 已规整目标列表
|
|
||||||
:param check_only: 是否仅检查
|
|
||||||
:return: 格式化命令参数列表
|
|
||||||
"""
|
|
||||||
command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'format']
|
|
||||||
if check_only:
|
|
||||||
command.append('--check')
|
|
||||||
command.extend(normalized_targets)
|
|
||||||
return command
|
|
||||||
|
|
||||||
def build_check_command(
|
|
||||||
self,
|
|
||||||
normalized_targets: list[str],
|
|
||||||
*,
|
|
||||||
check_only: bool,
|
|
||||||
fix: bool,
|
|
||||||
unsafe_fixes: bool,
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建 Ruff check 命令。
|
|
||||||
|
|
||||||
:param normalized_targets: 已规整目标列表
|
|
||||||
:param check_only: 是否仅检查
|
|
||||||
:param fix: 是否允许自动修复
|
|
||||||
:param unsafe_fixes: 是否允许不安全修复
|
|
||||||
:return: 检查命令参数列表
|
|
||||||
"""
|
|
||||||
command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'check']
|
|
||||||
if fix and not check_only:
|
|
||||||
command.append('--fix')
|
|
||||||
if unsafe_fixes:
|
|
||||||
command.append('--unsafe-fixes')
|
|
||||||
command.extend(normalized_targets)
|
|
||||||
return command
|
|
||||||
|
|
||||||
def build_pytest_command(
|
|
||||||
self,
|
|
||||||
normalized_targets: list[str],
|
|
||||||
*,
|
|
||||||
keyword: str,
|
|
||||||
maxfail: int,
|
|
||||||
quiet: bool,
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
|
||||||
构建 pytest 命令。
|
|
||||||
|
|
||||||
:param normalized_targets: 已规整目标列表
|
|
||||||
:param keyword: pytest `-k` 表达式
|
|
||||||
:param maxfail: 最大失败数
|
|
||||||
:param quiet: 是否启用简洁输出
|
|
||||||
:return: pytest 命令参数列表
|
|
||||||
"""
|
|
||||||
command = [self.runtime_environment.get_python_executable(), '-m', 'pytest']
|
|
||||||
if quiet:
|
|
||||||
command.append('-q')
|
|
||||||
if keyword:
|
|
||||||
command.extend(['-k', keyword])
|
|
||||||
if maxfail > 0:
|
|
||||||
command.append(f'--maxfail={maxfail}')
|
|
||||||
command.extend(normalized_targets)
|
|
||||||
return command
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import GEN_RUNTIME, GenRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['GEN_RUNTIME', 'GenRuntimeService']
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class GenInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
代码生成基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载 sqlglot、数据库会话、分页模型、配置对象、
|
|
||||||
业务异常和代码生成服务依赖,供代码生成运行时 facade 与其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_sqlglot_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取 `sqlglot` 模块。
|
|
||||||
|
|
||||||
:return: `sqlglot` 模块
|
|
||||||
"""
|
|
||||||
return import_module('sqlglot')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_sqlglot_expressions_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取 `sqlglot.expressions` 模块。
|
|
||||||
|
|
||||||
:return: `sqlglot.expressions` 模块
|
|
||||||
"""
|
|
||||||
return import_module('sqlglot.expressions')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_async_session_local() -> Any:
|
|
||||||
"""
|
|
||||||
获取异步数据库会话工厂。
|
|
||||||
|
|
||||||
:return: 异步数据库会话工厂
|
|
||||||
"""
|
|
||||||
return import_module('config.database').AsyncSessionLocal
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_page_model() -> Any:
|
|
||||||
"""
|
|
||||||
获取分页模型类型。
|
|
||||||
|
|
||||||
:return: 分页模型类型
|
|
||||||
"""
|
|
||||||
return import_module('common.vo').PageModel
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_database_config() -> Any:
|
|
||||||
"""
|
|
||||||
获取数据库配置对象。
|
|
||||||
|
|
||||||
:return: 数据库配置对象
|
|
||||||
"""
|
|
||||||
return import_module('config.env').DataBaseConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_gen_config() -> Any:
|
|
||||||
"""
|
|
||||||
获取代码生成配置对象。
|
|
||||||
|
|
||||||
:return: 代码生成配置对象
|
|
||||||
"""
|
|
||||||
return import_module('config.env').GenConfig
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_service_exception_class() -> type[Exception]:
|
|
||||||
"""
|
|
||||||
获取业务异常类型。
|
|
||||||
|
|
||||||
:return: 业务异常类型
|
|
||||||
"""
|
|
||||||
return import_module('exceptions.exception').ServiceException
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_user_vo_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取用户 VO 模块。
|
|
||||||
|
|
||||||
:return: 用户 VO 模块
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.entity.vo.user_vo')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_gen_vo_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取代码生成 VO 模块。
|
|
||||||
|
|
||||||
:return: 代码生成 VO 模块
|
|
||||||
"""
|
|
||||||
return import_module('module_generator.entity.vo.gen_vo')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_gen_table_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取代码生成服务类。
|
|
||||||
|
|
||||||
:return: 代码生成服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_generator.service.gen_service').GenTableService
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_gen_table_column_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取代码生成字段服务类。
|
|
||||||
|
|
||||||
:return: 代码生成字段服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_generator.service.gen_service').GenTableColumnService
|
|
||||||
@@ -1,412 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import GenInfrastructureGateway
|
|
||||||
from .support import GenDomainSupport
|
|
||||||
|
|
||||||
|
|
||||||
class GenRuntimeService:
|
|
||||||
"""
|
|
||||||
代码生成运行时服务。
|
|
||||||
|
|
||||||
该服务作为代码生成运行时 facade,对外统一暴露业务表、数据库物理表、
|
|
||||||
建表 SQL、代码预览、导出与数据库同步等入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 代码生成基础设施网关
|
|
||||||
:param domain_support: 代码生成领域支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: GenInfrastructureGateway | None = None,
|
|
||||||
domain_support: GenDomainSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化代码生成运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 代码生成基础设施网关
|
|
||||||
:param domain_support: 代码生成领域支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or GenInfrastructureGateway()
|
|
||||||
self.domain_support = domain_support or GenDomainSupport(self.infrastructure_gateway)
|
|
||||||
|
|
||||||
async def import_tables(self, table_names: list[str], *, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
导入数据库表到代码生成业务表。
|
|
||||||
|
|
||||||
:param table_names: 待导入表名列表
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 导入结果
|
|
||||||
"""
|
|
||||||
normalized_table_names = self.domain_support.normalize_table_names(table_names)
|
|
||||||
if not normalized_table_names:
|
|
||||||
return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR}
|
|
||||||
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
service_exception = self.infrastructure_gateway.get_service_exception_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
gen_table_list = await gen_table_service.get_gen_db_table_list_by_name_services(
|
|
||||||
session,
|
|
||||||
normalized_table_names,
|
|
||||||
)
|
|
||||||
matched_table_names = [gen_table.table_name for gen_table in gen_table_list if gen_table.table_name]
|
|
||||||
missing_table_names = [
|
|
||||||
table_name for table_name in normalized_table_names if table_name not in matched_table_names
|
|
||||||
]
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '导入表结构演练完成,未执行实际写入',
|
|
||||||
'dryRun': True,
|
|
||||||
'requestedTables': normalized_table_names,
|
|
||||||
'matchedTables': matched_table_names,
|
|
||||||
'missingTables': missing_table_names,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await gen_table_service.import_gen_table_services(
|
|
||||||
session,
|
|
||||||
gen_table_list,
|
|
||||||
self.domain_support.build_cli_current_user(),
|
|
||||||
)
|
|
||||||
except service_exception as exc:
|
|
||||||
return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': bool(result.is_success),
|
|
||||||
'message': result.message,
|
|
||||||
'requestedTables': normalized_table_names,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def list_gen_tables(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
table_name: str = '',
|
|
||||||
table_comment: str = '',
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
查询代码生成业务表列表。
|
|
||||||
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 查询结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
try:
|
|
||||||
gen_vo_module = self.infrastructure_gateway.get_gen_vo_module()
|
|
||||||
query_model = gen_vo_module.GenTablePageQueryModel(
|
|
||||||
tableName=table_name or None,
|
|
||||||
tableComment=table_comment or None,
|
|
||||||
pageNum=page_num,
|
|
||||||
pageSize=page_size,
|
|
||||||
)
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await gen_table_service.get_gen_table_list_services(session, query_model, is_page=paged)
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '读取代码生成业务表列表失败',
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': DATABASE_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
filters = {
|
|
||||||
'tableName': table_name,
|
|
||||||
'tableComment': table_comment,
|
|
||||||
'paged': paged,
|
|
||||||
'pageNum': page_num,
|
|
||||||
'pageSize': page_size,
|
|
||||||
}
|
|
||||||
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
|
|
||||||
|
|
||||||
async def list_gen_db_tables(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
table_name: str = '',
|
|
||||||
table_comment: str = '',
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
查询数据库中可导入的物理表列表。
|
|
||||||
|
|
||||||
:param table_name: 表名称过滤条件
|
|
||||||
:param table_comment: 表描述过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 查询结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
try:
|
|
||||||
gen_vo_module = self.infrastructure_gateway.get_gen_vo_module()
|
|
||||||
query_model = gen_vo_module.GenTablePageQueryModel(
|
|
||||||
tableName=table_name or None,
|
|
||||||
tableComment=table_comment or None,
|
|
||||||
pageNum=page_num,
|
|
||||||
pageSize=page_size,
|
|
||||||
)
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await gen_table_service.get_gen_db_table_list_services(session, query_model, is_page=paged)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取数据库表列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
filters = {
|
|
||||||
'tableName': table_name,
|
|
||||||
'tableComment': table_comment,
|
|
||||||
'paged': paged,
|
|
||||||
'pageNum': page_num,
|
|
||||||
'pageSize': page_size,
|
|
||||||
}
|
|
||||||
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
|
|
||||||
|
|
||||||
async def create_tables(self, sql: str, sql_file: str, *, dry_run: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
根据 SQL 创建表并导入代码生成业务表。
|
|
||||||
|
|
||||||
:param sql: 直接传入的 SQL 文本
|
|
||||||
:param sql_file: SQL 文件路径
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 创建结果
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
sql_text = self.domain_support.resolve_sql_text(sql, sql_file)
|
|
||||||
sql_statements, table_names = self.domain_support.parse_create_table_sql(sql_text)
|
|
||||||
except ValueError as exc:
|
|
||||||
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': ARGUMENT_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '建表语句演练完成,未执行实际建表',
|
|
||||||
'dryRun': True,
|
|
||||||
'statementCount': len(sql_statements),
|
|
||||||
'tableNames': table_names,
|
|
||||||
'sql': sql_text,
|
|
||||||
}
|
|
||||||
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
service_exception = self.infrastructure_gateway.get_service_exception_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await gen_table_service.create_table_services(
|
|
||||||
session,
|
|
||||||
sql_text,
|
|
||||||
self.domain_support.build_cli_current_user(),
|
|
||||||
)
|
|
||||||
except service_exception as exc:
|
|
||||||
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': bool(result.is_success),
|
|
||||||
'message': result.message,
|
|
||||||
'tableNames': table_names,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def preview_code(self, table_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
预览指定业务表的代码生成结果。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:return: 预览结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
service_exception = self.infrastructure_gateway.get_service_exception_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
preview_payload = await gen_table_service.preview_code_services(session, table_id)
|
|
||||||
except service_exception as exc:
|
|
||||||
return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'tableId': table_id,
|
|
||||||
'templateCount': len(preview_payload),
|
|
||||||
'preview': preview_payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_gen_table_detail(self, table_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取单个代码生成业务表详情。
|
|
||||||
|
|
||||||
:param table_id: 业务表 ID
|
|
||||||
:return: 详情结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
gen_table_column_service = self.infrastructure_gateway.get_gen_table_column_service()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
info = await gen_table_service.get_gen_table_by_id_services(session, table_id)
|
|
||||||
if not getattr(info, 'table_id', None):
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': f'代码生成业务表不存在:{table_id}',
|
|
||||||
'tableId': table_id,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
rows = await gen_table_column_service.get_gen_table_column_list_by_table_id_services(session, table_id)
|
|
||||||
tables = await gen_table_service.get_gen_table_all_services(session)
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '读取代码生成业务表详情失败',
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': DATABASE_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
detail_payload = {
|
|
||||||
'info': self.domain_support.serialize_gen_item(info),
|
|
||||||
'rows': self.domain_support.serialize_gen_items(rows),
|
|
||||||
'tables': self.domain_support.serialize_gen_items(tables),
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'tableId': table_id,
|
|
||||||
'tableName': detail_payload['info'].get('tableName', ''),
|
|
||||||
'columnCount': len(detail_payload['rows']),
|
|
||||||
'tableCount': len(detail_payload['tables']),
|
|
||||||
'detail': detail_payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def export_code(
|
|
||||||
self,
|
|
||||||
table_names: list[str],
|
|
||||||
*,
|
|
||||||
mode: str = 'zip',
|
|
||||||
output_file: str = '',
|
|
||||||
dry_run: bool = False,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
导出代码生成结果。
|
|
||||||
|
|
||||||
:param table_names: 业务表名称列表
|
|
||||||
:param mode: 导出模式,支持 `zip` 与 `local`
|
|
||||||
:param output_file: zip 导出目标文件路径
|
|
||||||
:param dry_run: 是否仅演练执行
|
|
||||||
:return: 导出结果
|
|
||||||
"""
|
|
||||||
normalized_table_names = self.domain_support.normalize_table_names(table_names)
|
|
||||||
if not normalized_table_names:
|
|
||||||
return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR}
|
|
||||||
if mode not in {'zip', 'local'}:
|
|
||||||
return {'ok': False, 'message': '导出模式仅支持 zip 或 local', 'exit_code': ARGUMENT_ERROR}
|
|
||||||
|
|
||||||
gen_config = self.infrastructure_gateway.get_gen_config()
|
|
||||||
if mode == 'local' and not gen_config.allow_overwrite:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '当前系统配置不允许生成文件覆盖到本地',
|
|
||||||
'hint': '请检查 GenConfig.allow_overwrite 配置',
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
if dry_run:
|
|
||||||
dry_run_payload: dict[str, Any] = {
|
|
||||||
'ok': True,
|
|
||||||
'message': '代码导出演练完成,未执行实际导出',
|
|
||||||
'dryRun': True,
|
|
||||||
'mode': mode,
|
|
||||||
'tableNames': normalized_table_names,
|
|
||||||
}
|
|
||||||
if mode == 'zip':
|
|
||||||
target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip'
|
|
||||||
dry_run_payload['outputFile'] = self.domain_support.resolve_output_file_path(target_file)
|
|
||||||
else:
|
|
||||||
dry_run_payload['genPath'] = gen_config.GEN_PATH
|
|
||||||
return dry_run_payload
|
|
||||||
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
service_exception = self.infrastructure_gateway.get_service_exception_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
if mode == 'zip':
|
|
||||||
zip_bytes = await gen_table_service.batch_gen_code_services(session, normalized_table_names)
|
|
||||||
else:
|
|
||||||
messages = []
|
|
||||||
for table_name in normalized_table_names:
|
|
||||||
result = await gen_table_service.generate_code_services(session, table_name)
|
|
||||||
messages.append({'tableName': table_name, 'message': result.message, 'ok': result.is_success})
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '代码已生成到本地目录',
|
|
||||||
'mode': mode,
|
|
||||||
'tableNames': normalized_table_names,
|
|
||||||
'genPath': gen_config.GEN_PATH,
|
|
||||||
'results': messages,
|
|
||||||
}
|
|
||||||
except service_exception as exc:
|
|
||||||
return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip'
|
|
||||||
try:
|
|
||||||
target_path = self.domain_support.write_export_zip(target_file, zip_bytes)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '写出导出文件失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '代码压缩包导出完成',
|
|
||||||
'mode': mode,
|
|
||||||
'tableNames': normalized_table_names,
|
|
||||||
'outputFile': target_path,
|
|
||||||
'size': len(zip_bytes),
|
|
||||||
}
|
|
||||||
|
|
||||||
async def sync_gen_table_from_db(self, table_name: str) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
将指定代码生成业务表与数据库表结构进行同步。
|
|
||||||
|
|
||||||
:param table_name: 业务表名称
|
|
||||||
:return: 同步结果
|
|
||||||
"""
|
|
||||||
normalized_table_name = table_name.strip()
|
|
||||||
if not normalized_table_name:
|
|
||||||
return {'ok': False, 'message': '表名不能为空', 'exit_code': ARGUMENT_ERROR}
|
|
||||||
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
|
|
||||||
service_exception = self.infrastructure_gateway.get_service_exception_class()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await gen_table_service.sync_db_services(session, normalized_table_name)
|
|
||||||
except service_exception as exc:
|
|
||||||
return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': bool(result.is_success),
|
|
||||||
'message': result.message,
|
|
||||||
'tableName': normalized_table_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
GEN_RUNTIME = GenRuntimeService()
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService
|
|
||||||
|
|
||||||
from .gateway import GenInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class GenDomainSupport:
|
|
||||||
"""
|
|
||||||
代码生成领域支持对象。
|
|
||||||
|
|
||||||
该对象负责 CLI 当前用户构建、表名规整、记录序列化、SQL 解析
|
|
||||||
以及导出路径处理,避免主运行时服务继续承载过多局部规则。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 代码生成基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
infrastructure_gateway: GenInfrastructureGateway,
|
|
||||||
operator_service: RuntimeOperatorService = RUNTIME_OPERATOR,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化代码生成领域支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 代码生成基础设施网关
|
|
||||||
:param operator_service: 运行时操作者解析服务
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
self.operator_service = operator_service
|
|
||||||
|
|
||||||
def build_cli_current_user(self) -> Any:
|
|
||||||
"""
|
|
||||||
构建 CLI 场景使用的最小当前用户模型。
|
|
||||||
|
|
||||||
:return: CLI 当前用户模型
|
|
||||||
"""
|
|
||||||
operator = self.operator_service.resolve_operator()
|
|
||||||
user_vo_module = self.infrastructure_gateway.get_user_vo_module()
|
|
||||||
return user_vo_module.CurrentUserModel(
|
|
||||||
permissions=[],
|
|
||||||
roles=['admin'],
|
|
||||||
user=user_vo_module.UserInfoModel(
|
|
||||||
user_id=1,
|
|
||||||
user_name=operator,
|
|
||||||
nick_name=operator,
|
|
||||||
user_type='00',
|
|
||||||
status='0',
|
|
||||||
del_flag='0',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def normalize_table_names(table_names: list[str]) -> list[str]:
|
|
||||||
"""
|
|
||||||
规范化表名列表。
|
|
||||||
|
|
||||||
:param table_names: 原始表名列表
|
|
||||||
:return: 规范化后的表名列表
|
|
||||||
"""
|
|
||||||
return [table_name.strip() for table_name in table_names if table_name.strip()]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def serialize_gen_item(item: Any) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
序列化单个代码生成表记录。
|
|
||||||
|
|
||||||
:param item: 原始记录对象
|
|
||||||
:return: 可输出的字典
|
|
||||||
"""
|
|
||||||
if hasattr(item, 'model_dump'):
|
|
||||||
return dict(item.model_dump(by_alias=True, exclude_none=True))
|
|
||||||
return dict(item)
|
|
||||||
|
|
||||||
def serialize_gen_items(self, items: list[Any]) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
序列化代码生成表记录列表。
|
|
||||||
|
|
||||||
:param items: 原始记录列表
|
|
||||||
:return: 序列化后的字典列表
|
|
||||||
"""
|
|
||||||
return [self.serialize_gen_item(item) for item in items]
|
|
||||||
|
|
||||||
def parse_create_table_sql(self, sql: str) -> tuple[list[Any], list[str]]:
|
|
||||||
"""
|
|
||||||
解析建表 SQL 并提取建表语句信息。
|
|
||||||
|
|
||||||
:param sql: 原始 SQL 文本
|
|
||||||
:return: SQL AST 列表与建表表名列表
|
|
||||||
:raises ValueError: SQL 非法时抛出异常
|
|
||||||
"""
|
|
||||||
sqlglot_module = self.infrastructure_gateway.get_sqlglot_module()
|
|
||||||
expressions_module = self.infrastructure_gateway.get_sqlglot_expressions_module()
|
|
||||||
database_config = self.infrastructure_gateway.get_database_config()
|
|
||||||
sql_statements = sqlglot_module.parse(sql, dialect=database_config.sqlglot_parse_dialect)
|
|
||||||
has_create = any(isinstance(sql_statement, expressions_module.Create) for sql_statement in sql_statements)
|
|
||||||
has_forbidden_keyword = any(
|
|
||||||
isinstance(
|
|
||||||
sql_statement,
|
|
||||||
(
|
|
||||||
expressions_module.Add,
|
|
||||||
expressions_module.Alter,
|
|
||||||
expressions_module.Delete,
|
|
||||||
expressions_module.Drop,
|
|
||||||
expressions_module.Insert,
|
|
||||||
expressions_module.TruncateTable,
|
|
||||||
expressions_module.Update,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for sql_statement in sql_statements
|
|
||||||
)
|
|
||||||
if not has_create or has_forbidden_keyword:
|
|
||||||
raise ValueError('建表语句不合法,仅允许 CREATE TABLE 语句')
|
|
||||||
|
|
||||||
table_names = [
|
|
||||||
sql_statement.find(expressions_module.Table).name
|
|
||||||
for sql_statement in sql_statements
|
|
||||||
if isinstance(sql_statement, expressions_module.Create)
|
|
||||||
]
|
|
||||||
if not table_names:
|
|
||||||
raise ValueError('未解析到建表表名')
|
|
||||||
return sql_statements, table_names
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_sql_text(sql: str, sql_file: str) -> str:
|
|
||||||
"""
|
|
||||||
解析命令输入中的 SQL 文本。
|
|
||||||
|
|
||||||
:param sql: 直接传入的 SQL 文本
|
|
||||||
:param sql_file: SQL 文件路径
|
|
||||||
:return: 最终 SQL 文本
|
|
||||||
:raises ValueError: 参数非法时抛出异常
|
|
||||||
"""
|
|
||||||
if bool(sql.strip()) == bool(sql_file.strip()):
|
|
||||||
raise ValueError('必须且只能传入 --sql 或 --sql-file 其中一种方式')
|
|
||||||
if sql.strip():
|
|
||||||
return sql.strip()
|
|
||||||
|
|
||||||
sql_path = Path(sql_file).expanduser().resolve()
|
|
||||||
if not sql_path.is_file():
|
|
||||||
raise ValueError(f'SQL 文件不存在:{sql_path}')
|
|
||||||
return sql_path.read_text(encoding='utf-8').strip()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_output_file_path(target_file: str) -> str:
|
|
||||||
"""
|
|
||||||
解析导出文件绝对路径。
|
|
||||||
|
|
||||||
:param target_file: 原始导出文件路径
|
|
||||||
:return: 绝对路径字符串
|
|
||||||
"""
|
|
||||||
return str(Path(target_file).expanduser().resolve())
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_export_zip(target_file: str, zip_bytes: bytes) -> str:
|
|
||||||
"""
|
|
||||||
将导出的 zip 数据写入目标文件。
|
|
||||||
|
|
||||||
:param target_file: 目标文件路径
|
|
||||||
:param zip_bytes: zip 二进制内容
|
|
||||||
:return: 实际写入的绝对路径
|
|
||||||
"""
|
|
||||||
target_path = Path(target_file).expanduser().resolve()
|
|
||||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
target_path.write_bytes(zip_bytes)
|
|
||||||
return str(target_path)
|
|
||||||
|
|
||||||
def build_list_payload(
|
|
||||||
self,
|
|
||||||
result: Any,
|
|
||||||
*,
|
|
||||||
filters: dict[str, Any],
|
|
||||||
paged: bool,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
统一构建代码生成列表返回结构。
|
|
||||||
|
|
||||||
:param result: 原始结果对象
|
|
||||||
:param filters: 查询过滤条件
|
|
||||||
:param paged: 是否分页
|
|
||||||
:return: 可输出结果
|
|
||||||
"""
|
|
||||||
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
|
|
||||||
page_payload = result.model_dump(by_alias=True)
|
|
||||||
page_payload['rows'] = self.serialize_gen_items(page_payload.get('rows', []))
|
|
||||||
return {'ok': True, 'filters': filters, 'page': page_payload}
|
|
||||||
|
|
||||||
items = self.serialize_gen_items(result)
|
|
||||||
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import JOB_RUNTIME, JobRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['JOB_RUNTIME', 'JobRuntimeService']
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class JobInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
定时任务基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载数据库会话、Redis、调度器和任务服务依赖,
|
|
||||||
供定时任务运行时 facade 与其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_page_model() -> Any:
|
|
||||||
"""
|
|
||||||
获取分页模型类型。
|
|
||||||
|
|
||||||
:return: 分页模型类型
|
|
||||||
"""
|
|
||||||
return import_module('common.vo').PageModel
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_async_session_local() -> Any:
|
|
||||||
"""
|
|
||||||
获取异步数据库会话工厂。
|
|
||||||
|
|
||||||
:return: 异步数据库会话工厂
|
|
||||||
"""
|
|
||||||
return import_module('config.database').AsyncSessionLocal
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Redis 工具类。
|
|
||||||
|
|
||||||
:return: Redis 工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_redis').RedisUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_scheduler_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取调度器工具类。
|
|
||||||
|
|
||||||
:return: 调度器工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_scheduler').SchedulerUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_job_vo_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取定时任务 VO 模块。
|
|
||||||
|
|
||||||
:return: 定时任务 VO 模块
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.entity.vo.job_vo')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_job_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取定时任务服务类。
|
|
||||||
|
|
||||||
:return: 定时任务服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.service.job_service').JobService
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_job_log_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取定时任务日志服务类。
|
|
||||||
|
|
||||||
:return: 定时任务日志服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.service.job_log_service').JobLogService
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import DATABASE_ERROR, RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import JobInfrastructureGateway
|
|
||||||
from .support import JobDomainSupport, JobSchedulerSupport
|
|
||||||
|
|
||||||
|
|
||||||
class JobRuntimeService:
|
|
||||||
"""
|
|
||||||
定时任务运行时服务。
|
|
||||||
|
|
||||||
该服务作为定时任务运行时 facade,对外统一暴露任务列表、详情、日志、
|
|
||||||
执行一次、暂停与恢复入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
:param domain_support: 定时任务领域支持对象
|
|
||||||
:param scheduler_support: 定时任务调度上下文支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: JobInfrastructureGateway | None = None,
|
|
||||||
domain_support: JobDomainSupport | None = None,
|
|
||||||
scheduler_support: JobSchedulerSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化定时任务运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
:param domain_support: 定时任务领域支持对象
|
|
||||||
:param scheduler_support: 定时任务调度上下文支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or JobInfrastructureGateway()
|
|
||||||
self.domain_support = domain_support or JobDomainSupport(self.infrastructure_gateway)
|
|
||||||
self.scheduler_support = scheduler_support or JobSchedulerSupport(self.infrastructure_gateway)
|
|
||||||
|
|
||||||
async def list_jobs(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
job_name: str = '',
|
|
||||||
job_group: str = '',
|
|
||||||
status: str | None = None,
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
查询定时任务列表。
|
|
||||||
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 状态过滤条件
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 定时任务列表结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
|
|
||||||
job_service = self.infrastructure_gateway.get_job_service()
|
|
||||||
query_model = job_vo_module.JobPageQueryModel(
|
|
||||||
jobName=job_name or None,
|
|
||||||
jobGroup=job_group or None,
|
|
||||||
status=status,
|
|
||||||
pageNum=page_num,
|
|
||||||
pageSize=page_size,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await job_service.get_job_list_services(session, query_model, is_page=paged)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取定时任务列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
filters = self.domain_support.build_filters(
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
|
|
||||||
|
|
||||||
async def get_job_detail(self, job_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取单个定时任务详情。
|
|
||||||
|
|
||||||
:param job_id: 任务 ID
|
|
||||||
:return: 定时任务详情结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
job_service = self.infrastructure_gateway.get_job_service()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
job_model = await job_service.job_detail_services(session, job_id)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取定时任务详情失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
job_payload = self.domain_support.serialize_job_item(job_model)
|
|
||||||
if not job_payload.get('jobId'):
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': f'定时任务不存在:{job_id}',
|
|
||||||
'jobId': job_id,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
return {'ok': True, 'job': job_payload}
|
|
||||||
|
|
||||||
async def list_job_logs(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
job_name: str = '',
|
|
||||||
job_group: str = '',
|
|
||||||
status: str | None = None,
|
|
||||||
begin_date: str = '',
|
|
||||||
end_date: str = '',
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
查询定时任务执行日志列表。
|
|
||||||
|
|
||||||
:param job_name: 任务名称过滤条件
|
|
||||||
:param job_group: 任务组过滤条件
|
|
||||||
:param status: 执行状态过滤条件
|
|
||||||
:param begin_date: 开始日期,格式 `YYYY-MM-DD`
|
|
||||||
:param end_date: 结束日期,格式 `YYYY-MM-DD`
|
|
||||||
:param paged: 是否启用分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 定时任务日志列表结果
|
|
||||||
"""
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
job_log_service = self.infrastructure_gateway.get_job_log_service()
|
|
||||||
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
|
|
||||||
query_model = job_vo_module.JobLogPageQueryModel(
|
|
||||||
jobName=job_name or None,
|
|
||||||
jobGroup=job_group or None,
|
|
||||||
status=status,
|
|
||||||
beginTime=begin_date or None,
|
|
||||||
endTime=end_date or None,
|
|
||||||
pageNum=page_num,
|
|
||||||
pageSize=page_size,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
result = await job_log_service.get_job_log_list_services(session, query_model, is_page=paged)
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取定时任务日志列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
|
|
||||||
|
|
||||||
filters = self.domain_support.build_filters(
|
|
||||||
job_name=job_name,
|
|
||||||
job_group=job_group,
|
|
||||||
status=status,
|
|
||||||
begin_date=begin_date,
|
|
||||||
end_date=end_date,
|
|
||||||
paged=paged,
|
|
||||||
page_num=page_num,
|
|
||||||
page_size=page_size,
|
|
||||||
)
|
|
||||||
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
|
|
||||||
|
|
||||||
async def run_with_scheduler_context(
|
|
||||||
self,
|
|
||||||
operation: str,
|
|
||||||
job_id: int,
|
|
||||||
*,
|
|
||||||
status: str | None = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
在调度器上下文中执行任务操作。
|
|
||||||
|
|
||||||
:param operation: 操作名称
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:param status: 目标状态
|
|
||||||
:return: 任务操作结果
|
|
||||||
"""
|
|
||||||
operation_metadata = self.domain_support.build_job_operation_metadata(operation)
|
|
||||||
redis = None
|
|
||||||
async_session_local = self.infrastructure_gateway.get_async_session_local()
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
|
|
||||||
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
|
|
||||||
job_service = self.infrastructure_gateway.get_job_service()
|
|
||||||
try:
|
|
||||||
async with async_session_local() as session:
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
await scheduler_util.init_system_scheduler(redis)
|
|
||||||
|
|
||||||
if operation == 'run-once':
|
|
||||||
result = await job_service.execute_job_once_services(session, job_vo_module.JobModel(jobId=job_id))
|
|
||||||
elif operation in {'pause', 'resume'}:
|
|
||||||
result = await job_service.edit_job_services(
|
|
||||||
session,
|
|
||||||
job_vo_module.EditJobModel(jobId=job_id, status=status, type='status'),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ValueError(f'不支持的任务操作:{operation}')
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '执行定时任务操作失败', 'error': str(exc), 'exit_code': 22}
|
|
||||||
finally:
|
|
||||||
await self.scheduler_support.close_scheduler_context(redis)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': bool(result.is_success),
|
|
||||||
'jobId': job_id,
|
|
||||||
'operation': operation,
|
|
||||||
'operationLabel': operation_metadata['operationLabel'],
|
|
||||||
'targetStatus': status,
|
|
||||||
'message': operation_metadata['successMessage'] if result.is_success else '定时任务操作执行失败',
|
|
||||||
'serviceMessage': result.message,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def run_job_once(self, job_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
执行一次指定定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:return: 执行结果
|
|
||||||
"""
|
|
||||||
return await self.run_with_scheduler_context('run-once', job_id)
|
|
||||||
|
|
||||||
async def pause_job(self, job_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
暂停指定定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:return: 暂停结果
|
|
||||||
"""
|
|
||||||
return await self.run_with_scheduler_context('pause', job_id, status='1')
|
|
||||||
|
|
||||||
async def resume_job(self, job_id: int) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
恢复指定定时任务。
|
|
||||||
|
|
||||||
:param job_id: 任务ID
|
|
||||||
:return: 恢复结果
|
|
||||||
"""
|
|
||||||
return await self.run_with_scheduler_context('resume', job_id, status='0')
|
|
||||||
|
|
||||||
|
|
||||||
JOB_RUNTIME = JobRuntimeService()
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
from typing import Any
|
|
||||||
|
|
||||||
from .gateway import JobInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
class JobDomainSupport:
|
|
||||||
"""
|
|
||||||
定时任务领域支持对象。
|
|
||||||
|
|
||||||
该对象负责任务序列化、分页结果规整和任务操作元数据定义,
|
|
||||||
避免主运行时服务继续承载细碎领域规则。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None:
|
|
||||||
"""
|
|
||||||
初始化定时任务领域支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def serialize_job_item(job_item: Any) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
序列化单个定时任务模型。
|
|
||||||
|
|
||||||
:param job_item: 原始定时任务模型或字典
|
|
||||||
:return: 可输出的定时任务字典
|
|
||||||
"""
|
|
||||||
if hasattr(job_item, 'model_dump'):
|
|
||||||
return dict(job_item.model_dump(by_alias=True, exclude_none=True))
|
|
||||||
return dict(job_item)
|
|
||||||
|
|
||||||
def serialize_job_items(self, job_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
||||||
"""
|
|
||||||
序列化任务列表结果。
|
|
||||||
|
|
||||||
:param job_items: 原始任务列表
|
|
||||||
:return: 可输出的任务列表
|
|
||||||
"""
|
|
||||||
return [self.serialize_job_item(item) for item in job_items]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_job_operation_metadata(operation: str) -> dict[str, str]:
|
|
||||||
"""
|
|
||||||
构建任务操作的统一元数据。
|
|
||||||
|
|
||||||
:param operation: 操作名称
|
|
||||||
:return: 操作元数据字典
|
|
||||||
:raises ValueError: 操作名称不受支持时抛出异常
|
|
||||||
"""
|
|
||||||
metadata_mapping = {
|
|
||||||
'run-once': {
|
|
||||||
'operationLabel': '执行一次任务',
|
|
||||||
'successMessage': '定时任务已触发一次执行',
|
|
||||||
},
|
|
||||||
'pause': {
|
|
||||||
'operationLabel': '暂停任务',
|
|
||||||
'successMessage': '定时任务已暂停',
|
|
||||||
},
|
|
||||||
'resume': {
|
|
||||||
'operationLabel': '恢复任务',
|
|
||||||
'successMessage': '定时任务已恢复',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if operation not in metadata_mapping:
|
|
||||||
raise ValueError(f'不支持的任务操作:{operation}')
|
|
||||||
return metadata_mapping[operation]
|
|
||||||
|
|
||||||
def build_filters(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
job_name: str = '',
|
|
||||||
job_group: str = '',
|
|
||||||
status: str | None = None,
|
|
||||||
begin_date: str = '',
|
|
||||||
end_date: str = '',
|
|
||||||
paged: bool = False,
|
|
||||||
page_num: int = 1,
|
|
||||||
page_size: int = 20,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建任务列表或日志查询过滤条件。
|
|
||||||
|
|
||||||
:param job_name: 任务名称
|
|
||||||
:param job_group: 任务分组
|
|
||||||
:param status: 状态
|
|
||||||
:param begin_date: 开始日期
|
|
||||||
:param end_date: 结束日期
|
|
||||||
:param paged: 是否分页
|
|
||||||
:param page_num: 页码
|
|
||||||
:param page_size: 每页数量
|
|
||||||
:return: 过滤条件字典
|
|
||||||
"""
|
|
||||||
filters = {
|
|
||||||
'jobName': job_name,
|
|
||||||
'jobGroup': job_group,
|
|
||||||
'status': status,
|
|
||||||
'paged': paged,
|
|
||||||
'pageNum': page_num,
|
|
||||||
'pageSize': page_size,
|
|
||||||
}
|
|
||||||
if begin_date or end_date:
|
|
||||||
filters['beginDate'] = begin_date
|
|
||||||
filters['endDate'] = end_date
|
|
||||||
return filters
|
|
||||||
|
|
||||||
def build_list_payload(
|
|
||||||
self,
|
|
||||||
result: Any,
|
|
||||||
*,
|
|
||||||
filters: dict[str, Any],
|
|
||||||
paged: bool,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
统一构建任务列表或日志列表返回结构。
|
|
||||||
|
|
||||||
:param result: 原始结果对象
|
|
||||||
:param filters: 查询过滤条件
|
|
||||||
:param paged: 是否分页
|
|
||||||
:return: 可输出结果
|
|
||||||
"""
|
|
||||||
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
|
|
||||||
page_payload = result.model_dump(by_alias=True)
|
|
||||||
page_payload['rows'] = self.serialize_job_items(page_payload.get('rows', []))
|
|
||||||
return {'ok': True, 'filters': filters, 'page': page_payload}
|
|
||||||
|
|
||||||
items = self.serialize_job_items(result)
|
|
||||||
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
|
|
||||||
|
|
||||||
|
|
||||||
class JobSchedulerSupport:
|
|
||||||
"""
|
|
||||||
定时任务调度上下文支持对象。
|
|
||||||
|
|
||||||
该对象负责统一关闭调度器上下文资源,避免调度型操作在 facade 中
|
|
||||||
持续堆叠资源清理细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None:
|
|
||||||
"""
|
|
||||||
初始化定时任务调度上下文支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 定时任务基础设施网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
|
|
||||||
async def close_scheduler_context(self, redis: Any) -> None:
|
|
||||||
"""
|
|
||||||
安全关闭任务调度相关资源。
|
|
||||||
|
|
||||||
:param redis: Redis 客户端实例
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
|
|
||||||
try:
|
|
||||||
await scheduler_util.close_system_scheduler()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if redis is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
await redis.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .service import OPERATIONS_RUNTIME, OperationsRuntimeService
|
|
||||||
|
|
||||||
__all__ = ['OPERATIONS_RUNTIME', 'OperationsRuntimeService']
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
from importlib import import_module
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class OperationsInfrastructureGateway:
|
|
||||||
"""
|
|
||||||
运维基础设施网关。
|
|
||||||
|
|
||||||
该对象负责延迟加载 Redis、调度器、服务器监控与系统工具依赖,
|
|
||||||
供运维运行时 facade 和其协作对象统一复用。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取 Redis 工具类。
|
|
||||||
|
|
||||||
:return: Redis 工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_redis').RedisUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redis_error_class() -> type[Exception]:
|
|
||||||
"""
|
|
||||||
获取 Redis 异常类型。
|
|
||||||
|
|
||||||
:return: Redis 异常类型
|
|
||||||
"""
|
|
||||||
return import_module('redis.exceptions').RedisError
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_scheduler_util() -> Any:
|
|
||||||
"""
|
|
||||||
获取调度器工具类。
|
|
||||||
|
|
||||||
:return: 调度器工具类
|
|
||||||
"""
|
|
||||||
return import_module('config.get_scheduler').SchedulerUtil
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_server_service() -> Any:
|
|
||||||
"""
|
|
||||||
获取服务器监控服务类。
|
|
||||||
|
|
||||||
:return: 服务器监控服务类
|
|
||||||
"""
|
|
||||||
return import_module('module_admin.service.server_service').ServerService
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_psutil_module() -> Any:
|
|
||||||
"""
|
|
||||||
获取 `psutil` 模块。
|
|
||||||
|
|
||||||
:return: `psutil` 模块
|
|
||||||
"""
|
|
||||||
return import_module('psutil')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_bytes2human() -> Any:
|
|
||||||
"""
|
|
||||||
获取字节数转可读文本函数。
|
|
||||||
|
|
||||||
:return: `bytes2human` 函数
|
|
||||||
"""
|
|
||||||
return import_module('utils.common_util').bytes2human
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import socket
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR, SCHEDULER_ERROR
|
|
||||||
|
|
||||||
from .gateway import OperationsInfrastructureGateway
|
|
||||||
from .support import (
|
|
||||||
OperationsDependencyInspector,
|
|
||||||
OperationsServerInfoSupport,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OperationsRuntimeService:
|
|
||||||
"""
|
|
||||||
运维运行时服务。
|
|
||||||
|
|
||||||
该服务作为运维运行时 facade,对外统一暴露依赖版本检查、Redis 探活、
|
|
||||||
调度同步以及服务器运行时信息采集入口。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 运维基础设施网关
|
|
||||||
:param dependency_inspector: 运维依赖检查器
|
|
||||||
:param server_info_support: 服务器信息支持对象
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
infrastructure_gateway: OperationsInfrastructureGateway | None = None,
|
|
||||||
dependency_inspector: OperationsDependencyInspector | None = None,
|
|
||||||
server_info_support: OperationsServerInfoSupport | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
初始化运维运行时服务。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 运维基础设施网关
|
|
||||||
:param dependency_inspector: 运维依赖检查器
|
|
||||||
:param server_info_support: 服务器信息支持对象
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway or OperationsInfrastructureGateway()
|
|
||||||
self.dependency_inspector = dependency_inspector or OperationsDependencyInspector()
|
|
||||||
self.server_info_support = server_info_support or OperationsServerInfoSupport(self.infrastructure_gateway)
|
|
||||||
|
|
||||||
def get_dependency_versions(self, *, include_dev: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取 CLI 和后端运行所依赖的核心 Python 包版本。
|
|
||||||
|
|
||||||
:param include_dev: 是否附带开发阶段依赖
|
|
||||||
:return: 依赖检查结果
|
|
||||||
"""
|
|
||||||
return self.dependency_inspector.inspect(include_dev=include_dev)
|
|
||||||
|
|
||||||
async def ping_redis(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
检查 Redis 连通性。
|
|
||||||
|
|
||||||
:return: Redis 检查结果
|
|
||||||
"""
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
redis_error = self.infrastructure_gateway.get_redis_error_class()
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
try:
|
|
||||||
await redis.ping()
|
|
||||||
return {'ok': True, 'message': 'Redis连接成功'}
|
|
||||||
except redis_error as exc:
|
|
||||||
return {'ok': False, 'message': 'Redis连接失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
|
|
||||||
finally:
|
|
||||||
await redis.close()
|
|
||||||
|
|
||||||
async def sync_jobs(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
同步调度任务配置。
|
|
||||||
|
|
||||||
:return: 任务同步执行结果
|
|
||||||
"""
|
|
||||||
redis = None
|
|
||||||
scheduler_util = None
|
|
||||||
try:
|
|
||||||
redis_util = self.infrastructure_gateway.get_redis_util()
|
|
||||||
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
|
|
||||||
redis = await redis_util.create_redis_pool(log_enabled=False)
|
|
||||||
await scheduler_util.init_system_scheduler(redis)
|
|
||||||
await scheduler_util.request_scheduler_sync()
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'operation': 'sync',
|
|
||||||
'operationLabel': '同步调度配置',
|
|
||||||
'schedulerSyncRequested': True,
|
|
||||||
'message': '调度配置同步请求已发送',
|
|
||||||
'isLeader': scheduler_util._is_leader,
|
|
||||||
}
|
|
||||||
except Exception as exc:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'operation': 'sync',
|
|
||||||
'operationLabel': '同步调度配置',
|
|
||||||
'schedulerSyncRequested': False,
|
|
||||||
'message': '调度配置同步失败',
|
|
||||||
'error': str(exc),
|
|
||||||
'exit_code': SCHEDULER_ERROR,
|
|
||||||
}
|
|
||||||
finally:
|
|
||||||
if scheduler_util is not None:
|
|
||||||
try:
|
|
||||||
await scheduler_util.close_system_scheduler()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if redis is not None:
|
|
||||||
try:
|
|
||||||
await redis.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def get_server_info(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
获取服务器运行信息。
|
|
||||||
|
|
||||||
:return: 服务器运行信息字典
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
server_service = self.infrastructure_gateway.get_server_service()
|
|
||||||
return {'ok': True, 'server': (await server_service.get_server_monitor_info()).model_dump(by_alias=True)}
|
|
||||||
except socket.gaierror:
|
|
||||||
try:
|
|
||||||
return {'ok': True, 'server': self.server_info_support.build_server_info_fallback()}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
|
|
||||||
except Exception as exc:
|
|
||||||
return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
|
|
||||||
|
|
||||||
|
|
||||||
OPERATIONS_RUNTIME = OperationsRuntimeService()
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
import os
|
|
||||||
import platform
|
|
||||||
import socket
|
|
||||||
import time
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from importlib import metadata
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from cli.exit_codes import RUNTIME_ERROR
|
|
||||||
|
|
||||||
from .gateway import OperationsInfrastructureGateway
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class OperationsDependencySpec:
|
|
||||||
"""
|
|
||||||
运维依赖检查项定义。
|
|
||||||
|
|
||||||
:param package_name: CLI 展示用包名
|
|
||||||
:param distribution: Python 分发名称
|
|
||||||
:param required: 是否为必需依赖
|
|
||||||
:param version: 显式版本,存在时跳过环境读取
|
|
||||||
"""
|
|
||||||
|
|
||||||
package_name: str
|
|
||||||
distribution: str
|
|
||||||
required: bool
|
|
||||||
version: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class OperationsDependencyInspector:
|
|
||||||
"""
|
|
||||||
运维依赖检查器。
|
|
||||||
|
|
||||||
该对象负责核心依赖声明、版本探测与依赖检查结果构建,
|
|
||||||
使运行时 facade 不再直接维护细碎依赖定义。
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def read_package_version(distribution_name: str) -> str | None:
|
|
||||||
"""
|
|
||||||
读取指定 Python 包的已安装版本号。
|
|
||||||
|
|
||||||
:param distribution_name: Python 包分发名称
|
|
||||||
:return: 已安装版本号,未安装时返回 `None`
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return metadata.version(distribution_name)
|
|
||||||
except metadata.PackageNotFoundError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_dependency_specs(*, include_dev: bool = False) -> list[OperationsDependencySpec]:
|
|
||||||
"""
|
|
||||||
构建当前依赖检查项列表。
|
|
||||||
|
|
||||||
:param include_dev: 是否附带开发阶段依赖
|
|
||||||
:return: 依赖检查项列表
|
|
||||||
"""
|
|
||||||
dependency_specs = [
|
|
||||||
OperationsDependencySpec('python', '', True, platform.python_version()),
|
|
||||||
OperationsDependencySpec('fastapi', 'fastapi', True),
|
|
||||||
OperationsDependencySpec('uvicorn', 'uvicorn', True),
|
|
||||||
OperationsDependencySpec('sqlalchemy', 'SQLAlchemy', True),
|
|
||||||
OperationsDependencySpec('alembic', 'alembic', True),
|
|
||||||
OperationsDependencySpec('redis', 'redis', True),
|
|
||||||
OperationsDependencySpec('pydantic', 'pydantic', True),
|
|
||||||
OperationsDependencySpec('pydantic-settings', 'pydantic-settings', True),
|
|
||||||
OperationsDependencySpec('typer', 'typer', True),
|
|
||||||
]
|
|
||||||
if include_dev:
|
|
||||||
dependency_specs.extend(
|
|
||||||
[
|
|
||||||
OperationsDependencySpec('pytest', 'pytest', False),
|
|
||||||
OperationsDependencySpec('ruff', 'ruff', False),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return dependency_specs
|
|
||||||
|
|
||||||
def inspect(self, *, include_dev: bool = False) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
读取 CLI 和后端运行所依赖的核心 Python 包版本。
|
|
||||||
|
|
||||||
:param include_dev: 是否附带开发阶段依赖
|
|
||||||
:return: 依赖检查结果
|
|
||||||
"""
|
|
||||||
packages: dict[str, dict[str, Any]] = {}
|
|
||||||
missing_required: list[str] = []
|
|
||||||
for dependency_spec in self.build_dependency_specs(include_dev=include_dev):
|
|
||||||
version = dependency_spec.version or self.read_package_version(dependency_spec.distribution)
|
|
||||||
installed = bool(version)
|
|
||||||
package_payload = {
|
|
||||||
'installed': installed,
|
|
||||||
'version': version or '',
|
|
||||||
'required': dependency_spec.required,
|
|
||||||
}
|
|
||||||
if dependency_spec.distribution:
|
|
||||||
package_payload['distribution'] = dependency_spec.distribution
|
|
||||||
packages[dependency_spec.package_name] = package_payload
|
|
||||||
if dependency_spec.required and not installed:
|
|
||||||
missing_required.append(dependency_spec.package_name)
|
|
||||||
|
|
||||||
if missing_required:
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'message': '存在缺失的核心运行依赖',
|
|
||||||
'missingRequired': missing_required,
|
|
||||||
'includeDev': include_dev,
|
|
||||||
'packages': packages,
|
|
||||||
'exit_code': RUNTIME_ERROR,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ok': True,
|
|
||||||
'message': '核心运行依赖已安装',
|
|
||||||
'missingRequired': [],
|
|
||||||
'includeDev': include_dev,
|
|
||||||
'packages': packages,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class OperationsServerInfoSupport:
|
|
||||||
"""
|
|
||||||
运维服务器信息支持对象。
|
|
||||||
|
|
||||||
该对象负责 CLI 兜底服务器信息构建与 IP 解析逻辑,
|
|
||||||
避免主运行时服务继续承载大量平台采集细节。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 运维基础设施网关
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, infrastructure_gateway: OperationsInfrastructureGateway) -> None:
|
|
||||||
"""
|
|
||||||
初始化服务器信息支持对象。
|
|
||||||
|
|
||||||
:param infrastructure_gateway: 运维基础设施网关
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.infrastructure_gateway = infrastructure_gateway
|
|
||||||
|
|
||||||
def resolve_server_ip(self, hostname: str) -> str:
|
|
||||||
"""
|
|
||||||
解析服务器 IP,优先使用主机名解析,失败时回退到网卡地址。
|
|
||||||
|
|
||||||
:param hostname: 当前主机名
|
|
||||||
:return: 可用的 IPv4 地址
|
|
||||||
"""
|
|
||||||
psutil = self.infrastructure_gateway.get_psutil_module()
|
|
||||||
try:
|
|
||||||
resolved_ip = socket.gethostbyname(hostname)
|
|
||||||
if resolved_ip:
|
|
||||||
return resolved_ip
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for interface_addresses in psutil.net_if_addrs().values():
|
|
||||||
for address_info in interface_addresses:
|
|
||||||
if (
|
|
||||||
address_info.family == socket.AF_INET
|
|
||||||
and address_info.address
|
|
||||||
and not address_info.address.startswith('127.')
|
|
||||||
):
|
|
||||||
return address_info.address
|
|
||||||
|
|
||||||
return '127.0.0.1'
|
|
||||||
|
|
||||||
def build_server_info_fallback(self) -> dict[str, Any]:
|
|
||||||
"""
|
|
||||||
构建 CLI 使用的服务器运行信息兜底数据。
|
|
||||||
|
|
||||||
:return: 与原服务输出结构兼容的服务器运行信息字典
|
|
||||||
"""
|
|
||||||
psutil = self.infrastructure_gateway.get_psutil_module()
|
|
||||||
bytes2human = self.infrastructure_gateway.get_bytes2human()
|
|
||||||
cpu_num = psutil.cpu_count(logical=True)
|
|
||||||
cpu_usage_percent = psutil.cpu_times_percent()
|
|
||||||
memory_info = psutil.virtual_memory()
|
|
||||||
|
|
||||||
hostname = socket.gethostname()
|
|
||||||
current_process = psutil.Process(os.getpid())
|
|
||||||
start_time_stamp = current_process.create_time()
|
|
||||||
current_time_stamp = time.time()
|
|
||||||
difference = current_time_stamp - start_time_stamp
|
|
||||||
days = int(difference // (24 * 60 * 60))
|
|
||||||
hours = int((difference % (24 * 60 * 60)) // (60 * 60))
|
|
||||||
minutes = int((difference % (60 * 60)) // 60)
|
|
||||||
process_memory_info = current_process.memory_info()
|
|
||||||
|
|
||||||
sys_files: list[dict[str, Any]] = []
|
|
||||||
for partition in psutil.disk_partitions():
|
|
||||||
try:
|
|
||||||
disk_usage = psutil.disk_usage(partition.mountpoint)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
mountpoint = partition.mountpoint.replace('\\', '')
|
|
||||||
sys_files.append(
|
|
||||||
{
|
|
||||||
'dirName': partition.device,
|
|
||||||
'sysTypeName': partition.fstype,
|
|
||||||
'typeName': f'本地固定磁盘({mountpoint})',
|
|
||||||
'total': bytes2human(disk_usage.total),
|
|
||||||
'used': bytes2human(disk_usage.used),
|
|
||||||
'free': bytes2human(disk_usage.free),
|
|
||||||
'usage': f'{disk_usage.percent}%',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'cpu': {
|
|
||||||
'cpuNum': cpu_num,
|
|
||||||
'used': cpu_usage_percent.user,
|
|
||||||
'sys': cpu_usage_percent.system,
|
|
||||||
'free': cpu_usage_percent.idle,
|
|
||||||
},
|
|
||||||
'mem': {
|
|
||||||
'total': bytes2human(memory_info.total),
|
|
||||||
'used': bytes2human(memory_info.used),
|
|
||||||
'free': bytes2human(memory_info.free),
|
|
||||||
'usage': memory_info.percent,
|
|
||||||
},
|
|
||||||
'sys': {
|
|
||||||
'computerIp': self.resolve_server_ip(hostname),
|
|
||||||
'computerName': platform.node(),
|
|
||||||
'osArch': platform.machine(),
|
|
||||||
'osName': platform.platform(),
|
|
||||||
'userDir': os.getcwd(),
|
|
||||||
},
|
|
||||||
'py': {
|
|
||||||
'name': current_process.name(),
|
|
||||||
'version': platform.python_version(),
|
|
||||||
'startTime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp)),
|
|
||||||
'runTime': f'{days}天{hours}小时{minutes}分钟',
|
|
||||||
'home': current_process.exe(),
|
|
||||||
'total': bytes2human(memory_info.available),
|
|
||||||
'used': bytes2human(process_memory_info.rss),
|
|
||||||
'free': bytes2human(memory_info.available - process_memory_info.rss),
|
|
||||||
'usage': round((process_memory_info.rss / memory_info.available) * 100, 2),
|
|
||||||
},
|
|
||||||
'sysFiles': sys_files,
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user