pixelarraythirdparty 1.2.0__py3-none-any.whl → 1.2.2__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.0"
16
+ __version__ = "1.2.2"
17
17
  __author__ = "Lu qi"
18
18
  __email__ = "qi.lu@pixelarrayai.com"
19
19
 
@@ -1,4 +1,5 @@
1
1
  from .unified_login import (
2
+ OAuth2Login,
2
3
  GoogleLogin,
3
4
  WechatLogin,
4
5
  GitHubLogin,
@@ -9,6 +10,7 @@ from .unified_login import (
9
10
  )
10
11
 
11
12
  __all__ = [
13
+ "OAuth2Login",
12
14
  "GoogleLogin",
13
15
  "WechatLogin",
14
16
  "GitHubLogin",
@@ -6,6 +6,248 @@ from typing import Dict, Optional, Tuple
6
6
  from pixelarraythirdparty.client import AsyncClient
7
7
 
8
8
 
9
+ class OAuth2Login(AsyncClient):
10
+ """
11
+ 统一的 OAuth2 登录客户端基类
12
+
13
+ 支持所有基于 OAuth2 的第三方登录(Google、微信、GitHub、GitLab、抖音等)
14
+ 通过配置不同的端点来支持不同的提供商
15
+
16
+ 使用示例:
17
+ ```
18
+ # Google登录
19
+ google = OAuth2Login(
20
+ api_key="your_api_key",
21
+ provider="google"
22
+ )
23
+ user_info, success = await google.login()
24
+
25
+ # 微信PC端扫码登录
26
+ wechat = OAuth2Login(
27
+ api_key="your_api_key",
28
+ provider="wechat",
29
+ login_type="desktop"
30
+ )
31
+ user_info, success = await wechat.login()
32
+
33
+ # 微信手机端登录
34
+ wechat_mobile = OAuth2Login(
35
+ api_key="your_api_key",
36
+ provider="wechat",
37
+ login_type="mobile"
38
+ )
39
+ user_info, success = await wechat_mobile.login()
40
+
41
+ # 支持refresh_token的提供商(如Google、GitLab)
42
+ if user_info.get("refresh_token"):
43
+ token_data, success = await google.refresh_access_token(
44
+ user_info.get("refresh_token")
45
+ )
46
+ ```
47
+ """
48
+
49
+ # 提供商端点映射
50
+ PROVIDER_ENDPOINTS = {
51
+ "google": {
52
+ "auth_url": "/api/unified-login/google/auth-url",
53
+ "wait_login": "/api/unified-login/google/wait-login",
54
+ "refresh_token": "/api/unified-login/google/refresh-token",
55
+ },
56
+ "wechat": {
57
+ "auth_url": "/api/unified-login/wechat/auth-url",
58
+ "wait_login": "/api/unified-login/wechat/wait-login",
59
+ },
60
+ "wechat-official": {
61
+ "auth_url": "/api/unified-login/wechat-official/auth-url",
62
+ "wait_login": "/api/unified-login/wechat-official/wait-login",
63
+ },
64
+ "github": {
65
+ "auth_url": "/api/unified-login/github/auth-url",
66
+ "wait_login": "/api/unified-login/github/wait-login",
67
+ },
68
+ "gitlab": {
69
+ "auth_url": "/api/unified-login/gitlab/auth-url",
70
+ "wait_login": "/api/unified-login/gitlab/wait-login",
71
+ "refresh_token": "/api/unified-login/gitlab/refresh-token",
72
+ },
73
+ "douyin": {
74
+ "auth_url": "/api/unified-login/douyin/auth-url",
75
+ "wait_login": "/api/unified-login/douyin/wait-login",
76
+ },
77
+ }
78
+
79
+ def __init__(
80
+ self,
81
+ api_key: str,
82
+ provider: str,
83
+ login_type: Optional[str] = None
84
+ ):
85
+ """
86
+ description:
87
+ 初始化OAuth2登录客户端
88
+ parameters:
89
+ api_key(str): API密钥
90
+ provider(str): 提供商名称,可选值:google, wechat, github, gitlab, douyin
91
+ login_type(str, optional): 登录类型,仅对微信有效,可选值:desktop(PC端扫码), mobile(手机端)
92
+ """
93
+ super().__init__(api_key)
94
+ self.provider = provider.lower()
95
+ self.login_type = login_type
96
+
97
+ # 验证提供商是否支持
98
+ if self.provider not in self.PROVIDER_ENDPOINTS:
99
+ raise ValueError(
100
+ f"不支持的提供商: {provider}。"
101
+ f"支持的提供商: {', '.join(self.PROVIDER_ENDPOINTS.keys())}"
102
+ )
103
+
104
+ # 微信特殊处理:根据login_type选择不同的端点
105
+ if self.provider == "wechat":
106
+ if login_type == "mobile":
107
+ self.provider = "wechat-official"
108
+
109
+ def _get_endpoint(self, endpoint_type: str) -> str:
110
+ """
111
+ description:
112
+ 获取指定类型的端点URL
113
+ parameters:
114
+ endpoint_type(str): 端点类型,可选值:auth_url, wait_login, refresh_token
115
+ return:
116
+ endpoint(str): 端点URL
117
+ """
118
+ endpoints = self.PROVIDER_ENDPOINTS.get(self.provider, {})
119
+ endpoint = endpoints.get(endpoint_type)
120
+ if not endpoint:
121
+ raise ValueError(
122
+ f"提供商 {self.provider} 不支持 {endpoint_type} 端点"
123
+ )
124
+ return endpoint
125
+
126
+ async def _get_auth_url(self) -> Tuple[Optional[Dict[str, str]], bool]:
127
+ """
128
+ description:
129
+ 获取OAuth授权URL
130
+ return:
131
+ auth_data(dict, optional): 授权数据字典,包含auth_url和state
132
+ success(bool): 是否成功
133
+ """
134
+ endpoint = self._get_endpoint("auth_url")
135
+ data, success = await self._request("POST", endpoint)
136
+ if not success:
137
+ return None, False
138
+ auth_url = data.get("auth_url")
139
+ if not auth_url:
140
+ return None, False
141
+ return data, True
142
+
143
+ def _extract_state(self, auth_url: Optional[str]) -> Optional[str]:
144
+ """
145
+ description:
146
+ 从授权URL中提取state参数
147
+ parameters:
148
+ auth_url(str, optional): 授权URL
149
+ return:
150
+ state(str, optional): state参数值,如果提取失败则返回None
151
+ """
152
+ if not auth_url:
153
+ return None
154
+ try:
155
+ parsed = urllib.parse.urlparse(auth_url)
156
+ query = urllib.parse.parse_qs(parsed.query)
157
+ values = query.get("state")
158
+ if values:
159
+ return values[0]
160
+ except Exception:
161
+ return None
162
+ return None
163
+
164
+ async def _wait_for_login(
165
+ self, state: str, timeout: int
166
+ ) -> Tuple[Dict, bool]:
167
+ """
168
+ description:
169
+ 等待登录结果,轮询检查登录状态
170
+ parameters:
171
+ state(str): 登录状态标识
172
+ timeout(int): 超时时间(秒)
173
+ return:
174
+ user_info(dict): 用户信息字典
175
+ success(bool): 是否成功
176
+ """
177
+ interval = 2
178
+ total_checks = max(1, timeout // interval) if timeout > 0 else 1
179
+ endpoint = self._get_endpoint("wait_login")
180
+
181
+ for _ in range(total_checks):
182
+ status, response = await self._request_raw(
183
+ "POST",
184
+ endpoint,
185
+ json={"state": state},
186
+ )
187
+
188
+ if status == 200 and response.get("success") is True:
189
+ return response.get("data", {}), True
190
+
191
+ if status in (400, 408):
192
+ break
193
+
194
+ await asyncio.sleep(interval)
195
+
196
+ return {}, False
197
+
198
+ async def login(self, timeout: int = 180) -> Tuple[Dict, bool]:
199
+ """
200
+ description:
201
+ 仿 Supabase CLI 的一键登录流程:打开浏览器完成授权,终端端轮询等待登录结果
202
+ parameters:
203
+ timeout(int, optional): 等待用户完成授权的超时时间(秒),默认为180
204
+ return:
205
+ user_info(dict): 用户信息字典
206
+ success(bool): 是否成功
207
+ """
208
+ auth_data, success = await self._get_auth_url()
209
+ if not success or not auth_data:
210
+ return {}, False
211
+
212
+ auth_url = auth_data.get("auth_url")
213
+ state = auth_data.get("state") or self._extract_state(auth_url)
214
+
215
+ if not auth_url or not state:
216
+ return {}, False
217
+
218
+ webbrowser.open(auth_url, new=2)
219
+
220
+ return await self._wait_for_login(state, timeout)
221
+
222
+ async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
223
+ """
224
+ description:
225
+ 使用refresh_token刷新access_token(仅支持Google和GitLab)
226
+
227
+ 注意:GitLab 采用 token rotation(令牌轮换)机制,每次刷新时会返回新的 refresh_token,
228
+ 旧的 refresh_token 会立即失效。必须保存新的 refresh_token 并替换旧的。
229
+ Google 也可能在某些情况下返回新的 refresh_token。
230
+ parameters:
231
+ refresh_token(str): OAuth refresh_token
232
+ return:
233
+ token_data(dict): 包含新的access_token和可能的refresh_token的字典
234
+ success(bool): 是否成功
235
+ """
236
+ try:
237
+ endpoint = self._get_endpoint("refresh_token")
238
+ except ValueError:
239
+ return {}, False
240
+
241
+ data, success = await self._request(
242
+ "POST",
243
+ endpoint,
244
+ json={"refresh_token": refresh_token},
245
+ )
246
+ if not success:
247
+ return {}, False
248
+ return data, True
249
+
250
+
9
251
  class GoogleLogin(AsyncClient):
10
252
  """
11
253
  Google OAuth2 登录客户端
@@ -574,6 +816,28 @@ class GitLabLogin(AsyncClient):
574
816
 
575
817
  return {}, False
576
818
 
819
+ async def refresh_access_token(self, refresh_token: str) -> Tuple[Dict, bool]:
820
+ """
821
+ description:
822
+ 使用refresh_token刷新access_token
823
+
824
+ 注意:GitLab 采用 token rotation(令牌轮换)机制,每次刷新时会返回新的 refresh_token,
825
+ 旧的 refresh_token 会立即失效。必须保存新的 refresh_token 并替换旧的。
826
+ parameters:
827
+ refresh_token(str): GitLab OAuth refresh_token
828
+ return:
829
+ token_data(dict): 包含新的access_token和可能的refresh_token的字典
830
+ success(bool): 是否成功
831
+ """
832
+ data, success = await self._request(
833
+ "POST",
834
+ "/api/unified-login/gitlab/refresh-token",
835
+ json={"refresh_token": refresh_token},
836
+ )
837
+ if not success:
838
+ return {}, False
839
+ return data, True
840
+
577
841
 
578
842
  class SMSLogin(AsyncClient):
579
843
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelarraythirdparty
3
- Version: 1.2.0
3
+ Version: 1.2.2
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=CavOkBWTHTd6_ND5ztZNmYWhOESL2XpC9XIdrwSS2Pk,582
1
+ pixelarraythirdparty/__init__.py,sha256=nJENJpeEcGI9wm3q8bM__BslEPtmFVRN3-ZqeV_0Bd4,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=_PeHBsyrPLfcNjbdcsBgQ_75fr1CoRZCNu7S33fsh_Y,291
14
- pixelarraythirdparty/unified_login/unified_login.py,sha256=A_VBbtIR5clT5HoP56w8cOjOy9T_QfM7s3m9zpxpYLw,23468
13
+ pixelarraythirdparty/unified_login/__init__.py,sha256=tzy3nmRv-qZID-6kYRFarqQVrmkUsEI7D6de_PFu7Tg,327
14
+ pixelarraythirdparty/unified_login/unified_login.py,sha256=Klz8Jb-8qIy6Sc1okQS7SQe_ByS0CpO_AvbDnLJnUwM,32700
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.0.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
18
- pixelarraythirdparty-1.2.0.dist-info/METADATA,sha256=NUdsnHIXVRU9OPFzt50AyoLkcGyWQZphj7oSOiIQxhM,993
19
- pixelarraythirdparty-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- pixelarraythirdparty-1.2.0.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
21
- pixelarraythirdparty-1.2.0.dist-info/RECORD,,
17
+ pixelarraythirdparty-1.2.2.dist-info/licenses/LICENSE,sha256=O-g1dUr0U50rSIvmWE9toiVkSgFpVt72_MHITbWvAqA,1067
18
+ pixelarraythirdparty-1.2.2.dist-info/METADATA,sha256=TC042i-Yca5TeDXqu5xereatr-bwINhP_9xLoTpi6qE,993
19
+ pixelarraythirdparty-1.2.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ pixelarraythirdparty-1.2.2.dist-info/top_level.txt,sha256=dzG2Ut8j7noUqj_0ZQjcIDAeHYCh_9WtlxjAxtoyufo,21
21
+ pixelarraythirdparty-1.2.2.dist-info/RECORD,,