pixelarraythirdparty 1.0.7__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.
Files changed (24) hide show
  1. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/PKG-INFO +1 -1
  2. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/__init__.py +2 -1
  3. pixelarraythirdparty-1.0.8/pixelarraythirdparty/client.py +36 -0
  4. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/cron/cron.py +37 -37
  5. pixelarraythirdparty-1.0.8/pixelarraythirdparty/filestorage/__init__.py +6 -0
  6. pixelarraythirdparty-1.0.8/pixelarraythirdparty/filestorage/filestorage.py +286 -0
  7. pixelarraythirdparty-1.0.8/pixelarraythirdparty/order/order.py +341 -0
  8. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/product/product.py +46 -47
  9. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/user/user.py +44 -44
  10. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty.egg-info/PKG-INFO +1 -1
  11. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty.egg-info/SOURCES.txt +2 -0
  12. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pyproject.toml +1 -1
  13. pixelarraythirdparty-1.0.7/pixelarraythirdparty/client.py +0 -47
  14. pixelarraythirdparty-1.0.7/pixelarraythirdparty/order/order.py +0 -702
  15. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/LICENSE +0 -0
  16. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/MANIFEST.in +0 -0
  17. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/cron/__init__.py +0 -0
  18. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/order/__init__.py +0 -0
  19. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/product/__init__.py +0 -0
  20. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty/user/__init__.py +0 -0
  21. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty.egg-info/dependency_links.txt +0 -0
  22. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty.egg-info/requires.txt +0 -0
  23. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/pixelarraythirdparty.egg-info/top_level.txt +0 -0
  24. {pixelarraythirdparty-1.0.7 → pixelarraythirdparty-1.0.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.0.7
3
+ Version: 1.0.8
4
4
  Summary: PixelArray 第三方微服务客户端
5
5
  Author-email: Lu qi <qi.lu@pixelarrayai.com>
6
6
  License-Expression: MIT
@@ -10,7 +10,7 @@ PixelArray 第三方微服务客户端
10
10
  - user: 用户管理模块
11
11
  """
12
12
 
13
- __version__ = "1.0.7"
13
+ __version__ = "1.0.8"
14
14
  __author__ = "Lu qi"
15
15
  __email__ = "qi.lu@pixelarrayai.com"
16
16
 
@@ -20,4 +20,5 @@ __all__ = [
20
20
  "cron",
21
21
  "user",
22
22
  "order",
23
+ "filestorage",
23
24
  ]
@@ -0,0 +1,36 @@
1
+ import aiohttp
2
+ from typing import Dict, Any, Tuple
3
+
4
+
5
+ class AsyncClient:
6
+ def __init__(self, api_key: str):
7
+ self.base_url = "https://thirdparty.pixelarrayai.com"
8
+ self.api_key = api_key
9
+ self.headers = {
10
+ "Content-Type": "application/json",
11
+ "X-API-Key": self.api_key,
12
+ }
13
+
14
+ async def _request(
15
+ self, method: str, url: str, **kwargs
16
+ ) -> Tuple[Dict[str, Any], bool]:
17
+ # 如果kwargs中有headers,则合并headers
18
+ headers = self.headers.copy()
19
+ if "headers" in kwargs:
20
+ headers.update(kwargs["headers"])
21
+ kwargs = {k: v for k, v in kwargs.items() if k != "headers"}
22
+
23
+ async with aiohttp.ClientSession() as session:
24
+ req_method = getattr(session, method.lower())
25
+ async with req_method(
26
+ f"{self.base_url}{url}", headers=headers, **kwargs
27
+ ) as resp:
28
+ if resp.status == 200:
29
+ try:
30
+ result = await resp.json()
31
+ if result.get("success") is True:
32
+ return result.get("data", {}), True
33
+ except:
34
+ # 如果不是JSON响应,返回空
35
+ pass
36
+ return {}, False
@@ -1,17 +1,17 @@
1
- from pixelarraythirdparty.client import Client
1
+ from pixelarraythirdparty.client import AsyncClient
2
2
 
3
3
 
4
- class CronManager(Client):
5
- def get_cron_status(self):
4
+ class CronManagerAsync(AsyncClient):
5
+ async def get_cron_status(self):
6
6
  """
7
- 获取Cron服务状态
8
-
7
+ 获取Cron服务状态(异步版本)
8
+
9
9
  功能说明:
10
10
  获取Cron服务的运行状态,包括已注册的任务、工作节点状态等。
11
-
11
+
12
12
  输入参数:
13
13
 
14
-
14
+
15
15
  返回字段:
16
16
  data (dict): Cron服务状态信息
17
17
  - registered_tasks (dict): 已注册的任务列表,按工作节点分组
@@ -19,50 +19,50 @@ class CronManager(Client):
19
19
  - scheduled_task_count (int): 定时任务数量
20
20
  - timestamp (str): 状态获取时间
21
21
  success (bool): 操作是否成功
22
-
22
+
23
23
  异常情况:
24
24
  - 获取Cron状态失败:返回错误信息"获取Cron状态失败"
25
25
  """
26
- data, success = self._request("GET", "/api/cron/status")
26
+ data, success = await self._request("GET", "/api/cron/status")
27
27
  if not success:
28
28
  return {}, False
29
29
  return data, True
30
30
 
31
- def get_cron_tasks(self):
31
+ async def get_cron_tasks(self):
32
32
  """
33
- 获取已注册任务列表
34
-
33
+ 获取已注册任务列表(异步版本)
34
+
35
35
  功能说明:
36
36
  获取所有已注册的Cron任务列表。
37
-
37
+
38
38
  输入参数:
39
39
 
40
-
40
+
41
41
  返回字段:
42
42
  data (dict): 任务列表信息
43
43
  - tasks (list): 已注册的任务名称列表
44
44
  - count (int): 任务数量
45
45
  - timestamp (str): 获取时间
46
46
  success (bool): 操作是否成功
47
-
47
+
48
48
  异常情况:
49
49
  - 获取任务列表失败:返回错误信息"获取任务列表失败"
50
50
  """
51
- data, success = self._request("GET", "/api/cron/tasks")
51
+ data, success = await self._request("GET", "/api/cron/tasks")
52
52
  if not success:
53
53
  return {}, False
54
54
  return data, True
55
55
 
56
- def get_cron_tasks_scheduled(self):
56
+ async def get_cron_tasks_scheduled(self):
57
57
  """
58
- 获取定时任务列表
59
-
58
+ 获取定时任务列表(异步版本)
59
+
60
60
  功能说明:
61
61
  获取所有配置的定时任务列表,包括任务详情、执行时间、状态等。
62
-
62
+
63
63
  输入参数:
64
64
 
65
-
65
+
66
66
  返回字段:
67
67
  data (dict): 定时任务列表信息
68
68
  - tasks (list): 定时任务列表
@@ -81,25 +81,25 @@ class CronManager(Client):
81
81
  - count (int): 任务数量
82
82
  - timestamp (str): 获取时间
83
83
  success (bool): 操作是否成功
84
-
84
+
85
85
  异常情况:
86
86
  - 获取定时任务列表失败:返回错误信息"获取定时任务列表失败"
87
87
  """
88
- data, success = self._request("GET", "/api/cron/tasks/scheduled")
88
+ data, success = await self._request("GET", "/api/cron/tasks/scheduled")
89
89
  if not success:
90
90
  return {}, False
91
91
  return data, True
92
92
 
93
- def get_cron_tasks_detail(self, task_name: str):
93
+ async def get_cron_tasks_detail(self, task_name: str):
94
94
  """
95
- 获取任务详情
96
-
95
+ 获取任务详情(异步版本)
96
+
97
97
  功能说明:
98
98
  根据任务名称获取指定任务的详细信息。
99
-
99
+
100
100
  输入参数:
101
101
  task_name (str): 任务名称,必填,需要URL编码
102
-
102
+
103
103
  返回字段:
104
104
  data (dict): 任务详细信息
105
105
  - task_name (str): 任务名称
@@ -112,28 +112,28 @@ class CronManager(Client):
112
112
  - registration_info (dict): 注册信息
113
113
  - timestamp (str): 获取时间
114
114
  success (bool): 操作是否成功
115
-
115
+
116
116
  异常情况:
117
117
  - 任务不存在:返回错误信息"任务不存在"
118
118
  - 获取任务详情失败:返回错误信息"获取任务详情失败"
119
119
  """
120
- data, success = self._request("GET", f"/api/cron/tasks/{task_name}")
120
+ data, success = await self._request("GET", f"/api/cron/tasks/{task_name}")
121
121
  if not success:
122
122
  return {}, False
123
123
  return data, True
124
124
 
125
- def trigger_cron_task(self, task_name: str, args: list, kwargs: dict):
125
+ async def trigger_cron_task(self, task_name: str, args: list, kwargs: dict):
126
126
  """
127
- 触发任务执行
128
-
127
+ 触发任务执行(异步版本)
128
+
129
129
  功能说明:
130
130
  手动触发指定任务的执行,支持传递参数。
131
-
131
+
132
132
  输入参数:
133
133
  task_name (str): 任务名称,必填,需要URL编码
134
134
  args (list): 任务参数列表,可选
135
135
  kwargs (dict): 任务关键字参数,可选
136
-
136
+
137
137
  返回字段:
138
138
  data (dict): 任务触发信息
139
139
  - task_id (str): 任务ID
@@ -141,12 +141,12 @@ class CronManager(Client):
141
141
  - status (str): 任务状态,初始为"PENDING"
142
142
  - message (str): 触发消息
143
143
  success (bool): 操作是否成功
144
-
144
+
145
145
  异常情况:
146
146
  - 任务不存在:返回错误信息"任务不存在"
147
147
  - 任务触发失败:返回错误信息"任务触发失败"
148
148
  """
149
- data, success = self._request(
149
+ data, success = await self._request(
150
150
  "POST",
151
151
  f"/api/cron/tasks/{task_name}/trigger",
152
152
  json={"args": args, "kwargs": kwargs},
@@ -0,0 +1,6 @@
1
+ from pixelarraythirdparty.filestorage.filestorage import (
2
+ FileStorageManagerAsync,
3
+ )
4
+
5
+ __all__ = ["FileStorageManagerAsync"]
6
+
@@ -0,0 +1,286 @@
1
+ from pixelarraythirdparty.client import AsyncClient
2
+ from typing import Dict, Any, Optional, List, Tuple, Callable, Union
3
+ import os
4
+ import asyncio
5
+ import aiohttp
6
+ import mimetypes
7
+
8
+
9
+ class FileStorageManagerAsync(AsyncClient):
10
+ async def upload(
11
+ self, file_path: str, parent_id: Optional[int] = None
12
+ ) -> Tuple[bool, Dict[str, Any]]:
13
+ """
14
+ description:
15
+ 上传文件(合并了初始化、分片上传、完成上传三个步骤)
16
+ parameters:
17
+ file_path: 文件路径(str)
18
+ parent_id: 父文件夹ID(可选)
19
+ return:
20
+ - data: 结果数据
21
+ - success: 是否成功
22
+ """
23
+ # 读取文件数据
24
+ with open(file_path, "rb") as f:
25
+ file_bytes = f.read()
26
+
27
+ total_size = len(file_bytes)
28
+ chunk_size = 2 * 1024 * 1024 # 2MB
29
+
30
+ # 1. 初始化上传
31
+ file_name = os.path.basename(file_path)
32
+ mime_type = mimetypes.guess_type(file_path)[0]
33
+ init_data = {
34
+ "filename": file_name,
35
+ "file_type": mime_type,
36
+ "total_size": total_size,
37
+ }
38
+ if parent_id is not None:
39
+ init_data["parent_id"] = parent_id
40
+
41
+ init_result, success = await self._request(
42
+ "POST", "/api/file_storage/upload/init", json=init_data
43
+ )
44
+ if not success:
45
+ return {}, False
46
+
47
+ upload_id = init_result.get("upload_id")
48
+ chunk_urls = init_result.get("chunk_urls", [])
49
+ total_chunks = len(chunk_urls)
50
+
51
+ if not upload_id or not chunk_urls:
52
+ return {}, False
53
+
54
+ # 2. 上传所有分片
55
+ parts = []
56
+
57
+ async def upload_single_chunk(chunk_index: int, chunk_data: bytes):
58
+ """上传单个分片"""
59
+ chunk_info = chunk_urls[chunk_index]
60
+ part_number = chunk_info.get("part_number")
61
+ url = chunk_info.get("url")
62
+
63
+ if not url or not part_number:
64
+ return None
65
+
66
+ # 使用预签名URL直接上传到OSS(PUT请求)
67
+ async with aiohttp.ClientSession() as session:
68
+ async with session.put(url, data=chunk_data) as resp:
69
+ if resp.status == 200:
70
+ etag = resp.headers.get("ETag", "").strip('"')
71
+ return {
72
+ "part_number": part_number,
73
+ "etag": etag,
74
+ "chunk_index": chunk_index,
75
+ }
76
+ return None
77
+
78
+ # 并发上传所有分片
79
+ tasks = []
80
+ for i in range(total_chunks):
81
+ start = i * chunk_size
82
+ end = min(start + chunk_size, total_size)
83
+ chunk_data = file_bytes[start:end]
84
+ tasks.append(upload_single_chunk(i, chunk_data))
85
+
86
+ # 等待所有分片上传完成,并收集结果
87
+ results = await asyncio.gather(*tasks)
88
+
89
+ # 检查上传结果并更新进度
90
+ # 按chunk_index排序,确保parts顺序正确
91
+ sorted_results = sorted(
92
+ [r for r in results if r is not None], key=lambda x: x.get("chunk_index", 0)
93
+ )
94
+
95
+ if len(sorted_results) != total_chunks:
96
+ return {}, False
97
+
98
+ for i, result in enumerate(sorted_results):
99
+ parts.append({"part_number": result["part_number"], "etag": result["etag"]})
100
+
101
+ # 3. 完成上传
102
+ complete_data = {
103
+ "upload_id": upload_id,
104
+ "parts": parts,
105
+ }
106
+ complete_result, success = await self._request(
107
+ "POST", "/api/file_storage/upload/complete", json=complete_data
108
+ )
109
+ if not success:
110
+ return {}, False
111
+
112
+ return complete_result, True
113
+
114
+ async def list_files(
115
+ self,
116
+ parent_id: Optional[int] = None,
117
+ is_folder: Optional[bool] = None,
118
+ page: int = 1,
119
+ page_size: int = 50,
120
+ ) -> Tuple[List[Dict[str, Any]], bool]:
121
+ """
122
+ description:
123
+ 获取文件列表
124
+ parameters:
125
+ parent_id: 父文件夹ID(可选)
126
+ is_folder: 是否只查询文件夹(可选)
127
+ page: 页码(可选)
128
+ page_size: 每页数量(可选)
129
+ return:
130
+ - data: 文件列表数据
131
+ - success: 是否成功
132
+ """
133
+ data = {
134
+ "page": page,
135
+ "page_size": page_size,
136
+ }
137
+ if parent_id is not None:
138
+ data["parent_id"] = parent_id
139
+ if is_folder is not None:
140
+ data["is_folder"] = is_folder
141
+
142
+ result, success = await self._request(
143
+ "POST", "/api/file_storage/files/list", json=data
144
+ )
145
+ if not success:
146
+ return {}, False
147
+ return result, True
148
+
149
+ async def create_folder(
150
+ self,
151
+ folder_name: str,
152
+ parent_id: Optional[int] = None,
153
+ ) -> Tuple[Dict[str, Any], bool]:
154
+ """
155
+ description:
156
+ 创建文件夹
157
+ parameters:
158
+ folder_name: 文件夹名称
159
+ parent_id: 父文件夹ID(可选)
160
+ return:
161
+ - data: 文件夹数据
162
+ - success: 是否成功
163
+ """
164
+ data = {
165
+ "folder_name": folder_name,
166
+ }
167
+ if parent_id is not None:
168
+ data["parent_id"] = parent_id
169
+
170
+ data, success = await self._request(
171
+ "POST", "/api/file_storage/files/folder/create", json=data
172
+ )
173
+ if not success:
174
+ return {}, False
175
+ return data, True
176
+
177
+ async def delete_file(
178
+ self,
179
+ record_id: int,
180
+ ) -> Tuple[Dict[str, Any], bool]:
181
+ """
182
+ description:
183
+ 删除文件或文件夹
184
+ parameters:
185
+ record_id: 文件或文件夹ID
186
+ return:
187
+ - data: 结果数据
188
+ - success: 是否成功
189
+ """
190
+ data, success = await self._request(
191
+ "DELETE", f"/api/file_storage/files/{record_id}"
192
+ )
193
+ if not success:
194
+ return {}, False
195
+ return data, True
196
+
197
+ async def get_folder_path(
198
+ self,
199
+ record_id: int,
200
+ ) -> Tuple[List[Dict[str, Any]], bool]:
201
+ """
202
+ description:
203
+ 获取文件夹的完整路径
204
+ parameters:
205
+ record_id: 文件夹ID
206
+ return:
207
+ - data: 文件夹路径列表
208
+ - success: 是否成功
209
+ """
210
+ data, success = await self._request(
211
+ "GET", f"/api/file_storage/files/{record_id}/path"
212
+ )
213
+ if not success:
214
+ return [], False
215
+ # 如果data是字典,尝试获取data字段(因为API返回的是{"data": [...]})
216
+ if isinstance(data, dict):
217
+ path_list = data.get("data", [])
218
+ if isinstance(path_list, list):
219
+ return path_list, True
220
+ # 如果data本身就是列表,直接返回
221
+ if isinstance(data, list):
222
+ return data, True
223
+ return [], False
224
+
225
+ async def generate_signed_url(
226
+ self,
227
+ record_id: int,
228
+ expires: int = 3600,
229
+ ) -> Tuple[Dict[str, Any], bool]:
230
+ """
231
+ 生成签名URL(异步版本)
232
+ """
233
+ data = {
234
+ "expires": expires,
235
+ }
236
+ data, success = await self._request(
237
+ "POST", f"/api/file_storage/files/{record_id}/generate_url", json=data
238
+ )
239
+ if not success:
240
+ return {}, False
241
+ return data, True
242
+
243
+ async def download(
244
+ self,
245
+ record_id: int,
246
+ save_path: str,
247
+ ) -> Tuple[Dict[str, Any], bool]:
248
+ """
249
+ description:
250
+ 下载文件
251
+ parameters:
252
+ record_id: 文件记录ID
253
+ save_path: 保存路径
254
+ return:
255
+ - data: 下载结果数据
256
+ - success: 是否成功
257
+ """
258
+ # 1. 生成签名URL
259
+ signed_url_data, success = await self.generate_signed_url(record_id)
260
+ if not success:
261
+ return {}, False
262
+
263
+ signed_url = signed_url_data.get("signed_url")
264
+ file_record = signed_url_data.get("file_record", {})
265
+ total_size = file_record.get("file_size", 0)
266
+
267
+ if not signed_url:
268
+ return {}, False
269
+
270
+ # 2. 下载文件
271
+ async with aiohttp.ClientSession() as session:
272
+ async with session.get(signed_url) as resp:
273
+ if resp.status != 200:
274
+ return {}, False
275
+
276
+ file_data = b""
277
+ downloaded = 0
278
+
279
+ async for chunk in resp.content.iter_chunked(8192): # 8KB chunks
280
+ file_data += chunk
281
+ downloaded += len(chunk)
282
+
283
+ with open(save_path, "wb") as f:
284
+ f.write(file_data)
285
+
286
+ return {"total_size": total_size, "success": True}, True