qrpa 1.0.5__tar.gz → 1.0.8__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,7 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.5
3
+ Version: 1.0.8
4
4
  Summary: qsir's rpa library
5
+ Author: QSir
5
6
  Author-email: QSir <1171725650@qq.com>
6
7
  License-Expression: GPL-3.0-or-later
7
8
  Description-Content-Type: text/markdown
9
+ Dynamic: author
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["setuptools>=61.0"]
2
+ requires = ["setuptools>=61.0","wheel"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qrpa"
7
- version = "1.0.5"
7
+ version = "1.0.8"
8
8
  description = "qsir's rpa library"
9
9
  authors = [{ name = "QSir", email = "1171725650@qq.com" }]
10
10
  readme = "README.md"
@@ -1,2 +1,8 @@
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
@@ -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
@@ -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
@@ -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