qrpa 1.0.8__py3-none-any.whl → 1.0.10__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 qrpa might be problematic. Click here for more details.

@@ -0,0 +1,50 @@
1
+ import os
2
+ import time
3
+ import threading
4
+ from collections import deque
5
+ from datetime import datetime
6
+ import traceback
7
+
8
+ class RateLimitedSender:
9
+ def __init__(self, sender_func, interval=60, max_batch_size=10):
10
+ """
11
+ :param sender_func: 实际的发送函数,参数是字符串消息
12
+ :param interval: 发送间隔(秒)
13
+ :param max_batch_size: 队列消息条数超过此值时立即发送
14
+ """
15
+ self.sender_func = sender_func
16
+ self.interval = interval
17
+ self.max_batch_size = max_batch_size
18
+ self.queue = deque()
19
+ self.lock = threading.Lock()
20
+ self.last_send_time = 0
21
+
22
+ self.thread = threading.Thread(target=self._worker, daemon=True)
23
+ self.thread.start()
24
+
25
+ def send(self, message):
26
+ """添加消息到队列(非阻塞)"""
27
+ with self.lock:
28
+ self.queue.append(message)
29
+ # 如果超过批量上限,立即发送
30
+ if len(self.queue) >= self.max_batch_size:
31
+ self._flush()
32
+
33
+ def _flush(self):
34
+ """立即发送队列消息(内部调用)"""
35
+ if not self.queue:
36
+ return
37
+ batch_message = "\n---\n".join(self.queue)
38
+ self.queue.clear()
39
+ try:
40
+ self.sender_func(batch_message)
41
+ except Exception as e:
42
+ print(f"[RateLimitedSender] 发送失败: {e}")
43
+ self.last_send_time = time.time()
44
+
45
+ def _worker(self):
46
+ while True:
47
+ with self.lock:
48
+ if self.queue and (time.time() - self.last_send_time >= self.interval):
49
+ self._flush()
50
+ time.sleep(1)
qrpa/__init__.py CHANGED
@@ -3,6 +3,11 @@ from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_
3
3
 
4
4
  from .shein_ziniao import ZiniaoRunner
5
5
 
6
- from .fun_base import log
6
+ from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename
7
7
 
8
- from .time_utils import TimeUtils
8
+ from .time_utils import TimeUtils
9
+
10
+ from .fun_file import read_dict_from_file, read_dict_from_file_ex, write_dict_to_file, write_dict_to_file_ex
11
+ from .fun_file import get_progress_json_ex, check_progress_json_ex, done_progress_json_ex
12
+
13
+ from .fun_web import fetch, fetch_via_iframe, find_all_iframe, full_screen_shot
qrpa/fun_base.py CHANGED
@@ -2,11 +2,15 @@ import inspect
2
2
  import os
3
3
  import traceback
4
4
  import socket
5
+ import hashlib
6
+ import shutil
5
7
 
6
8
  from datetime import datetime
7
9
 
8
10
  from .wxwork import WxWorkBot
9
11
 
12
+ from .RateLimitedSender import RateLimitedSender
13
+
10
14
  from typing import TypedDict
11
15
 
12
16
  # 定义一个 TypedDict 来提供配置结构的类型提示
@@ -20,7 +24,6 @@ class Config(TypedDict):
20
24
  wxwork_bot_exception: str
21
25
  ziniao: ZiNiao
22
26
  auto_dir: str
23
- shein_store_alias: str
24
27
 
25
28
  def log(*args, **kwargs):
26
29
  """封装 print 函数,使其行为与原 print 一致,并写入日志文件"""
@@ -33,9 +36,73 @@ def log(*args, **kwargs):
33
36
  def hostname():
34
37
  return socket.gethostname()
35
38
 
36
- def send_exception(config: Config, msg=None):
37
- error_msg = f'【{hostname()}】{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n{msg}\n'
39
+ # ================= WxWorkBot 限频异常发送 =================
40
+ def send_exception(msg=None):
41
+ """
42
+ 发送异常到 WxWorkBot,限制发送频率,支持异步批量
43
+ """
44
+ # 首次调用时初始化限频发送器
45
+ if not hasattr(send_exception, "_wx_sender"):
46
+ def wxwork_bot_send(message):
47
+ bot_id = os.getenv('wxwork_bot_exception', 'ee5a048a-1b9e-41e4-9382-aa0ee447898e')
48
+ WxWorkBot(bot_id).send_text(message)
49
+
50
+ send_exception._wx_sender = RateLimitedSender(
51
+ sender_func=wxwork_bot_send,
52
+ interval=30, # 60 秒发一次
53
+ max_batch_size=5 # 累积 5 条立即发
54
+ )
55
+
56
+ # 构造异常消息
57
+ error_msg = f'【{hostname()}】{datetime.now():%Y-%m-%d %H:%M:%S}\n{msg}\n'
38
58
  error_msg += f'{traceback.format_exc()}'
39
- WxWorkBot(config['wxwork_bot_exception']).send_text(error_msg)
40
59
  print(error_msg)
60
+
61
+ # 异步发送
62
+ send_exception._wx_sender.send(error_msg)
41
63
  return error_msg
64
+
65
+ def get_safe_value(data, key, default=0):
66
+ value = data.get(key)
67
+ return default if value is None else value
68
+
69
+ def md5_string(s):
70
+ # 需要先将字符串编码为 bytes
71
+ return hashlib.md5(s.encode('utf-8')).hexdigest()
72
+
73
+ # 将windows文件名不支持的字符替换成下划线
74
+ def sanitize_filename(filename):
75
+ # Windows 文件名非法字符
76
+ illegal_chars = r'\/:*?"<>|'
77
+ for char in illegal_chars:
78
+ filename = filename.replace(char, '_')
79
+
80
+ # 去除首尾空格和点
81
+ filename = filename.strip(' .')
82
+
83
+ # 替换连续多个下划线为单个
84
+ filename = '_'.join(filter(None, filename.split('_')))
85
+
86
+ return filename
87
+
88
+ def add_https(url):
89
+ if url and url.startswith('//'):
90
+ return 'https:' + url
91
+ return url
92
+
93
+ def create_file_path(file_path):
94
+ dir_name = os.path.dirname(file_path)
95
+ if dir_name and not os.path.exists(dir_name):
96
+ os.makedirs(dir_name, exist_ok=True) # 递归创建目录
97
+ return file_path
98
+
99
+ def copy_file(source, destination):
100
+ try:
101
+ shutil.copy2(source, destination)
102
+ print(f"文件已复制到 {destination}")
103
+ except FileNotFoundError:
104
+ print(f"错误:源文件 '{source}' 不存在")
105
+ except PermissionError:
106
+ print(f"错误:没有权限复制到 '{destination}'")
107
+ except Exception as e:
108
+ print(f"错误:发生未知错误 - {e}")