qrpa 1.0.9__tar.gz → 1.0.11__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 qrpa might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.9
3
+ Version: 1.0.11
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.11"
8
8
  description = "qsir's rpa library"
9
9
  authors = [{ name = "QSir", email = "1171725650@qq.com" }]
10
10
  readme = "README.md"
@@ -0,0 +1,45 @@
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):
10
+ """
11
+ :param sender_func: 实际的发送函数,参数是字符串消息
12
+ :param interval: 最短发送间隔(秒)
13
+ """
14
+ self.sender_func = sender_func
15
+ self.interval = interval
16
+ self.queue = deque()
17
+ self.lock = threading.Lock()
18
+ self.last_send_time = 0
19
+
20
+ self.thread = threading.Thread(target=self._worker, daemon=True)
21
+ self.thread.start()
22
+
23
+ def send(self, message):
24
+ """添加消息到队列(非阻塞)"""
25
+ with self.lock:
26
+ self.queue.append(message)
27
+
28
+ def _flush(self):
29
+ """立即发送队列消息(内部调用)"""
30
+ if not self.queue:
31
+ return
32
+ batch_message = "\n---\n".join(self.queue)
33
+ self.queue.clear()
34
+ try:
35
+ self.sender_func(batch_message)
36
+ except Exception as e:
37
+ print(f"[RateLimitedSender] 发送失败: {e}")
38
+ self.last_send_time = time.time()
39
+
40
+ def _worker(self):
41
+ while True:
42
+ with self.lock:
43
+ if self.queue and (time.time() - self.last_send_time >= self.interval):
44
+ self._flush()
45
+ time.sleep(1)
@@ -3,9 +3,12 @@ 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
14
+ from .fun_win import *
@@ -0,0 +1,107 @@
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, # 10 秒发一次
53
+ )
54
+
55
+ # 构造异常消息
56
+ error_msg = f'【{hostname()}】{datetime.now():%Y-%m-%d %H:%M:%S}\n{msg}\n'
57
+ error_msg += f'{traceback.format_exc()}'
58
+ print(error_msg)
59
+
60
+ # 异步发送
61
+ send_exception._wx_sender.send(error_msg)
62
+ return error_msg
63
+
64
+ def get_safe_value(data, key, default=0):
65
+ value = data.get(key)
66
+ return default if value is None else value
67
+
68
+ def md5_string(s):
69
+ # 需要先将字符串编码为 bytes
70
+ return hashlib.md5(s.encode('utf-8')).hexdigest()
71
+
72
+ # 将windows文件名不支持的字符替换成下划线
73
+ def sanitize_filename(filename):
74
+ # Windows 文件名非法字符
75
+ illegal_chars = r'\/:*?"<>|'
76
+ for char in illegal_chars:
77
+ filename = filename.replace(char, '_')
78
+
79
+ # 去除首尾空格和点
80
+ filename = filename.strip(' .')
81
+
82
+ # 替换连续多个下划线为单个
83
+ filename = '_'.join(filter(None, filename.split('_')))
84
+
85
+ return filename
86
+
87
+ def add_https(url):
88
+ if url and url.startswith('//'):
89
+ return 'https:' + url
90
+ return url
91
+
92
+ def create_file_path(file_path):
93
+ dir_name = os.path.dirname(file_path)
94
+ if dir_name and not os.path.exists(dir_name):
95
+ os.makedirs(dir_name, exist_ok=True) # 递归创建目录
96
+ return file_path
97
+
98
+ def copy_file(source, destination):
99
+ try:
100
+ shutil.copy2(source, destination)
101
+ print(f"文件已复制到 {destination}")
102
+ except FileNotFoundError:
103
+ print(f"错误:源文件 '{source}' 不存在")
104
+ except PermissionError:
105
+ print(f"错误:没有权限复制到 '{destination}'")
106
+ except Exception as e:
107
+ print(f"错误:发生未知错误 - {e}")