pytbox 0.0.4__tar.gz → 0.0.5__tar.gz
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-0.0.4 → pytbox-0.0.5}/PKG-INFO +3 -2
- {pytbox-0.0.4 → pytbox-0.0.5}/pyproject.toml +5 -2
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/alert/alert_handler.py +3 -3
- pytbox-0.0.5/src/pytbox/base.py +48 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/database/mongo.py +43 -2
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/database/victoriametrics.py +3 -3
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/log/logger.py +55 -23
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/log/victorialog.py +2 -2
- pytbox-0.0.5/src/pytbox/utils/timeutils.py +547 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox.egg-info/PKG-INFO +3 -2
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox.egg-info/SOURCES.txt +1 -1
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox.egg-info/requires.txt +2 -1
- pytbox-0.0.5/tests/test_base.py +9 -0
- pytbox-0.0.5/tests/test_feishu.py +11 -0
- pytbox-0.0.5/tests/test_logger.py +15 -0
- pytbox-0.0.5/tests/test_onepassword_connect.py +12 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/tests/test_onepassword_sa.py +1 -1
- {pytbox-0.0.4 → pytbox-0.0.5}/tests/test_victoriametrics.py +1 -3
- pytbox-0.0.4/src/pytbox/base.py +0 -41
- pytbox-0.0.4/src/pytbox/common/base.py +0 -0
- pytbox-0.0.4/src/pytbox/utils/timeutils.py +0 -53
- pytbox-0.0.4/tests/test_feishu.py +0 -17
- pytbox-0.0.4/tests/test_logger.py +0 -15
- pytbox-0.0.4/tests/test_onepassword_connect.py +0 -15
- {pytbox-0.0.4 → pytbox-0.0.5}/README.md +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/setup.cfg +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/alert/ping.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/alicloud/sls.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/common/__init__.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/dida365.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/feishu/client.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/feishu/endpoints.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/feishu/errors.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/feishu/helpers.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/feishu/typing.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/onepassword_connect.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/onepassword_sa.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/utils/env.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/utils/load_config.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/utils/ping_checker.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox/utils/response.py +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox.egg-info/dependency_links.txt +0 -0
- {pytbox-0.0.4 → pytbox-0.0.5}/src/pytbox.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytbox
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: A collection of Python integrations and utilities (Feishu, Dida365, VictoriaMetrics, ...)
|
|
5
5
|
Author-email: mingming hou <houm01@foxmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,8 +8,9 @@ Requires-Python: >=3.8
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: requests>=2.0
|
|
10
10
|
Requires-Dist: pydantic>=1.10
|
|
11
|
-
Requires-Dist: onepassword-sdk>=0.3.1
|
|
12
11
|
Requires-Dist: onepasswordconnectsdk>=1.0.0
|
|
12
|
+
Requires-Dist: loguru>=0.7.3
|
|
13
|
+
Requires-Dist: chinese_calendar>=1.10.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: pytest; extra == "dev"
|
|
15
16
|
Requires-Dist: black; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytbox"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "A collection of Python integrations and utilities (Feishu, Dida365, VictoriaMetrics, ...)"
|
|
9
9
|
authors = [{ name = "mingming hou", email = "houm01@foxmail.com" }]
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -14,8 +14,11 @@ requires-python = ">=3.8"
|
|
|
14
14
|
dependencies = [
|
|
15
15
|
"requests>=2.0",
|
|
16
16
|
"pydantic>=1.10",
|
|
17
|
-
|
|
17
|
+
# 这个包 28M, 如果从官方源下载太慢了, 先禁用一下,等以后版本稳定了,同步到国内源了再取消注释
|
|
18
|
+
# "onepassword-sdk>=0.3.1",
|
|
18
19
|
"onepasswordconnectsdk>=1.0.0",
|
|
20
|
+
"loguru>=0.7.3",
|
|
21
|
+
"chinese_calendar>=1.10.0"
|
|
19
22
|
]
|
|
20
23
|
|
|
21
24
|
[project.optional-dependencies]
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import uuid
|
|
5
5
|
from typing import Literal
|
|
6
6
|
from ..database.mongo import Mongo
|
|
7
|
-
from ..base import MongoClient
|
|
8
7
|
from ..feishu.client import Client as FeishuClient
|
|
9
8
|
from ..dida365 import Dida365
|
|
10
9
|
from ..utils.timeutils import TimeUtils
|
|
@@ -14,7 +13,7 @@ class AlertHandler:
|
|
|
14
13
|
|
|
15
14
|
def __init__(self,
|
|
16
15
|
config: dict=None,
|
|
17
|
-
mongo_client: Mongo=
|
|
16
|
+
mongo_client: Mongo=None,
|
|
18
17
|
feishu_client: FeishuClient=None,
|
|
19
18
|
dida_client: Dida365=None
|
|
20
19
|
):
|
|
@@ -70,7 +69,8 @@ class AlertHandler:
|
|
|
70
69
|
f'**事件内容**: {event_content}',
|
|
71
70
|
f'**告警实例**: {entity_name}',
|
|
72
71
|
f'**建议**: {suggestion}',
|
|
73
|
-
f'**故障排查**: {troubleshot}'
|
|
72
|
+
f'**故障排查**: {troubleshot}',
|
|
73
|
+
f'**历史告警**: {self.mongo.recent_alerts(event_content=event_content)}'
|
|
74
74
|
]
|
|
75
75
|
|
|
76
76
|
if event_type == "resolved":
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pytbox.database.mongo import Mongo
|
|
5
|
+
from pytbox.utils.load_config import load_config_by_file
|
|
6
|
+
from pytbox.database.victoriametrics import VictoriaMetrics
|
|
7
|
+
from pytbox.feishu.client import Client as FeishuClient
|
|
8
|
+
from pytbox.dida365 import Dida365
|
|
9
|
+
from pytbox.alert.alert_handler import AlertHandler
|
|
10
|
+
from pytbox.log.logger import AppLogger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
config = load_config_by_file(path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=os.environ.get('oc_vault_id'))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_mongo(collection):
|
|
17
|
+
return Mongo(
|
|
18
|
+
host=config['mongo']['host'],
|
|
19
|
+
port=config['mongo']['port'],
|
|
20
|
+
username=config['mongo']['username'],
|
|
21
|
+
password=config['mongo']['password'],
|
|
22
|
+
auto_source=config['mongo']['auto_source'],
|
|
23
|
+
db_name=config['mongo']['db_name'],
|
|
24
|
+
collection=collection
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
vm = VictoriaMetrics(url=config['victoriametrics']['url'])
|
|
28
|
+
|
|
29
|
+
feishu = FeishuClient(
|
|
30
|
+
app_id=config['feishu']['app_id'],
|
|
31
|
+
app_secret=config['feishu']['app_secret']
|
|
32
|
+
)
|
|
33
|
+
dida = Dida365(
|
|
34
|
+
cookie=config['dida']['cookie'],
|
|
35
|
+
access_token=config['dida']['access_token']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
alert_handler = AlertHandler(config=config, mongo_client=get_mongo('alert_test'), feishu_client=feishu, dida_client=dida)
|
|
39
|
+
|
|
40
|
+
def get_logger(app):
|
|
41
|
+
return AppLogger(
|
|
42
|
+
app_name=app,
|
|
43
|
+
enable_victorialog=True,
|
|
44
|
+
victorialog_url=config['victorialog']['url'],
|
|
45
|
+
feishu=feishu,
|
|
46
|
+
dida=dida,
|
|
47
|
+
mongo=get_mongo('alert_program')
|
|
48
|
+
)
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
import pymongo
|
|
4
|
-
|
|
4
|
+
from ..utils.timeutils import TimeUtils
|
|
5
|
+
from pytbox.utils import timeutils
|
|
5
6
|
|
|
6
7
|
class Mongo:
|
|
7
8
|
'''
|
|
8
9
|
当前主要使用的类
|
|
9
10
|
'''
|
|
10
|
-
def __init__(self, host, port, username, password, auto_source, db_name: str='automate', collection: str=None):
|
|
11
|
+
def __init__(self, host: str=None, port: int=27017, username: str=None, password: str=None, auto_source: str=None, db_name: str='automate', collection: str=None):
|
|
11
12
|
self.client = self._create_client(host, port, username, password, auto_source)
|
|
12
13
|
self.collection = self.client[db_name][collection]
|
|
13
14
|
|
|
@@ -56,3 +57,43 @@ class Mongo:
|
|
|
56
57
|
query['event_name'] = event_name
|
|
57
58
|
return self.collection.find(query)
|
|
58
59
|
|
|
60
|
+
def recent_alerts(self, event_content: str) -> str:
|
|
61
|
+
'''
|
|
62
|
+
获取最近 10 次告警
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
alarm_content (str): _description_
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: _description_
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
query = {
|
|
72
|
+
"event_content": event_content,
|
|
73
|
+
'resolved_time': {
|
|
74
|
+
'$exists': True, # 字段必须存在
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
fields = {"_id": 0, 'event_time': 1, 'resolved_time': 1}
|
|
78
|
+
results = self.collection.find(query, fields).sort('event_time', -1)
|
|
79
|
+
|
|
80
|
+
alarm_list = []
|
|
81
|
+
for result in results:
|
|
82
|
+
duration_minute = '持续 ' + str(int((result['resolved_time'] - result['event_time']).total_seconds() / 60)) + ' 分钟'
|
|
83
|
+
alarm_list.append('触发告警: ' + TimeUtils.convert_timeobj_to_str(timeobj=result['event_time']) + ' ' + duration_minute)
|
|
84
|
+
|
|
85
|
+
alarm_str = '\n'.join(alarm_list)
|
|
86
|
+
|
|
87
|
+
alarm_str_display_threshold = 10
|
|
88
|
+
|
|
89
|
+
if len(alarm_list) > alarm_str_display_threshold:
|
|
90
|
+
# 如果告警超过 10 个
|
|
91
|
+
alarm_counter = alarm_str_display_threshold
|
|
92
|
+
alarm_str = '\n'.join(alarm_list[:alarm_str_display_threshold])
|
|
93
|
+
else:
|
|
94
|
+
# 如果不超过 10 个
|
|
95
|
+
alarm_counter = len(alarm_list)
|
|
96
|
+
alarm_str = '\n'.join(alarm_list)
|
|
97
|
+
|
|
98
|
+
return '该告警出现过' + str(len(alarm_list)) + f'次\n最近 {alarm_counter} 次告警如下: \n' + alarm_str
|
|
99
|
+
|
|
@@ -86,8 +86,8 @@ class VictoriaMetrics:
|
|
|
86
86
|
|
|
87
87
|
return ReturnResponse(code=code, msg=msg, data=data)
|
|
88
88
|
|
|
89
|
-
def
|
|
90
|
-
direction: Literal['in', 'out'],
|
|
89
|
+
def check_interface_rate(self,
|
|
90
|
+
direction: Literal['in', 'out'],
|
|
91
91
|
sysName: str,
|
|
92
92
|
ifName:str,
|
|
93
93
|
last_minutes: Optional[int] = None
|
|
@@ -109,5 +109,5 @@ class VictoriaMetrics:
|
|
|
109
109
|
else:
|
|
110
110
|
query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
|
|
111
111
|
r = self.query(query)
|
|
112
|
-
rate = r.data['
|
|
112
|
+
rate = r.data[0]['value'][1]
|
|
113
113
|
return int(float(rate))
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
|
|
4
5
|
from loguru import logger
|
|
6
|
+
|
|
5
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
|
|
6
12
|
|
|
7
13
|
|
|
8
14
|
logger.remove()
|
|
@@ -21,6 +27,9 @@ 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
34
|
sls_url: str=None,
|
|
26
35
|
sls_access_key_id: str=None,
|
|
@@ -40,6 +49,9 @@ class AppLogger:
|
|
|
40
49
|
self.stream = stream
|
|
41
50
|
self.victorialog = Victorialog(url=victorialog_url)
|
|
42
51
|
self.enable_victorialog = enable_victorialog
|
|
52
|
+
self.mongo = mongo
|
|
53
|
+
self.feishu = feishu
|
|
54
|
+
self.dida = dida
|
|
43
55
|
|
|
44
56
|
def _get_caller_info(self) -> tuple[str, int, str]:
|
|
45
57
|
"""
|
|
@@ -90,10 +102,48 @@ class AppLogger:
|
|
|
90
102
|
logger.error(f"[{caller_filename}:{caller_lineno}:{caller_function}] {message}")
|
|
91
103
|
if self.enable_victorialog:
|
|
92
104
|
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
|
-
|
|
105
|
+
|
|
106
|
+
if self.feishu:
|
|
107
|
+
existing_message = self.mongo.collection.find_one({"message": message}, sort=[("time", -1)])
|
|
108
|
+
current_time = TimeUtils.get_now_time_mongo()
|
|
109
|
+
|
|
110
|
+
if not existing_message or TimeUtils.get_time_diff_hours(existing_message["time"], current_time) > 36:
|
|
111
|
+
self.mongo.collection.insert_one({
|
|
112
|
+
"message": message,
|
|
113
|
+
"time": current_time,
|
|
114
|
+
"file_name": caller_filename,
|
|
115
|
+
"line_number": caller_lineno,
|
|
116
|
+
"function_name": caller_function
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
content_list = [
|
|
121
|
+
f"{self.feishu.extensions.format_rich_text(text='app:', color='blue', bold=True)} {self.app_name}",
|
|
122
|
+
f"{self.feishu.extensions.format_rich_text(text='message:', color='blue', bold=True)} {message}",
|
|
123
|
+
f"{self.feishu.extensions.format_rich_text(text='file_name:', color='blue', bold=True)} {caller_filename}",
|
|
124
|
+
f"{self.feishu.extensions.format_rich_text(text='line_number:', color='blue', bold=True)} {caller_lineno}",
|
|
125
|
+
f"{self.feishu.extensions.format_rich_text(text='function_name:', color='blue', bold=True)} {caller_function}"
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
self.feishu.extensions.send_message_notify(
|
|
129
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
130
|
+
content="\n".join(content_list)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
dida_content_list = [
|
|
134
|
+
f"**app**: {self.app_name}",
|
|
135
|
+
f"**message**: {message}",
|
|
136
|
+
f"**file_name**: {caller_filename}",
|
|
137
|
+
f"**line_number**: {caller_lineno}",
|
|
138
|
+
f"**function_name**: {caller_function}"
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
self.dida.task_create(
|
|
142
|
+
project_id="65e87d2b3e73517c2cdd9d63",
|
|
143
|
+
title=f"自动化脚本告警: {self.app_name}",
|
|
144
|
+
content="\n".join(dida_content_list),
|
|
145
|
+
tags=['L-程序告警', 't-问题处理']
|
|
146
|
+
)
|
|
97
147
|
|
|
98
148
|
def critical(self, message: str):
|
|
99
149
|
"""记录严重错误级别日志"""
|
|
@@ -103,24 +153,6 @@ class AppLogger:
|
|
|
103
153
|
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
154
|
|
|
105
155
|
|
|
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
|
-
|
|
120
|
-
|
|
121
156
|
# 使用示例
|
|
122
157
|
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")
|
|
158
|
+
pass
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import pytz
|
|
5
|
+
import time
|
|
6
|
+
import datetime
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimeUtils:
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_time_object(now: bool=True):
|
|
15
|
+
'''
|
|
16
|
+
获取当前时间, 加入了时区信息, 简单是存储在 Mongo 中时格式为 ISODate
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
current_time(class): 时间, 格式: 2024-04-23 16:48:11.591589+08:00
|
|
20
|
+
'''
|
|
21
|
+
if now:
|
|
22
|
+
return datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def get_utc_time():
|
|
26
|
+
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def get_now_time_mongo():
|
|
30
|
+
return datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def convert_timeobj_to_str(timeobj: str=None, timezone_offset: int=8, time_format: Literal['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ']='%Y-%m-%d %H:%M:%S'):
|
|
34
|
+
time_obj_with_offset = timeobj + datetime.timedelta(hours=timezone_offset)
|
|
35
|
+
if time_format == '%Y-%m-%d %H:%M:%S':
|
|
36
|
+
return time_obj_with_offset.strftime("%Y-%m-%d %H:%M:%S")
|
|
37
|
+
elif time_format == '%Y-%m-%dT%H:%M:%SZ':
|
|
38
|
+
return time_obj_with_offset.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def get_time_diff_hours(time1, time2):
|
|
42
|
+
"""
|
|
43
|
+
计算两个datetime对象之间的小时差
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
time1: 第一个datetime对象
|
|
47
|
+
time2: 第二个datetime对象
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
float: 两个时间之间的小时差
|
|
51
|
+
"""
|
|
52
|
+
if not time1 or not time2:
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
# 确保两个时间都有时区信息
|
|
56
|
+
if time1.tzinfo is None:
|
|
57
|
+
time1 = time1.replace(tzinfo=pytz.timezone('Asia/Shanghai'))
|
|
58
|
+
if time2.tzinfo is None:
|
|
59
|
+
time2 = time2.replace(tzinfo=pytz.timezone('Asia/Shanghai'))
|
|
60
|
+
|
|
61
|
+
# 计算时间差(秒)
|
|
62
|
+
time_diff_seconds = abs((time2 - time1).total_seconds())
|
|
63
|
+
|
|
64
|
+
# 转换为小时
|
|
65
|
+
time_diff_hours = time_diff_seconds / 3600
|
|
66
|
+
|
|
67
|
+
return time_diff_hours
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def convert_syslog_huawei_str_to_8601(timestr):
|
|
71
|
+
"""
|
|
72
|
+
将华为 syslog 格式的时间字符串(如 '2025-08-02T04:34:24+08:00')转换为 ISO8601 格式的 UTC 时间字符串。
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
timestr (str): 原始时间字符串,格式如 '2025-08-02T04:34:24+08:00'
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
str: 转换后的 ISO8601 格式 UTC 时间字符串,如 '2025-08-01T20:34:24.000000Z'
|
|
79
|
+
"""
|
|
80
|
+
if timestr is None:
|
|
81
|
+
return None
|
|
82
|
+
try:
|
|
83
|
+
# 解析带时区的时间字符串
|
|
84
|
+
dt: datetime.datetime = datetime.datetime.strptime(timestr, "%Y-%m-%dT%H:%M:%S%z")
|
|
85
|
+
# 转换为 UTC
|
|
86
|
+
dt_utc: datetime.datetime = dt.astimezone(datetime.timezone.utc)
|
|
87
|
+
# 格式化为 ISO8601 字符串(带微秒,Z 结尾)
|
|
88
|
+
iso8601_utc: str = dt_utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
89
|
+
return iso8601_utc
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
# 日志记录异常(此处仅简单打印,实际项目建议用 logging)
|
|
92
|
+
print(f"时间转换失败: {e}, 输入: {timestr}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def convert_str_to_timestamp(timestr):
|
|
97
|
+
"""
|
|
98
|
+
将类似 '2025-04-16T00:08:28.000+0000' 格式的时间字符串转换为时间戳(秒级)。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
timestr (str): 时间字符串,格式如 '2025-04-16T00:08:28.000+0000'
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
int: 时间戳(秒级)
|
|
105
|
+
"""
|
|
106
|
+
if timestr is None:
|
|
107
|
+
return None
|
|
108
|
+
# 兼容带毫秒和时区的ISO8601格式
|
|
109
|
+
# 先将+0000或+08:00等时区格式标准化为+00:00
|
|
110
|
+
timestr_fixed = re.sub(r'([+-]\d{2})(\d{2})$', r'\1:\2', timestr)
|
|
111
|
+
|
|
112
|
+
# 处理毫秒部分(.000),如果没有毫秒也能兼容
|
|
113
|
+
try:
|
|
114
|
+
dt = datetime.datetime.fromisoformat(timestr_fixed)
|
|
115
|
+
except ValueError:
|
|
116
|
+
if len(timestr_fixed) == 8:
|
|
117
|
+
dt = datetime.datetime.strptime(timestr_fixed, "%Y%m%d")
|
|
118
|
+
else:
|
|
119
|
+
# 如果没有毫秒部分
|
|
120
|
+
dt = datetime.datetime.strptime(timestr_fixed, "%Y-%m-%dT%H:%M:%S%z")
|
|
121
|
+
# 返回秒级时间戳
|
|
122
|
+
return int(dt.timestamp()) * 1000
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def convert_str_to_datetime_lg_backup(time_str: str):
|
|
126
|
+
"""
|
|
127
|
+
将 '7/10/2025 3:52:43 PM' 这种格式的时间字符串转换为时间戳(秒级)。
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
time_str (str): 时间字符串,格式如 '7/10/2025 3:52:43 PM'
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
int: 时间戳(秒级)
|
|
134
|
+
"""
|
|
135
|
+
if time_str is None:
|
|
136
|
+
return None
|
|
137
|
+
# 先将字符串转换为 datetime 对象
|
|
138
|
+
dt = datetime.datetime.strptime(time_str, "%m/%d/%Y %I:%M:%S %p")
|
|
139
|
+
# 返回秒级时间戳
|
|
140
|
+
return int(dt.timestamp()) * 1000
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def convert_timestamp_to_str(timestamp: int, time_format: Literal['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S.000Z']='%Y-%m-%d %H:%M:%S', timezone_offset: int=8):
|
|
144
|
+
'''
|
|
145
|
+
_summary_
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
timestamp (_type_): _description_
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
_type_: _description_
|
|
152
|
+
'''
|
|
153
|
+
timestamp = int(timestamp)
|
|
154
|
+
if timestamp > 10000000000:
|
|
155
|
+
timestamp = timestamp / 1000
|
|
156
|
+
# 使用datetime模块的fromtimestamp方法将时间戳转换为datetime对象
|
|
157
|
+
dt_object = datetime.datetime.fromtimestamp(timestamp, tz=ZoneInfo(f'Etc/GMT-{timezone_offset}'))
|
|
158
|
+
|
|
159
|
+
# 使用strftime方法将datetime对象格式化为字符串
|
|
160
|
+
return dt_object.strftime(time_format)
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def timestamp_to_timestr_dida(timestamp):
|
|
164
|
+
# 将时间戳转换为 UTC 时间
|
|
165
|
+
utc_time = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
|
166
|
+
|
|
167
|
+
# 将 UTC 时间转换为指定时区(+08:00)
|
|
168
|
+
target_timezone = datetime.timezone(datetime.timedelta(hours=8))
|
|
169
|
+
local_time = utc_time.astimezone(target_timezone)
|
|
170
|
+
|
|
171
|
+
# 格式化为指定字符串
|
|
172
|
+
formatted_time = local_time.strftime('%Y-%m-%dT%H:%M:%S%z')
|
|
173
|
+
|
|
174
|
+
# 添加冒号到时区部分
|
|
175
|
+
formatted_time = formatted_time[:-2] + ':' + formatted_time[-2:]
|
|
176
|
+
return formatted_time
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def timestamp_to_datetime_obj(timestamp: int, timezone_offset: int=8):
|
|
180
|
+
return datetime.datetime.fromtimestamp(timestamp, tz=ZoneInfo(f'Etc/GMT-{timezone_offset}'))
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def datetime_obj_to_str(datetime_obj, add_timezone=False):
|
|
184
|
+
if add_timezone:
|
|
185
|
+
datetime_obj = datetime_obj.astimezone(pytz.timezone('Asia/Shanghai'))
|
|
186
|
+
return datetime_obj.strftime("%Y-%m-%d %H:%M:%S")
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def get_current_time(app: Literal['notion', 'dida365']):
|
|
190
|
+
if app == 'notion':
|
|
191
|
+
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
192
|
+
elif app == 'dida365':
|
|
193
|
+
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S%z")
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def get_current_date_str(output_format: Literal['%Y%m%d', '%Y-%m-%d']='%Y%m%d') -> str:
|
|
197
|
+
# 获取当前日期和时间
|
|
198
|
+
now = datetime.datetime.now()
|
|
199
|
+
|
|
200
|
+
# 格式化为字符串
|
|
201
|
+
current_date_str = now.strftime(output_format)
|
|
202
|
+
return current_date_str
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def get_date_n_days_from_now(n: int = 0, output_format: Literal['%Y%m%d', '%Y-%m-%d'] = '%Y%m%d') -> str:
|
|
206
|
+
"""
|
|
207
|
+
获取距离当前日期 N 天后的日期字符串。
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
n (int): 天数,可以为负数(表示 N 天前),默认为 0(即今天)。
|
|
211
|
+
output_format (Literal['%Y%m%d', '%Y-%m-%d']): 日期输出格式,默认为 '%Y%m%d'。
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
str: 格式化后的日期字符串。
|
|
215
|
+
|
|
216
|
+
示例:
|
|
217
|
+
>>> MyTime.get_date_n_days_from_now(1)
|
|
218
|
+
'20240620'
|
|
219
|
+
>>> MyTime.get_date_n_days_from_now(-1, '%Y-%m-%d')
|
|
220
|
+
'2024-06-18'
|
|
221
|
+
"""
|
|
222
|
+
target_date = datetime.datetime.now() + datetime.timedelta(days=n)
|
|
223
|
+
return target_date.strftime(output_format)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def get_yesterday_date_str() -> str:
|
|
228
|
+
# 获取昨天日期
|
|
229
|
+
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
|
|
230
|
+
yesterday_date_str = yesterday.strftime("%Y%m%d")
|
|
231
|
+
return yesterday_date_str
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def get_last_month_date_str() -> str:
|
|
235
|
+
# 获取上个月日期
|
|
236
|
+
last_month = datetime.datetime.now() - datetime.timedelta(days=30)
|
|
237
|
+
last_month_date_str = last_month.strftime("%Y-%m")
|
|
238
|
+
return last_month_date_str
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def get_current_time_str() -> str:
|
|
242
|
+
# 获取当前日期和时间
|
|
243
|
+
now = datetime.datetime.now()
|
|
244
|
+
|
|
245
|
+
# 格式化为字符串
|
|
246
|
+
current_date_str = now.strftime("%Y%m%d_%H%M")
|
|
247
|
+
return current_date_str
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def get_timestamp(now: bool=True, last_minutes: int=0, unit: Literal['ms', 's']='ms') -> int:
|
|
251
|
+
'''
|
|
252
|
+
获取当前时间戳, 减去 last_minutes 分钟
|
|
253
|
+
'''
|
|
254
|
+
if now:
|
|
255
|
+
if unit == 'ms':
|
|
256
|
+
return int(time.time()) * 1000
|
|
257
|
+
elif unit == 's':
|
|
258
|
+
return int(time.time())
|
|
259
|
+
|
|
260
|
+
if last_minutes == 0:
|
|
261
|
+
if unit == 'ms':
|
|
262
|
+
return int(time.time()) * 1000
|
|
263
|
+
elif unit == 's':
|
|
264
|
+
return int(time.time())
|
|
265
|
+
else:
|
|
266
|
+
if unit == 'ms':
|
|
267
|
+
return int(time.time()) * 1000 - last_minutes * 60 * 1000
|
|
268
|
+
elif unit == 's':
|
|
269
|
+
return int(time.time()) - last_minutes * 60
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def get_timestamp_tomorrow() -> int:
|
|
273
|
+
return int(time.time()) * 1000 + 24 * 60 * 60 * 1000
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def get_timestamp_last_day(last_days: int=0, unit: Literal['ms', 's']='ms') -> int:
|
|
277
|
+
if last_days == 0:
|
|
278
|
+
if unit == 'ms':
|
|
279
|
+
return int(time.time()) * 1000
|
|
280
|
+
elif unit == 's':
|
|
281
|
+
return int(time.time())
|
|
282
|
+
else:
|
|
283
|
+
return int(time.time()) * 1000 - last_days * 24 * 60 * 60 * 1000
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def get_today_timestamp() -> int:
|
|
287
|
+
return int(time.time()) * 1000
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def convert_str_to_datetime(time_str: str, app: Literal['lg_alert_trigger', 'lg_alert_resolved', 'mongo']='lg_alert_trigger') -> datetime.datetime:
|
|
291
|
+
"""
|
|
292
|
+
将字符串转换为datetime对象
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
time_str (str): 时间字符串
|
|
296
|
+
app (Literal['lg_alert_trigger', 'lg_alert_resolved', 'mongo'], optional): 应用类型. Defaults to 'lg_alert_trigger'.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
datetime.datetime: 带时区信息的datetime对象
|
|
300
|
+
"""
|
|
301
|
+
if app == 'lg_alert_trigger':
|
|
302
|
+
time_obj = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
|
|
303
|
+
elif app == 'lg_alert_resolved':
|
|
304
|
+
time_obj = datetime.datetime.strptime(time_str, "%Y年%m月%d日 %H:%M:%S")
|
|
305
|
+
elif app == 'mongo':
|
|
306
|
+
time_obj = datetime.datetime.fromisoformat(time_str.replace('Z', '+00:00'))
|
|
307
|
+
return time_obj # 已经包含时区信息,直接返回
|
|
308
|
+
|
|
309
|
+
# 对于没有时区信息的时间对象,设置为Asia/Shanghai时区
|
|
310
|
+
time_with_tz = time_obj.replace(tzinfo=ZoneInfo("Asia/Shanghai"))
|
|
311
|
+
return time_with_tz
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def convert_timeobj_to_str(timeobj: str=None, timezone_offset: int=8, time_format: Literal['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ']='%Y-%m-%d %H:%M:%S'):
|
|
315
|
+
time_obj_with_offset = timeobj + datetime.timedelta(hours=timezone_offset)
|
|
316
|
+
if time_format == '%Y-%m-%d %H:%M:%S':
|
|
317
|
+
return time_obj_with_offset.strftime("%Y-%m-%d %H:%M:%S")
|
|
318
|
+
elif time_format == '%Y-%m-%dT%H:%M:%SZ':
|
|
319
|
+
return time_obj_with_offset.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def convert_timestamp_to_timeobj(timestamp: int) -> datetime.datetime:
|
|
323
|
+
"""
|
|
324
|
+
将时间戳转换为时间对象
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
timestamp (int): 时间戳,单位为秒或毫秒
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
datetime.datetime: 转换后的时间对象,时区为 Asia/Shanghai
|
|
331
|
+
"""
|
|
332
|
+
# 如果时间戳是毫秒,转换为秒
|
|
333
|
+
if len(str(timestamp)) > 10:
|
|
334
|
+
timestamp = timestamp / 1000
|
|
335
|
+
|
|
336
|
+
return datetime.datetime.fromtimestamp(timestamp, tz=ZoneInfo("Asia/Shanghai"))
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def convert_timeobj_to_timestamp(timeobj: datetime.datetime) -> int:
|
|
340
|
+
"""
|
|
341
|
+
将时间对象转换为时间戳
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
timeobj (datetime.datetime): 时间对象
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
int: 时间戳,单位为秒或毫秒
|
|
348
|
+
"""
|
|
349
|
+
return int(timeobj.timestamp())
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def convert_timeobj_add_timezone(timeobj: datetime.datetime, timezone_offset: int=8) -> datetime.datetime:
|
|
353
|
+
return timeobj + datetime.timedelta(hours=timezone_offset)
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
def convert_mute_duration(mute_duration: str) -> datetime.datetime:
|
|
357
|
+
"""将屏蔽时长字符串转换为带 Asia/Shanghai 时区的 ``datetime`` 对象。
|
|
358
|
+
|
|
359
|
+
支持两类输入:
|
|
360
|
+
- 绝对时间: 例如 ``"2025-01-02 13:45"``,按本地上海时区解释。
|
|
361
|
+
- 相对时间: 形如 ``"10m"``、``"2h"``、``"1d"`` 分别表示分钟、小时、天。
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
mute_duration: 屏蔽时长字符串。
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
datetime.datetime: 带 ``Asia/Shanghai`` 时区信息的时间点。
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ValueError: 当输入字符串无法被解析时抛出。
|
|
371
|
+
"""
|
|
372
|
+
shanghai_tz = ZoneInfo("Asia/Shanghai")
|
|
373
|
+
now: datetime.datetime = datetime.datetime.now(tz=shanghai_tz)
|
|
374
|
+
mute_duration = mute_duration.strip()
|
|
375
|
+
|
|
376
|
+
# 绝对时间格式(按上海时区解释)
|
|
377
|
+
try:
|
|
378
|
+
abs_dt_naive = datetime.datetime.strptime(mute_duration, "%Y-%m-%d %H:%M")
|
|
379
|
+
return abs_dt_naive.replace(tzinfo=shanghai_tz)
|
|
380
|
+
except ValueError:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
# 相对时间格式
|
|
384
|
+
pattern = r"^(\d+)([dhm])$"
|
|
385
|
+
match = re.match(pattern, mute_duration)
|
|
386
|
+
if match:
|
|
387
|
+
value_str, unit = match.groups()
|
|
388
|
+
value = int(value_str)
|
|
389
|
+
if unit == "d":
|
|
390
|
+
return now + datetime.timedelta(days=value)
|
|
391
|
+
if unit == "h":
|
|
392
|
+
return now + datetime.timedelta(hours=value)
|
|
393
|
+
if unit == "m":
|
|
394
|
+
return now + datetime.timedelta(minutes=value)
|
|
395
|
+
|
|
396
|
+
raise ValueError(f"无法解析 mute_duration: {mute_duration}")
|
|
397
|
+
|
|
398
|
+
@staticmethod
|
|
399
|
+
def convert_mute_duration_to_str(mute_duration: str) -> str:
|
|
400
|
+
'''
|
|
401
|
+
将屏蔽时长字符串转换为距离当前时间的标准描述,形如 '2d3h'。
|
|
402
|
+
|
|
403
|
+
支持两类输入:
|
|
404
|
+
- 绝对时间: 'YYYY-MM-DD HH:MM'(按 Asia/Shanghai 解释)
|
|
405
|
+
- 相对时间: '10m'、'2h'、'1d'
|
|
406
|
+
|
|
407
|
+
如果目标时间已过去,则返回 '0h'。
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
mute_duration (str): 屏蔽时长字符串。
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
str: 与当前时间的距离描述,例如 '1d2h'、'3h'。
|
|
414
|
+
'''
|
|
415
|
+
shanghai_tz = ZoneInfo("Asia/Shanghai")
|
|
416
|
+
now: datetime.datetime = datetime.datetime.now(tz=shanghai_tz)
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
target_time: datetime.datetime = TimeUtils.convert_mute_duration(mute_duration)
|
|
420
|
+
except ValueError:
|
|
421
|
+
# 兜底:直接尝试绝对时间格式
|
|
422
|
+
try:
|
|
423
|
+
abs_dt = datetime.datetime.strptime(mute_duration.strip(), "%Y-%m-%d %H:%M")
|
|
424
|
+
target_time = abs_dt.replace(tzinfo=shanghai_tz)
|
|
425
|
+
except ValueError:
|
|
426
|
+
return mute_duration
|
|
427
|
+
|
|
428
|
+
# 计算与现在的差值(仅面向未来的剩余时长)
|
|
429
|
+
delta_seconds: float = (target_time - now).total_seconds()
|
|
430
|
+
if delta_seconds <= 0:
|
|
431
|
+
return "0h"
|
|
432
|
+
|
|
433
|
+
days: int = int(delta_seconds // 86400)
|
|
434
|
+
hours: int = int((delta_seconds % 86400) // 3600)
|
|
435
|
+
|
|
436
|
+
parts: list[str] = []
|
|
437
|
+
if days > 0:
|
|
438
|
+
parts.append(f"{days}d")
|
|
439
|
+
if hours > 0:
|
|
440
|
+
parts.append(f"{hours}h")
|
|
441
|
+
if not parts:
|
|
442
|
+
# 小于 1 小时
|
|
443
|
+
parts.append("0h")
|
|
444
|
+
|
|
445
|
+
return "".join(parts)
|
|
446
|
+
|
|
447
|
+
@staticmethod
|
|
448
|
+
def is_work_time(start_hour: int=9, end_hour: int=18) -> bool:
|
|
449
|
+
'''
|
|
450
|
+
判断是否为工作时间
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
start_hour (int, optional): 开始工作时间. Defaults to 9.
|
|
454
|
+
end_hour (int, optional): 结束工作时间. Defaults to 18.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
bool: 如果是工作时间, 返回 True, 否则返回 False
|
|
458
|
+
'''
|
|
459
|
+
current_time = datetime.datetime.now().time()
|
|
460
|
+
start = datetime.time(start_hour, 0, 0)
|
|
461
|
+
end = datetime.time(end_hour, 0, 0)
|
|
462
|
+
|
|
463
|
+
if start <= current_time <= end:
|
|
464
|
+
return True
|
|
465
|
+
else:
|
|
466
|
+
return False
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def is_work_day() -> bool:
|
|
470
|
+
'''
|
|
471
|
+
判断是否为工作日
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
bool: 如果是工作日, 返回 True, 否则返回 False
|
|
475
|
+
'''
|
|
476
|
+
from chinese_calendar import is_workday
|
|
477
|
+
date_now = datetime.datetime.date(datetime.datetime.now())
|
|
478
|
+
# print(date_now)
|
|
479
|
+
if is_workday(date_now):
|
|
480
|
+
return True
|
|
481
|
+
else:
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
@staticmethod
|
|
485
|
+
def get_week_number(offset: int=5) -> int:
|
|
486
|
+
'''
|
|
487
|
+
获取今天是哪一年的第几周
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
int: 返回一个元组,包含(年份, 周数)
|
|
491
|
+
'''
|
|
492
|
+
now = datetime.datetime.now()
|
|
493
|
+
# isocalendar()方法返回一个元组,包含年份、周数和周几
|
|
494
|
+
_year_unused, week, _ = now.isocalendar()
|
|
495
|
+
return week - offset
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def get_week_day(timestamp: int=None, offset: int=0) -> int:
|
|
499
|
+
'''
|
|
500
|
+
获取今天是周几
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
int: 周几的数字表示,1表示周一,7表示周日
|
|
504
|
+
'''
|
|
505
|
+
if timestamp is None:
|
|
506
|
+
now = datetime.datetime.now()
|
|
507
|
+
else:
|
|
508
|
+
if timestamp > 10000000000:
|
|
509
|
+
timestamp = timestamp / 1000
|
|
510
|
+
now = datetime.datetime.fromtimestamp(timestamp)
|
|
511
|
+
# weekday()方法返回0-6的数字,0表示周一,6表示周日
|
|
512
|
+
return now.weekday() + 1 + offset
|
|
513
|
+
|
|
514
|
+
@staticmethod
|
|
515
|
+
def get_week_of_year(customer: Literal['lululemon', 'other']='other') -> tuple[int, int]:
|
|
516
|
+
'''
|
|
517
|
+
获取今天是哪一年的第几周
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
tuple[int, int]: 返回一个元组,包含(年份, 周数)
|
|
521
|
+
'''
|
|
522
|
+
now = datetime.datetime.now()
|
|
523
|
+
# isocalendar()方法返回一个元组,包含年份、周数和周几
|
|
524
|
+
_year, week, _ = now.isocalendar()
|
|
525
|
+
if customer == 'lululemon':
|
|
526
|
+
week = week - 5
|
|
527
|
+
return _year, week
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def get_last_month_start_and_end_time() -> tuple[datetime.datetime, datetime.datetime]:
|
|
531
|
+
'''
|
|
532
|
+
获取上个月的开始和结束时间
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
tuple[datetime.datetime, datetime.datetime]: 返回一个元组,包含上个月的开始和结束时间
|
|
536
|
+
'''
|
|
537
|
+
today = datetime.datetime.now()
|
|
538
|
+
# 获取当前月份的第一天
|
|
539
|
+
first_day_of_current_month = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
540
|
+
# 上个月1号的0:00分
|
|
541
|
+
start_time = first_day_of_current_month - datetime.timedelta(days=1)
|
|
542
|
+
start_time = start_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
543
|
+
|
|
544
|
+
# 获取上个月的最后一天
|
|
545
|
+
end_time = first_day_of_current_month - datetime.timedelta(days=1)
|
|
546
|
+
end_time = end_time.replace(hour=23, minute=59, second=59, microsecond=0)
|
|
547
|
+
return start_time.strftime("%Y-%m-%dT%H:%M:%SZ"), end_time.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytbox
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: A collection of Python integrations and utilities (Feishu, Dida365, VictoriaMetrics, ...)
|
|
5
5
|
Author-email: mingming hou <houm01@foxmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,8 +8,9 @@ Requires-Python: >=3.8
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: requests>=2.0
|
|
10
10
|
Requires-Dist: pydantic>=1.10
|
|
11
|
-
Requires-Dist: onepassword-sdk>=0.3.1
|
|
12
11
|
Requires-Dist: onepasswordconnectsdk>=1.0.0
|
|
12
|
+
Requires-Dist: loguru>=0.7.3
|
|
13
|
+
Requires-Dist: chinese_calendar>=1.10.0
|
|
13
14
|
Provides-Extra: dev
|
|
14
15
|
Requires-Dist: pytest; extra == "dev"
|
|
15
16
|
Requires-Dist: black; extra == "dev"
|
|
@@ -13,7 +13,6 @@ src/pytbox/alert/alert_handler.py
|
|
|
13
13
|
src/pytbox/alert/ping.py
|
|
14
14
|
src/pytbox/alicloud/sls.py
|
|
15
15
|
src/pytbox/common/__init__.py
|
|
16
|
-
src/pytbox/common/base.py
|
|
17
16
|
src/pytbox/database/mongo.py
|
|
18
17
|
src/pytbox/database/victoriametrics.py
|
|
19
18
|
src/pytbox/feishu/client.py
|
|
@@ -28,6 +27,7 @@ src/pytbox/utils/load_config.py
|
|
|
28
27
|
src/pytbox/utils/ping_checker.py
|
|
29
28
|
src/pytbox/utils/response.py
|
|
30
29
|
src/pytbox/utils/timeutils.py
|
|
30
|
+
tests/test_base.py
|
|
31
31
|
tests/test_feishu.py
|
|
32
32
|
tests/test_logger.py
|
|
33
33
|
tests/test_onepassword_connect.py
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from pytbox.base import get_logger
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
log = get_logger('tests.test_logger')
|
|
8
|
+
|
|
9
|
+
def test_logger_info():
|
|
10
|
+
log.info('test_logger_info')
|
|
11
|
+
log.error('test error log2')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
test_logger_info()
|
pytbox-0.0.4/src/pytbox/base.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from pytbox.database.mongo import Mongo
|
|
5
|
-
from pytbox.utils.load_config import load_config_by_file
|
|
6
|
-
from pytbox.database.victoriametrics import VictoriaMetrics
|
|
7
|
-
from pytbox.feishu.client import Client as FeishuClient
|
|
8
|
-
from pytbox.dida365 import Dida365
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def MongoClient(collection, config_path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=None):
|
|
12
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
13
|
-
return Mongo(
|
|
14
|
-
host=config['mongo']['host'],
|
|
15
|
-
port=config['mongo']['port'],
|
|
16
|
-
username=config['mongo']['username'],
|
|
17
|
-
password=config['mongo']['password'],
|
|
18
|
-
auto_source=config['mongo']['auto_source'],
|
|
19
|
-
db_name=config['mongo']['db_name'],
|
|
20
|
-
collection=collection
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
def vm_client(config_path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=None):
|
|
24
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
25
|
-
return VictoriaMetrics(
|
|
26
|
-
url=config['victoriametrics']['url']
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
def feishu_client(config_path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=None):
|
|
30
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
31
|
-
return FeishuClient(
|
|
32
|
-
app_id=config['feishu']['app_id'],
|
|
33
|
-
app_secret=config['feishu']['app_secret']
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
def dida_client(config_path='/workspaces/pytbox/tests/alert/config_dev.toml', oc_vault_id=None):
|
|
37
|
-
config = load_config_by_file(path=config_path, oc_vault_id=oc_vault_id)
|
|
38
|
-
return Dida365(
|
|
39
|
-
cookie=config['dida']['cookie'],
|
|
40
|
-
access_token=config['dida']['access_token']
|
|
41
|
-
)
|
|
File without changes
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import time
|
|
5
|
-
import pytz
|
|
6
|
-
import datetime
|
|
7
|
-
from typing import Literal
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TimeUtils:
|
|
11
|
-
|
|
12
|
-
@staticmethod
|
|
13
|
-
def get_timestamp(now: bool=True) -> int:
|
|
14
|
-
'''
|
|
15
|
-
获取时间戳
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
now (bool, optional): _description_. Defaults to True.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
_type_: _description_
|
|
22
|
-
'''
|
|
23
|
-
if now:
|
|
24
|
-
return int(time.time())
|
|
25
|
-
else:
|
|
26
|
-
return int(time.time() * 1000)
|
|
27
|
-
|
|
28
|
-
@staticmethod
|
|
29
|
-
def get_time_object(now: bool=True):
|
|
30
|
-
'''
|
|
31
|
-
获取当前时间, 加入了时区信息, 简单是存储在 Mongo 中时格式为 ISODate
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
current_time(class): 时间, 格式: 2024-04-23 16:48:11.591589+08:00
|
|
35
|
-
'''
|
|
36
|
-
if now:
|
|
37
|
-
return datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
|
|
38
|
-
|
|
39
|
-
@staticmethod
|
|
40
|
-
def get_utc_time():
|
|
41
|
-
return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
42
|
-
|
|
43
|
-
@staticmethod
|
|
44
|
-
def get_now_time_mongo():
|
|
45
|
-
return datetime.datetime.now(pytz.timezone('Asia/Shanghai'))
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def convert_timeobj_to_str(timeobj: str=None, timezone_offset: int=8, time_format: Literal['%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%SZ']='%Y-%m-%d %H:%M:%S'):
|
|
49
|
-
time_obj_with_offset = timeobj + datetime.timedelta(hours=timezone_offset)
|
|
50
|
-
if time_format == '%Y-%m-%d %H:%M:%S':
|
|
51
|
-
return time_obj_with_offset.strftime("%Y-%m-%d %H:%M:%S")
|
|
52
|
-
elif time_format == '%Y-%m-%dT%H:%M:%SZ':
|
|
53
|
-
return time_obj_with_offset.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
from pytbox.feishu.client import Client as FeishuClient
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
feishu = FeishuClient(app_id=os.getenv("FEISHU_APP_ID"), app_secret=os.getenv("FEISHU_APP_SECRET"))
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_feishu_send_message_notify():
|
|
12
|
-
r = feishu.extensions.send_message_notify(title='test')
|
|
13
|
-
assert r.code == 0
|
|
14
|
-
|
|
15
|
-
if __name__ == "__main__":
|
|
16
|
-
pass
|
|
17
|
-
# print(r)
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pytbox.logger import AppLogger
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
log = AppLogger('test_logger', enable_victorialog=True, victorialog_url=os.getenv("VICTORIALOG_URL"))
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def test_logger_info():
|
|
11
|
-
log.info('test_logger_info')
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if __name__ == "__main__":
|
|
15
|
-
test_logger_info()
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
from pytbox.onepassword_connect import OnePasswordConnect
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
oc = OnePasswordConnect(vault_id="hcls5uxuq5dmxorw6rfewefdsa")
|
|
8
|
-
|
|
9
|
-
# r = oc.create_item()
|
|
10
|
-
# print(r)
|
|
11
|
-
|
|
12
|
-
# r = oc.search_item(tag='lululemon')
|
|
13
|
-
r = oc.update_item(item_id="xm2a67hysuw2emasp325i7dyki", name="demo_update3", username="demo03", password="password03", notes="notes03", tags=['test03'])
|
|
14
|
-
print(r)
|
|
15
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|