qrpa 1.0.35__tar.gz → 1.0.36__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.

Files changed (33) hide show
  1. {qrpa-1.0.35 → qrpa-1.0.36}/PKG-INFO +1 -1
  2. {qrpa-1.0.35 → qrpa-1.0.36}/pyproject.toml +1 -1
  3. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/__init__.py +3 -0
  4. qrpa-1.0.36/qrpa/feishu_client.py +336 -0
  5. qrpa-1.0.36/qrpa/feishu_logic.py +1443 -0
  6. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa.egg-info/PKG-INFO +1 -1
  7. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa.egg-info/SOURCES.txt +2 -0
  8. {qrpa-1.0.35 → qrpa-1.0.36}/README.md +0 -0
  9. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/RateLimitedSender.py +0 -0
  10. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/db_migrator.py +0 -0
  11. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/feishu_bot_app.py +0 -0
  12. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/fun_base.py +0 -0
  13. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/fun_excel.py +0 -0
  14. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/fun_file.py +0 -0
  15. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/fun_web.py +0 -0
  16. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/fun_win.py +0 -0
  17. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/shein_daily_report_model.py +0 -0
  18. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/shein_excel.py +0 -0
  19. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/shein_lib.py +0 -0
  20. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/shein_sqlite.py +0 -0
  21. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/shein_ziniao.py +0 -0
  22. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/temu_chrome.py +0 -0
  23. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/temu_excel.py +0 -0
  24. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/temu_lib.py +0 -0
  25. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/time_utils.py +0 -0
  26. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/time_utils_example.py +0 -0
  27. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa/wxwork.py +0 -0
  28. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa.egg-info/dependency_links.txt +0 -0
  29. {qrpa-1.0.35 → qrpa-1.0.36}/qrpa.egg-info/top_level.txt +0 -0
  30. {qrpa-1.0.35 → qrpa-1.0.36}/setup.cfg +0 -0
  31. {qrpa-1.0.35 → qrpa-1.0.36}/setup.py +0 -0
  32. {qrpa-1.0.35 → qrpa-1.0.36}/tests/test_db_migrator.py +0 -0
  33. {qrpa-1.0.35 → qrpa-1.0.36}/tests/test_wxwork.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qrpa
3
- Version: 1.0.35
3
+ Version: 1.0.36
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.35"
7
+ version = "1.0.36"
8
8
  description = "qsir's rpa library"
9
9
  authors = [{ name = "QSir", email = "1171725650@qq.com" }]
10
10
  readme = "README.md"
@@ -24,3 +24,6 @@ from .fun_excel import InsertImageV2
24
24
  from .temu_lib import TemuLib
25
25
  from .temu_excel import TemuExcel
26
26
  from .temu_chrome import temu_chrome_excute
27
+
28
+ from .feishu_logic import FeishuBusinessLogic
29
+ from .feishu_client import FeishuClient
@@ -0,0 +1,336 @@
1
+ # -*- coding: UTF-8 -*-
2
+ import requests
3
+ import json
4
+ import time
5
+ import base64, os
6
+ from urllib.parse import urlparse
7
+ from typing import Optional, Dict, List, Any
8
+
9
+ from fun_base import log
10
+
11
+ class FeishuClient:
12
+ def __init__(self, app_id: str, app_secret: str):
13
+ self.app_id = app_id
14
+ self.app_secret = app_secret
15
+ self.base_url = "https://open.feishu.cn/open-apis"
16
+ self.tenant_access_token = None
17
+ self.token_expire_time = 0
18
+ self._refresh_token()
19
+
20
+ def _refresh_token(self):
21
+ url = f"{self.base_url}/auth/v3/tenant_access_token/internal"
22
+ payload = {
23
+ "app_id" : self.app_id,
24
+ "app_secret": self.app_secret
25
+ }
26
+ headers = {
27
+ 'Content-Type': 'application/json; charset=utf-8'
28
+ }
29
+
30
+ response = requests.post(url, headers=headers, json=payload)
31
+ response.raise_for_status()
32
+ response_data = response.json()
33
+
34
+ if response_data.get("code") == 0:
35
+ self.tenant_access_token = response_data.get("tenant_access_token")
36
+ expire_seconds = response_data.get("expire", 7200)
37
+ self.token_expire_time = time.time() + expire_seconds - 300
38
+ else:
39
+ raise Exception(f"获取token失败: {response_data.get('msg', '未知错误')}")
40
+
41
+ def _ensure_valid_token(self):
42
+ if not self.tenant_access_token or time.time() > self.token_expire_time:
43
+ self._refresh_token()
44
+
45
+ def _make_request(self, method: str, url: str, **kwargs):
46
+ self._ensure_valid_token()
47
+
48
+ headers = kwargs.get('headers', {})
49
+ headers.update({
50
+ 'Authorization': f'Bearer {self.tenant_access_token}',
51
+ 'Content-Type' : 'application/json; charset=utf-8'
52
+ })
53
+ kwargs['headers'] = headers
54
+
55
+ response = requests.request(method, url, **kwargs)
56
+ response.raise_for_status()
57
+ response_data = response.json()
58
+
59
+ if response_data.get("code") == 0:
60
+ log('----------------------------------------------------')
61
+ log(f'url: {url}')
62
+ log(f'kwargs: {json.dumps(kwargs, ensure_ascii=False, indent=4, sort_keys=True)}')
63
+ log(f'data: {response_data["data"]}')
64
+ log('====================================================')
65
+ return response_data.get("data", {})
66
+ else:
67
+ raise Exception(f"API调用失败: {response_data.get('msg', '未知错误')}")
68
+
69
+ # ==================== 文件夹操作 ====================
70
+
71
+ def get_root_folder_meta(self):
72
+ url = f"{self.base_url}/drive/explorer/v2/root_folder/meta"
73
+ return self._make_request('GET', url)
74
+
75
+ def create_folder(self, name: str, parent_folder_token: str = ""):
76
+ url = f"{self.base_url}/drive/v1/files/create_folder"
77
+ payload = {
78
+ "name" : name,
79
+ "folder_token": parent_folder_token
80
+ }
81
+ return self._make_request('POST', url, json=payload)
82
+
83
+ def list_folder_files(self, folder_token: str, page_size: int = 20, page_token: str = None):
84
+ url = f"{self.base_url}/drive/v1/files"
85
+ params = {
86
+ "folder_token": folder_token,
87
+ "page_size" : page_size
88
+ }
89
+ if page_token:
90
+ params["page_token"] = page_token
91
+ return self._make_request('GET', url, params=params)
92
+
93
+ def delete_file(self, file_token: str, file_type: str = "file"):
94
+ url = f"{self.base_url}/drive/v1/files/{file_token}"
95
+ params = {"type": file_type}
96
+ return self._make_request('DELETE', url, params=params)
97
+
98
+ # ==================== 表格操作 ====================
99
+
100
+ def create_spreadsheet(self, title: str, folder_token: str):
101
+ url = f"{self.base_url}/sheets/v3/spreadsheets"
102
+ payload = {
103
+ "title" : title,
104
+ "folder_token": folder_token
105
+ }
106
+ return self._make_request('POST', url, json=payload)
107
+
108
+ def get_spreadsheet(self, spreadsheet_token: str, user_id_type: str = "open_id"):
109
+ url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}"
110
+ params = {"user_id_type": user_id_type}
111
+ return self._make_request('GET', url, params=params)
112
+
113
+ def query_sheets(self, spreadsheet_token: str):
114
+ url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}/sheets/query"
115
+ return self._make_request('GET', url)
116
+
117
+ def get_sheet(self, spreadsheet_token: str, sheet_id: str):
118
+ url = f"{self.base_url}/sheets/v3/spreadsheets/{spreadsheet_token}/sheets/{sheet_id}"
119
+ return self._make_request('GET', url)
120
+
121
+ def add_sheet(self, spreadsheet_token: str, title: str, index: int = None):
122
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
123
+ request_data = {
124
+ "addSheet": {
125
+ "properties": {"title": title}
126
+ }
127
+ }
128
+ if index is not None:
129
+ request_data["addSheet"]["properties"]["index"] = index
130
+
131
+ payload = {"requests": [request_data]}
132
+ return self._make_request('POST', url, json=payload)
133
+
134
+ def delete_sheet(self, spreadsheet_token: str, sheet_id: str):
135
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
136
+ payload = {
137
+ "requests": [{
138
+ "deleteSheet": {"sheetId": sheet_id}
139
+ }]
140
+ }
141
+ return self._make_request('POST', url, json=payload)
142
+
143
+ def update_sheet_properties(self, spreadsheet_token: str, properties: dict):
144
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/sheets_batch_update"
145
+ payload = {
146
+ "requests": [{
147
+ "updateSheet": {"properties": properties}
148
+ }]
149
+ }
150
+ return self._make_request('POST', url, json=payload)
151
+
152
+ def obtain_spreadsheet_metainfo(self, spreadsheet_token: str, extFields: str = 'protectedRange', user_id_type: str = "open_id"):
153
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/metainfo"
154
+ params = {
155
+ "extFields" : extFields,
156
+ "user_id_type": user_id_type
157
+ }
158
+ return self._make_request('GET', url, params=params)
159
+
160
+ # ==================== 数据操作 ====================
161
+
162
+ def read_sheet_data(self, spreadsheet_token: str, sheet_id: str, range_str: str = None,
163
+ value_render_option: str = "ToString", date_time_render_option: str = "FormattedString"):
164
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values/{sheet_id}"
165
+ params = {
166
+ "value_render_option" : value_render_option,
167
+ "date_time_render_option": date_time_render_option
168
+ }
169
+ if range_str:
170
+ params["range"] = range_str
171
+ return self._make_request('GET', url, params=params)
172
+
173
+ def read_multiple_ranges(self, spreadsheet_token: str, sheet_id: str, ranges: list,
174
+ user_id_type: str = "open_id", date_time_render_option: str = "FormattedString",
175
+ value_render_option: str = "ToString"):
176
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_get"
177
+ full_ranges = [f"{sheet_id}!{r}" for r in ranges]
178
+ params = {
179
+ "ranges" : full_ranges,
180
+ "user_id_type" : user_id_type,
181
+ "date_time_render_option": date_time_render_option,
182
+ "value_render_option" : value_render_option
183
+ }
184
+ return self._make_request('GET', url, params=params)
185
+
186
+ def write_data_to_range(self, spreadsheet_token: str, sheet_id: str, range_str: str, values: list, insert_data_option: str = 'INSERT_ROWS'):
187
+ """写入数据到指定范围"""
188
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_append"
189
+ payload = {
190
+ "valueRange" : {
191
+ "range" : f"{sheet_id}!{range_str}",
192
+ "values": values
193
+ },
194
+ "insertDataOption": insert_data_option
195
+ }
196
+ return self._make_request('POST', url, json=payload)
197
+
198
+ def write_data_to_multiple_ranges(self, spreadsheet_token: str, sheet_id: str, value_ranges: list):
199
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_batch_update"
200
+ formatted_ranges = []
201
+ for vr in value_ranges:
202
+ formatted_range = vr.copy()
203
+ if '!' not in formatted_range['range']:
204
+ formatted_range['range'] = f"{sheet_id}!{formatted_range['range']}"
205
+ formatted_ranges.append(formatted_range)
206
+
207
+ payload = {"value_ranges": formatted_ranges}
208
+ return self._make_request('POST', url, json=payload)
209
+
210
+ def image_url_to_path(self, image_url: str):
211
+ image_dir = r'C:\Users\Administrator\Desktop\auto\image\shein'
212
+ file_name = os.path.basename(urlparse(image_url).path) # 获取 URL 路径中的文件名
213
+ file_path = os.path.join(image_dir, file_name) # 拼接文件路径
214
+ if not os.path.exists(file_path):
215
+ headers = {
216
+ "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",
217
+ }
218
+ response = requests.get(image_url, headers=headers, timeout=10)
219
+ response.raise_for_status()
220
+ with open(file_path, 'wb') as f:
221
+ f.write(response.content)
222
+ return file_path
223
+
224
+ def write_image(self, spreadsheet_token: str, range_str: str, image_url: str):
225
+ image_path = self.image_url_to_path(image_url)
226
+ with open(image_path, "rb") as image_file:
227
+ fb = image_file.read()
228
+ misssing_padding = 4 - len(fb) % 4
229
+ if misssing_padding:
230
+ fb += b'=' * misssing_padding
231
+ fb = base64.b64encode(fb).decode('utf-8')
232
+
233
+ name = os.path.basename(image_path)
234
+
235
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/values_image"
236
+ payload = {
237
+ "range": f"{range_str}",
238
+ "image": fb,
239
+ "name" : name,
240
+ }
241
+ return self._make_request('POST', url, json=payload)
242
+
243
+ # ==================== 单元格样式操作 ====================
244
+
245
+ def merge_cells(self, spreadsheet_token: str, sheet_id: str, merge_type: str, range_str: str):
246
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/merge_cells"
247
+ payload = {
248
+ "range" : f"{sheet_id}!{range_str}",
249
+ "mergeType": merge_type
250
+ }
251
+ return self._make_request('POST', url, json=payload)
252
+
253
+ def unmerge_cells(self, spreadsheet_token: str, sheet_id: str, range_str: str):
254
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/unmerge_cells"
255
+ payload = {"range": f"{sheet_id}!{range_str}"}
256
+ return self._make_request('POST', url, json=payload)
257
+
258
+ def batch_set_cell_style(self, spreadsheet_token: str, data_list: list):
259
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/styles_batch_update"
260
+ payload = {"data": data_list}
261
+ return self._make_request('PUT', url, json=payload)
262
+
263
+ # ==================== 权限管理 ====================
264
+
265
+ def batch_create_permission_members(self, token: str, type: str, members: list, need_notification: bool = True):
266
+ url = f"{self.base_url}/drive/v1/permissions/{token}/members/batch_create"
267
+ params = {
268
+ "type" : type,
269
+ "need_notification": str(need_notification).lower()
270
+ }
271
+ payload = {"members": members}
272
+ return self._make_request('POST', url, params=params, json=payload)
273
+
274
+ def update_permission_member(self, token: str, type: str, member_type: str, member_id: str,
275
+ perm: str, need_notification: bool = True):
276
+ url = f"{self.base_url}/drive/v2/permissions/{token}/members/{member_id}"
277
+ params = {
278
+ "type" : type,
279
+ "member_type" : member_type,
280
+ "need_notification": str(need_notification).lower()
281
+ }
282
+ payload = {"perm": perm}
283
+ return self._make_request('PUT', url, params=params, json=payload)
284
+
285
+ def list_permission_members(self, token: str, type: str, page_size: int = 50, page_token: str = None):
286
+ url = f"{self.base_url}/drive/v2/permissions/{token}/members"
287
+ params = {
288
+ "type" : type,
289
+ "page_size": page_size
290
+ }
291
+ if page_token:
292
+ params["page_token"] = page_token
293
+ return self._make_request('GET', url, params=params)
294
+
295
+ def transfer_permission_owner(self, token: str, type: str, member_type: str, member_id: str, need_notification: bool = True):
296
+ url = f"{self.base_url}/drive/v2/permissions/{token}/members/transfer_owner"
297
+ params = {
298
+ "type" : type,
299
+ "need_notification": str(need_notification).lower()
300
+ }
301
+ payload = {
302
+ "member_type": member_type,
303
+ "member_id" : member_id
304
+ }
305
+ return self._make_request('POST', url, params=params, json=payload)
306
+
307
+ def check_permission_member_auth(self, token: str, type: str, action_type: str, member_type: str, member_id: str):
308
+ url = f"{self.base_url}/drive/v2/permissions/{token}/members/auth"
309
+ params = {
310
+ "type" : type,
311
+ "action_type": action_type,
312
+ "member_type": member_type,
313
+ "member_id" : member_id
314
+ }
315
+ return self._make_request('GET', url, params=params)
316
+
317
+ # ==================== 表格保护范围 ====================
318
+
319
+ def add_protected_dimension(self, spreadsheet_token: str, add_protected_dimension: list, user_id_type: str = "open_id"):
320
+ """增加电子表格保护范围的维度信息"""
321
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/protected_dimension"
322
+ params = {
323
+ "user_id_type": user_id_type
324
+ }
325
+ payload = {
326
+ "addProtectedDimension": add_protected_dimension
327
+ }
328
+ return self._make_request('POST', url, params=params, json=payload)
329
+
330
+ def batch_delete_protected_range(self, spreadsheet_token: str, protect_ids: list):
331
+ """批量删除电子表格保护范围"""
332
+ url = f"{self.base_url}/sheets/v2/spreadsheets/{spreadsheet_token}/protected_range_batch_del"
333
+ payload = {
334
+ "protectIds": protect_ids
335
+ }
336
+ return self._make_request('DELETE', url, json=payload)