调整部分前端布局和后端增删改逻辑
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .command import app
|
||||
|
||||
__all__ = ['app']
|
||||
@@ -0,0 +1,102 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,236 @@
|
||||
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]))
|
||||
@@ -0,0 +1,210 @@
|
||||
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}'
|
||||
Reference in New Issue
Block a user