ezKit 1.0.0__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.
- ezKit/__init__.py +1 -0
- ezKit/bottle.py +3809 -0
- ezKit/cipher.py +74 -0
- ezKit/database.py +168 -0
- ezKit/files.py +348 -0
- ezKit/http.py +92 -0
- ezKit/mongo.py +62 -0
- ezKit/plots.py +155 -0
- ezKit/redis.py +51 -0
- ezKit/reports.py +274 -0
- ezKit/sendemail.py +146 -0
- ezKit/utils.py +1257 -0
- ezKit/weixin.py +148 -0
- ezKit/xftp.py +194 -0
- ezKit/zabbix.py +866 -0
- ezKit-1.0.0.dist-info/LICENSE +674 -0
- ezKit-1.0.0.dist-info/METADATA +10 -0
- ezKit-1.0.0.dist-info/RECORD +20 -0
- ezKit-1.0.0.dist-info/WHEEL +5 -0
- ezKit-1.0.0.dist-info/top_level.txt +1 -0
ezKit/cipher.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
'''
|
2
|
+
https://docs.python.org/3.10/library/hashlib.html
|
3
|
+
https://www.pycrypto.org/
|
4
|
+
https://stackoverflow.com/a/21928790
|
5
|
+
'''
|
6
|
+
import base64
|
7
|
+
import hashlib
|
8
|
+
|
9
|
+
from Crypto import Random
|
10
|
+
from Crypto.Cipher import AES
|
11
|
+
from loguru import logger
|
12
|
+
|
13
|
+
|
14
|
+
class AESCipher(object):
|
15
|
+
|
16
|
+
def __init__(self, key: str = 'vB7DoRm9C2Kd', algorithm: str = 'sha256'):
|
17
|
+
|
18
|
+
self.bs = AES.block_size
|
19
|
+
|
20
|
+
# dir(hashlib)
|
21
|
+
match True:
|
22
|
+
case True if algorithm == 'md5':
|
23
|
+
self.key = hashlib.md5(key.encode()).digest()
|
24
|
+
case True if algorithm == 'sha1':
|
25
|
+
self.key = hashlib.sha1(key.encode()).digest()
|
26
|
+
case True if algorithm == 'sha224':
|
27
|
+
self.key = hashlib.sha224(key.encode()).digest()
|
28
|
+
case True if algorithm == 'sha256':
|
29
|
+
self.key = hashlib.sha256(key.encode()).digest()
|
30
|
+
case True if algorithm == 'sha384':
|
31
|
+
self.key = hashlib.sha384(key.encode()).digest()
|
32
|
+
case True if algorithm == 'sha512':
|
33
|
+
self.key = hashlib.sha512(key.encode()).digest()
|
34
|
+
case True if algorithm == 'sha3_224':
|
35
|
+
self.key = hashlib.sha3_224(key.encode()).digest()
|
36
|
+
case True if algorithm == 'sha3_256':
|
37
|
+
self.key = hashlib.sha3_256(key.encode()).digest()
|
38
|
+
case True if algorithm == 'sha3_384':
|
39
|
+
self.key = hashlib.sha3_384(key.encode()).digest()
|
40
|
+
case True if algorithm == 'sha3_512':
|
41
|
+
self.key = hashlib.sha3_512(key.encode()).digest()
|
42
|
+
case True if algorithm == 'shake_128':
|
43
|
+
self.key = hashlib.shake_128(key.encode()).digest()
|
44
|
+
case True if algorithm == 'shake_256':
|
45
|
+
self.key = hashlib.shake_256(key.encode()).digest()
|
46
|
+
case _:
|
47
|
+
self.key = hashlib.sha256(key.encode()).digest()
|
48
|
+
|
49
|
+
def encrypt(self, raw: str) -> str | None:
|
50
|
+
try:
|
51
|
+
raw = self._pad(raw)
|
52
|
+
iv = Random.new().read(AES.block_size)
|
53
|
+
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
54
|
+
return base64.b64encode(iv + cipher.encrypt(raw.encode())).decode('utf-8')
|
55
|
+
except Exception as e:
|
56
|
+
logger.exception(e)
|
57
|
+
return None
|
58
|
+
|
59
|
+
def decrypt(self, enc: str) -> str | None:
|
60
|
+
try:
|
61
|
+
enc = base64.b64decode(enc)
|
62
|
+
iv = enc[:AES.block_size]
|
63
|
+
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
64
|
+
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
|
65
|
+
except Exception as e:
|
66
|
+
logger.exception(e)
|
67
|
+
return None
|
68
|
+
|
69
|
+
def _pad(self, s):
|
70
|
+
return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
|
71
|
+
|
72
|
+
@staticmethod
|
73
|
+
def _unpad(s: bytes) -> bytes:
|
74
|
+
return s[:-ord(s[len(s) - 1:])]
|
ezKit/database.py
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
'''
|
2
|
+
Column, Table, MetaData API
|
3
|
+
https://docs.sqlalchemy.org/en/14/core/metadata.html#column-table-metadata-api
|
4
|
+
CursorResult
|
5
|
+
https://docs.sqlalchemy.org/en/20/core/connections.html#sqlalchemy.engine.CursorResult
|
6
|
+
PostgreSQL 14 Data Types
|
7
|
+
https://www.postgresql.org/docs/14/datatype.html
|
8
|
+
'''
|
9
|
+
import csv
|
10
|
+
|
11
|
+
from loguru import logger
|
12
|
+
from sqlalchemy import Index, create_engine, text
|
13
|
+
|
14
|
+
from . import utils
|
15
|
+
|
16
|
+
|
17
|
+
class Database(object):
|
18
|
+
|
19
|
+
engine = create_engine('sqlite://')
|
20
|
+
|
21
|
+
def __init__(self, engine_url=None, engine_options=None):
|
22
|
+
'''Initiation'''
|
23
|
+
if engine_url != None:
|
24
|
+
if utils.v_true(engine_options, dict):
|
25
|
+
self.engine = create_engine(engine_url, **engine_options)
|
26
|
+
else:
|
27
|
+
self.engine = create_engine(engine_url)
|
28
|
+
|
29
|
+
def initializer(self):
|
30
|
+
'''ensure the parent proc's database connections are not touched in the new connection pool'''
|
31
|
+
self.engine.dispose(close=False)
|
32
|
+
|
33
|
+
def connect_test(self):
|
34
|
+
info = '数据库连接测试'
|
35
|
+
try:
|
36
|
+
logger.info(f'{info}......')
|
37
|
+
self.engine.connect()
|
38
|
+
logger.success(f'{info}[成功]')
|
39
|
+
return True
|
40
|
+
except Exception as e:
|
41
|
+
logger.error(f'{info}[失败]')
|
42
|
+
logger.exception(e)
|
43
|
+
return False
|
44
|
+
|
45
|
+
def metadata_init(self, base, **kwargs):
|
46
|
+
# https://stackoverflow.com/questions/19175311/how-to-create-only-one-table-with-sqlalchemy
|
47
|
+
info = '初始化表'
|
48
|
+
try:
|
49
|
+
logger.info(f'{info}......')
|
50
|
+
base.metadata.drop_all(self.engine, **kwargs)
|
51
|
+
base.metadata.create_all(self.engine, **kwargs)
|
52
|
+
logger.success(f'{info}[成功]')
|
53
|
+
return True
|
54
|
+
except Exception as e:
|
55
|
+
logger.error(f'{info}[失败]')
|
56
|
+
logger.exception(e)
|
57
|
+
return False
|
58
|
+
|
59
|
+
def create_index(self, index_name, table_field):
|
60
|
+
# 创建索引
|
61
|
+
# https://stackoverflow.com/a/41254430
|
62
|
+
# 示例:
|
63
|
+
# index_name: a_share_list_code_idx1
|
64
|
+
# table_field: Table_a_share_list.code
|
65
|
+
info = '创建索引'
|
66
|
+
try:
|
67
|
+
logger.info(f'{info}')
|
68
|
+
idx = Index(index_name, table_field)
|
69
|
+
try:
|
70
|
+
idx.drop(bind=self.engine)
|
71
|
+
except:
|
72
|
+
pass
|
73
|
+
idx.create(bind=self.engine)
|
74
|
+
logger.success(f'{info}[成功]')
|
75
|
+
return True
|
76
|
+
except Exception as e:
|
77
|
+
logger.error(f'{info}[失败]')
|
78
|
+
logger.error(e)
|
79
|
+
return False
|
80
|
+
|
81
|
+
# 私有函数, 保存 execute 的结果到 CSV 文件
|
82
|
+
def _result_save(self, file, data, echo=True):
|
83
|
+
try:
|
84
|
+
outcsv = csv.writer(file)
|
85
|
+
outcsv.writerow(data.keys())
|
86
|
+
outcsv.writerows(data)
|
87
|
+
return True
|
88
|
+
except Exception as e:
|
89
|
+
logger.exception(e) if echo == True else next
|
90
|
+
return False
|
91
|
+
|
92
|
+
def execute(self, sql=None, sql_file=None, sql_file_kwargs=None, csv_file=None, csv_file_kwargs=None, echo=True):
|
93
|
+
'''
|
94
|
+
echo 是否打印日志
|
95
|
+
某些情况下只需要结果, 不需要日志, 将 echo 设置为 False 即可
|
96
|
+
'''
|
97
|
+
|
98
|
+
info_prefix = '[执行SQL]'
|
99
|
+
|
100
|
+
# ------------------------------------------------------------
|
101
|
+
|
102
|
+
# 提取 SQL
|
103
|
+
# 如果 sql 和 sql_file 同时存在, 优先执行 sql
|
104
|
+
sql_object = None
|
105
|
+
info = f'{info_prefix}提取SQL'
|
106
|
+
try:
|
107
|
+
logger.info(f'{info}......') if echo == True else next
|
108
|
+
if utils.v_true(sql, str):
|
109
|
+
sql_object = sql
|
110
|
+
elif utils.v_true(sql_file, str):
|
111
|
+
# 判断文件是否存在
|
112
|
+
if utils.check_file_type(sql_file, 'file') == False:
|
113
|
+
logger.error(f'文件不存在: {sql_file}') if echo == True else next
|
114
|
+
return False
|
115
|
+
# 读取文件内容
|
116
|
+
if utils.v_true(sql_file_kwargs, dict):
|
117
|
+
with open(sql_file, 'r', **sql_file_kwargs) as _file:
|
118
|
+
sql_object = _file.read()
|
119
|
+
else:
|
120
|
+
with open(sql_file, 'r') as _file:
|
121
|
+
sql_object = _file.read()
|
122
|
+
else:
|
123
|
+
logger.error(f'{info}[失败]') if echo == True else next
|
124
|
+
logger.error(f'{info_prefix}SQL 或 SQL文件 错误') if echo == True else next
|
125
|
+
return False
|
126
|
+
logger.success(f'{info}[成功]') if echo == True else next
|
127
|
+
except Exception as e:
|
128
|
+
logger.error(f'{info}[失败]') if echo == True else next
|
129
|
+
logger.exception(e) if echo == True else next
|
130
|
+
return False
|
131
|
+
|
132
|
+
# ------------------------------------------------------------
|
133
|
+
|
134
|
+
# 执行 SQL
|
135
|
+
info = f'{info_prefix}执行SQL'
|
136
|
+
try:
|
137
|
+
logger.info(f'{info}......') if echo == True else next
|
138
|
+
with self.engine.connect() as connect:
|
139
|
+
# 执行SQL
|
140
|
+
result = connect.execute(text(sql_object))
|
141
|
+
if csv_file == None:
|
142
|
+
# 如果 csv_file 没有定义, 则直接返回结果
|
143
|
+
logger.success(f'{info}[成功]') if echo == True else next
|
144
|
+
return result
|
145
|
+
else:
|
146
|
+
# 如果 csv_file 有定义, 则保存结果到 csv_file
|
147
|
+
info_of_save = f'{info_prefix}保存结果到文件: {csv_file}'
|
148
|
+
logger.info(f'{info_of_save} .......') if echo == True else next
|
149
|
+
# 保存结果
|
150
|
+
if utils.v_true(csv_file_kwargs, dict):
|
151
|
+
with open(csv_file, 'w', **csv_file_kwargs) as _file:
|
152
|
+
result_of_save = self._result_save(_file, result, echo=echo)
|
153
|
+
else:
|
154
|
+
with open(csv_file, 'w') as _file:
|
155
|
+
result_of_save = self._result_save(_file, result, echo=echo)
|
156
|
+
# 检查保存结果
|
157
|
+
if result_of_save == True:
|
158
|
+
logger.success(f'{info_of_save} [成功]') if echo == True else next
|
159
|
+
logger.success(f'{info}[成功]') if echo == True else next
|
160
|
+
return True
|
161
|
+
else:
|
162
|
+
logger.error(f'{info_of_save} [失败]') if echo == True else next
|
163
|
+
logger.error(f'{info}[失败]') if echo == True else next
|
164
|
+
return False
|
165
|
+
except Exception as e:
|
166
|
+
logger.error(f'{info}[失败]') if echo == True else next
|
167
|
+
logger.exception(e) if echo == True else next
|
168
|
+
return False
|
ezKit/files.py
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
import re
|
2
|
+
from copy import deepcopy
|
3
|
+
from socket import inet_aton
|
4
|
+
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
from . import plots, utils
|
8
|
+
|
9
|
+
|
10
|
+
class Files(object):
|
11
|
+
''' 文件 '''
|
12
|
+
|
13
|
+
''' Markdown 和 HTML 文件 '''
|
14
|
+
_markdown_file, _html_file = None, None
|
15
|
+
|
16
|
+
def __init__(self, markdown_file=None, html_file=None):
|
17
|
+
''' Initiation '''
|
18
|
+
self._markdown_file = markdown_file
|
19
|
+
self._html_file = html_file
|
20
|
+
|
21
|
+
'''
|
22
|
+
def multiple_pieces(
|
23
|
+
self,
|
24
|
+
title='',
|
25
|
+
description='',
|
26
|
+
data=[],
|
27
|
+
image={},
|
28
|
+
number_type=None,
|
29
|
+
number_unit=None,
|
30
|
+
number_handling=None,
|
31
|
+
sort_by_ip=False
|
32
|
+
):
|
33
|
+
|
34
|
+
'''
|
35
|
+
|
36
|
+
def unavailable_hosts(self, hosts=[], **kwargs):
|
37
|
+
''' 异常服务器处理 '''
|
38
|
+
|
39
|
+
logger.success('unavailable hosts | start')
|
40
|
+
|
41
|
+
try:
|
42
|
+
|
43
|
+
# 如果存在异常服务器, 则写入文件
|
44
|
+
if hosts != []:
|
45
|
+
with open(self._markdown_file, 'a') as _file:
|
46
|
+
_file.writelines([
|
47
|
+
'\n',
|
48
|
+
'**异常服务器:**\n',
|
49
|
+
'\n',
|
50
|
+
])
|
51
|
+
for _host in hosts:
|
52
|
+
_file.write('- <span style="color: red">{}</span>\n'.format(_host))
|
53
|
+
|
54
|
+
logger.success('unavailable hosts | end')
|
55
|
+
|
56
|
+
return True
|
57
|
+
|
58
|
+
except Exception as e:
|
59
|
+
logger.exception(e)
|
60
|
+
logger.error('unavailable hosts | end')
|
61
|
+
return False
|
62
|
+
|
63
|
+
def single_piece(
|
64
|
+
self,
|
65
|
+
title='',
|
66
|
+
description='',
|
67
|
+
data=[],
|
68
|
+
image={},
|
69
|
+
number_type=None,
|
70
|
+
number_unit=None,
|
71
|
+
number_handling=None,
|
72
|
+
table_header_title='Host',
|
73
|
+
table_header_data='Data',
|
74
|
+
sort_by_ip=False,
|
75
|
+
**kwargs
|
76
|
+
):
|
77
|
+
''' 单条数据 '''
|
78
|
+
|
79
|
+
match True:
|
80
|
+
case True if type(title) != str or title == '':
|
81
|
+
logger.error('ERROR!! title is not string or none')
|
82
|
+
return None
|
83
|
+
case True if type(description) != str or description == '':
|
84
|
+
logger.error('ERROR!! description is not string or none')
|
85
|
+
return None
|
86
|
+
case True if type(data) != list or data == []:
|
87
|
+
logger.error('ERROR!! data is not list or none')
|
88
|
+
return None
|
89
|
+
case True if type(image) != dict or image == 0:
|
90
|
+
logger.error('ERROR!! image is not dictionary or none')
|
91
|
+
return None
|
92
|
+
|
93
|
+
# 拷贝数据
|
94
|
+
_data = deepcopy(data)
|
95
|
+
|
96
|
+
# 根据 IP 排序
|
97
|
+
if sort_by_ip == True:
|
98
|
+
_ips = utils.list_sort([_host['interfaces'][0]['ip'] for _host in _data], key=inet_aton, deduplication=True)
|
99
|
+
_data = [_host for _ip in _ips for _host in _data if _ip == _host['interfaces'][0]['ip']]
|
100
|
+
|
101
|
+
logger.success('{} | start'.format(title))
|
102
|
+
|
103
|
+
try:
|
104
|
+
|
105
|
+
# 初始化变量
|
106
|
+
_data_idx, _data_result = [], []
|
107
|
+
|
108
|
+
for _host in _data:
|
109
|
+
_value = None
|
110
|
+
if _host.get('history') != None:
|
111
|
+
match True:
|
112
|
+
case True if number_type == 'int':
|
113
|
+
_value = int(_host['history']['value'])
|
114
|
+
case True if number_type == 'float':
|
115
|
+
_value = float(_host['history']['value'])
|
116
|
+
case _:
|
117
|
+
_value = _host['history']['value']
|
118
|
+
_data_idx.append(_host['name'])
|
119
|
+
_data_result.append(number_handling(_value) if callable(number_handling) == True else _value)
|
120
|
+
|
121
|
+
# 判断结果
|
122
|
+
if (len(_data_idx) > 0) and (image != {}):
|
123
|
+
|
124
|
+
with open(self._markdown_file, 'a') as _file:
|
125
|
+
|
126
|
+
_file.writelines([
|
127
|
+
'\n',
|
128
|
+
'## {}\n'.format(title),
|
129
|
+
'\n',
|
130
|
+
'{}\n'.format(description),
|
131
|
+
'\n',
|
132
|
+
'| {} | {} |\n'.format(table_header_title, table_header_data),
|
133
|
+
'| --- | --: |\n'
|
134
|
+
])
|
135
|
+
|
136
|
+
for _i, _v in enumerate(_data_idx):
|
137
|
+
_file.write('| {0} | {1:.2f}{2} |\n'.format(_v, _data_result[_i], number_unit))
|
138
|
+
|
139
|
+
_plot_image = {
|
140
|
+
'title': title,
|
141
|
+
'kind': image['kind'],
|
142
|
+
'size': (20, (len(_data_idx) / 5) * 2),
|
143
|
+
'path': image['path'],
|
144
|
+
'dpi': 300,
|
145
|
+
'width': 0.8
|
146
|
+
}
|
147
|
+
|
148
|
+
_plot_result = plots.bar({image['label']: _data_result}, _data_idx, _plot_image)
|
149
|
+
|
150
|
+
# '<img src="cid:{}">\n'.format(_image['cid'])
|
151
|
+
# '\n'.format(_image['cid'])
|
152
|
+
# 图片显示大小: 仅设置 width, height 自适应
|
153
|
+
if _plot_result == True:
|
154
|
+
_file.writelines([
|
155
|
+
'\n',
|
156
|
+
'<img src="cid:{}" width="1000px">\n'.format(image['cid'])
|
157
|
+
])
|
158
|
+
|
159
|
+
logger.success('{} | end'.format(title))
|
160
|
+
|
161
|
+
return True
|
162
|
+
|
163
|
+
except Exception as e:
|
164
|
+
logger.exception(e)
|
165
|
+
logger.error('{} | end'.format(title))
|
166
|
+
return False
|
167
|
+
|
168
|
+
def multiple_pieces(
|
169
|
+
self,
|
170
|
+
title='',
|
171
|
+
description='',
|
172
|
+
data=[],
|
173
|
+
image={},
|
174
|
+
number_type=None,
|
175
|
+
number_unit=None,
|
176
|
+
number_handling=None,
|
177
|
+
table_header_title='Host',
|
178
|
+
sort_by_ip=False,
|
179
|
+
**kwargs
|
180
|
+
):
|
181
|
+
''' 多条数据 '''
|
182
|
+
|
183
|
+
match True:
|
184
|
+
case True if type(title) != str or title == '':
|
185
|
+
logger.error('ERROR!! title is not string or none')
|
186
|
+
return None
|
187
|
+
case True if type(description) != str or description == '':
|
188
|
+
logger.error('ERROR!! description is not string or none')
|
189
|
+
return None
|
190
|
+
case True if type(data) != list or data == []:
|
191
|
+
logger.error('ERROR!! data is not list or none')
|
192
|
+
return None
|
193
|
+
case True if type(image) != dict or image == 0:
|
194
|
+
logger.error('ERROR!! image is not dictionary or none')
|
195
|
+
return None
|
196
|
+
|
197
|
+
# 拷贝数据
|
198
|
+
_data = deepcopy(data)
|
199
|
+
|
200
|
+
# 根据 IP 排序
|
201
|
+
if sort_by_ip == True:
|
202
|
+
_ips = utils.list_sort([_host['interfaces'][0]['ip'] for _host in _data], key=inet_aton, deduplication=True)
|
203
|
+
_data = [_host for _ip in _ips for _host in _data if _ip == _host['interfaces'][0]['ip']]
|
204
|
+
|
205
|
+
logger.success('{} | start'.format(title))
|
206
|
+
|
207
|
+
try:
|
208
|
+
|
209
|
+
# 初始化变量 (用于 pandas 处理数据)
|
210
|
+
_data_idx, _data_max, _data_avg, _data_min = [], [], [], []
|
211
|
+
|
212
|
+
# 提取数据
|
213
|
+
for _host in _data:
|
214
|
+
_num_max, _num_avg, _num_min = utils.mam_of_numbers([_x['value'] for _x in _host['history']], number_type)
|
215
|
+
if (_num_max != None) and (_num_avg != None) and (_num_min != None):
|
216
|
+
_data_idx.append(_host['name'])
|
217
|
+
_data_max.append(number_handling(_num_max) if callable(number_handling) == True else _num_max)
|
218
|
+
_data_avg.append(number_handling(_num_avg) if callable(number_handling) == True else _num_avg)
|
219
|
+
_data_min.append(number_handling(_num_min) if callable(number_handling) == True else _num_min)
|
220
|
+
|
221
|
+
# 判断结果
|
222
|
+
if (len(_data_idx) > 0) and (image != None):
|
223
|
+
|
224
|
+
# 写入文件
|
225
|
+
with open(self._markdown_file, 'a') as _file:
|
226
|
+
|
227
|
+
_file.writelines([
|
228
|
+
'\n',
|
229
|
+
'## {}\n'.format(title),
|
230
|
+
'\n',
|
231
|
+
'{}\n'.format(description),
|
232
|
+
'\n',
|
233
|
+
'| {} | Min | Average | Max |\n'.format(table_header_title),
|
234
|
+
'| --- | --: | --: | --: |\n'
|
235
|
+
])
|
236
|
+
|
237
|
+
for _i, _v in enumerate(_data_idx):
|
238
|
+
_file.write('| {0} | {1:.2f}{4} | {2:.2f}{4} | {3:.2f}{4} |\n'.format(_v, _data_min[_i], _data_avg[_i], _data_max[_i], number_unit))
|
239
|
+
|
240
|
+
_plot_data = [
|
241
|
+
{'key': 'max', 'label': 'Max', 'kind': 'barh', 'data': _data_max, 'color': '#E74C3C', 'width': 0.8},
|
242
|
+
{'key': 'avg', 'label': 'Avg', 'kind': 'barh', 'data': _data_avg, 'color': '#3498DB', 'width': 0.8},
|
243
|
+
{'key': 'min', 'label': 'Min', 'kind': 'barh', 'data': _data_min, 'color': '#2ECC71', 'width': 0.8}
|
244
|
+
]
|
245
|
+
|
246
|
+
_plot_image = {
|
247
|
+
'title': title,
|
248
|
+
'size': (20, (len(_data_idx) / 5) * 2),
|
249
|
+
'path': image['path'],
|
250
|
+
'dpi': 300
|
251
|
+
}
|
252
|
+
|
253
|
+
_plot_result = plots.bar_cover(_plot_data, _data_idx, _plot_image)
|
254
|
+
|
255
|
+
# '<img src="cid:{}">\n'.format(_image['cid'])
|
256
|
+
# '\n'.format(_image['cid'])
|
257
|
+
# 图片显示大小: 仅设置 width, height 自适应
|
258
|
+
if _plot_result == True:
|
259
|
+
_file.writelines([
|
260
|
+
'\n',
|
261
|
+
'<img src="cid:{}" width="1000px">\n'.format(image['cid'])
|
262
|
+
])
|
263
|
+
|
264
|
+
logger.success('{} | end'.format(title))
|
265
|
+
|
266
|
+
return True
|
267
|
+
|
268
|
+
except Exception as e:
|
269
|
+
logger.exception(e)
|
270
|
+
logger.error('{} | end'.format(title))
|
271
|
+
return False
|
272
|
+
|
273
|
+
def handling_html(self, *args, **kwargs):
|
274
|
+
''' 处理 HTML '''
|
275
|
+
|
276
|
+
logger.success('handling HTML | start')
|
277
|
+
|
278
|
+
try:
|
279
|
+
|
280
|
+
if utils.check_file_type(self._html_file, 'file'):
|
281
|
+
|
282
|
+
# HTML内容
|
283
|
+
_html_lines = None
|
284
|
+
|
285
|
+
# 移除自带样式
|
286
|
+
with open(self._html_file, "r") as _html_input:
|
287
|
+
_html_lines = _html_input.readlines()
|
288
|
+
del _html_lines[7:161]
|
289
|
+
|
290
|
+
# 遍历内容
|
291
|
+
for _i, _v in enumerate(_html_lines):
|
292
|
+
|
293
|
+
# 移除表格宽度
|
294
|
+
_table_width = re.compile(r'<table style="width:100%;">')
|
295
|
+
if len(_table_width.findall(_v)) > 0:
|
296
|
+
_html_lines[_i] = re.sub(_table_width, '<table>', _v)
|
297
|
+
|
298
|
+
# 移除 colgroup
|
299
|
+
_colgroup = re.compile(r'</?colgroup>')
|
300
|
+
if len(_colgroup.findall(_v)) > 0:
|
301
|
+
_html_lines[_i] = re.sub(_colgroup, '', _v)
|
302
|
+
|
303
|
+
# 移除列宽度
|
304
|
+
_col_width = re.compile(r'<col style="width: .*%" />')
|
305
|
+
if len(_col_width.findall(_v)) > 0:
|
306
|
+
_html_lines[_i] = re.sub(_col_width, '', _v)
|
307
|
+
|
308
|
+
# 写入文件
|
309
|
+
with open(self._html_file, "w") as _html_output:
|
310
|
+
_html_output.writelines(_html_lines)
|
311
|
+
|
312
|
+
logger.success('handling HTML | end')
|
313
|
+
return True
|
314
|
+
|
315
|
+
except Exception as e:
|
316
|
+
logger.exception(e)
|
317
|
+
logger.error('handling HTML | end')
|
318
|
+
return False
|
319
|
+
|
320
|
+
def markdown_to_html(self, dir='.', **kwargs):
|
321
|
+
'''
|
322
|
+
Markdown to HTML
|
323
|
+
使用 MacDown 生成 HTML, 然后提取样式到 markdown.html
|
324
|
+
pandoc 生成的 HTML 默认 max-width: 36em, 如果表格内容很长, 会导致表格样式难看
|
325
|
+
所以在 markdown.html 的 body{...} 中添加配置 max-width: unset, 解决内容过长的样式问题
|
326
|
+
所有 a 标签添加 text-decoration: none; 去除链接下划线
|
327
|
+
pandoc --no-highlight -s --quiet -f markdown -t html -H markdown.html -o history.html history.md
|
328
|
+
'''
|
329
|
+
|
330
|
+
logger.success('markdown to html | start')
|
331
|
+
|
332
|
+
try:
|
333
|
+
|
334
|
+
_result = utils.shell(
|
335
|
+
'pandoc --no-highlight -s --quiet -f markdown -t html -H {}/markdown.html -o {} {}'.format(dir, self._html_file, self._markdown_file)
|
336
|
+
)
|
337
|
+
|
338
|
+
if _result != None and _result.returncode == 0:
|
339
|
+
logger.success('markdown to html | end')
|
340
|
+
return True
|
341
|
+
else:
|
342
|
+
logger.error('markdown to html | end')
|
343
|
+
return False
|
344
|
+
|
345
|
+
except Exception as e:
|
346
|
+
logger.exception(e)
|
347
|
+
logger.error('markdown to html | end')
|
348
|
+
return False
|
ezKit/http.py
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
import json
|
2
|
+
from typing import Callable
|
3
|
+
|
4
|
+
import requests
|
5
|
+
from loguru import logger
|
6
|
+
|
7
|
+
from . import bottle, utils
|
8
|
+
|
9
|
+
|
10
|
+
def bottle_cors(fn: Callable):
|
11
|
+
"""
|
12
|
+
Bottle CORS
|
13
|
+
"""
|
14
|
+
"""
|
15
|
+
参考文档:
|
16
|
+
- https://stackoverflow.com/a/17262900
|
17
|
+
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
18
|
+
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
19
|
+
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
20
|
+
"""
|
21
|
+
def cors(*args, **kwargs):
|
22
|
+
bottle.response.headers['Access-Control-Allow-Headers'] = '*'
|
23
|
+
bottle.response.headers['Access-Control-Allow-Methods'] = '*'
|
24
|
+
bottle.response.headers['Access-Control-Allow-Origin'] = '*'
|
25
|
+
if bottle.request.method != 'OPTIONS':
|
26
|
+
return fn(*args, **kwargs)
|
27
|
+
return cors
|
28
|
+
|
29
|
+
|
30
|
+
def download(
|
31
|
+
request: dict,
|
32
|
+
file: dict,
|
33
|
+
chunks: bool = False,
|
34
|
+
iter_content: dict | None = None,
|
35
|
+
echo: bool = False,
|
36
|
+
info: str = None,
|
37
|
+
debug: bool = False
|
38
|
+
) -> bool:
|
39
|
+
"下载文件"
|
40
|
+
|
41
|
+
if utils.v_true(request, dict):
|
42
|
+
request_arguments = {'method': 'GET', 'stream': True, **request}
|
43
|
+
else:
|
44
|
+
return False
|
45
|
+
|
46
|
+
if utils.v_true(file, dict):
|
47
|
+
file_arguments = {'mode': 'wb', **file}
|
48
|
+
else:
|
49
|
+
return False
|
50
|
+
|
51
|
+
if utils.v_true(iter_content, dict):
|
52
|
+
iter_content_arguments = {'chunk_size': 1024, **iter_content}
|
53
|
+
else:
|
54
|
+
iter_content_arguments = {'chunk_size': 1024}
|
55
|
+
|
56
|
+
info = f'下载 {info}' if utils.v_true(info, str) else f'下载'
|
57
|
+
|
58
|
+
try:
|
59
|
+
|
60
|
+
logger.info(f'{info} ......') if utils.v_true(echo, bool) else next
|
61
|
+
|
62
|
+
response = requests.request(**request_arguments)
|
63
|
+
|
64
|
+
with open(**file_arguments) as _file:
|
65
|
+
|
66
|
+
if utils.v_true(chunks, bool):
|
67
|
+
for _chunk in response.iter_content(**iter_content_arguments):
|
68
|
+
_file.write(_chunk)
|
69
|
+
else:
|
70
|
+
_file.write(response.content)
|
71
|
+
|
72
|
+
logger.success(f'{info} [成功]') if utils.v_true(echo, bool) else next
|
73
|
+
|
74
|
+
return True
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
logger.exception(e) if debug is True else next
|
78
|
+
logger.error(f'{info} [失败]') if utils.v_true(echo, bool) else next
|
79
|
+
return False
|
80
|
+
|
81
|
+
|
82
|
+
def response_json(
|
83
|
+
data: any = None,
|
84
|
+
debug: bool = False,
|
85
|
+
**kwargs
|
86
|
+
) -> str:
|
87
|
+
"""解决字符编码问题: ensure_ascii=False"""
|
88
|
+
try:
|
89
|
+
return json.dumps(data, default=str, ensure_ascii=False, sort_keys=True, **kwargs)
|
90
|
+
except Exception as e:
|
91
|
+
logger.exception(e) if debug is True else next
|
92
|
+
return None
|