USTB-SSO 1.0.0__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.
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.1
2
+ Name: USTB-SSO
3
+ Version: 1.0.0
4
+ Summary: USTB Single Sign-On Authentication Library
5
+ License: MIT
6
+ Author: Harry Huang
7
+ Author-email: harryhuang2652@qq.com
8
+ Requires-Python: >=3.8,<4
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Provides-Extra: httpx
19
+ Requires-Dist: httpx (>=0.28,<0.29) ; extra == "httpx"
20
+ Description-Content-Type: text/markdown
21
+
22
+ USTB-SSO (Py)
23
+ ==========
24
+ USTB Single Sign-On Authentication Library (Python)
25
+ 北京科技大学单点登录(SSO)身份认证实现库(Python)
26
+
27
+ > This module is the Python implementation of [USTB-SSO](https://github.com/isHarryh/USTB-SSO).
28
+ > 此模块是 [USTB-SSO](https://github.com/isHarryh/USTB-SSO) 项目的 Python 实现。
29
+
30
+ <sup> This project only supports Chinese docs. If you are an English user, feel free to contact us. </sup>
31
+
32
+ ## 介绍 <sub>Intro</sub>
33
+
34
+ **特点:** 简单易用;自文档化;依赖最少原则;良好的类型注解;全面的错误反馈。
35
+
36
+ ### 实现的功能
37
+
38
+ - **发起认证:**
39
+ 支持向[北科大 SSO 服务器](https://sso.ustb.edu.cn)发起针对指定应用的身份认证请求;
40
+ - **进行认证:**
41
+ 支持使用微信二维码来完成身份认证;
42
+ - **完成认证:**
43
+ 支持在认证成功后,获取已被认证的客户端实例或 Cookie 实例。
44
+
45
+ ## 使用方法 <sub>Usage</sub>
46
+
47
+ ### 安装
48
+
49
+ 要求 [Python](https://www.python.org) >= 3.8,且安装有 [httpx](https://www.python-httpx.org/) 库(一个类似于 requests 的库)。使用 `pip` 安装:
50
+
51
+ ```bash
52
+ pip install httpx ustb_sso
53
+ ```
54
+
55
+ ### 前置知识
56
+
57
+ 要想实现通过 SSO 来北科大的某个指定的应用,需要先准备 3 个参数:
58
+
59
+ 1. 该应用的实体编号(`entity_id`);
60
+ 2. 该应用的认证终点 URL(`redirect_uri`);
61
+ 3. 该应用的内部状态名(`state`)。
62
+
63
+ 我们已经在 `ustb_sso.prefabs` 中以常量的形式存储了部分已知应用的参数。如果您需要接入其他应用,请自行在网页中抓取 `https://sso.ustb.edu.cn/idp/authCenter/authenticate` 这个请求的请求参数来获得。
64
+
65
+ ### 示例代码
66
+
67
+ 以下代码演示了如何获取[北科大 AI 助手](http://chat.ustb.edu.cn)(2025 年版)的令牌 Cookie。
68
+
69
+ ```py
70
+ from ustb_sso import HttpxAuthSession, prefabs
71
+
72
+ auth = HttpxAuthSession(**prefabs.CHAT_USTB_EDU_CN) # ※
73
+
74
+ print("Starting authentication...")
75
+ auth.open_auth().use_wechat_auth().use_qr_code()
76
+
77
+ with open(f"qr.png", "wb") as f:
78
+ f.write(auth.get_qr_image()) # ▲
79
+
80
+ print("Waiting for confirmation... Please scan the QR code")
81
+ pass_code = auth.wait_for_pass_code()
82
+
83
+ print("Validating...")
84
+ rsp = auth.complete_auth(pass_code)
85
+
86
+ print("Response status:", rsp.status_code)
87
+
88
+ cookie_name = "cookie_vjuid_login"
89
+ print("Cookie:", cookie_name, "=", auth.client.cookies[cookie_name])
90
+ ```
91
+
92
+ 当代码运行到 `▲` 位置时,您需要使用微信来扫描文件夹中生成的 `qr.png` 图片中的二维码,并在微信上确认登录。
93
+
94
+ #### 输出样例
95
+
96
+ ```txt
97
+ Starting authentication...
98
+ Waiting for confirmation... Please scan the QR code
99
+ Validating...
100
+ Response status: 200
101
+ Cookie: cookie_vjuid_login = xxxxxxxx...
102
+ ```
103
+
104
+ #### 补充解释
105
+
106
+ 代码的 `※` 位置使用了字典解包(`**`)操作符。它等价于:
107
+
108
+ ```python
109
+ auth = HttpxAuthSession(
110
+ entity_id=prefabs.CHAT_USTB_EDU_CN["entity_id"],
111
+ redirect_uri=prefabs.CHAT_USTB_EDU_CN["redirect_uri"],
112
+ state=prefabs.CHAT_USTB_EDU_CN["state"]
113
+ )
114
+ ```
115
+
116
+ 这里的 `prefabs.CHAT_USTB_EDU_CN` 就是我们预设的应用参数。
117
+
118
+ `auth.client` 是一个 `httpx.Client` 实例(类似于 `request.Session`),用于存储 Cookie 等客户端数据。后续如果需要使用 Cookie 令牌去做其他的 API 请求,可以直接调用 `auth.client` 的相关方法。
119
+
120
+ ## 开发指南 <sub>Dev Guide</sub>
121
+
122
+ 如果您想对 USTB-SSO (Py) 进行开发,以下指引可能有所帮助。
123
+
124
+ ### 开始开发
125
+
126
+ 1. 安装 Python;
127
+ 2. 安装依赖管理工具 [Poetry](https://python-poetry.org/docs/1.8) 1.8;
128
+ 3. 克隆仓库到本地;
129
+ 4. 使用 Poetry 创建虚拟环境,并安装所有依赖项:
130
+ ```bash
131
+ poetry env use python
132
+ poetry install -E httpx
133
+ ```
134
+
135
+ ### 测试
136
+
137
+ 1. 激活虚拟环境:
138
+ - 在 VS Code 中选择虚拟环境中的 Python 解释器(推荐);
139
+ - 或者,使用 `poetry shell` 命令进入虚拟环境。
140
+ 2. 运行测试代码:
141
+ - 在 VS Code 中运行任务 `Python: Test USTB-SSO`(推荐);
142
+ - 或者,使用 `python <文件名>` 命令来手动运行代码。
143
+
144
+ ## 许可证 <sub>Licensing</sub>
145
+
146
+ 本项目基于 **MIT 开源许可证**,详情参见 [License](https://github.com/isHarryh/USTB-SSO/blob/main/LICENSE) 页面。
147
+
@@ -0,0 +1,125 @@
1
+ USTB-SSO (Py)
2
+ ==========
3
+ USTB Single Sign-On Authentication Library (Python)
4
+ 北京科技大学单点登录(SSO)身份认证实现库(Python)
5
+
6
+ > This module is the Python implementation of [USTB-SSO](https://github.com/isHarryh/USTB-SSO).
7
+ > 此模块是 [USTB-SSO](https://github.com/isHarryh/USTB-SSO) 项目的 Python 实现。
8
+
9
+ <sup> This project only supports Chinese docs. If you are an English user, feel free to contact us. </sup>
10
+
11
+ ## 介绍 <sub>Intro</sub>
12
+
13
+ **特点:** 简单易用;自文档化;依赖最少原则;良好的类型注解;全面的错误反馈。
14
+
15
+ ### 实现的功能
16
+
17
+ - **发起认证:**
18
+ 支持向[北科大 SSO 服务器](https://sso.ustb.edu.cn)发起针对指定应用的身份认证请求;
19
+ - **进行认证:**
20
+ 支持使用微信二维码来完成身份认证;
21
+ - **完成认证:**
22
+ 支持在认证成功后,获取已被认证的客户端实例或 Cookie 实例。
23
+
24
+ ## 使用方法 <sub>Usage</sub>
25
+
26
+ ### 安装
27
+
28
+ 要求 [Python](https://www.python.org) >= 3.8,且安装有 [httpx](https://www.python-httpx.org/) 库(一个类似于 requests 的库)。使用 `pip` 安装:
29
+
30
+ ```bash
31
+ pip install httpx ustb_sso
32
+ ```
33
+
34
+ ### 前置知识
35
+
36
+ 要想实现通过 SSO 来北科大的某个指定的应用,需要先准备 3 个参数:
37
+
38
+ 1. 该应用的实体编号(`entity_id`);
39
+ 2. 该应用的认证终点 URL(`redirect_uri`);
40
+ 3. 该应用的内部状态名(`state`)。
41
+
42
+ 我们已经在 `ustb_sso.prefabs` 中以常量的形式存储了部分已知应用的参数。如果您需要接入其他应用,请自行在网页中抓取 `https://sso.ustb.edu.cn/idp/authCenter/authenticate` 这个请求的请求参数来获得。
43
+
44
+ ### 示例代码
45
+
46
+ 以下代码演示了如何获取[北科大 AI 助手](http://chat.ustb.edu.cn)(2025 年版)的令牌 Cookie。
47
+
48
+ ```py
49
+ from ustb_sso import HttpxAuthSession, prefabs
50
+
51
+ auth = HttpxAuthSession(**prefabs.CHAT_USTB_EDU_CN) # ※
52
+
53
+ print("Starting authentication...")
54
+ auth.open_auth().use_wechat_auth().use_qr_code()
55
+
56
+ with open(f"qr.png", "wb") as f:
57
+ f.write(auth.get_qr_image()) # ▲
58
+
59
+ print("Waiting for confirmation... Please scan the QR code")
60
+ pass_code = auth.wait_for_pass_code()
61
+
62
+ print("Validating...")
63
+ rsp = auth.complete_auth(pass_code)
64
+
65
+ print("Response status:", rsp.status_code)
66
+
67
+ cookie_name = "cookie_vjuid_login"
68
+ print("Cookie:", cookie_name, "=", auth.client.cookies[cookie_name])
69
+ ```
70
+
71
+ 当代码运行到 `▲` 位置时,您需要使用微信来扫描文件夹中生成的 `qr.png` 图片中的二维码,并在微信上确认登录。
72
+
73
+ #### 输出样例
74
+
75
+ ```txt
76
+ Starting authentication...
77
+ Waiting for confirmation... Please scan the QR code
78
+ Validating...
79
+ Response status: 200
80
+ Cookie: cookie_vjuid_login = xxxxxxxx...
81
+ ```
82
+
83
+ #### 补充解释
84
+
85
+ 代码的 `※` 位置使用了字典解包(`**`)操作符。它等价于:
86
+
87
+ ```python
88
+ auth = HttpxAuthSession(
89
+ entity_id=prefabs.CHAT_USTB_EDU_CN["entity_id"],
90
+ redirect_uri=prefabs.CHAT_USTB_EDU_CN["redirect_uri"],
91
+ state=prefabs.CHAT_USTB_EDU_CN["state"]
92
+ )
93
+ ```
94
+
95
+ 这里的 `prefabs.CHAT_USTB_EDU_CN` 就是我们预设的应用参数。
96
+
97
+ `auth.client` 是一个 `httpx.Client` 实例(类似于 `request.Session`),用于存储 Cookie 等客户端数据。后续如果需要使用 Cookie 令牌去做其他的 API 请求,可以直接调用 `auth.client` 的相关方法。
98
+
99
+ ## 开发指南 <sub>Dev Guide</sub>
100
+
101
+ 如果您想对 USTB-SSO (Py) 进行开发,以下指引可能有所帮助。
102
+
103
+ ### 开始开发
104
+
105
+ 1. 安装 Python;
106
+ 2. 安装依赖管理工具 [Poetry](https://python-poetry.org/docs/1.8) 1.8;
107
+ 3. 克隆仓库到本地;
108
+ 4. 使用 Poetry 创建虚拟环境,并安装所有依赖项:
109
+ ```bash
110
+ poetry env use python
111
+ poetry install -E httpx
112
+ ```
113
+
114
+ ### 测试
115
+
116
+ 1. 激活虚拟环境:
117
+ - 在 VS Code 中选择虚拟环境中的 Python 解释器(推荐);
118
+ - 或者,使用 `poetry shell` 命令进入虚拟环境。
119
+ 2. 运行测试代码:
120
+ - 在 VS Code 中运行任务 `Python: Test USTB-SSO`(推荐);
121
+ - 或者,使用 `python <文件名>` 命令来手动运行代码。
122
+
123
+ ## 许可证 <sub>Licensing</sub>
124
+
125
+ 本项目基于 **MIT 开源许可证**,详情参见 [License](https://github.com/isHarryh/USTB-SSO/blob/main/LICENSE) 页面。
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "USTB-SSO"
3
+ version = "1.0.0"
4
+ description = "USTB Single Sign-On Authentication Library"
5
+ authors = ["Harry Huang <harryhuang2652@qq.com>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ classifiers = [
9
+ "Programming Language :: Python :: 3",
10
+ "License :: OSI Approved :: MIT License",
11
+ "Operating System :: OS Independent",
12
+ ]
13
+
14
+ [tool.poetry.dependencies]
15
+ python = ">=3.8,<4"
16
+ httpx = { version = "~0.28", optional = true }
17
+
18
+ [tool.poetry.extras]
19
+ httpx = ["httpx"]
20
+
21
+ [[tool.poetry.source]]
22
+ name = "PyPI-Tsinghua"
23
+ url = "https://pypi.tuna.tsinghua.edu.cn/simple"
24
+ priority = "primary"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core"]
28
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,15 @@
1
+ from . import _exceptions as exceptions
2
+ from . import _prefabs as prefabs
3
+
4
+ try:
5
+ from ._auth_session import HttpxAuthSession
6
+ except ImportError:
7
+ pass
8
+
9
+ _all_impl = ('HttpxAuthSession',)
10
+
11
+ if all(i not in globals() for i in _all_impl):
12
+ raise ImportError(
13
+ f"None of these implementations is available: {_all_impl}"
14
+ + " , you may install httpx lib."
15
+ )
@@ -0,0 +1,290 @@
1
+ from typing import Generic, Optional, TypeVar
2
+ from typing_extensions import override, Self
3
+
4
+ import re
5
+ import time
6
+ from urllib.parse import parse_qs, unquote, urlparse
7
+ from html import unescape
8
+
9
+ from ._exceptions import (
10
+ APIError,
11
+ BadResponseError,
12
+ IllegalStateError,
13
+ TimeoutError,
14
+ UnsupportedMethodError
15
+ )
16
+
17
+ _T_CLI = TypeVar("_T_CLI")
18
+ _T_RSP = TypeVar("_T_RSP")
19
+
20
+
21
+ class AuthSessionBase(Generic[_T_CLI, _T_RSP]):
22
+ _SSO_AUTH_ENTRY = "https://sso.ustb.edu.cn/idp/authCenter/authenticate"
23
+ _SSO_QR_INFO = "https://sso.ustb.edu.cn/idp/authn/getMicroQr"
24
+ _SIS_QR_PAGE = "https://sis.ustb.edu.cn/connect/qrpage"
25
+ _SIS_QR_IMG = "https://sis.ustb.edu.cn/connect/qrimg"
26
+ _SIS_QR_STATE = "https://sis.ustb.edu.cn/connect/state"
27
+ QR_CODE_TIMEOUT = 180
28
+ POLLING_TIMEOUT = 16
29
+
30
+ _client: _T_CLI
31
+ _entity_id: str
32
+ _redirect_uri: str
33
+ _state: str
34
+ _lck: Optional[str]
35
+ _app_id: Optional[str]
36
+ _return_url: Optional[str]
37
+ _random_token: Optional[str]
38
+ _sid: Optional[str]
39
+
40
+ def __init__(
41
+ self,
42
+ entity_id: str,
43
+ redirect_uri: str,
44
+ state: str = "ustb",
45
+ client: Optional[_T_CLI] = None
46
+ ):
47
+ """Initializes a USTB SSO authentication session.
48
+
49
+ :param entity_id: The application's entity id;
50
+ :param redirect_uri: The redirection URI to the authentication destination;
51
+ :param state: The internal state of the application;
52
+ :param client: The optional networking client, leave `None` to create a new one;
53
+ """
54
+ self._client = self._new_client() if not client else client
55
+ self._entity_id = entity_id
56
+ self._redirect_uri = redirect_uri
57
+ self._state = state
58
+
59
+ self._lck = None
60
+ self._app_id = None
61
+ self._return_url = None
62
+ self._random_token = None
63
+ self._sid = None
64
+
65
+ def _get(self, url: str, redirect: bool = False, **kwargs) -> _T_RSP:
66
+ raise NotImplementedError()
67
+
68
+ def _post(self, url: str, redirect: bool = False, **kwargs) -> _T_RSP:
69
+ raise NotImplementedError()
70
+
71
+ def _dict(self, rsp: _T_RSP) -> dict:
72
+ raise NotImplementedError()
73
+
74
+ def _new_client(self) -> _T_CLI:
75
+ raise NotImplementedError()
76
+
77
+ @property
78
+ def client(self) -> _T_CLI:
79
+ """Gets the networking client.
80
+ """
81
+ return self._client
82
+
83
+ def open_auth(self) -> Self:
84
+ """Initiates the authentication workflow.
85
+ """
86
+ rsp = self._get(
87
+ AuthSessionBase._SSO_AUTH_ENTRY,
88
+ params={
89
+ "client_id": self._entity_id,
90
+ "redirect_uri": self._redirect_uri,
91
+ "login_return": "true",
92
+ "state": self._state,
93
+ "response_type": "code"
94
+ },
95
+ redirect=False
96
+ )
97
+
98
+ if rsp.status_code // 100 != 3:
99
+ raise APIError(f"HTTP status code: {rsp.status_code}, expected 3xx")
100
+
101
+ location = rsp.headers.get("Location")
102
+ if not location:
103
+ raise BadResponseError("Missing \"Location\" header in response")
104
+
105
+ qs = parse_qs(urlparse(location.replace("/#/", "/")).query)
106
+ self._lck = qs.get("lck", [None])[0]
107
+ if not self._lck:
108
+ raise BadResponseError("Failed to extract \"lck\" from Location header")
109
+
110
+ return self
111
+
112
+ def use_wechat_auth(self) -> Self:
113
+ """Prepares WeChat authentication info.
114
+ """
115
+ if not self._lck:
116
+ raise IllegalStateError("Authentication not initiated. Call `open_auth` first.")
117
+
118
+ rsp = self._post(
119
+ self._SSO_QR_INFO,
120
+ json={
121
+ "entityId": self._entity_id,
122
+ "lck": self._lck
123
+ }
124
+ )
125
+
126
+ data = self._dict(rsp)
127
+
128
+ if data.get("code") != "200":
129
+ raise APIError(f"API code {data.get('code')}: {data.get('message', '')}")
130
+
131
+ try:
132
+ self._app_id = data["data"]["appId"]
133
+ self._return_url = data["data"]["returnUrl"]
134
+ self._random_token = data["data"]["randomToken"]
135
+ except KeyError as e:
136
+ raise BadResponseError(f"Missing key in response") from e
137
+
138
+ return self
139
+
140
+ def use_qr_code(self) -> Self:
141
+ """Prepares QR code SID from QR page.
142
+ """
143
+ if any(not i for i in (self._app_id, self._return_url, self._random_token)):
144
+ raise IllegalStateError("Not in WeChat mode yet. Call `use_wechat_auth` first.")
145
+
146
+ rsp = self._get(
147
+ self._SIS_QR_PAGE,
148
+ params={
149
+ "appid": self._app_id,
150
+ "return_url": self._return_url,
151
+ "rand_token": self._random_token,
152
+ "embed_flag": "1"
153
+ }
154
+ )
155
+
156
+ if rsp.status_code != 200:
157
+ raise APIError(f"HTTP status code {rsp.status_code}, expected 200")
158
+
159
+ match = re.search(r"sid\s?=\s?(\w{32})", rsp.text)
160
+ if not match:
161
+ raise BadResponseError("SID not found in QR page")
162
+ self._sid = match.group(1)
163
+
164
+ return self
165
+
166
+ def get_qr_image(self) -> bytes:
167
+ """Downloads QR code image and returns it in bytes.
168
+ """
169
+ if not self._sid:
170
+ raise IllegalStateError("SID not available. Call `use_qr_code` first.")
171
+
172
+ rsp = self._get(
173
+ self._SIS_QR_IMG,
174
+ params={"sid": self._sid}
175
+ )
176
+
177
+ if rsp.status_code != 200:
178
+ raise APIError(
179
+ f"QR image request failed with HTTP status code {rsp.status_code}"
180
+ )
181
+
182
+ return rsp.content
183
+
184
+ def wait_for_pass_code(self) -> str:
185
+ """Polls the authentication status until completion or timeout.
186
+
187
+ Returns the pass code if completed. Raises exception when timed out.
188
+ """
189
+ if not self._sid:
190
+ raise IllegalStateError("SID not available. Call `use_qr_code` first.")
191
+
192
+ start_time = time.time()
193
+ while time.time() - start_time < self.QR_CODE_TIMEOUT:
194
+ try:
195
+ rsp = self._get(
196
+ self._SIS_QR_STATE,
197
+ params={
198
+ "sid": self._sid
199
+ },
200
+ timeout=self.POLLING_TIMEOUT
201
+ )
202
+ except Exception:
203
+ time.sleep(1)
204
+ continue
205
+
206
+ data = self._dict(rsp)
207
+
208
+ code = data.get("code")
209
+ if code == 1: # Success
210
+ return data["data"]
211
+ elif code in (3, 202): # Expired
212
+ raise TimeoutError("QR code expired")
213
+ elif code == 4: # Timeout
214
+ continue
215
+ elif code in (101, 102): # Invalid
216
+ raise APIError(f"API code {code}: {data.get('message', '')}")
217
+
218
+ raise TimeoutError("Authentication polling timed out")
219
+
220
+ def complete_auth(self, pass_code: str) -> _T_RSP:
221
+ """Completes authentication workflow.
222
+ """
223
+ if any(not i for i in (self._app_id, self._return_url, self._random_token)):
224
+ raise IllegalStateError("Authentication not well established")
225
+
226
+ params = {
227
+ "appid": self._app_id,
228
+ "auth_code": pass_code,
229
+ "rand_token": self._random_token
230
+ }
231
+ params.update(parse_qs(urlparse(self._return_url).query))
232
+
233
+ rsp = self._get(
234
+ self._return_url,
235
+ params=params,
236
+ redirect=True
237
+ )
238
+
239
+ action_type_match = re.search(r'var actionType\s*=\s*"([^"]+)"', rsp.text)
240
+ location_value_match = re.search(r'var locationValue\s*=\s*"([^"]+)"', rsp.text)
241
+ if action_type_match and location_value_match:
242
+ action_type = unescape(unquote(action_type_match.group(1)))
243
+ location_value = unescape(unquote(location_value_match.group(1)))
244
+ else:
245
+ raise BadResponseError("Failed to get authentication destination")
246
+
247
+ if action_type.upper() != "GET":
248
+ raise UnsupportedMethodError("Unsupported authentication destination method")
249
+
250
+ rsp_ = self._get(
251
+ location_value,
252
+ redirect=True
253
+ )
254
+
255
+ return rsp_
256
+
257
+
258
+ try:
259
+ import httpx
260
+
261
+ class HttpxAuthSession(AuthSessionBase[httpx.Client, httpx.Response]):
262
+ @override
263
+ def _get(self, url: str, redirect: bool = False, **kwargs) -> httpx.Response:
264
+ return self._client.get(url, follow_redirects=redirect, **kwargs)
265
+
266
+ @override
267
+ def _post(self, url: str, redirect: bool = False, **kwargs) -> httpx.Response:
268
+ return self._client.post(url, follow_redirects=redirect, **kwargs)
269
+
270
+ @override
271
+ def _dict(self, rsp: httpx.Response) -> dict:
272
+ if rsp.status_code != 200:
273
+ raise APIError(
274
+ f"HTTP status code: {rsp.status_code}, expected 200"
275
+ )
276
+
277
+ try:
278
+ data = rsp.json()
279
+ if not isinstance(data, dict):
280
+ raise TypeError("Not a dict")
281
+ return data
282
+ except Exception as e:
283
+ raise BadResponseError("Invalid JSON response") from e
284
+
285
+ @override
286
+ def _new_client(self):
287
+ return httpx.Client()
288
+
289
+ except ImportError:
290
+ pass
@@ -0,0 +1,28 @@
1
+ class AuthException(Exception):
2
+ """Base exception for authentication errors.
3
+ """
4
+
5
+
6
+ class APIError(AuthException):
7
+ """Exception raised for unexpected HTTP status code or API status code.
8
+ """
9
+
10
+
11
+ class BadResponseError(AuthException):
12
+ """Exception raised for unparsable API response.
13
+ """
14
+
15
+
16
+ class IllegalStateError(AuthException):
17
+ """Exception raised when authentication gets into illegal state.
18
+ """
19
+
20
+
21
+ class TimeoutError(AuthException):
22
+ """Exception raised when authentication times out or expired.
23
+ """
24
+
25
+
26
+ class UnsupportedMethodError(AuthException):
27
+ """Exception raised when an unsupported authentication method is given.
28
+ """
@@ -0,0 +1,21 @@
1
+ from typing import TypedDict
2
+
3
+
4
+ class ApplicationParam(TypedDict):
5
+ entity_id: str
6
+ redirect_uri: str
7
+ state: str
8
+
9
+ # Last updated: 2025-2-28
10
+
11
+ JWGL_USTB_EDU_CN: ApplicationParam = {
12
+ "entity_id": "NS2022062",
13
+ "redirect_uri": "https://jwgl.ustb.edu.cn/glht/Logon.do?method=weCharLogin",
14
+ "state": "test"
15
+ }
16
+
17
+ CHAT_USTB_EDU_CN: ApplicationParam = {
18
+ "entity_id": "YW2025007",
19
+ "redirect_uri": "http://chat.ustb.edu.cn/common/actionCasLogin?redirect_url=http%3A%2F%2Fchat.ustb.edu.cn%2Fpage%2Fsite%2FnewPc%3Flogin_return%3Dtrue",
20
+ "state": "ustb"
21
+ }