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/mongo.py ADDED
@@ -0,0 +1,62 @@
1
+ from loguru import logger
2
+ from pymongo import MongoClient
3
+
4
+ from . import utils
5
+
6
+
7
+ class Mongo():
8
+
9
+ mongo = MongoClient()
10
+
11
+ def __init__(self, mongo_url=None):
12
+ ''' Initiation '''
13
+ if mongo_url != None:
14
+ self.mongo = MongoClient(mongo_url)
15
+
16
+ def close(self):
17
+ try:
18
+ self.mongo.close()
19
+ except Exception as e:
20
+ logger.exception(e)
21
+
22
+ def connect_test(self, debug: bool = False):
23
+ info = 'MongoDB连接测试'
24
+ try:
25
+ logger.info(f'{info}[执行]')
26
+ self.mongo.server_info()
27
+ logger.success(f'{info}[成功]')
28
+ return True
29
+ except Exception as e:
30
+ logger.error(f'{info}[失败]')
31
+ logger.exception(e) if utils.v_true(debug, bool) else next
32
+ return False
33
+
34
+ def collection(self, database, name):
35
+ return self.mongo[database][name]
36
+
37
+ def collection_insert(self, database, collection, data, drop=None):
38
+ db_collection = self.mongo[database][collection]
39
+ info = '插入数据'
40
+ try:
41
+ logger.info(f'{info}[执行]')
42
+ # 是否删除 collection
43
+ if drop == True:
44
+ # 删除 collection
45
+ db_collection.drop()
46
+ # 插入数据
47
+ if utils.v_true(data, dict):
48
+ # 插入一条数据
49
+ result = db_collection.insert_one(data)
50
+ elif utils.v_true(data, list):
51
+ # 插入多条数据
52
+ result = db_collection.insert_many(data)
53
+ else:
54
+ logger.error(f'{info}[失败]')
55
+ logger.error('数据类型错误')
56
+ return False
57
+ logger.success(f'{info}[成功]')
58
+ return result
59
+ except Exception as e:
60
+ logger.error(f'{info}[失败]')
61
+ logger.exception(e)
62
+ return False
ezKit/plots.py ADDED
@@ -0,0 +1,155 @@
1
+ import matplotlib.patches as mpatches
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ import pandas as pd
5
+ from loguru import logger
6
+
7
+
8
+ def bar(data={}, index=[], image={}, **kwargs):
9
+ '''
10
+ 标准条形图
11
+ https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.plot.html
12
+ https://matplotlib.org/stable/api/pyplot_summary.html
13
+ '''
14
+ try:
15
+ _df = pd.DataFrame(data, index=index)
16
+ '''
17
+ title: 标题
18
+ kind: 类型
19
+ figsize: 图片大小, 以 100 为基数, 这里输出的图片为 2000 x 2000
20
+ width: 图形条宽度
21
+ dpi: 清晰度
22
+ bbox_inches='tight' 去除图片周边空白
23
+ '''
24
+ _ax = _df.plot(title=image.get('title'), kind=image.get('kind', 'bar'), figsize=image.get('size', (10, 10)), width=image.get('width', 0.8))
25
+ _ax.set_xlim(image.get('xlim'))
26
+ _ax.set_ylim(image.get('ylim'))
27
+ _ax.figure.savefig(image.get('path', 'image.png'), dpi=image.get('dpi', 300), bbox_inches='tight')
28
+ return True
29
+
30
+ except Exception as e:
31
+ logger.exception(e)
32
+ return False
33
+
34
+ def bar_cover(data=[], index=[], image={}, **kwargs):
35
+ '''
36
+ 重叠条形图
37
+ https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.plot.html
38
+ https://matplotlib.org/stable/api/pyplot_summary.html
39
+ df.plot.bar() 和 df.plot.barh() 中 stacked=True 的效果是叠加, 即所有图层堆积拼接在一起
40
+ 这里需要的效果是图层重叠, 即按从大到小的顺序依次重叠
41
+ 但是 pandas 没有好的处理方法, 为了实现重叠效果, 以下使用设置 ax 处理
42
+ '''
43
+
44
+ try:
45
+
46
+ # 创建数据实例
47
+ _df = pd.DataFrame({item['key']: item['data'] for item in data}, index)
48
+
49
+ '''
50
+ 设置图形类型、颜色、宽度
51
+ 注意顺序: 从大到小排列
52
+ 因为图层是叠加的, 所以后面的图层会覆盖前面的图层
53
+ 如果后面图层的较大, 就会前面较小的图层, 所以把图层较大的放在前面, 图层较小的放在后面
54
+ _, ax = plt.subplots()
55
+ df.MAX.plot(kind='barh', ax=ax, color='#E74C3C', width=0.8)
56
+ df.MID.plot(kind='barh', ax=ax, color='#3498DB', width=0.8)
57
+ df.MIN.plot(kind='barh', ax=ax, color='#2ECC71', width=0.8)
58
+ '''
59
+ _, _ax = plt.subplots()
60
+ for i in data:
61
+ _df[i['key']].plot(kind=i.get('kind', 'bar'), ax=_ax, color=i.get('color'), width=i.get('width', 0.8))
62
+
63
+ '''
64
+ 设置 Label
65
+ 即图片右上角的说明信息, 这里也有顺序, 会按照 handles 中的顺序显示
66
+ https://stackoverflow.com/a/69897921
67
+ patch_max = mpatches.Patch(color='#E74C3C', label='Max')
68
+ patch_mid = mpatches.Patch(color='#3498DB', label='Mid')
69
+ patch_min = mpatches.Patch(color='#2ECC71', label='Min')
70
+ plt.legend(handles=[patch_max, patch_mid, patch_min])
71
+ '''
72
+ plt.legend(handles=[mpatches.Patch(color=i.get('color'), label=i.get('label')) for i in data])
73
+
74
+ # 设置标题
75
+ plt.title(image.get('title'))
76
+
77
+ # 设置上下限
78
+ plt.xlim(image.get('xlim'))
79
+ plt.ylim(image.get('ylim'))
80
+
81
+ # 创建图片实例
82
+ _fig = plt.gcf()
83
+
84
+ # 设置图片大小
85
+ # https://www.zhihu.com/question/37221233
86
+ _fig.set_size_inches(image.get('size', (10, 10)))
87
+
88
+ # 保存图片
89
+ # bbox_inches='tight' 去除图片周边空白
90
+ _fig.savefig(image.get('path', 'image.png'), dpi=image.get('dpi', 300), bbox_inches='tight')
91
+
92
+ # Close a figure window
93
+ plt.close()
94
+
95
+ # Return
96
+ return True
97
+
98
+ except Exception as e:
99
+ logger.exception(e)
100
+ return False
101
+
102
+ def bar_extend(data=[], index=[], image={}, **kwargs):
103
+ '''
104
+ 扩展的条形图
105
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html
106
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.bar.html
107
+ https://matplotlib.org/stable/gallery/lines_bars_and_markers/barchart.html
108
+ https://stackoverflow.com/a/68107816
109
+ '''
110
+ try:
111
+
112
+ _x = np.arange(len(index))
113
+
114
+ _bar_width = image.get('width', 0.8)
115
+
116
+ _fig, _ax = plt.subplots()
117
+
118
+ _rects = _ax.bar(_x, data, _bar_width, label=image.get('label'), color=image.get('color'))
119
+
120
+ # Set base
121
+ _ax.set_title(image.get('title'))
122
+ _ax.set_xlabel(image.get('xlabel'))
123
+ _ax.set_ylabel(image.get('ylabel'))
124
+ _ax.set_xticks(_x, index)
125
+
126
+ # Set view limits
127
+ _ax.set_xlim(image.get('xlim'))
128
+ _ax.set_ylim(image.get('ylim'))
129
+
130
+ # 设置图例
131
+ _ax.legend()
132
+
133
+ # bar 顶部显示 value
134
+ _ax.bar_label(_rects, padding=3)
135
+
136
+ # 自动调整参数, 使图形适合图形区域
137
+ _fig.tight_layout()
138
+
139
+ # 图片大小
140
+ # 默认 300px 为 1 个单位
141
+ # 默认 1920px * 1440px, 即 6.4 * 4.8
142
+ _fig.set_size_inches(image.get('size', (6.4, 4.8)))
143
+
144
+ # 生成图片文件
145
+ # bbox_inches='tight' 去除图片周边空白
146
+ _fig.savefig(image.get('path', 'image.png'), dpi=image.get('dpi', 300), bbox_inches='tight')
147
+
148
+ # Close a figure window
149
+ plt.close()
150
+
151
+ return True
152
+
153
+ except Exception as e:
154
+ logger.exception(e)
155
+ return False
ezKit/redis.py ADDED
@@ -0,0 +1,51 @@
1
+ import redis as RedisClient
2
+ from loguru import logger
3
+
4
+ from . import utils
5
+
6
+
7
+ class Redis(object):
8
+
9
+ # https://redis.readthedocs.io/en/stable/_modules/redis/client.html#Redis
10
+ # https://github.com/redis/redis-py#client-classes-redis-and-strictredis
11
+ # redis-py 3.0 drops support for the legacy Redis client class.
12
+ # StrictRedis has been renamed to Redis and an alias named StrictRedis is provided so that users previously using StrictRedis can continue to run unchanged.
13
+ # redis-py 3.0 之后只有一个 Redis, StrictRedis 是 Redis 的别名
14
+ # 这里修改以下参数: host, port, socket_timeout, socket_connect_timeout, charset
15
+ redis = RedisClient.Redis()
16
+
17
+ def __init__(self, arguments=None):
18
+ '''Initiation'''
19
+ if utils.v_true(arguments, str):
20
+ self.redis = RedisClient.from_url(arguments)
21
+ elif utils.v_true(arguments, dict):
22
+ self.redis = RedisClient.Redis(**arguments)
23
+ else:
24
+ pass
25
+
26
+ def connect_test(self):
27
+ info = 'Redis连接测试'
28
+ try:
29
+ logger.info(f'{info}......')
30
+ self.redis.ping()
31
+ logger.success(f'{info}[成功]')
32
+ return True
33
+ except Exception as e:
34
+ logger.error(f'{info}[失败]')
35
+ logger.exception(e)
36
+ return False
37
+
38
+ def flush(self, all=None):
39
+ info = 'Redis数据清理'
40
+ try:
41
+ logger.info(f'{info}......')
42
+ if all == True:
43
+ self.redis.flushall()
44
+ else:
45
+ self.redis.flushdb()
46
+ logger.success(f'{info}[成功]')
47
+ return True
48
+ except Exception as e:
49
+ logger.error(f'{info}[失败]')
50
+ logger.exception(e)
51
+ return False
ezKit/reports.py ADDED
@@ -0,0 +1,274 @@
1
+ from . import files, utils
2
+
3
+ '''
4
+ reports.logout()
5
+
6
+ 生成报告完成以后, 退出 Zabbix
7
+
8
+ return _image
9
+
10
+ 返回图片信息, 发邮件时使用
11
+ '''
12
+
13
+ class Reports(object):
14
+
15
+ # Zabbix Instance
16
+ _zabbix = None
17
+
18
+ # Files Instance
19
+ _files = None
20
+
21
+ # Image Object
22
+ _image_dir = '.'
23
+ _image_name_prefix = 'image'
24
+
25
+ def __init__(self, zabbix, markdown_file, html_file, image_dir, image_name_prefix):
26
+ ''' Initiation '''
27
+ self._zabbix = zabbix
28
+ self._files = files.files(markdown_file, html_file)
29
+ self._image_dir = image_dir
30
+ self._image_name_prefix = image_name_prefix
31
+
32
+ def generic(
33
+ self,
34
+ pieces=None,
35
+ hosts=None,
36
+ time_from=None,
37
+ time_till=None,
38
+ item_keys=None,
39
+ data_type=None,
40
+ data_proc=None,
41
+ title=None,
42
+ description=None,
43
+ number_type=None,
44
+ number_unit=None,
45
+ number_handling=None,
46
+ table_header_title='Host',
47
+ table_header_data='Data',
48
+ sort_by_ip=None,
49
+ image_cid=None,
50
+ image_label=None,
51
+ image_kind=None
52
+ ):
53
+
54
+ _history = []
55
+
56
+ if utils.v_true(item_keys, str):
57
+ _history = utils.retry(10, self._zabbix.get_history_by_item_key, hosts, time_from, time_till, item_keys, data_type)
58
+
59
+ if utils.v_true(item_keys, list):
60
+ for _item_key in item_keys:
61
+ _history_slice = utils.retry(10, self._zabbix.get_history_by_item_key, hosts, time_from, time_till, _item_key, data_type)
62
+ if _history_slice != None:
63
+ if callable(data_proc) == True:
64
+ _history_slice = data_proc(_history_slice)
65
+ _history += _history_slice
66
+
67
+ if _history != None:
68
+
69
+ _files_func = self._files.multiple_pieces
70
+
71
+ if pieces == 'single':
72
+
73
+ _files_func = self._files.single_piece
74
+
75
+ for _data in _history:
76
+ if len(_data['history']) > 0:
77
+ _history_last = max(_data['history'], key=lambda i: i['clock'])
78
+ _data['history'] = _history_last
79
+
80
+ _image = {
81
+ 'cid': '{}'.format(image_cid),
82
+ 'path': '{}/{}_{}.png'.format(self._image_dir, self._image_name_prefix, image_cid),
83
+ 'label': image_label,
84
+ 'kind': image_kind
85
+ }
86
+
87
+ _files_result = _files_func(
88
+ title=title,
89
+ description=description,
90
+ data=_history,
91
+ image=_image,
92
+ number_type=number_type,
93
+ number_unit=number_unit,
94
+ number_handling=number_handling,
95
+ table_header_title=table_header_title,
96
+ table_header_data=table_header_data,
97
+ sort_by_ip=sort_by_ip
98
+ )
99
+
100
+ if _files_result == True:
101
+ return _image
102
+ else:
103
+ return None
104
+
105
+ else:
106
+
107
+ return None
108
+
109
+ def system_interface(self, hosts, interfaces, time_from, time_till, direction='in'):
110
+ ''' System Interface '''
111
+
112
+ _direction_name = 'Received'
113
+ _direction_alias = 'received'
114
+ _direction_info = '接收数据'
115
+
116
+ if direction == 'out':
117
+ _direction_name = 'Sent'
118
+ _direction_alias = 'sent'
119
+ _direction_info = '发送数据'
120
+
121
+ _history = utils.retry(10, self._zabbix.get_history_by_interface, hosts, interfaces, time_from, time_till, direction)
122
+
123
+ if utils.v_true(_history, list):
124
+
125
+ _image = {
126
+ 'cid': 'system_interface_'.format(_direction_alias),
127
+ 'path': '{}/{}_system_interface_{}.png'.format(self._image_dir, self._image_name_prefix, _direction_alias)
128
+ }
129
+
130
+ _ = self._files.multiple_pieces(
131
+ title='System Interface {}'.format(_direction_name),
132
+ description='说明: 网卡**{}**的速度'.format(_direction_info),
133
+ data=_history,
134
+ image=_image,
135
+ number_type='int',
136
+ number_unit='Kbps',
137
+ number_handling=utils.divisor_1000,
138
+ sort_by_ip=True
139
+ )
140
+
141
+ if _ == True:
142
+ return _image
143
+ else:
144
+ return None
145
+
146
+ else:
147
+
148
+ return None
149
+
150
+ def base_system(self, hosts, time_from, time_till, interfaces=None):
151
+
152
+ # Images
153
+ _images = []
154
+
155
+ # System CPU utilization
156
+ _image = self.generic(
157
+ hosts=hosts,
158
+ time_from=time_from,
159
+ time_till=time_till,
160
+ item_keys='system.cpu.util',
161
+ data_type=0,
162
+ title='System CPU utilization',
163
+ description='说明: 系统 CPU 使用率',
164
+ number_type='float',
165
+ number_unit='%',
166
+ sort_by_ip=True,
167
+ image_cid='system_cpu_utilization'
168
+ )
169
+ if _image != None:
170
+ _images.append(_image)
171
+
172
+ # System Memory utilization
173
+ _image = self.generic(
174
+ hosts=hosts,
175
+ time_from=time_from,
176
+ time_till=time_till,
177
+ item_keys='vm.memory.utilization',
178
+ data_type=0,
179
+ title='System Memory utilization',
180
+ description='说明: 系统 内存 使用率',
181
+ number_type='float',
182
+ number_unit='%',
183
+ sort_by_ip=True,
184
+ image_cid='system_memory_utilization'
185
+ )
186
+ if _image != None:
187
+ _images.append(_image)
188
+
189
+ # System root partition utilization
190
+ _image = self.generic(
191
+ pieces='single',
192
+ hosts=hosts,
193
+ time_from=time_from,
194
+ time_till=time_till,
195
+ item_keys='vfs.fs.size[/,pused]',
196
+ data_type=0,
197
+ title='System root partition utilization',
198
+ description='说明: 系统 根目录(/) 使用率',
199
+ number_type='float',
200
+ number_unit='%',
201
+ table_header_data='Used',
202
+ sort_by_ip=True,
203
+ image_cid='system_root_partition_utilization',
204
+ image_label='Used (%)',
205
+ image_kind='barh'
206
+ )
207
+ if _image != None:
208
+ _images.append(_image)
209
+
210
+ if interfaces != None:
211
+
212
+ # System Interface Received
213
+ _image = self.system_interface(hosts, interfaces, time_from, time_till, 'in')
214
+ if _image != None:
215
+ _images.append(_image)
216
+
217
+ # System Interface Sent
218
+ _image = self.system_interface(hosts, interfaces, time_from, time_till, 'out')
219
+ if _image != None:
220
+ _images.append(_image)
221
+
222
+ return _images
223
+
224
+ def base_generic(self, hosts, time_from, time_till, items=None):
225
+
226
+ # Images
227
+ _images = []
228
+
229
+ if items != None:
230
+
231
+ for _item in items:
232
+
233
+ # CPU utilization
234
+ _image = self.generic(
235
+ hosts=hosts,
236
+ time_from=time_from,
237
+ time_till=time_till,
238
+ item_keys=_item['keys'][0],
239
+ data_type=_item['types'][0],
240
+ data_proc=_item.get('data_proc'),
241
+ title='{} CPU utilization'.format(_item['name']),
242
+ description='说明: {} CPU 使用率'.format(_item['name']),
243
+ number_type='float',
244
+ number_unit='%',
245
+ table_header_title=_item.get('table_header_title', 'Host'),
246
+ table_header_data=_item.get('table_header_data', 'Data'),
247
+ sort_by_ip=True,
248
+ image_cid='{}_cpu_utilization'.format(_item['alias'])
249
+ )
250
+ if _image != None:
251
+ _images.append(_image)
252
+
253
+ # Memory used (RSS)
254
+ _image = self.generic(
255
+ hosts=hosts,
256
+ time_from=time_from,
257
+ time_till=time_till,
258
+ item_keys=_item['keys'][1],
259
+ data_type=_item['types'][1],
260
+ data_proc=_item.get('data_proc'),
261
+ title='{} Memory used (RSS)'.format(_item['name']),
262
+ description='说明: {} 内存 使用量'.format(_item['name']),
263
+ number_type='int',
264
+ number_unit='MB',
265
+ number_handling=utils.divisor_square_1024,
266
+ table_header_title=_item.get('table_header_title', 'Host'),
267
+ table_header_data=_item.get('table_header_data', 'Data'),
268
+ sort_by_ip=True,
269
+ image_cid='{}_memory_used_rss'.format(_item['alias'])
270
+ )
271
+ if _image != None:
272
+ _images.append(_image)
273
+
274
+ return _images
ezKit/sendemail.py ADDED
@@ -0,0 +1,146 @@
1
+ '''
2
+ https://stackoverflow.com/questions/882712/sending-html-email-using-python
3
+ '''
4
+ import smtplib
5
+ from email.header import Header
6
+ from email.mime.image import MIMEImage
7
+ from email.mime.multipart import MIMEMultipart
8
+ from email.mime.text import MIMEText
9
+ from email.utils import formataddr, parseaddr
10
+
11
+ from loguru import logger
12
+
13
+ from . import utils
14
+
15
+
16
+ def format_parse(s):
17
+ _name, _addr = parseaddr(s)
18
+ return formataddr((Header(_name, 'utf-8').encode(), _addr))
19
+
20
+ def related_html(smtp=None, sender=None, recipients=None, subject=None, html_file=None, images=None):
21
+ '''
22
+ smtp SMTP信息
23
+
24
+ server SMTP地址
25
+ port SMTP端口
26
+ ssl 是否使用SSL
27
+
28
+ sender 发件人信息
29
+
30
+ name 发件人名称
31
+ address 发件人邮箱地址
32
+ password 发件人邮箱密码(SMTP)
33
+
34
+ recipients 收件人列表
35
+ subject 邮件主题
36
+ html_file HTML文件
37
+
38
+ images 图片列表(可选)
39
+
40
+ cid 图片CID
41
+ path 图片路径
42
+ '''
43
+
44
+ # 参数判断
45
+ # match True:
46
+ # case True if utils.vTrue(smtp, dict) == False:
47
+ # logger.error('ERROR!! {} is not dictionary or none'.format('smtp'))
48
+ # return False
49
+ # case True if utils.vTrue(sender, dict) == False:
50
+ # logger.error('ERROR!! {} is not dictionary or none'.format('sender'))
51
+ # return False
52
+ # case True if (utils.vTrue(recipients, str) == False) and (utils.vTrue(recipients, list) == False):
53
+ # logger.error('ERROR!! {} is not list or none'.format('recipients'))
54
+ # return False
55
+ # case True if utils.vTrue(subject, str) == False:
56
+ # logger.error('ERROR!! {} is not string or none'.format('subject'))
57
+ # return False
58
+ # case True if utils.vTrue(html_file, str) == False:
59
+ # logger.error('ERROR!! {} is not string or none'.format('html_file'))
60
+ # return False
61
+
62
+ logger.success('sendemail start')
63
+
64
+ try:
65
+
66
+ _message = MIMEMultipart('related')
67
+
68
+ with open(html_file, 'r') as _html_file:
69
+
70
+ _message.attach(MIMEText(_html_file.read(), 'html', 'utf-8'))
71
+
72
+ if utils.v_true(images, list):
73
+
74
+ for _image in images:
75
+
76
+ try:
77
+
78
+ if utils.check_file_type(_image.get('path', ''), 'file'):
79
+
80
+ '''
81
+ 添加图片
82
+ with open(image_path, 'rb') as image_file:
83
+ mime_image = MIMEImage(image_file.read())
84
+ # Define the image's ID as referenced above
85
+ mime_image.add_header('Content-ID', '<CID>')
86
+ message.attach(mime_image)
87
+ '''
88
+
89
+ with open(_image['path'], 'rb') as _image_file:
90
+ _mime_image = MIMEImage(_image_file.read())
91
+ _mime_image.add_header('Content-ID', '<{}>'.format(_image['cid']))
92
+ _message.attach(_mime_image)
93
+
94
+ else:
95
+
96
+ next
97
+
98
+ except Exception as e:
99
+ logger.exception(e)
100
+ next
101
+
102
+ # 发件人
103
+ _message['From'] = formataddr([sender.get('name'), sender.get('address')])
104
+
105
+ # 收件人
106
+ if utils.v_true(recipients, str):
107
+ _message['To'] = format_parse(recipients)
108
+ elif utils.v_true(recipients, list):
109
+ _message['To'] = ", ".join(list(map(format_parse, recipients)))
110
+ else:
111
+ logger.error('recipients error')
112
+ return False
113
+
114
+ # 主题
115
+ _message['Subject'] = subject
116
+
117
+ '''
118
+ 发送邮件
119
+
120
+ SMTP.sendmail(from_addr, to_addrs, msg, mail_options=(), rcpt_options=())
121
+
122
+ to_addrs = sender_to + sender_cc
123
+ https://docs.python.org/3/library/smtplib.html#smtplib.SMTP.sendmail
124
+ https://gist.github.com/AO8/c5a6f747eeeca02351152ae8dc79b537
125
+ '''
126
+
127
+ if smtp.get('ssl', False) == True:
128
+
129
+ with smtplib.SMTP_SSL(smtp.get('server'), smtp.get('port')) as _smtp:
130
+ _smtp.login(sender.get('address'), sender.get('password'))
131
+ _smtp.sendmail(sender.get('address'), recipients, _message.as_string())
132
+
133
+ else:
134
+
135
+ with smtplib.SMTP(smtp.get('server'), smtp.get('port')) as _smtp:
136
+ _smtp.login(sender.get('address'), sender.get('password'))
137
+ _smtp.sendmail(sender.get('address'), recipients, _message.as_string())
138
+
139
+ logger.success('sendemail success')
140
+
141
+ return True
142
+
143
+ except Exception as e:
144
+ logger.error('sendemail error')
145
+ logger.exception(e)
146
+ return False