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.
- pytbox/alert/alert_handler.py +139 -0
- pytbox/alert/ping.py +24 -0
- pytbox/alicloud/sls.py +9 -14
- pytbox/base.py +121 -0
- pytbox/categraf/build_config.py +143 -0
- pytbox/categraf/instances.toml +39 -0
- pytbox/categraf/jinja2/__init__.py +6 -0
- pytbox/categraf/jinja2/input.cpu/cpu.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.disk/disk.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.diskio/diskio.toml.j2 +6 -0
- pytbox/categraf/jinja2/input.dns_query/dns_query.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.http_response/http_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.mem/mem.toml.j2 +5 -0
- pytbox/categraf/jinja2/input.net/net.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.net_response/net_response.toml.j2 +9 -0
- pytbox/categraf/jinja2/input.ping/ping.toml.j2 +11 -0
- pytbox/categraf/jinja2/input.prometheus/prometheus.toml.j2 +12 -0
- pytbox/categraf/jinja2/input.snmp/cisco_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/cisco_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/h3c_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/h3c_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/huawei_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/huawei_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_interface.toml.j2 +96 -0
- pytbox/categraf/jinja2/input.snmp/ruijie_system.toml.j2 +41 -0
- pytbox/categraf/jinja2/input.vsphere/vsphere.toml.j2 +211 -0
- pytbox/cli/__init__.py +7 -0
- pytbox/cli/categraf/__init__.py +7 -0
- pytbox/cli/categraf/commands.py +55 -0
- pytbox/cli/commands/vm.py +22 -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 +24 -0
- pytbox/cli.py +9 -0
- pytbox/database/mongo.py +99 -0
- pytbox/database/victoriametrics.py +404 -0
- pytbox/dida365.py +11 -17
- pytbox/excel.py +64 -0
- pytbox/feishu/endpoints.py +12 -9
- pytbox/{logger.py → log/logger.py} +78 -30
- pytbox/{victorialog.py → log/victorialog.py} +2 -2
- pytbox/mail/alimail.py +142 -0
- pytbox/mail/client.py +171 -0
- pytbox/mail/mail_detail.py +30 -0
- pytbox/mingdao.py +164 -0
- pytbox/network/meraki.py +537 -0
- pytbox/notion.py +731 -0
- pytbox/pyjira.py +612 -0
- pytbox/utils/cronjob.py +79 -0
- pytbox/utils/env.py +2 -2
- pytbox/utils/load_config.py +132 -0
- pytbox/utils/load_vm_devfile.py +45 -0
- pytbox/utils/response.py +1 -1
- pytbox/utils/richutils.py +31 -0
- pytbox/utils/timeutils.py +479 -14
- pytbox/vmware.py +120 -0
- pytbox/win/ad.py +30 -0
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/METADATA +13 -3
- pytbox-0.3.1.dist-info/RECORD +72 -0
- pytbox-0.3.1.dist-info/entry_points.txt +2 -0
- pytbox/common/base.py +0 -0
- pytbox/victoriametrics.py +0 -37
- pytbox-0.0.1.dist-info/RECORD +0 -21
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/WHEEL +0 -0
- {pytbox-0.0.1.dist-info → pytbox-0.3.1.dist-info}/top_level.txt +0 -0
pytbox/dida365.py
CHANGED
|
@@ -28,14 +28,8 @@ class Task:
|
|
|
28
28
|
completed_time: str
|
|
29
29
|
assignee: int
|
|
30
30
|
|
|
31
|
-
@dataclass
|
|
32
|
-
class DidaResponse:
|
|
33
|
-
code: int
|
|
34
|
-
message: str
|
|
35
|
-
data: dict
|
|
36
|
-
|
|
37
31
|
|
|
38
|
-
class
|
|
32
|
+
class ProcessReturnResponse:
|
|
39
33
|
@staticmethod
|
|
40
34
|
def status(status):
|
|
41
35
|
if status == 0:
|
|
@@ -89,7 +83,7 @@ class Dida365:
|
|
|
89
83
|
}
|
|
90
84
|
self.timeout = 10
|
|
91
85
|
|
|
92
|
-
def request(self, api_url: str=None, method: str='GET', payload: dict={}):
|
|
86
|
+
def request(self, api_url: str=None, method: str='GET', payload: dict={}) -> ReturnResponse:
|
|
93
87
|
"""发送请求。
|
|
94
88
|
|
|
95
89
|
Args:
|
|
@@ -108,14 +102,14 @@ class Dida365:
|
|
|
108
102
|
|
|
109
103
|
if response.status_code == 200:
|
|
110
104
|
if 'complete' in api_url:
|
|
111
|
-
return
|
|
105
|
+
return ReturnResponse(code=0, msg='success', data=None)
|
|
112
106
|
else:
|
|
113
107
|
try:
|
|
114
|
-
return
|
|
115
|
-
except Exception
|
|
116
|
-
return
|
|
108
|
+
return ReturnResponse(code=0, msg='success', data=response.json())
|
|
109
|
+
except Exception:
|
|
110
|
+
return ReturnResponse(code=1, msg='warning', data=response)
|
|
117
111
|
else:
|
|
118
|
-
return
|
|
112
|
+
return ReturnResponse(code=1, msg='error', data=response.json())
|
|
119
113
|
|
|
120
114
|
def task_list(self, project_id: str, enhancement: bool=True):
|
|
121
115
|
"""获取任务列表。
|
|
@@ -142,8 +136,8 @@ class Dida365:
|
|
|
142
136
|
desc=task.get('desc'),
|
|
143
137
|
start_date=task.get('startDate'),
|
|
144
138
|
due_date=task.get('dueDate'),
|
|
145
|
-
priority=
|
|
146
|
-
status=
|
|
139
|
+
priority=ProcessReturnResponse.priority(task.get('priority')),
|
|
140
|
+
status=ProcessReturnResponse.status(task.get('status')),
|
|
147
141
|
tags=task.get('tags'),
|
|
148
142
|
completed_time=task.get('completedTime'),
|
|
149
143
|
assignee=task.get('assignee'))
|
|
@@ -157,8 +151,8 @@ class Dida365:
|
|
|
157
151
|
desc=task.get('desc'),
|
|
158
152
|
start_date=task.get('startDate'),
|
|
159
153
|
due_date=task.get('dueDate'),
|
|
160
|
-
priority=
|
|
161
|
-
status=
|
|
154
|
+
priority=ProcessReturnResponse.priority(task.get('priority')),
|
|
155
|
+
status=ProcessReturnResponse.status(task.get('status')),
|
|
162
156
|
tags=task.get('tags'),
|
|
163
157
|
completed_time=task.get('completedTime'),
|
|
164
158
|
assignee=task.get('assignee'))
|
pytbox/excel.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from openpyxl.styles import Alignment
|
|
4
|
+
from openpyxl.styles import PatternFill
|
|
5
|
+
from openpyxl.styles.differential import DifferentialStyle
|
|
6
|
+
from openpyxl.styles import Font
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExcelFormat:
|
|
10
|
+
def __init__(self, ws):
|
|
11
|
+
self.ws = ws
|
|
12
|
+
|
|
13
|
+
def set_column(self):
|
|
14
|
+
for column in self.ws.columns:
|
|
15
|
+
max_length = 0
|
|
16
|
+
column = [cell for cell in column]
|
|
17
|
+
for cell in column:
|
|
18
|
+
try:
|
|
19
|
+
if len(str(cell.value)) > max_length:
|
|
20
|
+
max_length = len(cell.value)
|
|
21
|
+
except:
|
|
22
|
+
pass
|
|
23
|
+
adjusted_width = (max_length + 10)
|
|
24
|
+
self.ws.column_dimensions[column[0].column_letter].width = adjusted_width
|
|
25
|
+
|
|
26
|
+
def set_rows_center(self):
|
|
27
|
+
# 将所有单元格的文字居中
|
|
28
|
+
for row in self.ws.iter_rows():
|
|
29
|
+
|
|
30
|
+
for cell in row:
|
|
31
|
+
cell.alignment = Alignment(horizontal='center', vertical='center')
|
|
32
|
+
for row in self.ws.iter_rows():
|
|
33
|
+
self.ws.row_dimensions[row[0].row].height = 24
|
|
34
|
+
|
|
35
|
+
def set_freeze_first_row(self):
|
|
36
|
+
"""设置首行锁定/冻结首行"""
|
|
37
|
+
# 冻结首行,从第二行开始滚动
|
|
38
|
+
self.ws.freeze_panes = 'A2'
|
|
39
|
+
|
|
40
|
+
def set_freeze_first_column(self):
|
|
41
|
+
"""设置首列锁定/冻结首列"""
|
|
42
|
+
# 冻结首列,从第二列开始滚动
|
|
43
|
+
self.ws.freeze_panes = 'B1'
|
|
44
|
+
|
|
45
|
+
def set_first_row_bold_color(self, font_color='FF0000FF'):
|
|
46
|
+
"""设置首行字体加粗并改变颜色
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
font_color (str): 字体颜色的十六进制代码,默认为蓝色(FF0000FF)
|
|
50
|
+
格式:AARRGGBB 或 RRGGBB
|
|
51
|
+
例如:'FF0000FF'(蓝色), 'FFFF0000'(红色), 'FF008000'(绿色)
|
|
52
|
+
"""
|
|
53
|
+
# 遍历首行的所有单元格
|
|
54
|
+
for cell in self.ws[1]:
|
|
55
|
+
if cell.value is not None: # 只对有内容的单元格设置样式
|
|
56
|
+
# 设置字体为粗体并改变颜色
|
|
57
|
+
cell.font = Font(bold=True, color=font_color)
|
|
58
|
+
|
|
59
|
+
def set_freeze_first_row_and_column(self):
|
|
60
|
+
"""同时冻结首行和首列"""
|
|
61
|
+
# 冻结首行首列,从第二行第二列开始滚动
|
|
62
|
+
self.ws.freeze_panes = 'B2'
|
|
63
|
+
|
|
64
|
+
|
pytbox/feishu/endpoints.py
CHANGED
|
@@ -254,9 +254,9 @@ class MessageEndpoint(Endpoint):
|
|
|
254
254
|
body=payload
|
|
255
255
|
)
|
|
256
256
|
if r.code == 0:
|
|
257
|
-
return ReturnResponse(code=0,
|
|
257
|
+
return ReturnResponse(code=0, msg=f"{message_id} 回复 emoji [{emoji_type}] 成功")
|
|
258
258
|
else:
|
|
259
|
-
return ReturnResponse(code=1,
|
|
259
|
+
return ReturnResponse(code=1, msg=f"{message_id} 回复 emoji [{emoji_type}] 失败")
|
|
260
260
|
|
|
261
261
|
class BitableEndpoint(Endpoint):
|
|
262
262
|
|
|
@@ -432,10 +432,10 @@ class BitableEndpoint(Endpoint):
|
|
|
432
432
|
resp = self.parent.request(path=f'/bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}',
|
|
433
433
|
method='PUT',
|
|
434
434
|
body=payload)
|
|
435
|
-
return ReturnResponse(code=resp.code,
|
|
435
|
+
return ReturnResponse(code=resp.code, msg=f"记录已存在, 进行更新", data=resp.data)
|
|
436
436
|
else:
|
|
437
437
|
resp = self.add_record(app_token, table_id, fields)
|
|
438
|
-
return ReturnResponse(code=resp.code,
|
|
438
|
+
return ReturnResponse(code=resp.code, msg=f"记录不存在, 进行创建", data=resp.data)
|
|
439
439
|
|
|
440
440
|
def query_name_by_record_id(self, app_token: str=None, table_id: str=None, field_names: list=None, record_id: str='', name: str=''):
|
|
441
441
|
response = self.query_record(app_token=app_token, table_id=table_id, field_names=field_names)
|
|
@@ -930,9 +930,12 @@ class ExtensionsEndpoint(Endpoint):
|
|
|
930
930
|
final_data = fields[name][0].get('value')[0]['text']
|
|
931
931
|
|
|
932
932
|
elif isinstance(fields[name], int):
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
933
|
+
if len(str(fields[name])) >= 12 and fields[name] > 10**11:
|
|
934
|
+
final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name] / 1000))
|
|
935
|
+
elif len(str(fields[name])) == 10 and 10**9 < fields[name] < 10**11:
|
|
936
|
+
final_data = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(fields[name]))
|
|
937
|
+
else:
|
|
938
|
+
final_data = fields[name]
|
|
936
939
|
elif isinstance(fields[name], dict):
|
|
937
940
|
if fields[name].get('type') == 1:
|
|
938
941
|
final_data = fields[name].get('value')[0]['text']
|
|
@@ -964,9 +967,9 @@ class ExtensionsEndpoint(Endpoint):
|
|
|
964
967
|
body=payload)
|
|
965
968
|
if response.code == 0:
|
|
966
969
|
if get == 'open_id':
|
|
967
|
-
return ReturnResponse(code=0,
|
|
970
|
+
return ReturnResponse(code=0, msg=f'根据用户输入的 {user_input}, 获取用户信息成功', data=response.data['user_list'][0]['user_id'])
|
|
968
971
|
else:
|
|
969
|
-
return ReturnResponse(code=response.code,
|
|
972
|
+
return ReturnResponse(code=response.code, msg=f"获取时失败, 报错请见 data 字段", data=response.data)
|
|
970
973
|
|
|
971
974
|
def format_rich_text(self, text: str, color: Literal['red', 'green', 'yellow', 'blue'], bold: bool=False):
|
|
972
975
|
if bold:
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
|
|
4
5
|
from loguru import logger
|
|
5
|
-
from .victorialog import Victorialog
|
|
6
6
|
|
|
7
|
+
from .victorialog import Victorialog
|
|
8
|
+
from ..database.mongo import Mongo
|
|
9
|
+
from ..feishu.client import Client as FeishuClient
|
|
10
|
+
from ..utils.timeutils import TimeUtils
|
|
11
|
+
from ..dida365 import Dida365
|
|
12
|
+
from ..alicloud.sls import AliCloudSls
|
|
7
13
|
|
|
8
14
|
logger.remove()
|
|
9
15
|
logger.add(sys.stdout, colorize=True, format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <level>{message}</level>")
|
|
@@ -21,8 +27,10 @@ class AppLogger:
|
|
|
21
27
|
stream: str='automation',
|
|
22
28
|
enable_victorialog: bool=False,
|
|
23
29
|
victorialog_url: str=None,
|
|
30
|
+
mongo: Mongo=None,
|
|
31
|
+
feishu: FeishuClient=None,
|
|
32
|
+
dida: Dida365=None,
|
|
24
33
|
enable_sls: bool=False,
|
|
25
|
-
sls_url: str=None,
|
|
26
34
|
sls_access_key_id: str=None,
|
|
27
35
|
sls_access_key_secret: str=None,
|
|
28
36
|
sls_project: str=None,
|
|
@@ -40,7 +48,17 @@ class AppLogger:
|
|
|
40
48
|
self.stream = stream
|
|
41
49
|
self.victorialog = Victorialog(url=victorialog_url)
|
|
42
50
|
self.enable_victorialog = enable_victorialog
|
|
43
|
-
|
|
51
|
+
self.enable_sls = enable_sls
|
|
52
|
+
self.mongo = mongo
|
|
53
|
+
self.feishu = feishu
|
|
54
|
+
self.dida = dida
|
|
55
|
+
self.sls = AliCloudSls(
|
|
56
|
+
access_key_id=sls_access_key_id,
|
|
57
|
+
access_key_secret=sls_access_key_secret,
|
|
58
|
+
project=sls_project,
|
|
59
|
+
logstore=sls_logstore
|
|
60
|
+
)
|
|
61
|
+
|
|
44
62
|
def _get_caller_info(self) -> tuple[str, int, str]:
|
|
45
63
|
"""
|
|
46
64
|
获取调用者信息
|
|
@@ -66,6 +84,9 @@ class AppLogger:
|
|
|
66
84
|
logger.debug(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
67
85
|
if self.enable_victorialog:
|
|
68
86
|
self.victorialog.send_program_log(stream=self.stream, level="DEBUG", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
87
|
+
if self.enable_sls:
|
|
88
|
+
self.sls.put_logs(level="DEBUG", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
89
|
+
|
|
69
90
|
|
|
70
91
|
def info(self, message: str='', feishu_notify: bool=False):
|
|
71
92
|
"""记录信息级别日志"""
|
|
@@ -73,27 +94,71 @@ class AppLogger:
|
|
|
73
94
|
logger.info(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
74
95
|
if self.enable_victorialog:
|
|
75
96
|
r = self.victorialog.send_program_log(stream=self.stream, level="INFO", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
76
|
-
|
|
97
|
+
if self.enable_sls:
|
|
98
|
+
self.sls.put_logs(level="INFO", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
77
99
|
if feishu_notify:
|
|
78
100
|
self.feishu(message)
|
|
79
|
-
|
|
101
|
+
|
|
80
102
|
def warning(self, message: str):
|
|
81
103
|
"""记录警告级别日志"""
|
|
82
104
|
caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
|
|
83
105
|
logger.warning(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
84
106
|
if self.enable_victorialog:
|
|
85
107
|
self.victorialog.send_program_log(stream=self.stream, level="WARN", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
86
|
-
|
|
108
|
+
if self.enable_sls:
|
|
109
|
+
self.sls.put_logs(level="WARN", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
110
|
+
|
|
87
111
|
def error(self, message: str):
|
|
88
112
|
"""记录错误级别日志"""
|
|
89
113
|
caller_filename, caller_lineno, caller_function, call_full_filename = self._get_caller_info()
|
|
90
114
|
logger.error(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
91
115
|
if self.enable_victorialog:
|
|
92
116
|
self.victorialog.send_program_log(stream=self.stream, level="ERROR", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
if self.enable_sls:
|
|
118
|
+
self.sls.put_logs(level="ERROR", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
119
|
+
|
|
120
|
+
if self.feishu:
|
|
121
|
+
existing_message = self.mongo.collection.find_one({"message": message}, sort=[("time", -1)])
|
|
122
|
+
current_time = TimeUtils.get_now_time_mongo()
|
|
123
|
+
|
|
124
|
+
if not existing_message or TimeUtils.get_time_diff_hours(existing_message["time"], current_time) > 36:
|
|
125
|
+
self.mongo.collection.insert_one({
|
|
126
|
+
"message": message,
|
|
127
|
+
"time": current_time,
|
|
128
|
+
"file_name": caller_filename,
|
|
129
|
+
"line_number": caller_lineno,
|
|
130
|
+
"function_name": caller_function
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
content_list = [
|
|
135
|
+
f"{self.feishu.extensions.format_rich_text(text='app:', color='blue', bold=True)} {self.app_name}",
|
|
136
|
+
f"{self.feishu.extensions.format_rich_text(text='message:', color='blue', bold=True)} {message}",
|
|
137
|
+
f"{self.feishu.extensions.format_rich_text(text='file_name:', color='blue', bold=True)} {caller_filename}",
|
|
138
|
+
f"{self.feishu.extensions.format_rich_text(text='line_number:', color='blue', bold=True)} {caller_lineno}",
|
|
139
|
+
f"{self.feishu.extensions.format_rich_text(text='function_name:', color='blue', bold=True)} {caller_function}"
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
self.feishu.extensions.send_message_notify(
|
|
143
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
144
|
+
content="\n".join(content_list)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
dida_content_list = [
|
|
148
|
+
f"**app**: {self.app_name}",
|
|
149
|
+
f"**message**: {message}",
|
|
150
|
+
f"**file_name**: {caller_filename}",
|
|
151
|
+
f"**line_number**: {caller_lineno}",
|
|
152
|
+
f"**function_name**: {caller_function}"
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
if self.dida:
|
|
156
|
+
self.dida.task_create(
|
|
157
|
+
project_id="65e87d2b3e73517c2cdd9d63",
|
|
158
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
159
|
+
content="\n".join(dida_content_list),
|
|
160
|
+
tags=['L-程序告警', 't-问题处理']
|
|
161
|
+
)
|
|
97
162
|
|
|
98
163
|
def critical(self, message: str):
|
|
99
164
|
"""记录严重错误级别日志"""
|
|
@@ -101,26 +166,9 @@ class AppLogger:
|
|
|
101
166
|
logger.critical(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
102
167
|
if self.enable_victorialog:
|
|
103
168
|
self.victorialog.send_program_log(stream=self.stream, level="CRITICAL", message=message, app_name=self.app_name, file_name=call_full_filename, line_number=caller_lineno, function_name=caller_function)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def get_logger(app_name: str, enable_) -> AppLogger:
|
|
107
|
-
"""
|
|
108
|
-
获取应用日志记录器实例
|
|
109
|
-
|
|
110
|
-
Args:
|
|
111
|
-
app_name: 应用名称
|
|
112
|
-
log_level: 日志级别
|
|
113
|
-
enable_influx: 是否启用InfluxDB记录
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
AppLogger: 日志记录器实例
|
|
117
|
-
"""
|
|
118
|
-
return AppLogger(app_name)
|
|
119
|
-
|
|
169
|
+
if self.enable_sls:
|
|
170
|
+
self.sls.put_logs(level="CRITICAL", msg=message, app=self.app_name, caller_filename=caller_filename, caller_lineno=caller_lineno, caller_function=caller_function, call_full_filename=call_full_filename)
|
|
120
171
|
|
|
121
172
|
# 使用示例
|
|
122
173
|
if __name__ == "__main__":
|
|
123
|
-
|
|
124
|
-
log.info("That's it, beautiful and simple logging!")
|
|
125
|
-
log.warning("That's it, beautiful and simple logging!")
|
|
126
|
-
log.error("That's it, beautiful and simple logging!11")
|
|
174
|
+
pass
|
pytbox/mail/alimail.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from ..utils.response import ReturnResponse
|
|
8
|
+
from .mail_detail import MailDetail
|
|
9
|
+
from ..utils.timeutils import TimeUtils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AliMail:
|
|
14
|
+
'''
|
|
15
|
+
_summary_
|
|
16
|
+
'''
|
|
17
|
+
def __init__(self, mail_address: str=None, client_id: str=None, client_secret: str=None, timeout: int=3):
|
|
18
|
+
self.email_address = mail_address
|
|
19
|
+
self.client_id = client_id
|
|
20
|
+
self.client_secret = client_secret
|
|
21
|
+
self.base_url = "https://alimail-cn.aliyuncs.com/v2"
|
|
22
|
+
self.timeout = timeout
|
|
23
|
+
self.headers = {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"Authorization": f"bearer {self._get_access_token_by_request()}"
|
|
26
|
+
}
|
|
27
|
+
self.session = requests.Session()
|
|
28
|
+
self.session.headers.update(self.headers)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_access_token_by_request(self):
|
|
32
|
+
'''
|
|
33
|
+
https://mailhelp.aliyun.com/openapi/index.html#/markdown/authorization.md
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: _description_
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
_type_: _description_
|
|
40
|
+
'''
|
|
41
|
+
# 定义接口URL
|
|
42
|
+
interface_url = "https://alimail-cn.aliyuncs.com/oauth2/v2.0/token"
|
|
43
|
+
# 设置请求头,指定内容类型
|
|
44
|
+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
|
45
|
+
# 准备请求数据
|
|
46
|
+
data = {
|
|
47
|
+
"grant_type": "client_credentials",
|
|
48
|
+
"client_id": self.client_id,
|
|
49
|
+
"client_secret": self.client_secret
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
response = requests.post(interface_url, headers=headers, data=data, timeout=self.timeout)
|
|
53
|
+
response_json = response.json()
|
|
54
|
+
current_time = datetime.now()
|
|
55
|
+
data = {
|
|
56
|
+
'token_type': response_json["token_type"],
|
|
57
|
+
'access_token': response_json["access_token"],
|
|
58
|
+
'expires_in': response_json["expires_in"],
|
|
59
|
+
'expiration_time': current_time + timedelta(seconds=response_json["expires_in"])
|
|
60
|
+
}
|
|
61
|
+
return data.get("access_token")
|
|
62
|
+
except requests.RequestException as e:
|
|
63
|
+
# 处理请求失败异常
|
|
64
|
+
raise e
|
|
65
|
+
except (KeyError, ValueError) as e:
|
|
66
|
+
# 处理解析响应失败异常
|
|
67
|
+
raise e
|
|
68
|
+
|
|
69
|
+
def get_mail_folders(self):
|
|
70
|
+
response = self.session.get(
|
|
71
|
+
url=f"{self.base_url}/users/{self.email_address}/mailFolders",
|
|
72
|
+
headers=self.headers
|
|
73
|
+
)
|
|
74
|
+
return response.json().get('folders')
|
|
75
|
+
|
|
76
|
+
def get_folder_id(self, folder_name: Literal['inbox']='inbox'):
|
|
77
|
+
folders = self.get_mail_folders()
|
|
78
|
+
for folder in folders:
|
|
79
|
+
if folder.get('displayName') == folder_name:
|
|
80
|
+
return folder.get('id')
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_mail_detail(self, mail_id: str):
|
|
84
|
+
params = {
|
|
85
|
+
"$select": "body,toRecipients,internetMessageId,internetMessageHeaders"
|
|
86
|
+
}
|
|
87
|
+
response = self.session.get(
|
|
88
|
+
url=f"{self.base_url}/users/{self.email_address}/messages/{mail_id}",
|
|
89
|
+
headers=self.headers,
|
|
90
|
+
params=params,
|
|
91
|
+
timeout=3
|
|
92
|
+
)
|
|
93
|
+
return response.json().get('message')
|
|
94
|
+
|
|
95
|
+
def get_mail_list(self, folder_name: str='inbox', size: int=100):
|
|
96
|
+
folder_id = self.get_folder_id(folder_name=folder_name)
|
|
97
|
+
params = {
|
|
98
|
+
"size": size,
|
|
99
|
+
# "$select": "toRecipients"
|
|
100
|
+
}
|
|
101
|
+
response = self.session.get(
|
|
102
|
+
url=f"{self.base_url}/users/{self.email_address}/mailFolders/{folder_id}/messages",
|
|
103
|
+
headers=self.headers,
|
|
104
|
+
params=params,
|
|
105
|
+
timeout=3
|
|
106
|
+
)
|
|
107
|
+
messages = response.json().get("messages")
|
|
108
|
+
sent_to_list = []
|
|
109
|
+
for message in messages:
|
|
110
|
+
mail_id = message.get("id")
|
|
111
|
+
detail = self.get_mail_detail(mail_id=mail_id)
|
|
112
|
+
|
|
113
|
+
for to_recipient in detail.get('toRecipients'):
|
|
114
|
+
sent_to_list.append(to_recipient.get('email'))
|
|
115
|
+
|
|
116
|
+
yield MailDetail(
|
|
117
|
+
uid=message.get('id'),
|
|
118
|
+
sent_from=message.get('from').get('email'),
|
|
119
|
+
sent_to=sent_to_list,
|
|
120
|
+
date=TimeUtils.convert_str_to_datetime(time_str=message.get('sentDateTime'), app='alimail'),
|
|
121
|
+
cc="",
|
|
122
|
+
subject=message.get('subject'),
|
|
123
|
+
body_plain=message.get('summary'),
|
|
124
|
+
body_html=""
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def move(self, uid: str, destination_folder: str) -> ReturnResponse:
|
|
128
|
+
params = {
|
|
129
|
+
"ids": [uid],
|
|
130
|
+
"destinationFolderId": self.get_folder_id(destination_folder)
|
|
131
|
+
}
|
|
132
|
+
r = self.session.post(
|
|
133
|
+
url=f"{self.base_url}/users/{self.email_address}/messages/move",
|
|
134
|
+
params=params
|
|
135
|
+
)
|
|
136
|
+
if r.status_code == 200:
|
|
137
|
+
return ReturnResponse(code=0, msg=f'邮件移动到 {destination_folder} 成功', data=None)
|
|
138
|
+
else:
|
|
139
|
+
return ReturnResponse(code=1, msg=f'邮件移动到 {destination_folder} 失败', data=r.json())
|
|
140
|
+
|
|
141
|
+
if __name__ == '__main__':
|
|
142
|
+
ali_mail = AliMail()
|
pytbox/mail/client.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
import yagmail
|
|
6
|
+
from imap_tools import MailBox, MailMessageFlags, AND
|
|
7
|
+
from .mail_detail import MailDetail
|
|
8
|
+
from ..utils.response import ReturnResponse
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MailClient:
|
|
12
|
+
'''
|
|
13
|
+
_summary_
|
|
14
|
+
'''
|
|
15
|
+
def __init__(self,
|
|
16
|
+
mail_address: str=None,
|
|
17
|
+
password: str=None
|
|
18
|
+
):
|
|
19
|
+
|
|
20
|
+
self.mail_address = mail_address
|
|
21
|
+
self.password = password
|
|
22
|
+
|
|
23
|
+
if '163.com' in mail_address:
|
|
24
|
+
self.smtp_address = 'smtp.163.com'
|
|
25
|
+
self.imap_address = 'imap.163.com'
|
|
26
|
+
# self.imbox_client = self._create_imbox_object()
|
|
27
|
+
elif 'foxmail.com' in mail_address:
|
|
28
|
+
self.smtp_address = 'smtp.qq.com'
|
|
29
|
+
self.imap_address = 'imap.qq.com'
|
|
30
|
+
|
|
31
|
+
elif 'mail' in mail_address and 'cn' in mail_address:
|
|
32
|
+
self.smtp_address = "smtpdm.aliyun.com"
|
|
33
|
+
self.imap_address = ""
|
|
34
|
+
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f'不支持的邮箱地址: {mail_address}')
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def get_mailbox(self, readonly=False):
|
|
40
|
+
"""
|
|
41
|
+
创建并返回一个已登录的 MailBox 上下文管理器。
|
|
42
|
+
|
|
43
|
+
使用方式:
|
|
44
|
+
with self.get_mailbox() as mailbox:
|
|
45
|
+
# 使用 mailbox 进行操作
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
readonly (bool): 是否以只读模式打开邮箱,只读模式下不会将邮件标记为已读
|
|
50
|
+
|
|
51
|
+
Yields:
|
|
52
|
+
MailBox: 已登录的邮箱对象
|
|
53
|
+
"""
|
|
54
|
+
mailbox = MailBox(self.imap_address).login(self.mail_address, self.password)
|
|
55
|
+
if readonly:
|
|
56
|
+
# 以只读模式选择收件箱,防止邮件被标记为已读
|
|
57
|
+
mailbox.folder.set('INBOX', readonly=True)
|
|
58
|
+
try:
|
|
59
|
+
yield mailbox
|
|
60
|
+
finally:
|
|
61
|
+
mailbox.logout()
|
|
62
|
+
|
|
63
|
+
def send_mail(self, receiver: list=None, cc: list=None, subject: str='', contents: str='', attachments: list=None, tips: str=None):
|
|
64
|
+
'''
|
|
65
|
+
_summary_
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
receiver (list, optional): _description_. Defaults to None.
|
|
69
|
+
cc (list, optional): _description_. Defaults to None.
|
|
70
|
+
subject (str, optional): _description_. Defaults to ''.
|
|
71
|
+
contents (str, optional): _description_. Defaults to ''.
|
|
72
|
+
attachments (list, optional): _description_. Defaults to None.
|
|
73
|
+
'''
|
|
74
|
+
with yagmail.SMTP(user=self.mail_address, password=self.password, port=465, host=self.smtp_address) as yag:
|
|
75
|
+
try:
|
|
76
|
+
if tips:
|
|
77
|
+
contents = contents + '\n' + '<p style="color: red;">本邮件为系统自动发送</p>'
|
|
78
|
+
yag.send(to=receiver, cc=cc, subject=subject, contents=contents, attachments=attachments)
|
|
79
|
+
return True
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def get_mail_list(self, seen: bool=False, readonly: bool=True):
|
|
84
|
+
'''
|
|
85
|
+
获取邮件
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
seen (bool, optional): 默认获取未读邮件, 如果为 True, 则获取已读邮件
|
|
89
|
+
readonly (bool, optional): 是否以只读模式获取邮件,默认为 True,防止邮件被标记为已读
|
|
90
|
+
|
|
91
|
+
Yields:
|
|
92
|
+
MailDetail: 邮件详情
|
|
93
|
+
'''
|
|
94
|
+
with self.get_mailbox(readonly=readonly) as mailbox:
|
|
95
|
+
for msg in mailbox.fetch(AND(seen=seen)):
|
|
96
|
+
yield MailDetail(
|
|
97
|
+
uid=msg.uid,
|
|
98
|
+
sent_from=msg.from_,
|
|
99
|
+
sent_to=msg.to,
|
|
100
|
+
date=msg.date,
|
|
101
|
+
cc=msg.cc,
|
|
102
|
+
subject=msg.subject,
|
|
103
|
+
body_plain=msg.text,
|
|
104
|
+
body_html=msg.html
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def mark_as_read(self, uid):
|
|
108
|
+
"""
|
|
109
|
+
标记邮件为已读。
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
uid (str): 邮件的唯一标识符
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
with self.get_mailbox() as mailbox:
|
|
116
|
+
# 使用 imap_tools 的 flag 方法标记邮件为已读
|
|
117
|
+
# 第一个参数是 uid,第二个参数是要设置的标志,第三个参数 True 表示添加标志
|
|
118
|
+
mailbox.flag(uid, [MailMessageFlags.SEEN], True)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return ReturnResponse(code=1, msg='邮件删除失败', data=e)
|
|
121
|
+
|
|
122
|
+
def delete(self, uid):
|
|
123
|
+
"""
|
|
124
|
+
删除邮件。
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
uid (str): 邮件的唯一标识符
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
with self.get_mailbox() as mailbox:
|
|
131
|
+
# 使用 imap_tools 的 delete 方法删除邮件
|
|
132
|
+
mailbox.delete(uid)
|
|
133
|
+
# log.info(f'删除邮件{uid}')
|
|
134
|
+
except Exception as e:
|
|
135
|
+
# log.error(f'删除邮件{uid}失败: {e}')
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def move(self, uid: str, destination_folder: str) -> ReturnResponse:
|
|
139
|
+
"""
|
|
140
|
+
移动邮件到指定文件夹。
|
|
141
|
+
|
|
142
|
+
注意:部分 IMAP 服务商(如 QQ 邮箱)在移动邮件时,实际上是"复制到目标文件夹并从原文件夹删除",
|
|
143
|
+
这会导致邮件在原文件夹中消失,表现为"被删除"。但邮件会在目标文件夹中存在,并未彻底丢失。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
uid (str): 邮件的唯一标识符。
|
|
147
|
+
destination_folder (str): 目标文件夹名称。
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
ReturnResponse: 移动邮件结果
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
Exception: 移动过程中底层 imap 库抛出的异常。
|
|
154
|
+
"""
|
|
155
|
+
try:
|
|
156
|
+
with self.get_mailbox() as mailbox:
|
|
157
|
+
# 使用 imap_tools 的 move 方法移动邮件
|
|
158
|
+
mailbox.move(uid, destination_folder)
|
|
159
|
+
return ReturnResponse(code=0, msg=f'邮件 {uid} 移动到 {destination_folder} 成功', data=None)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return ReturnResponse(code=1, msg=f'邮件 {uid} 移动到 {destination_folder} 失败', data=e)
|
|
162
|
+
|
|
163
|
+
def get_folder_list(self):
|
|
164
|
+
'''
|
|
165
|
+
获取文件夹列表
|
|
166
|
+
'''
|
|
167
|
+
with self.get_mailbox() as mailbox:
|
|
168
|
+
return ReturnResponse(code=0, msg='获取文件夹列表成功', data=mailbox.folder.list())
|
|
169
|
+
|
|
170
|
+
if __name__ == '__main__':
|
|
171
|
+
pass
|