pixelarraythirdparty 1.2.3__py3-none-any.whl → 1.2.5__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.
@@ -13,7 +13,7 @@ PixelArray 第三方微服务客户端
13
13
  - project: 项目管理模块
14
14
  """
15
15
 
16
- __version__ = "1.2.3"
16
+ __version__ = "1.2.5"
17
17
  __author__ = "Lu qi"
18
18
  __email__ = "qi.lu@pixelarrayai.com"
19
19
 
@@ -4,6 +4,7 @@ from .unified_login import (
4
4
  WechatLogin,
5
5
  GitHubLogin,
6
6
  DouyinLogin,
7
+ TiktokLogin,
7
8
  GitLabLogin,
8
9
  SMSLogin,
9
10
  EmailLogin,
@@ -15,6 +16,7 @@ __all__ = [
15
16
  "WechatLogin",
16
17
  "GitHubLogin",
17
18
  "DouyinLogin",
19
+ "TiktokLogin",
18
20
  "GitLabLogin",
19
21
  "SMSLogin",
20
22
  "EmailLogin",
@@ -9,57 +9,29 @@ from pixelarraythirdparty.client import AsyncClient
9
9
  class OAuth2Login(AsyncClient):
10
10
  """
11
11
  统一的 OAuth2 登录客户端基类
12
-
13
- 支持所有基于 OAuth2 的第三方登录(Google、微信、GitHub、GitLab、抖音等)
12
+
13
+ 支持所有基于 OAuth2 的第三方登录(Google、微信、GitHub、GitLab、抖音、TikTok等)
14
14
  通过配置不同的端点来支持不同的提供商
15
-
15
+
16
16
  使用示例:
17
17
  ```
18
18
  # 服务端使用场景(推荐)
19
- google = OAuth2Login(
19
+ oauth2_login = OAuth2Login(
20
20
  api_key="your_api_key",
21
- provider="google"
21
+ provider="google" # 支持的提供商:google, wechat, github, gitlab, douyin
22
22
  )
23
23
  # 1. 获取授权URL
24
- auth_data, success = await google.get_auth_url()
24
+ auth_data, success = await oauth2_login.get_auth_url()
25
25
  if success:
26
26
  auth_url = auth_data.get("auth_url")
27
27
  state = auth_data.get("state")
28
28
  # 将auth_url返回给前端,让用户点击授权
29
29
  # 2. 等待登录结果(在服务端轮询)
30
- user_info, success = await google.wait_for_login(state, timeout=180)
31
-
32
- # 测试场景(仅用于本地测试,会打开浏览器)
33
- google = OAuth2Login(
34
- api_key="your_api_key",
35
- provider="google"
36
- )
37
- user_info, success = await google.login()
38
-
39
- # 微信PC端扫码登录
40
- wechat = OAuth2Login(
41
- api_key="your_api_key",
42
- provider="wechat",
43
- login_type="desktop"
44
- )
45
- user_info, success = await wechat.login()
46
-
47
- # 微信手机端登录
48
- wechat_mobile = OAuth2Login(
49
- api_key="your_api_key",
50
- provider="wechat",
51
- login_type="mobile"
52
- )
53
- user_info, success = await wechat_mobile.login()
54
-
55
- # 支持refresh_token的提供商(如Google、GitLab)
56
- if user_info.get("refresh_token"):
57
- token_data, success = await google.refresh_access_token(
58
- user_info.get("refresh_token")
59
- )
30
+ user_info, success = await oauth2_login.wait_for_login(state, timeout=180)
31
+ print(user_info, success)
60
32
  ```
61
33
  """
62
-
34
+
63
35
  # 提供商端点映射
64
36
  PROVIDER_ENDPOINTS = {
65
37
  "google": {
@@ -88,38 +60,37 @@ class OAuth2Login(AsyncClient):
88
60
  "auth_url": "/api/unified-login/douyin/auth-url",
89
61
  "wait_login": "/api/unified-login/douyin/wait-login",
90
62
  },
63
+ "tiktok": {
64
+ "auth_url": "/api/unified-login/tiktok/auth-url",
65
+ "wait_login": "/api/unified-login/tiktok/wait-login",
66
+ },
91
67
  }
92
-
93
- def __init__(
94
- self,
95
- api_key: str,
96
- provider: str,
97
- login_type: Optional[str] = None
98
- ):
68
+
69
+ def __init__(self, api_key: str, provider: str, login_type: Optional[str] = None):
99
70
  """
100
71
  description:
101
72
  初始化OAuth2登录客户端
102
73
  parameters:
103
74
  api_key(str): API密钥
104
- provider(str): 提供商名称,可选值:google, wechat, github, gitlab, douyin
75
+ provider(str): 提供商名称,可选值:google, wechat, github, gitlab, douyin, tiktok
105
76
  login_type(str, optional): 登录类型,仅对微信有效,可选值:desktop(PC端扫码), mobile(手机端)
106
77
  """
107
78
  super().__init__(api_key)
108
79
  self.provider = provider.lower()
109
80
  self.login_type = login_type
110
-
81
+
111
82
  # 验证提供商是否支持
112
83
  if self.provider not in self.PROVIDER_ENDPOINTS:
113
84
  raise ValueError(
114
85
  f"不支持的提供商: {provider}。"
115
86
  f"支持的提供商: {', '.join(self.PROVIDER_ENDPOINTS.keys())}"
116
87
  )
117
-
88
+
118
89
  # 微信特殊处理:根据login_type选择不同的端点
119
90
  if self.provider == "wechat":
120
91
  if login_type == "mobile":
121
92
  self.provider = "wechat-official"
122
-
93
+
123
94
  def _get_endpoint(self, endpoint_type: str) -> str:
124
95
  """
125
96
  description:
@@ -132,16 +103,14 @@ class OAuth2Login(AsyncClient):
132
103
  endpoints = self.PROVIDER_ENDPOINTS.get(self.provider, {})
133
104
  endpoint = endpoints.get(endpoint_type)
134
105
  if not endpoint:
135
- raise ValueError(
136
- f"提供商 {self.provider} 不支持 {endpoint_type} 端点"
137
- )
106
+ raise ValueError(f"提供商 {self.provider} 不支持 {endpoint_type} 端点")
138
107
  return endpoint
139
-
108
+
140
109
  async def get_auth_url(self) -> Tuple[Optional[Dict[str, str]], bool]:
141
110
  """
142
111
  description:
143
112
  获取OAuth授权URL(公共方法,供服务端调用)
144
-
113
+
145
114
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
146
115
  获取到state后,使用wait_for_login方法等待登录结果。
147
116
  return:
@@ -156,35 +125,12 @@ class OAuth2Login(AsyncClient):
156
125
  if not auth_url:
157
126
  return None, False
158
127
  return data, True
159
-
160
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
161
- """
162
- description:
163
- 从授权URL中提取state参数
164
- parameters:
165
- auth_url(str, optional): 授权URL
166
- return:
167
- state(str, optional): state参数值,如果提取失败则返回None
168
- """
169
- if not auth_url:
170
- return None
171
- try:
172
- parsed = urllib.parse.urlparse(auth_url)
173
- query = urllib.parse.parse_qs(parsed.query)
174
- values = query.get("state")
175
- if values:
176
- return values[0]
177
- except Exception:
178
- return None
179
- return None
180
-
181
- async def wait_for_login(
182
- self, state: str, timeout: int = 180
183
- ) -> Tuple[Dict, bool]:
128
+
129
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
184
130
  """
185
131
  description:
186
132
  等待登录结果,轮询检查登录状态(公共方法,供服务端调用)
187
-
133
+
188
134
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
189
135
  此方法会持续轮询直到登录成功或超时。
190
136
  parameters:
@@ -197,56 +143,29 @@ class OAuth2Login(AsyncClient):
197
143
  interval = 2
198
144
  total_checks = max(1, timeout // interval) if timeout > 0 else 1
199
145
  endpoint = self._get_endpoint("wait_login")
200
-
146
+
201
147
  for _ in range(total_checks):
202
148
  status, response = await self._request_raw(
203
149
  "POST",
204
150
  endpoint,
205
151
  json={"state": state},
206
152
  )
207
-
153
+
208
154
  if status == 200 and response.get("success") is True:
209
155
  return response.get("data", {}), True
210
-
156
+
211
157
  if status in (400, 408):
212
158
  break
213
-
159
+
214
160
  await asyncio.sleep(interval)
215
-
161
+
216
162
  return {}, False
217
-
218
- async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
219
- """
220
- description:
221
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
222
-
223
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
224
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
225
- parameters:
226
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
227
- return:
228
- user_info(dict): 用户信息字典
229
- success(bool): 是否成功
230
- """
231
- auth_data, success = await self.get_auth_url()
232
- if not success or not auth_data:
233
- return {}, False
234
-
235
- auth_url = auth_data.get("auth_url")
236
- state = auth_data.get("state") or self._extract_state(auth_url)
237
-
238
- if not auth_url or not state:
239
- return {}, False
240
-
241
- webbrowser.open(auth_url, new=2)
242
-
243
- return await self.wait_for_login(state, timeout)
244
-
163
+
245
164
  async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
246
165
  """
247
166
  description:
248
167
  使用refresh_token刷新access_token(仅支持Google和GitLab)
249
-
168
+
250
169
  注意:GitLab 采用 token rotation(令牌轮换)机制,每次刷新时会返回新的 refresh_token,
251
170
  旧的 refresh_token 会立即失效。必须保存新的 refresh_token 并替换旧的。
252
171
  Google 也可能在某些情况下返回新的 refresh_token。
@@ -260,7 +179,7 @@ class OAuth2Login(AsyncClient):
260
179
  endpoint = self._get_endpoint("refresh_token")
261
180
  except ValueError:
262
181
  return {}, False
263
-
182
+
264
183
  data, success = await self._request(
265
184
  "POST",
266
185
  endpoint,
@@ -290,16 +209,6 @@ class GoogleLogin(AsyncClient):
290
209
  if success:
291
210
  access_token = user_info.get("access_token")
292
211
  refresh_token = user_info.get("refresh_token")
293
-
294
- # 使用refresh_token刷新access_token
295
- if refresh_token:
296
- token_data, success = await google.refresh_access_token(refresh_token)
297
- if success:
298
- new_access_token = token_data.get("access_token")
299
-
300
- # 测试场景(仅用于本地测试,会打开浏览器)
301
- google = GoogleLogin(api_key="your_api_key")
302
- user_info, success = await google.login()
303
212
  ```
304
213
  """
305
214
 
@@ -310,7 +219,7 @@ class GoogleLogin(AsyncClient):
310
219
  """
311
220
  description:
312
221
  获取Google OAuth授权URL(公共方法,供服务端调用)
313
-
222
+
314
223
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
315
224
  获取到state后,使用wait_for_login方法等待登录结果。
316
225
  return:
@@ -327,53 +236,11 @@ class GoogleLogin(AsyncClient):
327
236
  return None, False
328
237
  return data, True
329
238
 
330
- async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
331
- """
332
- description:
333
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
334
-
335
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
336
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
337
- parameters:
338
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
339
- return:
340
- user_info(dict): 用户信息字典
341
- success(bool): 是否成功
342
- """
343
- auth_data, success = await self.get_auth_url()
344
- if not success or not auth_data:
345
- return {}, False
346
-
347
- auth_url = auth_data.get("auth_url")
348
- state = auth_data.get("state") or self._extract_state(auth_url)
349
-
350
- if not auth_url or not state:
351
- return {}, False
352
-
353
- webbrowser.open(auth_url, new=2)
354
-
355
- return await self.wait_for_login(state, timeout)
356
-
357
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
358
- if not auth_url:
359
- return None
360
- try:
361
- parsed = urllib.parse.urlparse(auth_url)
362
- query = urllib.parse.parse_qs(parsed.query)
363
- values = query.get("state")
364
- if values:
365
- return values[0]
366
- except Exception:
367
- return None
368
- return None
369
-
370
- async def wait_for_login(
371
- self, state: str, timeout: int = 180
372
- ) -> Tuple[Dict, bool]:
239
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
373
240
  """
374
241
  description:
375
242
  等待Google登录结果,轮询检查登录状态(公共方法,供服务端调用)
376
-
243
+
377
244
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
378
245
  此方法会持续轮询直到登录成功或超时。
379
246
  parameters:
@@ -426,7 +293,7 @@ class GoogleLogin(AsyncClient):
426
293
  class WechatLogin(AsyncClient):
427
294
  """
428
295
  微信 OAuth2 登录客户端
429
-
296
+
430
297
  支持两种登录方式:
431
298
  - desktop: PC端扫码登录(使用微信开放平台)
432
299
  - mobile: 微信公众号OAuth登录(手机端微信内打开)
@@ -443,7 +310,8 @@ class WechatLogin(AsyncClient):
443
310
  # 将auth_url返回给前端,让用户扫码授权
444
311
  # 等待登录结果
445
312
  user_info, success = await wechat.wait_for_login(state, timeout=180, login_type="desktop")
446
-
313
+ print(user_info, success)
314
+
447
315
  # 微信公众号登录(手机端)
448
316
  auth_data, success = await wechat.get_auth_url(login_type="mobile")
449
317
  if success:
@@ -452,21 +320,20 @@ class WechatLogin(AsyncClient):
452
320
  # 将auth_url返回给前端,让用户在微信内打开授权
453
321
  # 等待登录结果
454
322
  user_info, success = await wechat.wait_for_login(state, timeout=180, login_type="mobile")
455
-
456
- # 测试场景(仅用于本地测试,会打开浏览器)
457
- wechat = WechatLogin(api_key="your_api_key")
458
- user_info, success = await wechat.login(login_type="desktop")
323
+ print(user_info, success)
459
324
  ```
460
325
  """
461
326
 
462
327
  def __init__(self, api_key: str):
463
328
  super().__init__(api_key)
464
329
 
465
- async def get_auth_url(self, login_type: str = "desktop") -> Tuple[Optional[Dict[str, str]], bool]:
330
+ async def get_auth_url(
331
+ self, login_type: str = "desktop"
332
+ ) -> Tuple[Optional[Dict[str, str]], bool]:
466
333
  """
467
334
  description:
468
335
  获取微信授权URL(公共方法,供服务端调用)
469
-
336
+
470
337
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
471
338
  获取到state后,使用wait_for_login方法等待登录结果。
472
339
  parameters:
@@ -481,7 +348,7 @@ class WechatLogin(AsyncClient):
481
348
  else:
482
349
  # PC端扫码登录
483
350
  endpoint = "/api/unified-login/wechat/auth-url"
484
-
351
+
485
352
  data, success = await self._request("POST", endpoint)
486
353
  if not success:
487
354
  return None, False
@@ -490,62 +357,13 @@ class WechatLogin(AsyncClient):
490
357
  return None, False
491
358
  return data, True
492
359
 
493
- async def login(self, login_type: str = "desktop", timeout: int = 180) -> Tuple[Dict, bool]:
494
- """
495
- description:
496
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
497
-
498
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
499
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
500
- parameters:
501
- login_type(str, optional): 登录类型,desktop表示PC端扫码登录,mobile表示微信公众号登录,默认为desktop
502
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
503
- return:
504
- user_info(dict): 用户信息字典
505
- success(bool): 是否成功
506
- """
507
- auth_data, success = await self.get_auth_url(login_type)
508
- if not success or not auth_data:
509
- return {}, False
510
-
511
- auth_url = auth_data.get("auth_url")
512
- state = auth_data.get("state") or self._extract_state(auth_url)
513
-
514
- if not auth_url or not state:
515
- return {}, False
516
-
517
- webbrowser.open(auth_url, new=2)
518
-
519
- return await self.wait_for_login(state, timeout, login_type)
520
-
521
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
522
- """
523
- description:
524
- 从授权URL中提取state参数
525
- parameters:
526
- auth_url(str, optional): 授权URL
527
- return:
528
- state(str, optional): state参数值,如果提取失败则返回None
529
- """
530
- if not auth_url:
531
- return None
532
- try:
533
- parsed = urllib.parse.urlparse(auth_url)
534
- query = urllib.parse.parse_qs(parsed.query)
535
- values = query.get("state")
536
- if values:
537
- return values[0]
538
- except Exception:
539
- return None
540
- return None
541
-
542
360
  async def wait_for_login(
543
361
  self, state: str, timeout: int = 180, login_type: str = "desktop"
544
362
  ) -> Tuple[Dict, bool]:
545
363
  """
546
364
  description:
547
365
  等待微信登录结果,轮询检查登录状态(公共方法,供服务端调用)
548
-
366
+
549
367
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
550
368
  此方法会持续轮询直到登录成功或超时。
551
369
  parameters:
@@ -599,10 +417,7 @@ class GitHubLogin(AsyncClient):
599
417
  # 将auth_url返回给前端,让用户点击授权
600
418
  # 2. 等待登录结果(在服务端轮询)
601
419
  user_info, success = await github.wait_for_login(state, timeout=180)
602
-
603
- # 测试场景(仅用于本地测试,会打开浏览器)
604
- github = GitHubLogin(api_key="your_api_key")
605
- user_info, success = await github.login()
420
+ print(user_info, success)
606
421
  ```
607
422
  """
608
423
 
@@ -613,7 +428,7 @@ class GitHubLogin(AsyncClient):
613
428
  """
614
429
  description:
615
430
  获取GitHub OAuth授权URL(公共方法,供服务端调用)
616
-
431
+
617
432
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
618
433
  获取到state后,使用wait_for_login方法等待登录结果。
619
434
  return:
@@ -630,61 +445,11 @@ class GitHubLogin(AsyncClient):
630
445
  return None, False
631
446
  return data, True
632
447
 
633
- async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
634
- """
635
- description:
636
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
637
-
638
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
639
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
640
- parameters:
641
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
642
- return:
643
- user_info(dict): 用户信息字典
644
- success(bool): 是否成功
645
- """
646
- auth_data, success = await self.get_auth_url()
647
- if not success or not auth_data:
648
- return {}, False
649
-
650
- auth_url = auth_data.get("auth_url")
651
- state = auth_data.get("state") or self._extract_state(auth_url)
652
-
653
- if not auth_url or not state:
654
- return {}, False
655
-
656
- webbrowser.open(auth_url, new=2)
657
-
658
- return await self.wait_for_login(state, timeout)
659
-
660
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
661
- """
662
- description:
663
- 从授权URL中提取state参数
664
- parameters:
665
- auth_url(str, optional): 授权URL
666
- return:
667
- state(str, optional): state参数值,如果提取失败则返回None
668
- """
669
- if not auth_url:
670
- return None
671
- try:
672
- parsed = urllib.parse.urlparse(auth_url)
673
- query = urllib.parse.parse_qs(parsed.query)
674
- values = query.get("state")
675
- if values:
676
- return values[0]
677
- except Exception:
678
- return None
679
- return None
680
-
681
- async def wait_for_login(
682
- self, state: str, timeout: int = 180
683
- ) -> Tuple[Dict, bool]:
448
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
684
449
  """
685
450
  description:
686
451
  等待GitHub登录结果,轮询检查登录状态(公共方法,供服务端调用)
687
-
452
+
688
453
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
689
454
  此方法会持续轮询直到登录成功或超时。
690
455
  parameters:
@@ -731,10 +496,7 @@ class DouyinLogin(AsyncClient):
731
496
  # 将auth_url返回给前端,让用户点击授权
732
497
  # 2. 等待登录结果(在服务端轮询)
733
498
  user_info, success = await douyin.wait_for_login(state, timeout=180)
734
-
735
- # 测试场景(仅用于本地测试,会打开浏览器)
736
- douyin = DouyinLogin(api_key="your_api_key")
737
- user_info, success = await douyin.login()
499
+ print(user_info, success)
738
500
  ```
739
501
  """
740
502
 
@@ -745,7 +507,7 @@ class DouyinLogin(AsyncClient):
745
507
  """
746
508
  description:
747
509
  获取抖音OAuth授权URL(公共方法,供服务端调用)
748
-
510
+
749
511
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
750
512
  获取到state后,使用wait_for_login方法等待登录结果。
751
513
  return:
@@ -762,61 +524,90 @@ class DouyinLogin(AsyncClient):
762
524
  return None, False
763
525
  return data, True
764
526
 
765
- async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
527
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
766
528
  """
767
529
  description:
768
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
769
-
770
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
771
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
530
+ 等待抖音登录结果,轮询检查登录状态(公共方法,供服务端调用)
531
+
532
+ 服务端在用户点击授权后,调用此方法轮询等待登录结果。
533
+ 此方法会持续轮询直到登录成功或超时。
772
534
  parameters:
773
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
535
+ state(str): 登录状态标识,从get_auth_url返回的auth_data中获取
536
+ timeout(int, optional): 超时时间(秒),默认为180
774
537
  return:
775
538
  user_info(dict): 用户信息字典
776
539
  success(bool): 是否成功
777
540
  """
778
- auth_data, success = await self.get_auth_url()
779
- if not success or not auth_data:
780
- return {}, False
541
+ interval = 2
542
+ total_checks = max(1, timeout // interval) if timeout > 0 else 1
781
543
 
782
- auth_url = auth_data.get("auth_url")
783
- state = auth_data.get("state") or self._extract_state(auth_url)
544
+ for _ in range(total_checks):
545
+ status, response = await self._request_raw(
546
+ "POST",
547
+ "/api/unified-login/douyin/wait-login",
548
+ json={"state": state},
549
+ )
784
550
 
785
- if not auth_url or not state:
786
- return {}, False
551
+ if status == 200 and response.get("success") is True:
552
+ return response.get("data", {}), True
787
553
 
788
- webbrowser.open(auth_url, new=2)
554
+ if status in (400, 408):
555
+ break
556
+
557
+ await asyncio.sleep(interval)
558
+
559
+ return {}, False
560
+
561
+
562
+ class TiktokLogin(AsyncClient):
563
+ """
564
+ TikTok OAuth2 登录客户端
565
+
566
+ 使用示例:
567
+ ```
568
+ # 服务端使用场景(推荐)
569
+ tiktok = TiktokLogin(api_key="your_api_key")
570
+ # 1. 获取授权URL
571
+ auth_data, success = await tiktok.get_auth_url()
572
+ if success:
573
+ auth_url = auth_data.get("auth_url")
574
+ state = auth_data.get("state")
575
+ # 将auth_url返回给前端,让用户点击授权
576
+ # 2. 等待登录结果(在服务端轮询)
577
+ user_info, success = await tiktok.wait_for_login(state, timeout=180)
578
+ print(user_info, success)
579
+ ```
580
+ """
789
581
 
790
- return await self.wait_for_login(state, timeout)
582
+ def __init__(self, api_key: str):
583
+ super().__init__(api_key)
791
584
 
792
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
585
+ async def get_auth_url(self) -> Tuple[Optional[Dict[str, str]], bool]:
793
586
  """
794
587
  description:
795
- 从授权URL中提取state参数
796
- parameters:
797
- auth_url(str, optional): 授权URL
588
+ 获取TikTok OAuth授权URL(公共方法,供服务端调用)
589
+
590
+ 服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
591
+ 获取到state后,使用wait_for_login方法等待登录结果。
798
592
  return:
799
- state(str, optional): state参数值,如果提取失败则返回None
593
+ auth_data(dict, optional): 授权数据字典,包含auth_url和state
594
+ success(bool): 是否成功
800
595
  """
596
+ data, success = await self._request(
597
+ "POST", "/api/unified-login/tiktok/auth-url"
598
+ )
599
+ if not success:
600
+ return None, False
601
+ auth_url = data.get("auth_url")
801
602
  if not auth_url:
802
- return None
803
- try:
804
- parsed = urllib.parse.urlparse(auth_url)
805
- query = urllib.parse.parse_qs(parsed.query)
806
- values = query.get("state")
807
- if values:
808
- return values[0]
809
- except Exception:
810
- return None
811
- return None
603
+ return None, False
604
+ return data, True
812
605
 
813
- async def wait_for_login(
814
- self, state: str, timeout: int = 180
815
- ) -> Tuple[Dict, bool]:
606
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
816
607
  """
817
608
  description:
818
- 等待抖音登录结果,轮询检查登录状态(公共方法,供服务端调用)
819
-
609
+ 等待TikTok登录结果,轮询检查登录状态(公共方法,供服务端调用)
610
+
820
611
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
821
612
  此方法会持续轮询直到登录成功或超时。
822
613
  parameters:
@@ -832,7 +623,7 @@ class DouyinLogin(AsyncClient):
832
623
  for _ in range(total_checks):
833
624
  status, response = await self._request_raw(
834
625
  "POST",
835
- "/api/unified-login/douyin/wait-login",
626
+ "/api/unified-login/tiktok/wait-login",
836
627
  json={"state": state},
837
628
  )
838
629
 
@@ -863,13 +654,7 @@ class GitLabLogin(AsyncClient):
863
654
  # 将auth_url返回给前端,让用户点击授权
864
655
  # 2. 等待登录结果(在服务端轮询)
865
656
  user_info, success = await gitlab.wait_for_login(state, timeout=180)
866
- if success and user_info.get("refresh_token"):
867
- # 使用refresh_token刷新access_token
868
- token_data, success = await gitlab.refresh_access_token(user_info.get("refresh_token"))
869
-
870
- # 测试场景(仅用于本地测试,会打开浏览器)
871
- gitlab = GitLabLogin(api_key="your_api_key")
872
- user_info, success = await gitlab.login()
657
+ print(user_info, success)
873
658
  ```
874
659
  """
875
660
 
@@ -880,7 +665,7 @@ class GitLabLogin(AsyncClient):
880
665
  """
881
666
  description:
882
667
  获取GitLab OAuth授权URL(公共方法,供服务端调用)
883
-
668
+
884
669
  服务端应该调用此方法获取授权URL,然后将URL返回给前端让用户点击授权。
885
670
  获取到state后,使用wait_for_login方法等待登录结果。
886
671
  return:
@@ -897,53 +682,11 @@ class GitLabLogin(AsyncClient):
897
682
  return None, False
898
683
  return data, True
899
684
 
900
- async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
901
- """
902
- description:
903
- 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
904
-
905
- 注意:此方法仅用于本地测试场景,会自动打开浏览器。
906
- 在生产环境的服务端场景中,应该使用get_auth_url和wait_for_login方法。
907
- parameters:
908
- timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
909
- return:
910
- user_info(dict): 用户信息字典
911
- success(bool): 是否成功
912
- """
913
- auth_data, success = await self.get_auth_url()
914
- if not success or not auth_data:
915
- return {}, False
916
-
917
- auth_url = auth_data.get("auth_url")
918
- state = auth_data.get("state") or self._extract_state(auth_url)
919
-
920
- if not auth_url or not state:
921
- return {}, False
922
-
923
- webbrowser.open(auth_url, new=2)
924
-
925
- return await self.wait_for_login(state, timeout)
926
-
927
- def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
928
- if not auth_url:
929
- return None
930
- try:
931
- parsed = urllib.parse.urlparse(auth_url)
932
- query = urllib.parse.parse_qs(parsed.query)
933
- values = query.get("state")
934
- if values:
935
- return values[0]
936
- except Exception:
937
- return None
938
- return None
939
-
940
- async def wait_for_login(
941
- self, state: str, timeout: int = 180
942
- ) -> Tuple[Dict, bool]:
685
+ async def wait_for_login(self, state: str, timeout: int = 180) -> Tuple[Dict, bool]:
943
686
  """
944
687
  description:
945
688
  等待GitLab登录结果,轮询检查登录状态(公共方法,供服务端调用)
946
-
689
+
947
690
  服务端在用户点击授权后,调用此方法轮询等待登录结果。
948
691
  此方法会持续轮询直到登录成功或超时。
949
692
  parameters:
@@ -977,7 +720,7 @@ class GitLabLogin(AsyncClient):
977
720
  """
978
721
  description:
979
722
  使用refresh_token刷新access_token
980
-
723
+
981
724
  注意:GitLab 采用 token rotation(令牌轮换)机制,每次刷新时会返回新的 refresh_token,
982
725
  旧的 refresh_token 会立即失效。必须保存新的 refresh_token 并替换旧的。
983
726
  parameters:
@@ -1041,9 +784,7 @@ class SMSLogin(AsyncClient):
1041
784
  )
1042
785
  return bool(success)
1043
786
 
1044
- async def _wait_for_login(
1045
- self, phone: str, timeout: int
1046
- ) -> Tuple[Dict, bool]:
787
+ async def _wait_for_login(self, phone: str, timeout: int) -> Tuple[Dict, bool]:
1047
788
  """
1048
789
  等待短信登录结果
1049
790
 
@@ -1133,9 +874,7 @@ class EmailLogin(AsyncClient):
1133
874
  )
1134
875
  return bool(success)
1135
876
 
1136
- async def _wait_for_login(
1137
- self, email: str, timeout: int
1138
- ) -> Tuple[Dict, bool]:
877
+ async def _wait_for_login(self, email: str, timeout: int) -> Tuple[Dict, bool]:
1139
878
  """
1140
879
  等待邮箱登录结果
1141
880
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: PixelArray 第三方微服务客户端
5
5
  Author-email: Lu qi <qi.lu@pixelarrayai.com>
6
6
  License-Expression: MIT
@@ -1,4 +1,4 @@
1
- pixelarraythirdparty/__init__.py,sha256=zQsr327IH9uZgfbsXIpwvP6B_KJltOQFfaWJ5NqGHAo,582
1
+ pixelarraythirdparty/__init__.py,sha256=1wu0ftHnyYYWJvp1G00K3Y5GskiADKukHYRQIwrEGxA,582
2
2
  pixelarraythirdparty/client.py,sha256=Ym_IZ6cdoZJoMKW61ZRTfQ81-Hay5dpLTgExoANZwI4,3171
3
3
  pixelarraythirdparty/cron/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pixelarraythirdparty/cron/cron.py,sha256=nv4e2hX_UkEJ-kbEARbInU2J6aREyYZ61dZ-4b9UWJI,3146
@@ -10,12 +10,12 @@ pixelarraythirdparty/product/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
10
10
  pixelarraythirdparty/product/product.py,sha256=5fgv2Ck860epYXxipY83vePziubCIlocFu43mGt_bhM,7497
11
11
  pixelarraythirdparty/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  pixelarraythirdparty/project/project.py,sha256=a8swjckyn4y3NlIx0-tMbcRwDY9woOijGi7lb1wN7Ok,2455
13
- pixelarraythirdparty/unified_login/__init__.py,sha256=tzy3nmRv-qZID-6kYRFarqQVrmkUsEI7D6de_PFu7Tg,327
14
- pixelarraythirdparty/unified_login/unified_login.py,sha256=A1Sl0g0s9MrBdSmrFiui2r15sA0Xd17iTQIUfJsZEFQ,41483
13
+ pixelarraythirdparty/unified_login/__init__.py,sha256=w1KfgvH7aplOUGcHMyo391Yls-Qw5pKIXrBv2npyes0,363
14
+ pixelarraythirdparty/unified_login/unified_login.py,sha256=HRx5qcuO2jb7UDdTkAOtrlbc6wZgo4BBcSglo9awAJM,31945
15
15
  pixelarraythirdparty/user/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  pixelarraythirdparty/user/user.py,sha256=SqufSAVMxQElz-NqtlOZs_dG7UtbuE-ygLAyml9oKpY,5761
17
- pixelarraythirdparty-1.2.3.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
18
- pixelarraythirdparty-1.2.3.dist-info/METADATA,sha256=_TNjpH-NiF7st_CYAxM9Gz8LWs01m-Prlzjw7vZ4708,993
19
- pixelarraythirdparty-1.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- pixelarraythirdparty-1.2.3.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
21
- pixelarraythirdparty-1.2.3.dist-info/RECORD,,
17
+ pixelarraythirdparty-1.2.5.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
18
+ pixelarraythirdparty-1.2.5.dist-info/METADATA,sha256=3_xcei9sv8zaCaaY-5PRDkUWknZuIZl55znXfmDIK_A,993
19
+ pixelarraythirdparty-1.2.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ pixelarraythirdparty-1.2.5.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
21
+ pixelarraythirdparty-1.2.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5