pytbox 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pytbox might be problematic. Click here for more details.
- pytbox/alert/alert_handler.py +3 -3
- pytbox/base.py +26 -19
- pytbox/categraf/build_config.py +53 -0
- pytbox/cli/__init__.py +7 -0
- pytbox/cli/categraf/__init__.py +7 -0
- pytbox/cli/categraf/commands.py +55 -0
- pytbox/cli/common/__init__.py +6 -0
- pytbox/cli/common/options.py +42 -0
- pytbox/cli/common/utils.py +269 -0
- pytbox/cli/formatters/__init__.py +7 -0
- pytbox/cli/formatters/output.py +155 -0
- pytbox/cli/main.py +22 -0
- pytbox/cli.py +9 -0
- pytbox/database/mongo.py +43 -2
- pytbox/database/victoriametrics.py +3 -3
- pytbox/feishu/endpoints.py +6 -3
- pytbox/log/logger.py +55 -24
- pytbox/log/victorialog.py +2 -2
- pytbox/utils/richutils.py +21 -0
- pytbox/utils/timeutils.py +513 -19
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/METADATA +9 -2
- pytbox-0.0.6.dist-info/RECORD +39 -0
- pytbox-0.0.6.dist-info/entry_points.txt +2 -0
- pytbox/common/base.py +0 -0
- pytbox-0.0.4.dist-info/RECORD +0 -27
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/WHEEL +0 -0
- {pytbox-0.0.4.dist-info → pytbox-0.0.6.dist-info}/top_level.txt +0 -0
pytbox/alert/alert_handler.py
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import uuid
|
|
5
5
|
from typing import Literal
|
|
6
6
|
from ..database.mongo import Mongo
|
|
7
|
-
from ..base import MongoClient
|
|
8
7
|
from ..feishu.client import Client as FeishuClient
|
|
9
8
|
from ..dida365 import Dida365
|
|
10
9
|
from ..utils.timeutils import TimeUtils
|
|
@@ -14,7 +13,7 @@ class AlertHandler:
|
|
|
14
13
|
|
|
15
14
|
def __init__(self,
|
|
16
15
|
config: dict=None,
|
|
17
|
-
mongo_client: Mongo=
|
|
16
|
+
mongo_client: Mongo=None,
|
|
18
17
|
feishu_client: FeishuClient=None,
|
|
19
18
|
dida_client: Dida365=None
|
|
20
19
|
):
|
|
@@ -70,7 +69,8 @@ class AlertHandler:
|
|
|
70
69
|
f'**事件内容**: {event_content}',
|
|
71
70
|
f'**告警实例**: {entity_name}',
|
|
72
71
|
f'**建议**: {suggestion}',
|
|
73
|
-
f'**故障排查**: {troubleshot}'
|
|
72
|
+
f'**故障排查**: {troubleshot}',
|
|
73
|
+
f'**历史告警**: {self.mongo.recent_alerts(event_content=event_content)}'
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
if event_type == "resolved":
|
pytbox/base.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import os
|
|
4
4
|
from pytbox.database.mongo import Mongo
|
|
5
5
|
from pytbox.utils.load_config import load_config_by_file
|
|
6
6
|
from pytbox.database.victoriametrics import VictoriaMetrics
|
|
7
7
|
from pytbox.feishu.client import Client as FeishuClient
|
|
8
8
|
from pytbox.dida365 import Dida365
|
|
9
|
+
from pytbox.alert.alert_handler import AlertHandler
|
|
10
|
+
from pytbox.log.logger import AppLogger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
config = load_config_by_file(path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=os.environ.get('oc_vault_id'))
|
|
9
14
|
|
|
10
15
|
|
|
11
|
-
def
|
|
12
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
16
|
+
def get_mongo(collection):
|
|
13
17
|
return Mongo(
|
|
14
18
|
host=config['mongo']['host'],
|
|
15
19
|
port=config['mongo']['port'],
|
|
@@ -20,22 +24,25 @@ def MongoClient(collection, config_path='/workspaces/pytbox/tests/alert/config_d
|
|
|
20
24
|
collection=collection
|
|
21
25
|
)
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
25
|
-
return VictoriaMetrics(
|
|
26
|
-
url=config['victoriametrics']['url']
|
|
27
|
-
)
|
|
27
|
+
vm = VictoriaMetrics(url=config['victoriametrics']['url'])
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
config
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
feishu = FeishuClient(
|
|
30
|
+
app_id=config['feishu']['app_id'],
|
|
31
|
+
app_secret=config['feishu']['app_secret']
|
|
32
|
+
)
|
|
33
|
+
dida = Dida365(
|
|
34
|
+
cookie=config['dida']['cookie'],
|
|
35
|
+
access_token=config['dida']['access_token']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
alert_handler = AlertHandler(config=config, mongo_client=get_mongo('alert_test'), feishu_client=feishu, dida_client=dida)
|
|
35
39
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
def get_logger(app):
|
|
41
|
+
return AppLogger(
|
|
42
|
+
app_name=app,
|
|
43
|
+
enable_victorialog=True,
|
|
44
|
+
victorialog_url=config['victorialog']['url'],
|
|
45
|
+
feishu=feishu,
|
|
46
|
+
dida=dida,
|
|
47
|
+
mongo=get_mongo('alert_program')
|
|
41
48
|
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from pytbox.utils.load_config import load_config_by_file
|
|
6
|
+
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
jinja2_path = Path(__file__).parent / 'jinja2'
|
|
11
|
+
env = Environment(loader=FileSystemLoader(jinja2_path))
|
|
12
|
+
|
|
13
|
+
ping_template = env.get_template('input.ping/ping.toml.j2')
|
|
14
|
+
prometheus_template = env.get_template('input.prometheus/prometheus.toml.j2')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BuildConfig:
|
|
18
|
+
'''
|
|
19
|
+
生成配置
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
instances (_type_): _description_
|
|
23
|
+
output_dir (_type_): _description_
|
|
24
|
+
'''
|
|
25
|
+
def __init__(self, instances, output_dir):
|
|
26
|
+
self.instances = load_config_by_file(instances)
|
|
27
|
+
self.output_dir = output_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ping(self):
|
|
32
|
+
instances = self.instances['ping']['instance']
|
|
33
|
+
render_data = ping_template.render(instances=instances)
|
|
34
|
+
target_dir = Path(self.output_dir) / 'input.ping'
|
|
35
|
+
if not target_dir.exists():
|
|
36
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
with open(Path(self.output_dir) / 'input.ping' / 'ping.toml', 'w', encoding='utf-8') as f:
|
|
39
|
+
f.write(render_data)
|
|
40
|
+
|
|
41
|
+
def prometheus(self):
|
|
42
|
+
instances = self.instances['prometheus']['urls']
|
|
43
|
+
render_data = prometheus_template.render(instances=instances)
|
|
44
|
+
target_dir = Path(self.output_dir) / 'input.prometheus'
|
|
45
|
+
if not target_dir.exists():
|
|
46
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
with open(Path(self.output_dir) / 'input.prometheus' / 'prometheus.toml', 'w', encoding='utf-8') as f:
|
|
49
|
+
f.write(render_data)
|
|
50
|
+
|
|
51
|
+
def run(self):
|
|
52
|
+
# self.ping()
|
|
53
|
+
self.prometheus()
|
pytbox/cli/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Categraf 相关命令 - 支持 rich 美化输出
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import click
|
|
8
|
+
from ...utils.richutils import RichUtils
|
|
9
|
+
from ...categraf.build_config import BuildConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
rich_utils = RichUtils()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group()
|
|
17
|
+
def categraf_group():
|
|
18
|
+
"""Categraf 配置管理工具"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@categraf_group.command('get-instances')
|
|
23
|
+
@click.option('--output-dir', '-o', type=click.Path(exists=True), default='.')
|
|
24
|
+
def get_instances(output_dir):
|
|
25
|
+
"""获取 Categraf 实例配置"""
|
|
26
|
+
instances_template_path = Path(__file__).parent.parent.parent / 'categraf' / 'instances.toml'
|
|
27
|
+
dest_path = Path(output_dir) / 'instances.toml'
|
|
28
|
+
shutil.copy(instances_template_path, dest_path)
|
|
29
|
+
rich_utils.print(msg=f'已将 {instances_template_path} 复制到 {dest_path}', style='info')
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@categraf_group.command('build-config')
|
|
33
|
+
@click.option('--instances', '-i', type=click.Path(exists=True), default='.')
|
|
34
|
+
@click.option('--output-dir', '-o', type=click.Path(exists=True), default='.')
|
|
35
|
+
def build_config(instances, output_dir):
|
|
36
|
+
'''
|
|
37
|
+
生成配置
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
instances (_type_): _description_
|
|
41
|
+
output_dir (_type_): _description_
|
|
42
|
+
'''
|
|
43
|
+
# ping_template_path = Path(__file__).parent.parent.parent / 'categraf' / 'ping.toml'
|
|
44
|
+
# dest_path = Path(output_dir) / 'ping.toml'
|
|
45
|
+
# shutil.copy(ping_template_path, dest_path)
|
|
46
|
+
# rich_utils.print(msg=f'已将 {ping_template_path} 复制到 {dest_path}', style='info')
|
|
47
|
+
# 获取 instances 和 output_dir 的绝对路径
|
|
48
|
+
instances_abs = str(Path(instances).resolve())
|
|
49
|
+
output_dir_abs = str(Path(output_dir).resolve())
|
|
50
|
+
|
|
51
|
+
rich_utils.print(msg=f'instances 绝对路径: {instances_abs}', style='info')
|
|
52
|
+
rich_utils.print(msg=f'output_dir 绝对路径: {output_dir_abs}', style='info')
|
|
53
|
+
|
|
54
|
+
build_config = BuildConfig(instances_abs, output_dir_abs)
|
|
55
|
+
build_config.run()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
通用的 Click 选项定义
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
# 通用选项
|
|
8
|
+
output_option = click.option(
|
|
9
|
+
'--output', '-o',
|
|
10
|
+
type=click.Path(),
|
|
11
|
+
help='输出到文件'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
format_option = click.option(
|
|
15
|
+
'--format', 'output_format',
|
|
16
|
+
type=click.Choice(['toml', 'json', 'yaml']),
|
|
17
|
+
default='toml',
|
|
18
|
+
help='输出格式'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
data_option = click.option(
|
|
22
|
+
'--data', '-d',
|
|
23
|
+
help='JSON 格式的模板变量'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
data_file_option = click.option(
|
|
27
|
+
'--data-file',
|
|
28
|
+
type=click.Path(exists=True),
|
|
29
|
+
help='包含模板变量的 JSON 文件'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
verbose_option = click.option(
|
|
33
|
+
'--verbose', '-v',
|
|
34
|
+
is_flag=True,
|
|
35
|
+
help='显示详细信息'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
quiet_option = click.option(
|
|
39
|
+
'--quiet', '-q',
|
|
40
|
+
is_flag=True,
|
|
41
|
+
help='静默模式,只显示错误'
|
|
42
|
+
)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI 通用工具函数 - 集成 rich 支持
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Dict, Any, Union
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.progress import track
|
|
15
|
+
from rich.syntax import Syntax
|
|
16
|
+
from rich.tree import Tree
|
|
17
|
+
RICH_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
RICH_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
# 如果 rich 不可用,使用标准输出
|
|
22
|
+
import click
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Logger:
|
|
26
|
+
"""增强的日志器,支持 rich 格式化输出"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, verbose: bool = False, quiet: bool = False):
|
|
29
|
+
self.verbose = verbose
|
|
30
|
+
self.quiet = quiet
|
|
31
|
+
|
|
32
|
+
if RICH_AVAILABLE:
|
|
33
|
+
self.console = Console()
|
|
34
|
+
else:
|
|
35
|
+
self.console = None
|
|
36
|
+
|
|
37
|
+
def info(self, message: str, style: str = "info"):
|
|
38
|
+
"""信息日志"""
|
|
39
|
+
if self.quiet:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if RICH_AVAILABLE:
|
|
43
|
+
if style == "success":
|
|
44
|
+
self.console.print(f"✅ {message}", style="bold green")
|
|
45
|
+
elif style == "warning":
|
|
46
|
+
self.console.print(f"⚠️ {message}", style="bold yellow")
|
|
47
|
+
elif style == "error":
|
|
48
|
+
self.console.print(f"❌ {message}", style="bold red")
|
|
49
|
+
else:
|
|
50
|
+
self.console.print(f"ℹ️ {message}", style="bold blue")
|
|
51
|
+
else:
|
|
52
|
+
click.echo(message)
|
|
53
|
+
|
|
54
|
+
def success(self, message: str):
|
|
55
|
+
"""成功日志"""
|
|
56
|
+
self.info(message, "success")
|
|
57
|
+
|
|
58
|
+
def warning(self, message: str):
|
|
59
|
+
"""警告日志"""
|
|
60
|
+
self.info(message, "warning")
|
|
61
|
+
|
|
62
|
+
def error(self, message: str):
|
|
63
|
+
"""错误日志"""
|
|
64
|
+
if RICH_AVAILABLE:
|
|
65
|
+
self.console.print(f"❌ {message}", style="bold red", err=True)
|
|
66
|
+
else:
|
|
67
|
+
click.echo(f"错误: {message}", err=True)
|
|
68
|
+
|
|
69
|
+
def debug(self, message: str):
|
|
70
|
+
"""调试日志"""
|
|
71
|
+
if self.verbose:
|
|
72
|
+
if RICH_AVAILABLE:
|
|
73
|
+
self.console.print(f"🔍 {message}", style="dim")
|
|
74
|
+
else:
|
|
75
|
+
click.echo(f"DEBUG: {message}")
|
|
76
|
+
|
|
77
|
+
def print_panel(self, content: str, title: str = "", style: str = "info"):
|
|
78
|
+
"""打印面板"""
|
|
79
|
+
if self.quiet:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if RICH_AVAILABLE:
|
|
83
|
+
if style == "success":
|
|
84
|
+
panel_style = "green"
|
|
85
|
+
elif style == "warning":
|
|
86
|
+
panel_style = "yellow"
|
|
87
|
+
elif style == "error":
|
|
88
|
+
panel_style = "red"
|
|
89
|
+
else:
|
|
90
|
+
panel_style = "blue"
|
|
91
|
+
|
|
92
|
+
panel = Panel(content, title=title, border_style=panel_style)
|
|
93
|
+
self.console.print(panel)
|
|
94
|
+
else:
|
|
95
|
+
if title:
|
|
96
|
+
click.echo(f"=== {title} ===")
|
|
97
|
+
click.echo(content)
|
|
98
|
+
|
|
99
|
+
def print_table(self, data: list, headers: list, title: str = ""):
|
|
100
|
+
"""打印表格"""
|
|
101
|
+
if self.quiet:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
if RICH_AVAILABLE:
|
|
105
|
+
table = Table(title=title, show_header=True, header_style="bold magenta")
|
|
106
|
+
|
|
107
|
+
for header in headers:
|
|
108
|
+
table.add_column(header)
|
|
109
|
+
|
|
110
|
+
for row in data:
|
|
111
|
+
table.add_row(*[str(cell) for cell in row])
|
|
112
|
+
|
|
113
|
+
self.console.print(table)
|
|
114
|
+
else:
|
|
115
|
+
if title:
|
|
116
|
+
click.echo(f"=== {title} ===")
|
|
117
|
+
click.echo("\t".join(headers))
|
|
118
|
+
for row in data:
|
|
119
|
+
click.echo("\t".join(str(cell) for cell in row))
|
|
120
|
+
|
|
121
|
+
def print_syntax(self, code: str, language: str = "toml", title: str = ""):
|
|
122
|
+
"""打印语法高亮的代码"""
|
|
123
|
+
if self.quiet:
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
if RICH_AVAILABLE:
|
|
127
|
+
syntax = Syntax(code, language, theme="monokai", line_numbers=True)
|
|
128
|
+
if title:
|
|
129
|
+
panel = Panel(syntax, title=title, border_style="blue")
|
|
130
|
+
self.console.print(panel)
|
|
131
|
+
else:
|
|
132
|
+
self.console.print(syntax)
|
|
133
|
+
else:
|
|
134
|
+
if title:
|
|
135
|
+
click.echo(f"=== {title} ===")
|
|
136
|
+
click.echo(code)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# 全局日志器实例
|
|
140
|
+
logger = Logger()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def set_logger_config(verbose: bool = False, quiet: bool = False):
|
|
144
|
+
"""设置日志器配置"""
|
|
145
|
+
global logger
|
|
146
|
+
logger = Logger(verbose=verbose, quiet=quiet)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def handle_error(error: Exception):
|
|
150
|
+
"""统一的错误处理"""
|
|
151
|
+
logger.error(str(error))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def write_output(content: str, output_path: Optional[str] = None, content_type: str = "text"):
|
|
155
|
+
"""统一的输出处理"""
|
|
156
|
+
if output_path:
|
|
157
|
+
try:
|
|
158
|
+
output_file = Path(output_path)
|
|
159
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
162
|
+
f.write(content)
|
|
163
|
+
|
|
164
|
+
logger.success(f"内容已保存到: {output_path}")
|
|
165
|
+
logger.debug(f"文件大小: {len(content)} 字符")
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"保存文件失败: {e}")
|
|
169
|
+
raise
|
|
170
|
+
else:
|
|
171
|
+
# 根据内容类型选择合适的显示方式
|
|
172
|
+
if content_type == "json":
|
|
173
|
+
logger.print_syntax(content, "json", "JSON 内容")
|
|
174
|
+
elif content_type == "yaml":
|
|
175
|
+
logger.print_syntax(content, "yaml", "YAML 内容")
|
|
176
|
+
elif content_type == "toml":
|
|
177
|
+
logger.print_syntax(content, "toml", "TOML 内容")
|
|
178
|
+
elif content_type == "template":
|
|
179
|
+
logger.print_syntax(content, "jinja2", "模板内容")
|
|
180
|
+
else:
|
|
181
|
+
logger.print_panel(content, "输出内容")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def load_template_vars(data_str: Optional[str] = None, data_file: Optional[str] = None) -> Dict[str, Any]:
|
|
185
|
+
"""加载模板变量"""
|
|
186
|
+
template_vars = {}
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
if data_file:
|
|
190
|
+
logger.debug(f"从文件加载变量: {data_file}")
|
|
191
|
+
with open(data_file, 'r', encoding='utf-8') as f:
|
|
192
|
+
file_vars = json.load(f)
|
|
193
|
+
template_vars.update(file_vars)
|
|
194
|
+
logger.debug(f"从文件加载了 {len(file_vars)} 个变量")
|
|
195
|
+
|
|
196
|
+
if data_str:
|
|
197
|
+
logger.debug("从命令行加载变量")
|
|
198
|
+
cli_vars = json.loads(data_str)
|
|
199
|
+
template_vars.update(cli_vars)
|
|
200
|
+
logger.debug(f"从命令行加载了 {len(cli_vars)} 个变量")
|
|
201
|
+
|
|
202
|
+
if template_vars:
|
|
203
|
+
logger.debug(f"总计加载变量: {list(template_vars.keys())}")
|
|
204
|
+
|
|
205
|
+
return template_vars
|
|
206
|
+
|
|
207
|
+
except json.JSONDecodeError as e:
|
|
208
|
+
raise ValueError(f"JSON 格式错误: {e}")
|
|
209
|
+
except FileNotFoundError as e:
|
|
210
|
+
raise FileNotFoundError(f"数据文件不存在: {e}")
|
|
211
|
+
except Exception as e:
|
|
212
|
+
raise Exception(f"加载模板变量失败: {e}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def show_progress(items, description: str = "处理中..."):
|
|
216
|
+
"""显示进度条"""
|
|
217
|
+
if RICH_AVAILABLE and not logger.quiet:
|
|
218
|
+
return track(items, description=description)
|
|
219
|
+
else:
|
|
220
|
+
return items
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def create_tree_view(data: dict, title: str = "数据结构") -> None:
|
|
224
|
+
"""创建树形视图显示数据"""
|
|
225
|
+
if logger.quiet:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if RICH_AVAILABLE:
|
|
229
|
+
tree = Tree(title)
|
|
230
|
+
|
|
231
|
+
def add_dict_to_tree(node, data_dict):
|
|
232
|
+
for key, value in data_dict.items():
|
|
233
|
+
if isinstance(value, dict):
|
|
234
|
+
child = node.add(f"[bold blue]{key}[/bold blue]")
|
|
235
|
+
add_dict_to_tree(child, value)
|
|
236
|
+
elif isinstance(value, list):
|
|
237
|
+
child = node.add(f"[bold green]{key}[/bold green] ({len(value)} items)")
|
|
238
|
+
for i, item in enumerate(value):
|
|
239
|
+
if isinstance(item, dict):
|
|
240
|
+
item_node = child.add(f"[dim]Item {i}[/dim]")
|
|
241
|
+
add_dict_to_tree(item_node, item)
|
|
242
|
+
else:
|
|
243
|
+
child.add(f"[dim]{item}[/dim]")
|
|
244
|
+
else:
|
|
245
|
+
node.add(f"[yellow]{key}[/yellow]: [white]{value}[/white]")
|
|
246
|
+
|
|
247
|
+
add_dict_to_tree(tree, data)
|
|
248
|
+
logger.console.print(tree)
|
|
249
|
+
else:
|
|
250
|
+
# 简单的文本输出
|
|
251
|
+
logger.info(f"=== {title} ===")
|
|
252
|
+
|
|
253
|
+
def print_dict(data_dict, indent=0):
|
|
254
|
+
for key, value in data_dict.items():
|
|
255
|
+
prefix = " " * indent
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
click.echo(f"{prefix}{key}:")
|
|
258
|
+
print_dict(value, indent + 1)
|
|
259
|
+
elif isinstance(value, list):
|
|
260
|
+
click.echo(f"{prefix}{key}: ({len(value)} items)")
|
|
261
|
+
for item in value:
|
|
262
|
+
if isinstance(item, dict):
|
|
263
|
+
print_dict(item, indent + 1)
|
|
264
|
+
else:
|
|
265
|
+
click.echo(f"{prefix} - {item}")
|
|
266
|
+
else:
|
|
267
|
+
click.echo(f"{prefix}{key}: {value}")
|
|
268
|
+
|
|
269
|
+
print_dict(data)
|