qrpa 1.0.9__tar.gz → 1.0.10__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.9
3
+ Version: 1.0.10
4
4
  Summary: qsir's rpa library
5
5
  Author: QSir
6
6
  Author-email: QSir <1171725650@qq.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qrpa"
7
- version = "1.0.9"
7
+ version = "1.0.10"
8
8
  description = "qsir's rpa library"
9
9
  authors = [{ name = "QSir", email = "1171725650@qq.com" }]
10
10
  readme = "README.md"
@@ -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)
@@ -3,9 +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
8
  from .time_utils import TimeUtils
9
9
 
10
10
  from .fun_file import read_dict_from_file, read_dict_from_file_ex, write_dict_to_file, write_dict_to_file_ex
11
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
@@ -0,0 +1,108 @@
1
+ import inspect
2
+ import os
3
+ import traceback
4
+ import socket
5
+ import hashlib
6
+ import shutil
7
+
8
+ from datetime import datetime
9
+
10
+ from .wxwork import WxWorkBot
11
+
12
+ from .RateLimitedSender import RateLimitedSender
13
+
14
+ from typing import TypedDict
15
+
16
+ # 定义一个 TypedDict 来提供配置结构的类型提示
17
+
18
+ class ZiNiao(TypedDict):
19
+ company: str
20
+ username: str
21
+ password: str
22
+
23
+ class Config(TypedDict):
24
+ wxwork_bot_exception: str
25
+ ziniao: ZiNiao
26
+ auto_dir: str
27
+
28
+ def log(*args, **kwargs):
29
+ """封装 print 函数,使其行为与原 print 一致,并写入日志文件"""
30
+ stack = inspect.stack()
31
+ fi = stack[1] if len(stack) > 1 else None
32
+ log_message = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}][{os.path.basename(fi.filename) if fi else 'unknown'}:{fi.lineno if fi else 0}:{fi.function if fi else 'unknown'}] " + " ".join(map(str, args))
33
+
34
+ print(log_message, **kwargs)
35
+
36
+ def hostname():
37
+ return socket.gethostname()
38
+
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'
58
+ error_msg += f'{traceback.format_exc()}'
59
+ print(error_msg)
60
+
61
+ # 异步发送
62
+ send_exception._wx_sender.send(error_msg)
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}")