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.
- {qrpa-1.0.9 → qrpa-1.0.10}/PKG-INFO +1 -1
- {qrpa-1.0.9 → qrpa-1.0.10}/pyproject.toml +1 -1
- qrpa-1.0.10/qrpa/RateLimitedSender.py +50 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/__init__.py +3 -1
- qrpa-1.0.10/qrpa/fun_base.py +108 -0
- qrpa-1.0.10/qrpa/fun_excel.py +2758 -0
- qrpa-1.0.10/qrpa/fun_web.py +148 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/shein_ziniao.py +51 -42
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/time_utils.py +55 -55
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/wxwork.py +11 -13
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa.egg-info/PKG-INFO +1 -1
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa.egg-info/SOURCES.txt +3 -0
- qrpa-1.0.9/qrpa/fun_base.py +0 -41
- {qrpa-1.0.9 → qrpa-1.0.10}/README.md +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/db_migrator.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/fun_file.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/fun_win.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa/time_utils_example.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa.egg-info/dependency_links.txt +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/qrpa.egg-info/top_level.txt +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/setup.cfg +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/setup.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/tests/test_db_migrator.py +0 -0
- {qrpa-1.0.9 → qrpa-1.0.10}/tests/test_wxwork.py +0 -0
|
@@ -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}")
|