pixelarraythirdparty 1.0.8__py3-none-any.whl → 1.0.9__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.
@@ -8,9 +8,10 @@ PixelArray 第三方微服务客户端
8
8
  - product: 产品管理模块
9
9
  - cron: 定时任务管理模块
10
10
  - user: 用户管理模块
11
+ - unified_login: 统一登录模块
11
12
  """
12
13
 
13
- __version__ = "1.0.8"
14
+ __version__ = "1.0.9"
14
15
  __author__ = "Lu qi"
15
16
  __email__ = "qi.lu@pixelarrayai.com"
16
17
 
@@ -21,4 +22,5 @@ __all__ = [
21
22
  "user",
22
23
  "order",
23
24
  "filestorage",
25
+ "unified_login",
24
26
  ]
@@ -1,15 +1,16 @@
1
1
  from pixelarraythirdparty.client import AsyncClient
2
- from typing import Dict, Any, Optional, List, Tuple, Callable, Union
2
+ from typing import Dict, Any, Optional, List, Tuple, AsyncGenerator
3
3
  import os
4
- import asyncio
5
4
  import aiohttp
6
5
  import mimetypes
6
+ import math
7
+ import time
7
8
 
8
9
 
9
10
  class FileStorageManagerAsync(AsyncClient):
10
11
  async def upload(
11
12
  self, file_path: str, parent_id: Optional[int] = None
12
- ) -> Tuple[bool, Dict[str, Any]]:
13
+ ) -> Tuple[Dict[str, Any], bool]:
13
14
  """
14
15
  description:
15
16
  上传文件(合并了初始化、分片上传、完成上传三个步骤)
@@ -20,14 +21,30 @@ class FileStorageManagerAsync(AsyncClient):
20
21
  - data: 结果数据
21
22
  - success: 是否成功
22
23
  """
23
- # 读取文件数据
24
+ final_result: Dict[str, Any] = {}
25
+ final_success = False
26
+ async for progress in self.upload_stream(file_path, parent_id):
27
+ event = progress.get("event")
28
+ if event == "error":
29
+ return {}, False
30
+ if event == "complete" and progress.get("success"):
31
+ final_result = progress.get("result", {})
32
+ final_success = True
33
+ return final_result, final_success
34
+
35
+ async def upload_stream(
36
+ self, file_path: str, parent_id: Optional[int] = None
37
+ ) -> AsyncGenerator[Dict[str, Any], None]:
38
+ """
39
+ description:
40
+ 上传文件(流式,返回生成器,包含进度信息)
41
+ """
42
+ chunk_size = 2 * 1024 * 1024 # 2MB
43
+ upload_start = time.perf_counter()
24
44
  with open(file_path, "rb") as f:
25
45
  file_bytes = f.read()
26
46
 
27
47
  total_size = len(file_bytes)
28
- chunk_size = 2 * 1024 * 1024 # 2MB
29
-
30
- # 1. 初始化上传
31
48
  file_name = os.path.basename(file_path)
32
49
  mime_type = mimetypes.guess_type(file_path)[0]
33
50
  init_data = {
@@ -42,63 +59,135 @@ class FileStorageManagerAsync(AsyncClient):
42
59
  "POST", "/api/file_storage/upload/init", json=init_data
43
60
  )
44
61
  if not success:
45
- return {}, False
62
+ yield {
63
+ "event": "error",
64
+ "percentage": 0,
65
+ "total_chunks": 0,
66
+ "remaining_chunks": 0,
67
+ "total_bytes": total_size,
68
+ "processed_bytes": 0,
69
+ "speed": 0,
70
+ "message": "初始化上传失败",
71
+ "success": False,
72
+ }
73
+ return
46
74
 
47
75
  upload_id = init_result.get("upload_id")
48
76
  chunk_urls = init_result.get("chunk_urls", [])
49
77
  total_chunks = len(chunk_urls)
50
78
 
51
79
  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")
80
+ yield {
81
+ "event": "error",
82
+ "percentage": 0,
83
+ "total_chunks": 0,
84
+ "remaining_chunks": 0,
85
+ "total_bytes": total_size,
86
+ "processed_bytes": 0,
87
+ "speed": 0,
88
+ "message": "缺少上传ID或分片信息",
89
+ "success": False,
90
+ }
91
+ return
92
+
93
+ yield {
94
+ "event": "init",
95
+ "percentage": 0,
96
+ "total_chunks": total_chunks,
97
+ "remaining_chunks": total_chunks,
98
+ "total_bytes": total_size,
99
+ "processed_bytes": 0,
100
+ "speed": 0,
101
+ "message": "初始化完成,开始上传分片",
102
+ "success": True,
103
+ }
62
104
 
63
- if not url or not part_number:
64
- return None
105
+ parts: List[Dict[str, Any]] = []
106
+ uploaded_bytes = 0
65
107
 
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:
108
+ async with aiohttp.ClientSession() as session:
109
+ for idx, chunk_info in enumerate(chunk_urls):
110
+ part_number = chunk_info.get("part_number")
111
+ url = chunk_info.get("url")
112
+ start = idx * chunk_size
113
+ end = min(start + chunk_size, total_size)
114
+ chunk_data = file_bytes[start:end]
115
+
116
+ if not url or not part_number:
117
+ percentage = (
118
+ 0
119
+ if total_size == 0
120
+ else min((uploaded_bytes / total_size) * 100, 100)
121
+ )
122
+ yield {
123
+ "event": "error",
124
+ "percentage": percentage,
125
+ "total_chunks": total_chunks,
126
+ "remaining_chunks": total_chunks - idx,
127
+ "total_bytes": total_size,
128
+ "processed_bytes": uploaded_bytes,
129
+ "speed": 0,
130
+ "message": "分片信息缺失",
131
+ "success": False,
132
+ }
133
+ return
134
+
135
+ chunk_start = time.perf_counter()
136
+ try:
137
+ async with session.put(url, data=chunk_data) as resp:
138
+ if resp.status != 200:
139
+ raise RuntimeError(
140
+ f"分片上传失败,状态码:{resp.status}"
141
+ )
70
142
  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
- )
143
+ parts.append(
144
+ {
145
+ "part_number": part_number,
146
+ "etag": etag,
147
+ }
148
+ )
149
+ except Exception as exc:
150
+ percentage = (
151
+ 0
152
+ if total_size == 0
153
+ else min((uploaded_bytes / total_size) * 100, 100)
154
+ )
155
+ yield {
156
+ "event": "error",
157
+ "percentage": percentage,
158
+ "total_chunks": total_chunks,
159
+ "remaining_chunks": max(total_chunks - idx, 0),
160
+ "total_bytes": total_size,
161
+ "processed_bytes": uploaded_bytes,
162
+ "speed": 0,
163
+ "message": f"分片上传异常:{exc}",
164
+ "success": False,
165
+ }
166
+ return
167
+
168
+ uploaded_bytes += len(chunk_data)
169
+ duration = max(time.perf_counter() - chunk_start, 1e-6)
170
+ speed = len(chunk_data) / duration
171
+ percentage = (
172
+ 100
173
+ if total_size == 0
174
+ else min((uploaded_bytes / total_size) * 100, 100)
175
+ )
176
+
177
+ yield {
178
+ "event": "chunk",
179
+ "percentage": percentage,
180
+ "total_chunks": total_chunks,
181
+ "remaining_chunks": max(total_chunks - (idx + 1), 0),
182
+ "total_bytes": total_size,
183
+ "processed_bytes": uploaded_bytes,
184
+ "chunk_index": idx,
185
+ "chunk_size": len(chunk_data),
186
+ "speed": speed,
187
+ "message": f"分片{idx + 1}/{total_chunks}上传完成",
188
+ "success": True,
189
+ }
94
190
 
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
191
  complete_data = {
103
192
  "upload_id": upload_id,
104
193
  "parts": parts,
@@ -107,9 +196,32 @@ class FileStorageManagerAsync(AsyncClient):
107
196
  "POST", "/api/file_storage/upload/complete", json=complete_data
108
197
  )
109
198
  if not success:
110
- return {}, False
111
-
112
- return complete_result, True
199
+ yield {
200
+ "event": "error",
201
+ "percentage": 100,
202
+ "total_chunks": total_chunks,
203
+ "remaining_chunks": 0,
204
+ "total_bytes": total_size,
205
+ "processed_bytes": total_size,
206
+ "speed": 0,
207
+ "message": "完成上传失败",
208
+ "success": False,
209
+ }
210
+ return
211
+
212
+ total_duration = max(time.perf_counter() - upload_start, 1e-6)
213
+ yield {
214
+ "event": "complete",
215
+ "percentage": 100,
216
+ "total_chunks": total_chunks,
217
+ "remaining_chunks": 0,
218
+ "total_bytes": total_size,
219
+ "processed_bytes": total_size,
220
+ "speed": total_size / total_duration if total_duration else 0,
221
+ "message": "上传完成",
222
+ "success": True,
223
+ "result": complete_result,
224
+ }
113
225
 
114
226
  async def list_files(
115
227
  self,
@@ -255,32 +367,164 @@ class FileStorageManagerAsync(AsyncClient):
255
367
  - data: 下载结果数据
256
368
  - success: 是否成功
257
369
  """
258
- # 1. 生成签名URL
370
+ final_result: Dict[str, Any] = {}
371
+ final_success = False
372
+ async for progress in self.download_stream(record_id, save_path):
373
+ event = progress.get("event")
374
+ if event == "error":
375
+ return {}, False
376
+ if event == "complete" and progress.get("success"):
377
+ final_result = progress.get("result", {})
378
+ final_success = True
379
+ return final_result, final_success
380
+
381
+ async def download_stream(
382
+ self,
383
+ record_id: int,
384
+ save_path: str,
385
+ ) -> AsyncGenerator[Dict[str, Any], None]:
386
+ """
387
+ description:
388
+ 下载文件(流式,返回生成器)
389
+ """
390
+ chunk_size = 2 * 1024 * 1024
259
391
  signed_url_data, success = await self.generate_signed_url(record_id)
260
392
  if not success:
261
- return {}, False
393
+ yield {
394
+ "event": "error",
395
+ "percentage": 0,
396
+ "total_chunks": 0,
397
+ "remaining_chunks": 0,
398
+ "total_bytes": 0,
399
+ "processed_bytes": 0,
400
+ "speed": 0,
401
+ "message": "生成签名URL失败",
402
+ "success": False,
403
+ }
404
+ return
262
405
 
263
406
  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)
407
+ file_record = signed_url_data.get("file_record", {}) or {}
408
+ total_size = file_record.get("file_size", 0) or 0
266
409
 
267
410
  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
411
+ yield {
412
+ "event": "error",
413
+ "percentage": 0,
414
+ "total_chunks": 0,
415
+ "remaining_chunks": 0,
416
+ "total_bytes": total_size,
417
+ "processed_bytes": 0,
418
+ "speed": 0,
419
+ "message": "签名URL为空",
420
+ "success": False,
421
+ }
422
+ return
423
+
424
+ total_chunks = math.ceil(total_size / chunk_size) if total_size else 0
425
+
426
+ yield {
427
+ "event": "init",
428
+ "percentage": 0,
429
+ "total_chunks": total_chunks,
430
+ "remaining_chunks": total_chunks,
431
+ "total_bytes": total_size,
432
+ "processed_bytes": 0,
433
+ "speed": 0,
434
+ "message": "开始下载文件",
435
+ "success": True,
436
+ }
278
437
 
279
- async for chunk in resp.content.iter_chunked(8192): # 8KB chunks
280
- file_data += chunk
281
- downloaded += len(chunk)
438
+ os.makedirs(os.path.dirname(save_path) or ".", exist_ok=True)
282
439
 
283
- with open(save_path, "wb") as f:
284
- f.write(file_data)
440
+ downloaded_bytes = 0
441
+ chunk_index = 0
442
+ download_start = time.perf_counter()
285
443
 
286
- return {"total_size": total_size, "success": True}, True
444
+ async with aiohttp.ClientSession() as session:
445
+ try:
446
+ async with session.get(signed_url) as resp:
447
+ if resp.status != 200:
448
+ raise RuntimeError(f"文件下载失败,状态码:{resp.status}")
449
+
450
+ header_size = resp.headers.get("Content-Length")
451
+ if total_size == 0 and header_size:
452
+ try:
453
+ total_size = int(header_size)
454
+ total_chunks = (
455
+ math.ceil(total_size / chunk_size)
456
+ if total_size
457
+ else 0
458
+ )
459
+ except ValueError:
460
+ total_size = 0
461
+
462
+ with open(save_path, "wb") as f:
463
+ async for chunk in resp.content.iter_chunked(chunk_size):
464
+ chunk_start = time.perf_counter()
465
+ chunk_index += 1
466
+ downloaded_bytes += len(chunk)
467
+ f.write(chunk)
468
+
469
+ chunk_duration = max(
470
+ time.perf_counter() - chunk_start, 1e-6
471
+ )
472
+ instant_speed = len(chunk) / chunk_duration
473
+ percentage = (
474
+ 0
475
+ if total_size == 0
476
+ else min(
477
+ (downloaded_bytes / total_size) * 100, 100
478
+ )
479
+ )
480
+ remaining = (
481
+ max(total_chunks - chunk_index, 0)
482
+ if total_chunks
483
+ else 0
484
+ )
485
+
486
+ yield {
487
+ "event": "chunk",
488
+ "percentage": percentage,
489
+ "total_chunks": total_chunks,
490
+ "remaining_chunks": remaining,
491
+ "total_bytes": total_size,
492
+ "processed_bytes": downloaded_bytes,
493
+ "chunk_index": chunk_index - 1,
494
+ "chunk_size": len(chunk),
495
+ "speed": instant_speed,
496
+ "message": f"分片{chunk_index}/{total_chunks or '?'}下载完成",
497
+ "success": True,
498
+ }
499
+ except Exception as exc:
500
+ yield {
501
+ "event": "error",
502
+ "percentage": 0,
503
+ "total_chunks": total_chunks,
504
+ "remaining_chunks": total_chunks,
505
+ "total_bytes": total_size,
506
+ "processed_bytes": downloaded_bytes,
507
+ "speed": 0,
508
+ "message": f"下载过程中发生错误:{exc}",
509
+ "success": False,
510
+ }
511
+ return
512
+
513
+ total_duration = max(time.perf_counter() - download_start, 1e-6)
514
+ result = {
515
+ "total_size": total_size,
516
+ "success": True,
517
+ }
518
+ yield {
519
+ "event": "complete",
520
+ "percentage": 100,
521
+ "total_chunks": total_chunks,
522
+ "remaining_chunks": 0,
523
+ "total_bytes": total_size,
524
+ "processed_bytes": total_size if total_size else downloaded_bytes,
525
+ "speed": (total_size or downloaded_bytes)
526
+ / max(total_duration, 1e-6),
527
+ "message": "下载完成",
528
+ "success": True,
529
+ "result": result,
530
+ }
@@ -0,0 +1,16 @@
1
+ from .unified_login import (
2
+ ThirdPartyAuthUrlRequest,
3
+ ThirdPartyAuthUrlResponse,
4
+ ThirdPartyLoginRequest,
5
+ ThirdPartyLoginResponse,
6
+ UnifiedLoginClientAsync,
7
+ )
8
+
9
+ __all__ = [
10
+ "ThirdPartyAuthUrlRequest",
11
+ "ThirdPartyAuthUrlResponse",
12
+ "ThirdPartyLoginRequest",
13
+ "ThirdPartyLoginResponse",
14
+ "UnifiedLoginClientAsync",
15
+ ]
16
+
@@ -0,0 +1,18 @@
1
+ from client.pypi.pixelarraythirdparty.client import AsyncClient
2
+
3
+
4
+ class GoogleLogin(AsyncClient):
5
+ async def _get_auth_url(self):
6
+ pass
7
+
8
+ async def _get_code_from_redirect_uri(self, redirect_uri: str):
9
+ pass
10
+
11
+ async def _get_user_info(self, token: str) -> dict:
12
+ pass
13
+
14
+ async def login(self) -> dict:
15
+ pass
16
+
17
+ async def logout(self) -> dict:
18
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Summary: PixelArray 第三方微服务客户端
5
5
  Author-email: Lu qi <qi.lu@pixelarrayai.com>
6
6
  License-Expression: MIT
@@ -1,17 +1,19 @@
1
- pixelarraythirdparty/__init__.py,sha256=IaxhSmTdxqDsLwS2tQ9Bg6vqPG1ZSAO_OHTj14jcTbY,473
1
+ pixelarraythirdparty/__init__.py,sha256=4Ow9-OH9uZppYgQhTOYXcxQoApTGNf6b2vdv-wgGcEs,530
2
2
  pixelarraythirdparty/client.py,sha256=hYLLzW8uhw6Qe0rLN1j0qx1HOUMSiclrr6nHjmzJwUw,1305
3
3
  pixelarraythirdparty/cron/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pixelarraythirdparty/cron/cron.py,sha256=2g2Js23X5fINmyq41H3wosDuCbUbHjKqFpBK9EqSEeY,5661
5
5
  pixelarraythirdparty/filestorage/__init__.py,sha256=R4aObBoUs9EuLxNxtcFrouHFoitWdf5MOtXv3A60smk,130
6
- pixelarraythirdparty/filestorage/filestorage.py,sha256=p2unkhtjxMdt_MLN6f10WFNMA9CJtEahZe7PpXySAmU,8893
6
+ pixelarraythirdparty/filestorage/filestorage.py,sha256=MGsGeU57DdK1KW7RPTaIJe2RXxqtXd5wKAanJO9PCj8,18583
7
7
  pixelarraythirdparty/order/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  pixelarraythirdparty/order/order.py,sha256=EamnHxiZqXxDVctb44T6fwWtJhTgLpzgGArxQRSvR14,14178
9
9
  pixelarraythirdparty/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  pixelarraythirdparty/product/product.py,sha256=a6BFUnq0D-s8fVlWV3RKyCsBNE_5sqgwq5W5GmmRNto,10521
11
+ pixelarraythirdparty/unified_login/__init__.py,sha256=JRIBS9vwCXAJWzxTusb2Pt2f5m3u0braB9YeLYJpkEE,351
12
+ pixelarraythirdparty/unified_login/unified_login.py,sha256=OngSCSYL738JJaX9RaxaEq2ilGSkcc7gtbwXxq-0HV4,405
11
13
  pixelarraythirdparty/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
14
  pixelarraythirdparty/user/user.py,sha256=1Dna4-St7AnDZRbjVuOuI84ho5wcnL7Hr3sq8oXo3bo,8383
13
- pixelarraythirdparty-1.0.8.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
14
- pixelarraythirdparty-1.0.8.dist-info/METADATA,sha256=y_iQZnBl2dUb0MO4ZKpMc2l9q5oPcKvZz4PPNJElRlg,993
15
- pixelarraythirdparty-1.0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- pixelarraythirdparty-1.0.8.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
17
- pixelarraythirdparty-1.0.8.dist-info/RECORD,,
15
+ pixelarraythirdparty-1.0.9.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
16
+ pixelarraythirdparty-1.0.9.dist-info/METADATA,sha256=Qk-0DzGTjnXsTe7dFOlS0S0_Ia-XRX5RbMC9ILAA4Mk,993
17
+ pixelarraythirdparty-1.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
18
+ pixelarraythirdparty-1.0.9.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
19
+ pixelarraythirdparty-1.0.9.dist-info/RECORD,,