pytbox 0.0.1__py3-none-any.whl → 0.3.1__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.

Files changed (68) hide show
  1. pytbox/alert/alert_handler.py +139 -0
  2. pytbox/alert/ping.py +24 -0
  3. pytbox/alicloud/sls.py +9 -14
  4. pytbox/base.py +121 -0
  5. pytbox/categraf/build_config.py +143 -0
  6. pytbox/categraf/instances.toml +39 -0
  7. pytbox/categraf/jinja2/__init__.py +6 -0
  8. pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
  9. pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
  10. pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
  11. pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
  12. pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
  13. pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
  14. pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
  15. pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
  16. pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
  17. pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
  18. pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
  19. pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
  20. pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
  21. pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
  22. pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
  23. pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
  24. pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
  25. pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
  26. pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
  27. pytbox/cli/__init__.py +7 -0
  28. pytbox/cli/categraf/__init__.py +7 -0
  29. pytbox/cli/categraf/commands.py +55 -0
  30. pytbox/cli/commands/vm.py +22 -0
  31. pytbox/cli/common/__init__.py +6 -0
  32. pytbox/cli/common/options.py +42 -0
  33. pytbox/cli/common/utils.py +269 -0
  34. pytbox/cli/formatters/__init__.py +7 -0
  35. pytbox/cli/formatters/output.py +155 -0
  36. pytbox/cli/main.py +24 -0
  37. pytbox/cli.py +9 -0
  38. pytbox/database/mongo.py +99 -0
  39. pytbox/database/victoriametrics.py +404 -0
  40. pytbox/dida365.py +11 -17
  41. pytbox/excel.py +64 -0
  42. pytbox/feishu/endpoints.py +12 -9
  43. pytbox/{logger.py → log/logger.py} +78 -30
  44. pytbox/{victorialog.py → log/victorialog.py} +2 -2
  45. pytbox/mail/alimail.py +142 -0
  46. pytbox/mail/client.py +171 -0
  47. pytbox/mail/mail_detail.py +30 -0
  48. pytbox/mingdao.py +164 -0
  49. pytbox/network/meraki.py +537 -0
  50. pytbox/notion.py +731 -0
  51. pytbox/pyjira.py +612 -0
  52. pytbox/utils/cronjob.py +79 -0
  53. pytbox/utils/env.py +2 -2
  54. pytbox/utils/load_config.py +132 -0
  55. pytbox/utils/load_vm_devfile.py +45 -0
  56. pytbox/utils/response.py +1 -1
  57. pytbox/utils/richutils.py +31 -0
  58. pytbox/utils/timeutils.py +479 -14
  59. pytbox/vmware.py +120 -0
  60. pytbox/win/ad.py +30 -0
  61. {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/METADATA +13 -3
  62. pytbox-0.3.1.dist-info/RECORD +72 -0
  63. pytbox-0.3.1.dist-info/entry_points.txt +2 -0
  64. pytbox/common/base.py +0 -0
  65. pytbox/victoriametrics.py +0 -37
  66. pytbox-0.0.1.dist-info/RECORD +0 -21
  67. {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/WHEEL +0 -0
  68. {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,211 @@
1
+ # # collect interval
2
+ # interval = 15
3
+
4
+ # Read metrics from one or many vCenters
5
+ {% for instance in instances %}
6
+ {% for name, vcenter_info in instance.items() %}
7
+ [[instances]]
8
+ vcenter = "{{ vcenter_info['vcenter'] }}"
9
+ username = "{{ vcenter_info['username'] }}"
10
+ password = "{{ vcenter_info['password'] }}"
11
+
12
+ cluster_metric_exlcude = ["*"]
13
+ ## VMs
14
+ ## Typical VM metrics (if omitted or empty, all metrics are collected)
15
+ # vm_include = [ "/*/vm/**"] # Inventory path to VMs to collect (by default all are collected)
16
+ # vm_exclude = [] # Inventory paths to exclude
17
+ vm_metric_include = [
18
+ # "config.hardware.numCPU",
19
+ # "config.hardware.memoryMB",
20
+ "cpu.demand.average",
21
+ "cpu.idle.summation",
22
+ "cpu.latency.average",
23
+ "cpu.readiness.average",
24
+ "cpu.ready.summation",
25
+ "cpu.run.summation",
26
+ "cpu.usagemhz.average",
27
+ "cpu.usage.average",
28
+ "cpu.used.summation",
29
+ "cpu.wait.summation",
30
+ "mem.active.average",
31
+ "mem.granted.average",
32
+ "mem.latency.average",
33
+ "mem.swapin.average",
34
+ "mem.swapinRate.average",
35
+ "mem.swapout.average",
36
+ "mem.swapoutRate.average",
37
+ "mem.usage.average",
38
+ "mem.vmmemctl.average",
39
+ "net.bytesRx.average",
40
+ "net.bytesTx.average",
41
+ "net.droppedRx.summation",
42
+ "net.droppedTx.summation",
43
+ "net.usage.average",
44
+ "power.power.average",
45
+ "virtualDisk.numberReadAveraged.average",
46
+ "virtualDisk.numberWriteAveraged.average",
47
+ "virtualDisk.read.average",
48
+ "virtualDisk.readOIO.latest",
49
+ "virtualDisk.throughput.usage.average",
50
+ "virtualDisk.totalReadLatency.average",
51
+ "virtualDisk.totalWriteLatency.average",
52
+ "virtualDisk.write.average",
53
+ "virtualDisk.writeOIO.latest",
54
+ "sys.uptime.latest",
55
+ ]
56
+ # vm_metric_exclude = [] ## Nothing is excluded by default
57
+ # vm_instances = true ## true by default
58
+
59
+ ## Hosts
60
+ ## Typical host metrics (if omitted or empty, all metrics are collected)
61
+ # host_include = [ "/*/host/**"] # Inventory path to hosts to collect (by default all are collected)
62
+ # host_exclude [] # Inventory paths to exclude
63
+ host_metric_include = [
64
+ "cpu.coreUtilization.average",
65
+ "cpu.costop.summation",
66
+ "cpu.demand.average",
67
+ "cpu.idle.summation",
68
+ "cpu.latency.average",
69
+ "cpu.readiness.average",
70
+ "cpu.ready.summation",
71
+ "cpu.swapwait.summation",
72
+ "cpu.usage.average",
73
+ "cpu.usagemhz.average",
74
+ "cpu.used.summation",
75
+ "cpu.utilization.average",
76
+ "cpu.wait.summation",
77
+ "disk.deviceReadLatency.average",
78
+ "disk.deviceWriteLatency.average",
79
+ "disk.kernelReadLatency.average",
80
+ "disk.kernelWriteLatency.average",
81
+ "disk.numberReadAveraged.average",
82
+ "disk.numberWriteAveraged.average",
83
+ "disk.read.average",
84
+ "disk.totalReadLatency.average",
85
+ "disk.totalWriteLatency.average",
86
+ "disk.write.average",
87
+ "mem.active.average",
88
+ "mem.latency.average",
89
+ "mem.state.latest",
90
+ "mem.swapin.average",
91
+ "mem.swapinRate.average",
92
+ "mem.swapout.average",
93
+ "mem.swapoutRate.average",
94
+ "mem.totalCapacity.average",
95
+ "mem.usage.average",
96
+ "mem.vmmemctl.average",
97
+ "net.bytesRx.average",
98
+ "net.bytesTx.average",
99
+ "net.droppedRx.summation",
100
+ "net.droppedTx.summation",
101
+ "net.errorsRx.summation",
102
+ "net.errorsTx.summation",
103
+ "net.usage.average",
104
+ "power.power.average",
105
+ "storageAdapter.numberReadAveraged.average",
106
+ "storageAdapter.numberWriteAveraged.average",
107
+ "storageAdapter.read.average",
108
+ "storageAdapter.write.average",
109
+ "sys.uptime.latest",
110
+ ]
111
+
112
+
113
+ # host_instances = true ## true by default
114
+ # host_include = [] ## Nothing included by default
115
+ # host_exclude = [] ## Nothing excluded by default
116
+ # host_metric_include = [] ## Nothing included by default
117
+ # host_metric_exclude = [] ## Nothing excluded by default
118
+
119
+
120
+ ## Clusters
121
+ # cluster_include = [ "/*/host/**"] # Inventory path to clusters to collect (by default all are collected)
122
+ # cluster_exclude = [] # Inventory paths to exclude
123
+ # cluster_metric_include = [] ## if omitted or empty, all metrics are collected
124
+ # cluster_metric_exclude = [] ## Nothing excluded by default
125
+ # cluster_instances = false ## false by default
126
+
127
+ ## Resource Pools
128
+ # resoucepool_include = [ "/*/host/**"] # Inventory path to datastores to collect (by default all are collected)
129
+ # resoucepool_exclude = [] # Inventory paths to exclude
130
+ # resoucepool_metric_include = [] ## if omitted or empty, all metrics are collected
131
+ # resoucepool_metric_exclude = [] ## Nothing excluded by default
132
+ # resoucepool_instances = false ## false by default
133
+
134
+ ## Datastores
135
+ # datastore_include = [ "/*/datastore/**"] # Inventory path to datastores to collect (by default all are collected)
136
+ # datastore_exclude = [] # Inventory paths to exclude
137
+ # datastore_metric_include = [] ## if omitted or empty, all metrics are collected
138
+ # datastore_metric_exclude = [] ## Nothing excluded by default
139
+ # datastore_instances = false ## false by default
140
+
141
+ ## Datacenters
142
+ # datacenter_include = [ "/*/host/**"] # Inventory path to clusters to collect (by default all are collected)
143
+ # datacenter_exclude = [] # Inventory paths to exclude
144
+ # datacenter_metric_include = [] ## if omitted or empty, all metrics are collected
145
+ # datacenter_metric_exclude = [ "*" ] ## Datacenters are not collected by default.
146
+ # datacenter_instances = false ## false by default
147
+
148
+ ## Plugin Settings
149
+ ## separator character to use for measurement and field names (default: "_")
150
+ # separator = "_"
151
+
152
+ ## Collect IP addresses? Valid values are "ipv4" and "ipv6"
153
+ # ip_addresses = ["ipv6", "ipv4" ]
154
+
155
+ ## When set to true, all samples are sent as integers. This makes the output
156
+ ## data types backwards compatible with Telegraf 1.9 or lower. Normally all
157
+ ## samples from vCenter, with the exception of percentages, are integer
158
+ ## values, but under some conditions, some averaging takes place internally in
159
+ ## the plugin. Setting this flag to "false" will send values as floats to
160
+ ## preserve the full precision when averaging takes place.
161
+ # use_int_samples = true
162
+
163
+ ## Custom attributes from vCenter can be very useful for queries in order to slice the
164
+ ## metrics along different dimension and for forming ad-hoc relationships. They are disabled
165
+ ## by default, since they can add a considerable amount of tags to the resulting metrics. To
166
+ ## enable, simply set custom_attribute_exclude to [] (empty set) and use custom_attribute_include
167
+ ## to select the attributes you want to include.
168
+ ## By default, since they can add a considerable amount of tags to the resulting metrics. To
169
+ ## enable, simply set custom_attribute_exclude to [] (empty set) and use custom_attribute_include
170
+ ## to select the attributes you want to include.
171
+ # custom_attribute_include = []
172
+ # custom_attribute_exclude = ["*"]
173
+
174
+
175
+ ## The number of vSphere 5 minute metric collection cycles to look back for non-realtime metrics. In
176
+ ## some versions (6.7, 7.0 and possible more), certain metrics, such as cluster metrics, may be reported
177
+ ## with a significant delay (>30min). If this happens, try increasing this number. Please note that increasing
178
+ ## it too much may cause performance issues.
179
+ # metric_lookback = 3
180
+
181
+ ## number of objects to retrieve per query for realtime resources (vms and hosts)
182
+ ## set to 64 for vCenter 5.5 and 6.0 (default: 256)
183
+ # max_query_objects = 256
184
+
185
+ ## number of metrics to retrieve per query for non-realtime resources (clusters and datastores)
186
+ ## set to 64 for vCenter 5.5 and 6.0 (default: 256)
187
+ # max_query_metrics = 256
188
+
189
+ ## number of go routines to use for collection and discovery of objects and metrics
190
+ # collect_concurrency = 1
191
+ # discover_concurrency = 1
192
+
193
+ ## the interval before (re)discovering objects subject to metrics collection (default: 300s)
194
+ # object_discovery_interval = "300s"
195
+
196
+ ## timeout applies to any of the api request made to vcenter
197
+ # timeout = "60s"
198
+
199
+ ## Optional SSL Config
200
+ use_tls = true
201
+ # tls_ca = "/path/to/cafile"
202
+ # tls_cert = "/path/to/certfile"
203
+ # tls_key = "/path/to/keyfile"
204
+ ## Use SSL but skip chain & host verification
205
+ insecure_skip_verify = true
206
+
207
+ ## The Historical Interval value must match EXACTLY the interval in the daily
208
+ # "Interval Duration" found on the VCenter server under Configure > General > Statistics > Statistic intervals
209
+ # historical_interval = "5m"
210
+ {% endfor %}
211
+ {% endfor %}
pytbox/cli/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ Pytbox CLI 包
3
+ """
4
+
5
+ from .main import main
6
+
7
+ __all__ = ['main']
@@ -0,0 +1,7 @@
1
+ """
2
+ Categraf CLI 模块
3
+ """
4
+
5
+ from .commands import categraf_group
6
+
7
+ __all__ = ['categraf_group']
@@ -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,22 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import click
4
+ from pytbox.database.victoriametrics import VictoriaMetrics
5
+ from pytbox.utils.richutils import RichUtils
6
+
7
+ rich_utils = RichUtils()
8
+
9
+ @click.group()
10
+ def vm_group():
11
+ """VictoriaMetrics 查询工具"""
12
+ pass
13
+
14
+
15
+ @vm_group.command('query')
16
+ @click.option('--url', '-u', type=str, required=True)
17
+ @click.option('--query', '-q', type=str, required=True)
18
+ def query(url, query):
19
+ """查询 VM 数据"""
20
+ vm_client = VictoriaMetrics(url=url)
21
+ r = vm_client.query(query, output_format='json')
22
+ rich_utils.print(msg=r)
@@ -0,0 +1,6 @@
1
+ """
2
+ CLI 通用模块
3
+ """
4
+
5
+ from .options import *
6
+ from .utils import *
@@ -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)
@@ -0,0 +1,7 @@
1
+ """
2
+ 输出格式化器模块
3
+ """
4
+
5
+ from .output import OutputFormatter
6
+
7
+ __all__ = ['OutputFormatter']