qrpa 1.0.5__py3-none-any.whl → 1.0.6__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.
- qrpa/__init__.py +9 -0
- qrpa/fun_base.py +41 -0
- qrpa/fun_file.py +173 -0
- qrpa/fun_win.py +31 -0
- qrpa/shein_ziniao.py +441 -0
- qrpa/time_utils.py +845 -0
- qrpa/time_utils_example.py +243 -0
- qrpa/wxwork.py +273 -273
- {qrpa-1.0.5.dist-info → qrpa-1.0.6.dist-info}/METADATA +3 -1
- qrpa-1.0.6.dist-info/RECORD +13 -0
- qrpa-1.0.5.dist-info/RECORD +0 -7
- {qrpa-1.0.5.dist-info → qrpa-1.0.6.dist-info}/WHEEL +0 -0
- {qrpa-1.0.5.dist-info → qrpa-1.0.6.dist-info}/top_level.txt +0 -0
qrpa/__init__.py
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
from .wxwork import WxWorkBot, WxWorkAppBot
|
|
2
2
|
from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_default_migrator
|
|
3
|
+
|
|
4
|
+
from .shein_ziniao import ZiniaoRunner
|
|
5
|
+
|
|
6
|
+
from .fun_base import log
|
|
7
|
+
|
|
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
|
qrpa/fun_base.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
import traceback
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .wxwork import WxWorkBot
|
|
9
|
+
|
|
10
|
+
from typing import TypedDict
|
|
11
|
+
|
|
12
|
+
# 定义一个 TypedDict 来提供配置结构的类型提示
|
|
13
|
+
|
|
14
|
+
class ZiNiao(TypedDict):
|
|
15
|
+
company: str
|
|
16
|
+
username: str
|
|
17
|
+
password: str
|
|
18
|
+
|
|
19
|
+
class Config(TypedDict):
|
|
20
|
+
wxwork_bot_exception: str
|
|
21
|
+
ziniao: ZiNiao
|
|
22
|
+
auto_dir: str
|
|
23
|
+
shein_store_alias: str
|
|
24
|
+
|
|
25
|
+
def log(*args, **kwargs):
|
|
26
|
+
"""封装 print 函数,使其行为与原 print 一致,并写入日志文件"""
|
|
27
|
+
stack = inspect.stack()
|
|
28
|
+
fi = stack[1] if len(stack) > 1 else None
|
|
29
|
+
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))
|
|
30
|
+
|
|
31
|
+
print(log_message, **kwargs)
|
|
32
|
+
|
|
33
|
+
def hostname():
|
|
34
|
+
return socket.gethostname()
|
|
35
|
+
|
|
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'
|
|
38
|
+
error_msg += f'{traceback.format_exc()}'
|
|
39
|
+
WxWorkBot(config['wxwork_bot_exception']).send_text(error_msg)
|
|
40
|
+
print(error_msg)
|
|
41
|
+
return error_msg
|
qrpa/fun_file.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
file_lock = threading.Lock() # 线程锁
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime, timedelta, timezone
|
|
8
|
+
|
|
9
|
+
from .fun_base import log
|
|
10
|
+
|
|
11
|
+
def read_dict_from_file(file_path, cache_interval=3600 * 24 * 365 * 10):
|
|
12
|
+
"""
|
|
13
|
+
从文件中读取字典。
|
|
14
|
+
如果文件的修改时间未超过一个小时,则返回字典;否则返回 None。
|
|
15
|
+
|
|
16
|
+
:param file_path: 文件路径
|
|
17
|
+
:return: 字典或 None
|
|
18
|
+
"""
|
|
19
|
+
with file_lock: # 使用锁保护文件操作
|
|
20
|
+
# 检查文件是否存在
|
|
21
|
+
if not os.path.exists(file_path):
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
# 获取文件的最后修改时间
|
|
25
|
+
modification_time = os.path.getmtime(file_path)
|
|
26
|
+
modification_time = datetime.fromtimestamp(modification_time)
|
|
27
|
+
|
|
28
|
+
# 获取当前时间
|
|
29
|
+
current_time = datetime.now()
|
|
30
|
+
|
|
31
|
+
interval = current_time - modification_time
|
|
32
|
+
log(f'缓存文件 {file_path} 缓存时长 {timedelta(seconds=int(cache_interval))} 已过时长 {interval}')
|
|
33
|
+
|
|
34
|
+
# 判断文件的修改时间是否超过一个小时
|
|
35
|
+
if interval <= timedelta(seconds=int(cache_interval)):
|
|
36
|
+
# 如果未超过一个小时,则读取文件内容
|
|
37
|
+
with open(file_path, "r", encoding='utf-8') as file:
|
|
38
|
+
return json.load(file)
|
|
39
|
+
else:
|
|
40
|
+
# 如果超过一个小时,则返回 None
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
def write_dict_to_file(file_path, data):
|
|
44
|
+
"""
|
|
45
|
+
将字典写入文件。
|
|
46
|
+
|
|
47
|
+
:param file_path: 文件路径
|
|
48
|
+
:param data: 要写入的字典
|
|
49
|
+
"""
|
|
50
|
+
with file_lock: # 使用锁保护文件操作
|
|
51
|
+
# 确保目标文件夹存在
|
|
52
|
+
dir_name = os.path.dirname(file_path)
|
|
53
|
+
if dir_name and not os.path.exists(dir_name):
|
|
54
|
+
os.makedirs(dir_name, exist_ok=True) # 递归创建目录
|
|
55
|
+
|
|
56
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
57
|
+
# 使用 json.dump() 并设置 ensure_ascii=False
|
|
58
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
59
|
+
|
|
60
|
+
def read_dict_from_file_ex(file_path, key, cache_interval=3600 * 24 * 365 * 10, default='dict'):
|
|
61
|
+
"""
|
|
62
|
+
从 JSON 文件中读取指定键的值。
|
|
63
|
+
|
|
64
|
+
:param file_path: JSON 文件路径
|
|
65
|
+
:param key: 要读取的键
|
|
66
|
+
:param default: 如果文件不存在、解析失败或键不存在时返回的默认值
|
|
67
|
+
:return: 对应键的值,或 default
|
|
68
|
+
"""
|
|
69
|
+
with file_lock: # 使用锁保护文件操作
|
|
70
|
+
if not os.path.exists(file_path):
|
|
71
|
+
return {} if default == 'dict' else []
|
|
72
|
+
|
|
73
|
+
# 获取文件的最后修改时间
|
|
74
|
+
modification_time = os.path.getmtime(file_path)
|
|
75
|
+
modification_time = datetime.fromtimestamp(modification_time)
|
|
76
|
+
|
|
77
|
+
# 获取当前时间
|
|
78
|
+
current_time = datetime.now()
|
|
79
|
+
|
|
80
|
+
interval = current_time - modification_time
|
|
81
|
+
log(f'缓存文件 {file_path} 缓存时长 {timedelta(seconds=cache_interval)} 已过时长 {interval}')
|
|
82
|
+
|
|
83
|
+
# 判断文件的修改时间是否超过一个小时
|
|
84
|
+
if interval <= timedelta(seconds=cache_interval):
|
|
85
|
+
# 如果未超过一个小时,则读取文件内容
|
|
86
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
87
|
+
data = json.load(f)
|
|
88
|
+
return data.get(key, {})
|
|
89
|
+
else:
|
|
90
|
+
# 如果超过一个小时,则返回 None
|
|
91
|
+
return {} if default == 'dict' else []
|
|
92
|
+
|
|
93
|
+
def write_dict_to_file_ex(file_path, data, update_keys=None):
|
|
94
|
+
"""
|
|
95
|
+
将字典写入文件,可选择性地只更新指定键。
|
|
96
|
+
|
|
97
|
+
:param file_path: 文件路径
|
|
98
|
+
:param data: 要写入的字典数据
|
|
99
|
+
:param update_keys: 可选,需要更新的键列表。如果为None,则替换整个文件内容
|
|
100
|
+
"""
|
|
101
|
+
with file_lock: # 使用锁保护文件操作
|
|
102
|
+
# 确保目标文件夹存在
|
|
103
|
+
dir_name = os.path.dirname(file_path)
|
|
104
|
+
if dir_name and not os.path.exists(dir_name):
|
|
105
|
+
os.makedirs(dir_name, exist_ok=True) # 递归创建目录
|
|
106
|
+
|
|
107
|
+
# 如果指定了update_keys,先读取现有数据然后合并
|
|
108
|
+
if update_keys is not None:
|
|
109
|
+
try:
|
|
110
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
111
|
+
existing_data = json.load(f)
|
|
112
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
113
|
+
existing_data = {}
|
|
114
|
+
|
|
115
|
+
# 只更新指定的键
|
|
116
|
+
for key in update_keys:
|
|
117
|
+
if key in data:
|
|
118
|
+
existing_data[key] = data[key]
|
|
119
|
+
data = existing_data
|
|
120
|
+
|
|
121
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
122
|
+
json.dump(data, f, ensure_ascii=False, indent=4)
|
|
123
|
+
|
|
124
|
+
######################################################################################################
|
|
125
|
+
def getTaskStoreKey(key_id, store_name):
|
|
126
|
+
return f'{key_id}_{store_name}'
|
|
127
|
+
|
|
128
|
+
def generate_progress_file(config, key_id):
|
|
129
|
+
return f'{config.auto_dir}/progress/progress_{key_id}.json'
|
|
130
|
+
|
|
131
|
+
def get_progress_index_ex(config, task_key, store_name):
|
|
132
|
+
task_store_key = getTaskStoreKey(task_key, store_name)
|
|
133
|
+
progress_file = generate_progress_file(config, task_key)
|
|
134
|
+
dict = read_dict_from_file(progress_file)
|
|
135
|
+
if len(dict) > 0:
|
|
136
|
+
count = 0
|
|
137
|
+
for key, value in dict.items():
|
|
138
|
+
if key == task_store_key:
|
|
139
|
+
return count
|
|
140
|
+
count += 1
|
|
141
|
+
return len(dict)
|
|
142
|
+
|
|
143
|
+
def get_progress_json_ex(config, task_key, store_name):
|
|
144
|
+
task_store_key = getTaskStoreKey(task_key, store_name)
|
|
145
|
+
progress_file = generate_progress_file(config, task_key)
|
|
146
|
+
dict = read_dict_from_file_ex(progress_file, task_store_key)
|
|
147
|
+
if len(dict) > 0:
|
|
148
|
+
return dict[0] == 1
|
|
149
|
+
else:
|
|
150
|
+
length = get_progress_index_ex(config, task_key, store_name)
|
|
151
|
+
write_dict_to_file_ex(progress_file, {task_store_key: [0, length + 1, datetime.now().strftime('%Y-%m-%d %H:%M:%S')]}, [task_store_key])
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
def done_progress_json_ex(config, task_key, store_name):
|
|
155
|
+
task_store_key = getTaskStoreKey(task_key, store_name)
|
|
156
|
+
progress_file = generate_progress_file(config, task_key)
|
|
157
|
+
length = get_progress_index_ex(config, task_key, store_name)
|
|
158
|
+
write_dict_to_file_ex(progress_file, {task_store_key: [1, length + 1, datetime.now().strftime('%Y-%m-%d %H:%M:%S')]}, [task_store_key])
|
|
159
|
+
|
|
160
|
+
def check_progress_json_ex(config, task_key, just_store_username=None):
|
|
161
|
+
progress_file = generate_progress_file(config, task_key)
|
|
162
|
+
dict = read_dict_from_file(progress_file)
|
|
163
|
+
if len(dict) > 0:
|
|
164
|
+
for task_store_key, data_list in dict.items():
|
|
165
|
+
if just_store_username and len(just_store_username) > 0:
|
|
166
|
+
if all([store_username not in task_store_key for store_username in just_store_username]):
|
|
167
|
+
continue
|
|
168
|
+
if 'run_' not in task_store_key and int(data_list[0]) == 0:
|
|
169
|
+
log(task_store_key, just_store_username)
|
|
170
|
+
return False
|
|
171
|
+
else:
|
|
172
|
+
log(f"进度文件不存在或为空: {progress_file}")
|
|
173
|
+
return True
|
qrpa/fun_win.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import win32com.client
|
|
3
|
+
|
|
4
|
+
from .fun_base import log
|
|
5
|
+
|
|
6
|
+
def find_software_install_path(app_keyword: str):
|
|
7
|
+
"""从开始菜单或桌面查找指定软件的安装路径"""
|
|
8
|
+
possible_dirs = [
|
|
9
|
+
os.environ.get('PROGRAMDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
|
|
10
|
+
os.environ.get('APPDATA', '') + r'\Microsoft\Windows\Start Menu\Programs',
|
|
11
|
+
os.environ.get('USERPROFILE', '') + r'\Desktop',
|
|
12
|
+
os.environ.get('PUBLIC', '') + r'\Desktop'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
shell = win32com.client.Dispatch("WScript.Shell")
|
|
16
|
+
|
|
17
|
+
for base_dir in possible_dirs:
|
|
18
|
+
for root, _, files in os.walk(base_dir):
|
|
19
|
+
for file in files:
|
|
20
|
+
if file.lower().endswith('.lnk') and app_keyword.lower() in file.lower():
|
|
21
|
+
lnk_path = os.path.join(root, file)
|
|
22
|
+
try:
|
|
23
|
+
shortcut = shell.CreateShortcut(lnk_path)
|
|
24
|
+
target_path = shortcut.Targetpath
|
|
25
|
+
if os.path.exists(target_path):
|
|
26
|
+
return target_path
|
|
27
|
+
except Exception as e:
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
log(f'未能查找到{str}安装位置')
|
|
31
|
+
return None
|