pytbox 0.0.3__py3-none-any.whl → 0.0.5__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.
@@ -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=MongoClient(collection='alert'),
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":
pytbox/base.py CHANGED
@@ -1,15 +1,19 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
-
3
+ import os
4
4
  from pytbox.database.mongo import Mongo
5
5
  from pytbox.utils.load_config import load_config_by_file
6
6
  from pytbox.database.victoriametrics import VictoriaMetrics
7
7
  from pytbox.feishu.client import Client as FeishuClient
8
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'))
9
14
 
10
15
 
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)
16
+ def get_mongo(collection):
13
17
  return Mongo(
14
18
  host=config['mongo']['host'],
15
19
  port=config['mongo']['port'],
@@ -20,22 +24,25 @@ def MongoClient(collection, config_path='/workspaces/pytbox/tests/alert/config_d
20
24
  collection=collection
21
25
  )
22
26
 
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
- )
27
+ vm = VictoriaMetrics(url=config['victoriametrics']['url'])
28
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
- )
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)
35
39
 
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']
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')
41
48
  )
pytbox/database/mongo.py CHANGED
@@ -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
+
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ from typing import Literal, Optional
3
4
  import requests
4
5
  from ..utils.response import ReturnResponse
5
6
 
@@ -38,7 +39,16 @@ class VictoriaMetrics:
38
39
  return ReturnResponse(code=2, msg=f"[{query}] 没有查询到结果", data=r.json())
39
40
  else:
40
41
  return ReturnResponse(code=1, msg=f"[{query}] 查询失败: {r.json().get('error')}", data=r.json())
41
-
42
+
43
+ def get_labels(self, metric_name: str) -> ReturnResponse:
44
+ url = f"{self.url}/api/v1/series?match[]={metric_name}"
45
+ response = requests.get(url, timeout=self.timeout)
46
+ results = response.json()
47
+ if results['status'] == 'success':
48
+ return ReturnResponse(code=0, msg=f"metric name: {metric_name} 获取到 {len(results['data'])} 条数据", data=results['data'])
49
+ else:
50
+ return ReturnResponse(code=1, msg=f"metric name: {metric_name} 查询失败")
51
+
42
52
  def check_ping_result(self, target: str, last_minute: int=10) -> ReturnResponse:
43
53
  '''
44
54
  检查ping结果
@@ -60,7 +70,7 @@ class VictoriaMetrics:
60
70
  msg = f"已检查 {target} 最近 {last_minute} 分钟是正常的!"
61
71
  else:
62
72
  if all(str(item[1]) == "1" for item in values):
63
- code = 2
73
+ code = 1
64
74
  msg = f"已检查 {target} 最近 {last_minute} 分钟是异常的!"
65
75
  else:
66
76
  code = 0
@@ -74,4 +84,30 @@ class VictoriaMetrics:
74
84
  except KeyError:
75
85
  data = r.data
76
86
 
77
- return ReturnResponse(code=code, msg=msg, data=data)
87
+ return ReturnResponse(code=code, msg=msg, data=data)
88
+
89
+ def check_interface_rate(self,
90
+ direction: Literal['in', 'out'],
91
+ sysName: str,
92
+ ifName:str,
93
+ last_minutes: Optional[int] = None
94
+ ) -> ReturnResponse:
95
+ """查询指定设备的入方向总流量速率(bps)。
96
+
97
+ 使用 PromQL 对 `snmp_interface_ifHCInOctets` 进行速率计算并聚合到设备级别,
98
+ 将结果从字节每秒转换为比特每秒(乘以 8)。
99
+
100
+ Args:
101
+ sysName: 设备 `sysName` 标签值。
102
+ last_minutes: 计算速率的时间窗口(分钟)。未提供时默认使用 5 分钟窗口。
103
+
104
+ Returns:
105
+ ReturnResponse: 查询结果包装。
106
+ """
107
+ if direction == 'in':
108
+ query = f'(rate(snmp_interface_ifHCInOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
109
+ else:
110
+ query = f'(rate(snmp_interface_ifHCOutOctets{{sysName="{sysName}", ifName="{ifName}"}}[{last_minutes}m])) * 8 / 1000000'
111
+ r = self.query(query)
112
+ rate = r.data[0]['value'][1]
113
+ return int(float(rate))
pytbox/log/logger.py CHANGED
@@ -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
- from src.library.monitor.insert_program import insert_error_message
94
- # 传递清理后的消息给监控系统
95
- message.replace('#', '')
96
- insert_error_message(message, self.app_name, caller_filename, caller_lineno, caller_function)
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
- log = get_logger(app_name='test')
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
pytbox/log/victorialog.py CHANGED
@@ -3,8 +3,8 @@
3
3
  from typing import Literal
4
4
  import requests
5
5
  import time
6
- from .utils.response import ReturnResponse
7
- from .utils.timeutils import TimeUtils
6
+ from ..utils.response import ReturnResponse
7
+ from ..utils.timeutils import TimeUtils
8
8
 
9
9
 
10
10
 
pytbox/utils/timeutils.py CHANGED
@@ -1,30 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
-
4
- import time
3
+ import re
5
4
  import pytz
5
+ import time
6
6
  import datetime
7
+ from zoneinfo import ZoneInfo
7
8
  from typing import Literal
8
9
 
9
10
 
10
11
  class TimeUtils:
11
12
 
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
13
  @staticmethod
29
14
  def get_time_object(now: bool=True):
30
15
  '''
@@ -50,4 +35,513 @@ class TimeUtils:
50
35
  if time_format == '%Y-%m-%d %H:%M:%S':
51
36
  return time_obj_with_offset.strftime("%Y-%m-%d %H:%M:%S")
52
37
  elif time_format == '%Y-%m-%dT%H:%M:%SZ':
53
- return time_obj_with_offset.strftime("%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
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"
@@ -1,27 +1,26 @@
1
- pytbox/base.py,sha256=nk65USdvAhltFJGCMXPmr02F9sTM7gcvlYuzL4sUT1k,1621
1
+ pytbox/base.py,sha256=_SpfeIiJE4xbQMsYghnMehcNzHP-mBfrKONX43b0OQk,1490
2
2
  pytbox/dida365.py,sha256=pUMPB9AyLZpTTbaz2LbtzdEpyjvuGf4YlRrCvM5sbJo,10545
3
3
  pytbox/onepassword_connect.py,sha256=nD3xTl1ykQ4ct_dCRRF138gXCtk-phPfKYXuOn-P7Z8,3064
4
4
  pytbox/onepassword_sa.py,sha256=08iUcYud3aEHuQcUsem9bWNxdXKgaxFbMy9yvtr-DZQ,6995
5
- pytbox/alert/alert_handler.py,sha256=arQkm95q2aqZomwflh_WWFwDw7aZyui-N5-FrxgRTZ8,5012
5
+ pytbox/alert/alert_handler.py,sha256=FePPQS4LyGphSJ0QMv0_pLWaXxEqsRlcTKMfUjtsNfk,5048
6
6
  pytbox/alert/ping.py,sha256=g36X0U3U8ndZqfpVIcuoxJJ0X5gST3I_IwjTQC1roHA,779
7
7
  pytbox/alicloud/sls.py,sha256=UR4GdI86dCKAFI2xt_1DELu7q743dpd3xrYtuNpfC5A,4065
8
8
  pytbox/common/__init__.py,sha256=3JWfgCQZKZuSH5NCE7OCzKwq82pkyop9l7sH5YSNyfU,122
9
- pytbox/common/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- pytbox/database/mongo.py,sha256=QyCeSoyWoJdIFXxjDmbV5t-xU7wN3x-Ig0I8pvRmBwg,1864
11
- pytbox/database/victoriametrics.py,sha256=qs3IQZbcKGSIObQcwUujZEyPshq2yOIVvnc4yfTkXJ0,2642
9
+ pytbox/database/mongo.py,sha256=CSpHC7iR-M0BaVxXz5j6iXjMKPgXJX_G7MrjCj5Gm8Q,3478
10
+ pytbox/database/victoriametrics.py,sha256=PfeshtlgZNfbiq7Fo4ibJruWYeAKryBXu8ckl8YC1jM,4389
12
11
  pytbox/feishu/client.py,sha256=kwGLseGT_iQUFmSqpuS2_77WmxtHstD64nXvktuQ3B4,5865
13
12
  pytbox/feishu/endpoints.py,sha256=z_nHGXAjlIyYdFnbAWY4hfzzHrENJvoNOqY9sA6-FLk,40009
14
13
  pytbox/feishu/errors.py,sha256=79qFAHZw7jDj3gnWAjI1-W4tB0q1_aSfdjee4xzXeuI,1179
15
14
  pytbox/feishu/helpers.py,sha256=jhSkHiUw4822QBXx2Jw8AksogZdakZ-3QqvC3lB3qEI,201
16
15
  pytbox/feishu/typing.py,sha256=3hWkJgOi-v2bt9viMxkyvNHsPgrbAa0aZOxsZYg2vdM,122
17
- pytbox/log/logger.py,sha256=4IHmMg14Rwekmc7AeKGDbsDZryhcD6elaJZsvpdYJ80,5659
18
- pytbox/log/victorialog.py,sha256=50cVPxq9PzzzkzQHhgv_785K3GeF8B2mEnChdeekZWU,4137
16
+ pytbox/log/logger.py,sha256=N7KOM7Xl_FZI_RR8oggjV9sVph-Lrmspl-yBMJr2LLQ,7437
17
+ pytbox/log/victorialog.py,sha256=gffEiq38adv9sC5oZeMcyKghd3SGfRuqtZOFuqHQF6E,4139
19
18
  pytbox/utils/env.py,sha256=jO_-BKbGuDU7lIL9KYkcxGCzQwTXfxD4mIYtSAjREmI,622
20
19
  pytbox/utils/load_config.py,sha256=wNCDPLH7xet5b9pUlTz6VsBejRsJZ7LP85wWMaITBYg,3042
21
20
  pytbox/utils/ping_checker.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
22
21
  pytbox/utils/response.py,sha256=kXjlwt0WVmLRam2eu1shzX2cQ7ux4cCQryaPGYwle5g,1247
23
- pytbox/utils/timeutils.py,sha256=7ZI-TU2FCUV0Gyc_QUjhSreto8SVIs54e2CZgGix5pI,1628
24
- pytbox-0.0.3.dist-info/METADATA,sha256=WWQcYi7uThTfbjMuhEsV9vKgqInlBZWn1VJr-0llPDQ,6006
25
- pytbox-0.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
26
- pytbox-0.0.3.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
27
- pytbox-0.0.3.dist-info/RECORD,,
22
+ pytbox/utils/timeutils.py,sha256=XbK2KB-SVi7agNqoQN7i40wysrZvrGuwebViv1Cw-Ok,20226
23
+ pytbox-0.0.5.dist-info/METADATA,sha256=cNFwHW1aNu_zPUuXq7KOBRj-6cx6oSO-UbAPbJFQOdU,6037
24
+ pytbox-0.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
+ pytbox-0.0.5.dist-info/top_level.txt,sha256=YADgWue-Oe128ptN3J2hS3GB0Ncc5uZaSUM3e1rwswE,7
26
+ pytbox-0.0.5.dist-info/RECORD,,
pytbox/common/base.py DELETED
File without changes
File without changes