qrpa 1.0.13__py3-none-any.whl → 1.1.50__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.
- qrpa/RateLimitedSender.py +45 -45
- qrpa/__init__.py +31 -17
- qrpa/db_migrator.py +600 -600
- qrpa/feishu_bot_app.py +268 -0
- qrpa/feishu_client.py +410 -0
- qrpa/feishu_logic.py +1443 -0
- qrpa/fun_base.py +339 -107
- qrpa/fun_excel.py +907 -262
- qrpa/fun_file.py +319 -173
- qrpa/fun_web.py +258 -148
- qrpa/fun_win.py +198 -198
- qrpa/mysql_module/__init__.py +0 -0
- qrpa/mysql_module/new_product_analysis_model.py +556 -0
- qrpa/mysql_module/shein_ledger_model.py +468 -0
- qrpa/mysql_module/shein_product_model.py +495 -0
- qrpa/mysql_module/shein_return_order_model.py +569 -0
- qrpa/mysql_module/shein_store_model.py +595 -0
- qrpa/shein_daily_report_model.py +375 -0
- qrpa/shein_excel.py +3318 -12
- qrpa/shein_lib.py +3784 -100
- qrpa/shein_mysql.py +92 -0
- qrpa/shein_sqlite.py +154 -0
- qrpa/shein_ziniao.py +529 -450
- qrpa/temu_chrome.py +56 -0
- qrpa/temu_excel.py +139 -0
- qrpa/temu_lib.py +156 -0
- qrpa/time_utils.py +87 -50
- qrpa/time_utils_example.py +243 -243
- qrpa/wxwork.py +46 -0
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/METADATA +1 -1
- qrpa-1.1.50.dist-info/RECORD +33 -0
- qrpa-1.0.13.dist-info/RECORD +0 -18
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/WHEEL +0 -0
- {qrpa-1.0.13.dist-info → qrpa-1.1.50.dist-info}/top_level.txt +0 -0
qrpa/feishu_bot_app.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# pip install lark-oapi -U
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import lark_oapi as lark
|
|
8
|
+
from lark_oapi.api.im.v1 import *
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FeishuBot:
|
|
12
|
+
"""飞书机器人类,封装所有机器人相关功能"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config):
|
|
15
|
+
"""
|
|
16
|
+
初始化飞书机器人
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
config: 配置对象,包含应用ID、应用密钥和群组信息
|
|
20
|
+
"""
|
|
21
|
+
self.config = config
|
|
22
|
+
self._client = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def client(self):
|
|
26
|
+
"""获取飞书客户端,使用懒加载模式"""
|
|
27
|
+
if self._client is None:
|
|
28
|
+
self._client = lark.Client.builder() \
|
|
29
|
+
.app_id(self.config.feishu_bot.app_id) \
|
|
30
|
+
.app_secret(self.config.feishu_bot.app_secret) \
|
|
31
|
+
.log_level(lark.LogLevel.INFO) \
|
|
32
|
+
.build()
|
|
33
|
+
return self._client
|
|
34
|
+
|
|
35
|
+
def _get_chat_id(self, bot_name: str) -> Optional[str]:
|
|
36
|
+
"""
|
|
37
|
+
根据群组别名获取群组ID
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
bot_name: 群组别名
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
群组ID,如果别名不存在则返回None
|
|
44
|
+
"""
|
|
45
|
+
return self.config.dict_feishu_group.get(bot_name)
|
|
46
|
+
|
|
47
|
+
def _handle_response_error(self, response, operation_name: str):
|
|
48
|
+
"""
|
|
49
|
+
处理API响应错误
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
response: API响应对象
|
|
53
|
+
operation_name: 操作名称,用于错误日志
|
|
54
|
+
"""
|
|
55
|
+
if not response.success():
|
|
56
|
+
lark.logger.error(
|
|
57
|
+
f"{operation_name} failed, code: {response.code}, "
|
|
58
|
+
f"msg: {response.msg}, log_id: {response.get_log_id()}, "
|
|
59
|
+
f"resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
|
|
60
|
+
)
|
|
61
|
+
return True
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def send_text(self, content: str, bot_name: str = 'test') -> bool:
|
|
65
|
+
"""
|
|
66
|
+
发送文本消息
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
content: 文本内容
|
|
70
|
+
bot_name: 群组别名,默认为'test'
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
发送是否成功
|
|
74
|
+
"""
|
|
75
|
+
chat_id = self._get_chat_id(bot_name)
|
|
76
|
+
if not chat_id:
|
|
77
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
message_content = {"text": content}
|
|
81
|
+
|
|
82
|
+
# 构造请求对象
|
|
83
|
+
request: CreateMessageRequest = CreateMessageRequest.builder() \
|
|
84
|
+
.receive_id_type("chat_id") \
|
|
85
|
+
.request_body(CreateMessageRequestBody.builder()
|
|
86
|
+
.receive_id(chat_id)
|
|
87
|
+
.msg_type("text")
|
|
88
|
+
.content(json.dumps(message_content))
|
|
89
|
+
.uuid(str(uuid.uuid4()))
|
|
90
|
+
.build()) \
|
|
91
|
+
.build()
|
|
92
|
+
|
|
93
|
+
# 发起请求
|
|
94
|
+
response: CreateMessageResponse = self.client.im.v1.message.create(request)
|
|
95
|
+
|
|
96
|
+
# 处理失败返回
|
|
97
|
+
if self._handle_response_error(response, "send_text"):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# 处理业务结果
|
|
101
|
+
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
def send_image(self, file_path: str, bot_name: str = 'test') -> bool:
|
|
105
|
+
"""
|
|
106
|
+
发送图片消息
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: 图片文件路径
|
|
110
|
+
bot_name: 群组别名,默认为'test'
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
发送是否成功
|
|
114
|
+
"""
|
|
115
|
+
# 先上传图片获取image_key
|
|
116
|
+
image_key = self.upload_image(file_path)
|
|
117
|
+
if not image_key:
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
chat_id = self._get_chat_id(bot_name)
|
|
121
|
+
if not chat_id:
|
|
122
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
message_content = {"image_key": image_key}
|
|
126
|
+
|
|
127
|
+
# 构造请求对象
|
|
128
|
+
request: CreateMessageRequest = CreateMessageRequest.builder() \
|
|
129
|
+
.receive_id_type("chat_id") \
|
|
130
|
+
.request_body(CreateMessageRequestBody.builder()
|
|
131
|
+
.receive_id(chat_id)
|
|
132
|
+
.msg_type("image")
|
|
133
|
+
.content(json.dumps(message_content))
|
|
134
|
+
.uuid(str(uuid.uuid4()))
|
|
135
|
+
.build()) \
|
|
136
|
+
.build()
|
|
137
|
+
|
|
138
|
+
# 发起请求
|
|
139
|
+
response: CreateMessageResponse = self.client.im.v1.message.create(request)
|
|
140
|
+
|
|
141
|
+
# 处理失败返回
|
|
142
|
+
if self._handle_response_error(response, "send_image"):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
# 处理业务结果
|
|
146
|
+
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
def send_excel(self, file_path: str, bot_name: str = 'test') -> bool:
|
|
150
|
+
"""
|
|
151
|
+
发送Excel文件
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
file_path: Excel文件路径
|
|
155
|
+
bot_name: 群组别名,默认为'test'
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
发送是否成功
|
|
159
|
+
"""
|
|
160
|
+
# 先上传文件获取file_key
|
|
161
|
+
file_key = self.upload_excel(file_path)
|
|
162
|
+
if not file_key:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
chat_id = self._get_chat_id(bot_name)
|
|
166
|
+
if not chat_id:
|
|
167
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
message_content = {"file_key": file_key}
|
|
171
|
+
|
|
172
|
+
# 构造请求对象
|
|
173
|
+
request: CreateMessageRequest = CreateMessageRequest.builder() \
|
|
174
|
+
.receive_id_type("chat_id") \
|
|
175
|
+
.request_body(CreateMessageRequestBody.builder()
|
|
176
|
+
.receive_id(chat_id)
|
|
177
|
+
.msg_type("file")
|
|
178
|
+
.content(json.dumps(message_content))
|
|
179
|
+
.uuid(str(uuid.uuid4()))
|
|
180
|
+
.build()) \
|
|
181
|
+
.build()
|
|
182
|
+
|
|
183
|
+
# 发起请求
|
|
184
|
+
response: CreateMessageResponse = self.client.im.v1.message.create(request)
|
|
185
|
+
|
|
186
|
+
# 处理失败返回
|
|
187
|
+
if self._handle_response_error(response, "send_excel"):
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
# 处理业务结果
|
|
191
|
+
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
def upload_excel(self, file_path: str) -> Optional[str]:
|
|
195
|
+
"""
|
|
196
|
+
上传Excel文件
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
file_path: 文件路径
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
文件key,上传失败返回None
|
|
203
|
+
"""
|
|
204
|
+
if not os.path.exists(file_path):
|
|
205
|
+
lark.logger.error(f"文件不存在: {file_path}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
with open(file_path, "rb") as file:
|
|
210
|
+
file_name = os.path.basename(file_path)
|
|
211
|
+
request: CreateFileRequest = CreateFileRequest.builder() \
|
|
212
|
+
.request_body(CreateFileRequestBody.builder()
|
|
213
|
+
.file_type("xls")
|
|
214
|
+
.file_name(file_name)
|
|
215
|
+
.file(file)
|
|
216
|
+
.build()) \
|
|
217
|
+
.build()
|
|
218
|
+
|
|
219
|
+
# 发起请求
|
|
220
|
+
response: CreateFileResponse = self.client.im.v1.file.create(request)
|
|
221
|
+
|
|
222
|
+
# 处理失败返回
|
|
223
|
+
if self._handle_response_error(response, "upload_excel"):
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
# 处理业务结果
|
|
227
|
+
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
228
|
+
return response.data.file_key
|
|
229
|
+
except Exception as e:
|
|
230
|
+
lark.logger.error(f"上传Excel文件时发生错误: {e}")
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
def upload_image(self, file_path: str) -> Optional[str]:
|
|
234
|
+
"""
|
|
235
|
+
上传图片文件
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
file_path: 图片文件路径
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
图片key,上传失败返回None
|
|
242
|
+
"""
|
|
243
|
+
if not os.path.exists(file_path):
|
|
244
|
+
lark.logger.error(f"文件不存在: {file_path}")
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
with open(file_path, "rb") as file:
|
|
249
|
+
request: CreateImageRequest = CreateImageRequest.builder() \
|
|
250
|
+
.request_body(CreateImageRequestBody.builder()
|
|
251
|
+
.image_type("message")
|
|
252
|
+
.image(file)
|
|
253
|
+
.build()) \
|
|
254
|
+
.build()
|
|
255
|
+
|
|
256
|
+
# 发起请求
|
|
257
|
+
response: CreateImageResponse = self.client.im.v1.image.create(request)
|
|
258
|
+
|
|
259
|
+
# 处理失败返回
|
|
260
|
+
if self._handle_response_error(response, "upload_image"):
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
# 处理业务结果
|
|
264
|
+
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
265
|
+
return response.data.image_key
|
|
266
|
+
except Exception as e:
|
|
267
|
+
lark.logger.error(f"上传图片文件时发生错误: {e}")
|
|
268
|
+
return None
|
qrpa/feishu_client.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import requests
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import base64, os
|
|
6
|
+
import threading
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
from typing import Optional, Dict, List, Any
|
|
9
|
+
from collections import deque
|
|
10
|
+
|
|
11
|
+
from .fun_base import log
|
|
12
|
+
|
|
13
|
+
class FeishuClient:
|
|
14
|
+
def __init__(self, app_id: str, app_secret: str, max_requests_per_second: int = 90, skip_token_refresh: bool = False):
|
|
15
|
+
self.app_id = app_id
|
|
16
|
+
self.app_secret = app_secret
|
|
17
|
+
self.base_url = "https://open.feishu.cn/open-apis"
|
|
18
|
+
self.tenant_access_token = None
|
|
19
|
+
self.token_expire_time = 0
|
|
20
|
+
|
|
21
|
+
# 速率限制相关参数
|
|
22
|
+
self.max_requests_per_second = max_requests_per_second
|
|
23
|
+
self.request_times = deque() # 存储请求时间戳
|
|
24
|
+
self.rate_limit_lock = threading.Lock() # 保证线程安全
|
|
25
|
+
|
|
26
|
+
# 只有在不跳过token刷新时才获取token
|
|
27
|
+
if not skip_token_refresh:
|
|
28
|
+
self._refresh_token()
|
|
29
|
+
|
|
30
|
+
def _refresh_token(self):
|
|
31
|
+
url = f"{self.base_url}/auth/v3/tenant_access_token/internal"
|
|
32
|
+
payload = {
|
|
33
|
+
"app_id" : self.app_id,
|
|
34
|
+
"app_secret": self.app_secret
|
|
35
|
+
}
|
|
36
|
+
headers = {
|
|
37
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
response = requests.post(url, headers=headers, json=payload)
|
|
41
|
+
response.raise_for_status()
|
|
42
|
+
response_data = response.json()
|
|
43
|
+
|
|
44
|
+
if response_data.get("code") == 0:
|
|
45
|
+
self.tenant_access_token = response_data.get("tenant_access_token")
|
|
46
|
+
expire_seconds = response_data.get("expire", 7200)
|
|
47
|
+
self.token_expire_time = time.time() + expire_seconds - 300
|
|
48
|
+
else:
|
|
49
|
+
raise Exception(f"获取token失败: {response_data.get('msg', '未知错误')}")
|
|
50
|
+
|
|
51
|
+
def _ensure_valid_token(self):
|
|
52
|
+
if not self.tenant_access_token or time.time() > self.token_expire_time:
|
|
53
|
+
self._refresh_token()
|
|
54
|
+
|
|
55
|
+
def _wait_for_rate_limit(self):
|
|
56
|
+
"""
|
|
57
|
+
实现请求频率限制,确保不超过90次/秒
|
|
58
|
+
使用滑动窗口算法控制请求频率
|
|
59
|
+
"""
|
|
60
|
+
with self.rate_limit_lock:
|
|
61
|
+
current_time = time.time()
|
|
62
|
+
|
|
63
|
+
# 清理1秒前的请求记录
|
|
64
|
+
while self.request_times and current_time - self.request_times[0] > 1.0:
|
|
65
|
+
self.request_times.popleft()
|
|
66
|
+
|
|
67
|
+
# 如果当前秒内请求数已达到限制,则等待
|
|
68
|
+
if len(self.request_times) >= self.max_requests_per_second:
|
|
69
|
+
# 计算需要等待的时间
|
|
70
|
+
oldest_request_time = self.request_times[0]
|
|
71
|
+
wait_time = 1.0 - (current_time - oldest_request_time)
|
|
72
|
+
if wait_time > 0:
|
|
73
|
+
log(f"请求频率限制: 等待 {wait_time:.3f} 秒")
|
|
74
|
+
time.sleep(wait_time)
|
|
75
|
+
# 重新获取当前时间并清理过期记录
|
|
76
|
+
current_time = time.time()
|
|
77
|
+
while self.request_times and current_time - self.request_times[0] > 1.0:
|
|
78
|
+
self.request_times.popleft()
|
|
79
|
+
|
|
80
|
+
# 记录当前请求时间
|
|
81
|
+
self.request_times.append(current_time)
|
|
82
|
+
|
|
83
|
+
def get_current_request_rate(self):
|
|
84
|
+
"""
|
|
85
|
+
获取当前的请求频率 (最近1秒内的请求数)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
int: 最近1秒内的请求数
|
|
89
|
+
"""
|
|
90
|
+
with self.rate_limit_lock:
|
|
91
|
+
current_time = time.time()
|
|
92
|
+
# 清理1秒前的请求记录
|
|
93
|
+
while self.request_times and current_time - self.request_times[0] > 1.0:
|
|
94
|
+
self.request_times.popleft()
|
|
95
|
+
return len(self.request_times)
|
|
96
|
+
|
|
97
|
+
def set_rate_limit(self, max_requests_per_second: int):
|
|
98
|
+
"""
|
|
99
|
+
动态设置请求频率限制
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
max_requests_per_second: 每秒最大请求数
|
|
103
|
+
"""
|
|
104
|
+
with self.rate_limit_lock:
|
|
105
|
+
self.max_requests_per_second = max_requests_per_second
|
|
106
|
+
log(f"请求频率限制已更新为: {max_requests_per_second} 次/秒")
|
|
107
|
+
|
|
108
|
+
def reset_rate_limit_counter(self):
|
|
109
|
+
"""
|
|
110
|
+
重置频率限制计数器
|
|
111
|
+
"""
|
|
112
|
+
with self.rate_limit_lock:
|
|
113
|
+
self.request_times.clear()
|
|
114
|
+
log("频率限制计数器已重置")
|
|
115
|
+
|
|
116
|
+
def _make_request(self, method: str, url: str, **kwargs):
|
|
117
|
+
# 在发送请求前进行速率限制检查
|
|
118
|
+
self._wait_for_rate_limit()
|
|
119
|
+
|
|
120
|
+
self._ensure_valid_token()
|
|
121
|
+
|
|
122
|
+
headers = kwargs.get('headers', {})
|
|
123
|
+
headers.update({
|
|
124
|
+
'Authorization': f'Bearer {self.tenant_access_token}',
|
|
125
|
+
'Content-Type' : 'application/json; charset=utf-8'
|
|
126
|
+
})
|
|
127
|
+
kwargs['headers'] = headers
|
|
128
|
+
|
|
129
|
+
response = requests.request(method, url, **kwargs)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
response_data = response.json()
|
|
132
|
+
|
|
133
|
+
if response_data.get("code") == 0:
|
|
134
|
+
log('----------------------------------------------------')
|
|
135
|
+
log(f'url: {url}')
|
|
136
|
+
log(f'kwargs: {json.dumps(kwargs, ensure_ascii=False, indent=4, sort_keys=True)}')
|
|
137
|
+
log(f'data: {response_data["data"]}')
|
|
138
|
+
log('====================================================')
|
|
139
|
+
return response_data.get("data", {})
|
|
140
|
+
else:
|
|
141
|
+
raise Exception(f"API调用失败: {response_data.get('msg', '未知错误')}")
|
|
142
|
+
|
|
143
|
+
# ==================== 文件夹操作 ====================
|
|
144
|
+
|
|
145
|
+
def get_root_folder_meta(self):
|
|
146
|
+
url = f"{self.base_url}/drive/explorer/v2/root_folder/meta"
|
|
147
|
+
return self._make_request('GET', url)
|
|
148
|
+
|
|
149
|
+
def create_folder(self, name: str, parent_folder_token: str = ""):
|
|
150
|
+
url = f"{self.base_url}/drive/v1/files/create_folder"
|
|
151
|
+
payload = {
|
|
152
|
+
"name" : name,
|
|
153
|
+
"folder_token": parent_folder_token
|
|
154
|
+
}
|
|
155
|
+
return self._make_request('POST', url, json=payload)
|
|
156
|
+
|
|
157
|
+
def list_folder_files(self, folder_token: str, page_size: int = 20, page_token: str = None):
|
|
158
|
+
url = f"{self.base_url}/drive/v1/files"
|
|
159
|
+
params = {
|
|
160
|
+
"folder_token": folder_token,
|
|
161
|
+
"page_size" : page_size
|
|
162
|
+
}
|
|
163
|
+
if page_token:
|
|
164
|
+
params["page_token"] = page_token
|
|
165
|
+
return self._make_request('GET', url, params=params)
|
|
166
|
+
|
|
167
|
+
def delete_file(self, file_token: str, file_type: str = "file"):
|
|
168
|
+
url = f"{self.base_url}/drive/v1/files/{file_token}"
|
|
169
|
+
params = {"type": file_type}
|
|
170
|
+
return self._make_request('DELETE', url, params=params)
|
|
171
|
+
|
|
172
|
+
# ==================== 表格操作 ====================
|
|
173
|
+
|
|
174
|
+
def create_spreadsheet(self, title: str, folder_token: str):
|
|
175
|
+
url = f"{self.base_url}/sheets/v3/spreadsheets"
|
|
176
|
+
payload = {
|
|
177
|
+
"title" : title,
|
|
178
|
+
"folder_token": folder_token
|
|
179
|
+
}
|
|
180
|
+
return self._make_request('POST', url, json=payload)
|
|
181
|
+
|
|
182
|
+
def get_spreadsheet(self, spreadsheet_token: str, user_id_type: str = "open_id"):
|
|
183
|
+
url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}"
|
|
184
|
+
params = {"user_id_type": user_id_type}
|
|
185
|
+
return self._make_request('GET', url, params=params)
|
|
186
|
+
|
|
187
|
+
def query_sheets(self, spreadsheet_token: str):
|
|
188
|
+
url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}/sheets/query"
|
|
189
|
+
return self._make_request('GET', url)
|
|
190
|
+
|
|
191
|
+
def get_sheet(self, spreadsheet_token: str, sheet_id: str):
|
|
192
|
+
url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}/sheets/{sheet_id}"
|
|
193
|
+
return self._make_request('GET', url)
|
|
194
|
+
|
|
195
|
+
def add_sheet(self, spreadsheet_token: str, title: str, index: int = None):
|
|
196
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
|
|
197
|
+
request_data = {
|
|
198
|
+
"addSheet": {
|
|
199
|
+
"properties": {"title": title}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if index is not None:
|
|
203
|
+
request_data["addSheet"]["properties"]["index"] = index
|
|
204
|
+
|
|
205
|
+
payload = {"requests": [request_data]}
|
|
206
|
+
return self._make_request('POST', url, json=payload)
|
|
207
|
+
|
|
208
|
+
def delete_sheet(self, spreadsheet_token: str, sheet_id: str):
|
|
209
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
|
|
210
|
+
payload = {
|
|
211
|
+
"requests": [{
|
|
212
|
+
"deleteSheet": {"sheetId": sheet_id}
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
return self._make_request('POST', url, json=payload)
|
|
216
|
+
|
|
217
|
+
def update_sheet_properties(self, spreadsheet_token: str, properties: dict):
|
|
218
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
|
|
219
|
+
payload = {
|
|
220
|
+
"requests": [{
|
|
221
|
+
"updateSheet": {"properties": properties}
|
|
222
|
+
}]
|
|
223
|
+
}
|
|
224
|
+
return self._make_request('POST', url, json=payload)
|
|
225
|
+
|
|
226
|
+
def obtain_spreadsheet_metainfo(self, spreadsheet_token: str, extFields: str = 'protectedRange', user_id_type: str = "open_id"):
|
|
227
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/metainfo"
|
|
228
|
+
params = {
|
|
229
|
+
"extFields" : extFields,
|
|
230
|
+
"user_id_type": user_id_type
|
|
231
|
+
}
|
|
232
|
+
return self._make_request('GET', url, params=params)
|
|
233
|
+
|
|
234
|
+
# ==================== 数据操作 ====================
|
|
235
|
+
|
|
236
|
+
def read_sheet_data(self, spreadsheet_token: str, sheet_id: str, range_str: str = None,
|
|
237
|
+
value_render_option: str = "ToString", date_time_render_option: str = "FormattedString"):
|
|
238
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values/{sheet_id}"
|
|
239
|
+
params = {
|
|
240
|
+
"value_render_option" : value_render_option,
|
|
241
|
+
"date_time_render_option": date_time_render_option
|
|
242
|
+
}
|
|
243
|
+
if range_str:
|
|
244
|
+
params["range"] = range_str
|
|
245
|
+
return self._make_request('GET', url, params=params)
|
|
246
|
+
|
|
247
|
+
def read_multiple_ranges(self, spreadsheet_token: str, sheet_id: str, ranges: list,
|
|
248
|
+
user_id_type: str = "open_id", date_time_render_option: str = "FormattedString",
|
|
249
|
+
value_render_option: str = "ToString"):
|
|
250
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_get"
|
|
251
|
+
full_ranges = [f"{sheet_id}!{r}" for r in ranges]
|
|
252
|
+
params = {
|
|
253
|
+
"ranges" : full_ranges,
|
|
254
|
+
"user_id_type" : user_id_type,
|
|
255
|
+
"date_time_render_option": date_time_render_option,
|
|
256
|
+
"value_render_option" : value_render_option
|
|
257
|
+
}
|
|
258
|
+
return self._make_request('GET', url, params=params)
|
|
259
|
+
|
|
260
|
+
def write_data_to_range(self, spreadsheet_token: str, sheet_id: str, range_str: str, values: list, insert_data_option: str = 'INSERT_ROWS'):
|
|
261
|
+
"""写入数据到指定范围"""
|
|
262
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_append"
|
|
263
|
+
payload = {
|
|
264
|
+
"valueRange" : {
|
|
265
|
+
"range" : f"{sheet_id}!{range_str}",
|
|
266
|
+
"values": values
|
|
267
|
+
},
|
|
268
|
+
"insertDataOption": insert_data_option
|
|
269
|
+
}
|
|
270
|
+
return self._make_request('POST', url, json=payload)
|
|
271
|
+
|
|
272
|
+
def write_data_to_multiple_ranges(self, spreadsheet_token: str, sheet_id: str, value_ranges: list):
|
|
273
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_update"
|
|
274
|
+
formatted_ranges = []
|
|
275
|
+
for vr in value_ranges:
|
|
276
|
+
formatted_range = vr.copy()
|
|
277
|
+
if '!' not in formatted_range['range']:
|
|
278
|
+
formatted_range['range'] = f"{sheet_id}!{formatted_range['range']}"
|
|
279
|
+
formatted_ranges.append(formatted_range)
|
|
280
|
+
|
|
281
|
+
payload = {"value_ranges": formatted_ranges}
|
|
282
|
+
return self._make_request('POST', url, json=payload)
|
|
283
|
+
|
|
284
|
+
def image_url_to_path(self, image_url: str):
|
|
285
|
+
image_dir = r'C:\Users\Administrator\Desktop\auto\image\shein'
|
|
286
|
+
file_name = os.path.basename(urlparse(image_url).path) # 获取 URL 路径中的文件名
|
|
287
|
+
file_path = os.path.join(image_dir, file_name) # 拼接文件路径
|
|
288
|
+
if not os.path.exists(file_path):
|
|
289
|
+
headers = {
|
|
290
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
|
|
291
|
+
}
|
|
292
|
+
response = requests.get(image_url, headers=headers, timeout=10)
|
|
293
|
+
response.raise_for_status()
|
|
294
|
+
with open(file_path, 'wb') as f:
|
|
295
|
+
f.write(response.content)
|
|
296
|
+
return file_path
|
|
297
|
+
|
|
298
|
+
def write_image(self, spreadsheet_token: str, range_str: str, image_url: str):
|
|
299
|
+
image_path = self.image_url_to_path(image_url)
|
|
300
|
+
with open(image_path, "rb") as image_file:
|
|
301
|
+
fb = image_file.read()
|
|
302
|
+
misssing_padding = 4 - len(fb) % 4
|
|
303
|
+
if misssing_padding:
|
|
304
|
+
fb += b'=' * misssing_padding
|
|
305
|
+
fb = base64.b64encode(fb).decode('utf-8')
|
|
306
|
+
|
|
307
|
+
name = os.path.basename(image_path)
|
|
308
|
+
|
|
309
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_image"
|
|
310
|
+
payload = {
|
|
311
|
+
"range": f"{range_str}",
|
|
312
|
+
"image": fb,
|
|
313
|
+
"name" : name,
|
|
314
|
+
}
|
|
315
|
+
return self._make_request('POST', url, json=payload)
|
|
316
|
+
|
|
317
|
+
# ==================== 单元格样式操作 ====================
|
|
318
|
+
|
|
319
|
+
def merge_cells(self, spreadsheet_token: str, sheet_id: str, merge_type: str, range_str: str):
|
|
320
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/merge_cells"
|
|
321
|
+
payload = {
|
|
322
|
+
"range" : f"{sheet_id}!{range_str}",
|
|
323
|
+
"mergeType": merge_type
|
|
324
|
+
}
|
|
325
|
+
return self._make_request('POST', url, json=payload)
|
|
326
|
+
|
|
327
|
+
def unmerge_cells(self, spreadsheet_token: str, sheet_id: str, range_str: str):
|
|
328
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/unmerge_cells"
|
|
329
|
+
payload = {"range": f"{sheet_id}!{range_str}"}
|
|
330
|
+
return self._make_request('POST', url, json=payload)
|
|
331
|
+
|
|
332
|
+
def batch_set_cell_style(self, spreadsheet_token: str, data_list: list):
|
|
333
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/styles_batch_update"
|
|
334
|
+
payload = {"data": data_list}
|
|
335
|
+
return self._make_request('PUT', url, json=payload)
|
|
336
|
+
|
|
337
|
+
# ==================== 权限管理 ====================
|
|
338
|
+
|
|
339
|
+
def batch_create_permission_members(self, token: str, type: str, members: list, need_notification: bool = True):
|
|
340
|
+
url = f"{self.base_url}/drive/v1/permissions/{token}/members/batch_create"
|
|
341
|
+
params = {
|
|
342
|
+
"type" : type,
|
|
343
|
+
"need_notification": str(need_notification).lower()
|
|
344
|
+
}
|
|
345
|
+
payload = {"members": members}
|
|
346
|
+
return self._make_request('POST', url, params=params, json=payload)
|
|
347
|
+
|
|
348
|
+
def update_permission_member(self, token: str, type: str, member_type: str, member_id: str,
|
|
349
|
+
perm: str, need_notification: bool = True):
|
|
350
|
+
url = f"{self.base_url}/drive/v2/permissions/{token}/members/{member_id}"
|
|
351
|
+
params = {
|
|
352
|
+
"type" : type,
|
|
353
|
+
"member_type" : member_type,
|
|
354
|
+
"need_notification": str(need_notification).lower()
|
|
355
|
+
}
|
|
356
|
+
payload = {"perm": perm}
|
|
357
|
+
return self._make_request('PUT', url, params=params, json=payload)
|
|
358
|
+
|
|
359
|
+
def list_permission_members(self, token: str, type: str, page_size: int = 50, page_token: str = None):
|
|
360
|
+
url = f"{self.base_url}/drive/v2/permissions/{token}/members"
|
|
361
|
+
params = {
|
|
362
|
+
"type" : type,
|
|
363
|
+
"page_size": page_size
|
|
364
|
+
}
|
|
365
|
+
if page_token:
|
|
366
|
+
params["page_token"] = page_token
|
|
367
|
+
return self._make_request('GET', url, params=params)
|
|
368
|
+
|
|
369
|
+
def transfer_permission_owner(self, token: str, type: str, member_type: str, member_id: str, need_notification: bool = True):
|
|
370
|
+
url = f"{self.base_url}/drive/v2/permissions/{token}/members/transfer_owner"
|
|
371
|
+
params = {
|
|
372
|
+
"type" : type,
|
|
373
|
+
"need_notification": str(need_notification).lower()
|
|
374
|
+
}
|
|
375
|
+
payload = {
|
|
376
|
+
"member_type": member_type,
|
|
377
|
+
"member_id" : member_id
|
|
378
|
+
}
|
|
379
|
+
return self._make_request('POST', url, params=params, json=payload)
|
|
380
|
+
|
|
381
|
+
def check_permission_member_auth(self, token: str, type: str, action_type: str, member_type: str, member_id: str):
|
|
382
|
+
url = f"{self.base_url}/drive/v2/permissions/{token}/members/auth"
|
|
383
|
+
params = {
|
|
384
|
+
"type" : type,
|
|
385
|
+
"action_type": action_type,
|
|
386
|
+
"member_type": member_type,
|
|
387
|
+
"member_id" : member_id
|
|
388
|
+
}
|
|
389
|
+
return self._make_request('GET', url, params=params)
|
|
390
|
+
|
|
391
|
+
# ==================== 表格保护范围 ====================
|
|
392
|
+
|
|
393
|
+
def add_protected_dimension(self, spreadsheet_token: str, add_protected_dimension: list, user_id_type: str = "open_id"):
|
|
394
|
+
"""增加电子表格保护范围的维度信息"""
|
|
395
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/protected_dimension"
|
|
396
|
+
params = {
|
|
397
|
+
"user_id_type": user_id_type
|
|
398
|
+
}
|
|
399
|
+
payload = {
|
|
400
|
+
"addProtectedDimension": add_protected_dimension
|
|
401
|
+
}
|
|
402
|
+
return self._make_request('POST', url, params=params, json=payload)
|
|
403
|
+
|
|
404
|
+
def batch_delete_protected_range(self, spreadsheet_token: str, protect_ids: list):
|
|
405
|
+
"""批量删除电子表格保护范围"""
|
|
406
|
+
url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/protected_range_batch_del"
|
|
407
|
+
payload = {
|
|
408
|
+
"protectIds": protect_ids
|
|
409
|
+
}
|
|
410
|
+
return self._make_request('DELETE', url, json=payload)
|