调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
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',
|
||||
]
|
||||
@@ -0,0 +1,363 @@
|
||||
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()
|
||||
@@ -0,0 +1,121 @@
|
||||
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()
|
||||
@@ -0,0 +1,239 @@
|
||||
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)
|
||||
@@ -0,0 +1,137 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user