feat: 按钮快捷构造、event_id 语义修正与 TokenError

This commit is contained in:
snowy-yang committed 2026-09-16 21:42:33 +08:00
1 parent cff0c142c2
commit 097bc96d8b
16 files changed
+431 -35

No files matched your search

+8
View File
@@ -8,6 +8,8 @@
### 新增
- Markdown 与内嵌按钮一等支持:`post_group_message`/`post_c2c_message` 新增 `markdown`/`keyboard` 参数(自动置 msg_type=2,与 content 互斥);新增 `respond_interaction()` 回应按钮回调;`Interaction` payload 补齐 `chat_type`/`scene`/`group_openid` 等官方字段
- `Event` 新增 `event_id` 属性(推送信封顶层的事件 id,与 op/t/d 平级):非消息事件(GROUP_ADD_ROBOT 等)的被动回复不再需要手动 `event.raw["id"]`
- 按钮快捷构造与点击处理:`button()`/`keyboard()` 生成内嵌按钮(免手写嵌套 dict);`Interaction` 新增 `button_data` 属性(被点按钮的 action.data);examples/group_bot 补 `/btn` 卡片与 `INTERACTION_CREATE` 回调示例
### 变更
@@ -17,6 +19,12 @@
- 音乐插件示例(`plugins/`、根目录 `main.py`)已拆分为独立的点歌 bot 项目(含 Markdown 卡片、按钮回调与内置短网址多线程下载代理),不再随 SDK 仓库分发
### 修复
- token 请求失败时抛出 `TokenError`(携带 QQ 返回的 `code`/`message`),不再抛出掩盖真实原因的 `KeyError: 'access_token'`
- 修正被动回复 `event_id` 的指引:官方要求取网关帧最外层的 id(`Event.event_id`),事件体里的裸 UUID(`Interaction.id` 等)不能作 `event_id`,此前文档与 docstring 的说法有误
- 响应 `Content-Type` 非 JSON 时不再抛 `ContentTypeError`(个别接口如回应互动返回 200 text/plain + `{}` body);JSON 解析统一切换 ujson
## [0.1.0] - 2026-09-14
首个公开版本:QQ 官方机器人 Python SDK(Python 3.13 / asyncio / aiohttp)。
+6 -1
View File
@@ -69,7 +69,8 @@ async def handler(...): ...
| `.user_id` | 用户标识(兼容 `user_id`/`from_user_id`) |
| `.group_id` | `group_openid` |
| `.content` | 消息文本 |
| `.message_id` | `d["id"]`,被动回复用 |
| `.message_id` | `d["id"]`,消息事件的被动回复凭据(msg_id) |
| `.event_id` | 推送信封顶层的事件 id(与 op/t/d 平级,形如 `INTERACTION_CREATE:uuid`);非消息事件(`INTERACTION_CREATE`/`GROUP_ADD_ROBOT` 等)的被动回复凭据只有这里能拿到,事件体里的裸 UUID 不行(官方点名的常见坑) |
## Config
@@ -102,6 +103,8 @@ async def handler(...): ...
常量:`MSG_TYPE_TEXT=0`、`MSG_TYPE_MARKDOWN=2`、`MSG_TYPE_MEDIA=7`;`FILE_TYPE_IMAGE=1`、`FILE_TYPE_VIDEO=2`、`FILE_TYPE_VOICE=3`、`FILE_TYPE_FILE=4`。
按钮构造:`button(label, data, *, type=1, permission=2, style=1)` 生成单个按钮 dict,`keyboard(*rows)` 组装为 `keyboard=` 参数(单按钮独占一行,按钮列表同行并排)。点击回调经 `INTERACTION_CREATE` 事件处理:`Interaction.button_data` 取被点按钮的 `data`,`Interaction.id` 用于 `respond_interaction`,**被动回复的 `event_id` 用 `Event.event_id`(网关帧最外层 id),不能用 `Interaction.id`**。
### v2 群聊(群 openid)
| 方法 | 端点 | 说明 |
@@ -164,6 +167,8 @@ async def handler(...): ...
| `.code` | 官方业务 code(响应体中的 `code`) |
| `.message` | 官方错误描述 |
鉴权阶段(换取 access_token)的失败抛 `TokenError`(`from qqbotsdk.token import TokenError`),与 `ApiError` 同构(`RuntimeError` 子类,同样携带 `.status`/`.code`/`.message`)——appid/secret 配置错误在这里暴露 QQ 返回的真实原因。
## 事件 payload
`qqbotsdk.payloads` 定义事件 `d` 的 dataclass。handler 标注类型即得解析对象;未知事件类型回退原始 dict。所有字段均可为 None(线上缺省即 None),嵌套 `author` 自动解析为 `Author`。
+25 -11
View File
@@ -106,7 +106,7 @@ handler 参数按标注解析的完整规则:
| 参数标注 | 得到 |
|---|---|
| payload dataclass(如 `GroupAtMessage`) | 解析后的 dataclass 对象;未收录事件回退原始 dict |
| `Event` | 事件封装对象(`.raw`/`.data`/`.typed`/`.user_id`/`.group_id`/`.content`/`.message_id`) |
| `Event` | 事件封装对象(`.raw`/`.data`/`.typed`/`.user_id`/`.group_id`/`.content`/`.message_id`/`.event_id`) |
| 组件类型(`BotApi`/`Session`/`Config` 等) | `emitter.services` 中按类型登记的实例;未登记回退传原始 dict |
| 无标注 | 原始事件 dict |
@@ -121,7 +121,7 @@ handler 参数按标注解析的完整规则:
### 被动回复(最常用)
携带 `msg_id`(回复某条消息)或 `event_id`(响应某类事件,二选一)即为被动回复,**不需要任何消息频率白名单**:
携带 `msg_id`(回复某条消息)或 `event_id`(响应某类事件,二选一)即为被动回复,**不需要任何消息频率白名单**。两个 id 来源不同:`msg_id` 是消息事件体里的 `d.id`;`event_id` 是**网关推送最外层**的 id(`event.event_id`,形如 `INTERACTION_CREATE:uuid`),不是事件体里的裸 UUID:
- 群聊:收到消息后 **5 分钟**内可回复,同一 `msg_id` 最多 **5 次**;
- 单聊:**60 分钟**内可回复,同一 `msg_id` 最多 **4 次**;
@@ -164,23 +164,36 @@ await api.post_group_message(group_openid, content="# 标题\n正文",
### Markdown 与按钮(msg_type=2)
`post_group_message`/`post_c2c_message` 原生支持 Markdown 与内嵌按钮(keyboard):
`button()`/`keyboard()`(`from qqbotsdk.api import button, keyboard`)构造内嵌按钮,配合 `markdown=` 一起发(msg_type 自动置 2):
```python
await api.post_group_message(
group_openid,
markdown="## 标题\n**加粗**正文", # msg_type 自动置 2,与 content 互斥
keyboard={"content": {"rows": [{"buttons": [{
"id": "btn1",
"render_data": {"label": "点我", "style": 1},
"action": {"type": 1, "permission": {"type": 2}, "data": "我的按钮数据"},
}]}}]},
markdown="## 标题\n**加粗**正文", # 与 content 互斥
keyboard=keyboard(
button("点我", data="我的按钮数据"), # 单按钮独占一行
[button("赞", data="like"), button("踩", data="dislike")], # 列表同行并排
),
msg_id=msg.id,
)
```
- `action.type`:1 回调(点击推 `INTERACTION_CREATE` 事件,`data.resolved.button_data` 携带按钮 data)、2 指令(往输入框插入 data)、0 跳转链接;
- 收到 `INTERACTION_CREATE` 后必须调 `api.respond_interaction(interaction_id)` 应答,否则用户端按钮一直 loading;事件体的 `id` 可作 `event_id` 被动回复;
- `button(label, data, *, type=1, permission=2, style=1)`:`type` 1 回调(点击推 `INTERACTION_CREATE`)、0 跳转链接(data 为 url)、2 指令(往输入框插入 data);`permission` 2 所有人 / 1 管理员 / 0 指定用户;`label` 官方限 10 字以内;要补嵌套字段(如 `visited_label`、`prompt`)直接修改返回的 dict。
**处理按钮点击**:点击回调推 `INTERACTION_CREATE` 事件,`inter.button_data` 即被点按钮的 `data`:
```python
@ee.on("INTERACTION_CREATE")
async def on_button(inter: Interaction, event: Event, api: BotApi):
# 必须应答,否则用户端按钮一直 loading(inter.id 是事件体里的互动 id)
await api.respond_interaction(inter.id)
await api.post_group_message(
inter.group_openid, content=f"你点了:{inter.button_data}",
event_id=event.event_id,
)
```
- **`event_id` 要取网关帧最外层的 id(`event.event_id`,形如 `INTERACTION_CREATE:uuid`),不能用事件体里的裸 UUID `inter.id`**——`inter.id` 只用于 `respond_interaction`,这是官方文档点名的常见坑;
- 需要在 intents.toml 开启 `INTERACTION` 分组;
- Markdown 有平台权限要求,无权限时发送会抛 `ApiError`(业务 code 304036/40034127),按需回退纯文本。
@@ -202,6 +215,7 @@ uv run python main.py
- **启动后频繁重连 / READY 收不到**:QQ 网关接口有频率限制(code 100017),连续重启需间隔约 1 分钟;SDK 已内置退避,等即可。
- **`ApiError`**:HTTP ≥400 时抛出,`e.status`/`e.code`/`e.message` 可用于排查(如被动回复超时、msg_seq 重复、内容审核不通过)。
- **日志出现 `TokenError: 获取 access_token 失败`**:appid/secret 配置错误或被平台拒绝,`code`/`message` 就是 QQ 返回的真实原因,对照官方错误码排查即可。
- **handler 没被触发**:检查 intents.toml 对应事件是否为 `true`;群/单聊事件需开通对应能力;频道消息事件区分公私域。
- **想收原始数据**:handler 裸参数或标 `Event`,`event.raw` 是完整线上 payload(含 op/t/d)。
- **日志**:SDK 用 loguru 输出中文日志,连接、重连、API 失败、handler 异常均有记录,可直接用 loguru 配置格式与落盘。
+34 -8
View File
@@ -1,8 +1,8 @@
"""群机器人示例:@消息回复、富媒体图片回复、入群欢迎、单聊回复。
"""群机器人示例:@消息回复、富媒体图片、按钮卡片与点击回调、入群欢迎、单聊回复。
运行前提:
1. 项目根目录 .env 配好 APPID/APPSECRET(CONNECTER 用默认 websocket);
2. intents.toml 开启 GROUP_AND_C2C_EVENT 分组的事件;
2. intents.toml 开启 GROUP_AND_C2C_EVENT 与 INTERACTION 分组的事件;
3. 机器人已在开放平台开通群聊能力,并被拉入测试群。
运行:uv run python examples/group_bot/main.py
@@ -11,9 +11,9 @@
from loguru import logger
from qqbotsdk import EventEmitter, EventQueue, main
from qqbotsdk.api import BotApi
from qqbotsdk.api import BotApi, button, keyboard
from qqbotsdk.events import Event
from qqbotsdk.payloads import C2CMessage, GroupAtMessage
from qqbotsdk.payloads import C2CMessage, GroupAtMessage, Interaction
ee = EventEmitter(EventQueue())
@@ -34,19 +34,45 @@ async def on_group_message(msg: GroupAtMessage, api: BotApi) -> None:
)
return
# "/btn":发一张带回调按钮的 Markdown 卡片
if (msg.content or "").strip() == "/btn":
await api.post_group_message(
msg.group_openid,
markdown="## 按钮示例\n点下面的按钮试试:",
keyboard=keyboard(
button("打个招呼", data="btn|hello"),
[button("赞", data="btn|like"), button("踩", data="btn|dislike")],
),
msg_id=msg.id,
)
return
# 普通文本:带 msg_id 被动回复(5 分钟内有效,同一条消息多次回复递增 msg_seq)
await api.post_group_message(
msg.group_openid, content=f"你说:{msg.content}", msg_id=msg.id,
)
@ee.on("INTERACTION_CREATE")
async def on_button_click(inter: Interaction, event: Event, api: BotApi) -> None:
if inter.chat_type != 1 or not inter.group_openid:
return # 本示例只处理群聊的按钮回调
# 必须应答,否则用户端按钮一直 loading(这里的 id 是事件体里的互动 id)
await api.respond_interaction(inter.id)
# 被动回复的 event_id 是网关帧最外层的 id(形如 INTERACTION_CREATE:uuid),
# 不是事件体里的裸 UUID——官方文档点名的常见坑
await api.post_group_message(
inter.group_openid, content=f"你点了:{inter.button_data}",
event_id=event.event_id,
)
@ee.on("GROUP_ADD_ROBOT")
async def on_robot_added(event: Event, api: BotApi) -> None:
group_openid = event.group_id
# 入群是事件而非消息:事件 ID 在推送 envelope 顶层 id 字段,
# 作 event_id 即可在 5 分钟内被动回复欢迎语(拿不到则跳过)
event_id = event.raw.get("id")
if not (group_openid and isinstance(event_id, str) and event_id):
# 入群是事件而非消息:用信封顶层的事件 id 作 event_id 被动回复(拿不到则跳过)
event_id = event.event_id
if not (group_openid and event_id):
logger.info(f"机器人加入群聊 {group_openid}(无事件 ID,跳过欢迎语)")
return
await api.post_group_message(
+1
View File
@@ -16,6 +16,7 @@ dependencies = [
"loguru>=0.7.3",
"pyee>=14.0.0",
"python-dotenv>=1.2.3",
"ujson>=6.0.0",
]
[project.scripts]
+40 -1
View File
@@ -8,7 +8,9 @@
"""
from typing import Any
from uuid import uuid4
import ujson
from aiohttp import ClientSession
from loguru import logger
@@ -26,6 +28,41 @@ FILE_TYPE_VOICE = 3
FILE_TYPE_FILE = 4
def button(
label: str,
data: str = "",
*,
type: int = 1,
permission: int = 2,
style: int = 1,
id: str | None = None,
) -> dict:
"""构造单个内嵌按钮 dict,供 keyboard() 组装后传入 keyboard= 参数。
type: 1 回调(点击推 INTERACTION_CREATE)/ 0 跳转链接(data 为 url)/
2 指令(data 插入输入框)。permission: 2 所有人 / 1 管理员 / 0 指定
用户。label 官方限 10 字以内;要补嵌套字段(如 visited_label、
prompt)直接修改返回的 dict。
"""
return {
"id": id or uuid4().hex[:8],
"render_data": {"label": label, "style": style},
"action": {"type": type, "permission": {"type": permission}, "data": data},
}
def keyboard(*rows: dict | list[dict]) -> dict:
"""组装内嵌键盘(keyboard= 参数):每个位置参数为一行,
单个按钮独占一行,按钮列表同行并排。"""
return {
"content": {
"rows": [
{"buttons": row if isinstance(row, list) else [row]} for row in rows
]
}
}
class ApiError(RuntimeError):
"""OpenAPI 调用失败:HTTP 非 2xx 或业务 code 非 0。"""
@@ -53,7 +90,9 @@ class BotApi:
headers=headers,
**kwargs,
) as resp:
data = await resp.json()
# 个别接口 200 响应的 Content-Type 不是 JSON(body 仍是 JSON,
# 如回应互动返回 200 text/plain + {}),跳过标头校验
data = await resp.json(loads=ujson.loads, content_type=None)
if resp.status >= 400:
logger.warning(f"{method} {path} 失败: {data}")
code = data.get("code") if isinstance(data, dict) else None
+4 -4
View File
@@ -8,11 +8,11 @@
"""
import asyncio
import json
from time import monotonic
from typing import Protocol
import aiohttp
import ujson
from aiohttp import WSMsgType, web
from loguru import logger
@@ -74,7 +74,7 @@ class WebsocketConnecter:
async with self._http.get(
f"{self._config.base_url}/gateway", headers=headers
) as resp:
data = await resp.json()
data = await resp.json(loads=ujson.loads)
if data.get("code") == 100017:
raise RateLimitError
@@ -101,7 +101,7 @@ class WebsocketConnecter:
if msg.type != WSMsgType.TEXT:
logger.warning(f"WebSocket 收到非文本帧,连接终止: {msg.type}")
break
data: Payload = msg.json()
data: Payload = msg.json(loads=ujson.loads)
op = data.get("op")
if op == Opcode.HELLO:
d = data.get("d")
@@ -169,7 +169,7 @@ class WebhookConnecter:
async def _handle(self, request: web.Request) -> web.Response:
body = await request.read()
payload: Payload = json.loads(body)
payload: Payload = ujson.loads(body)
if payload.get("op") == Opcode.VALIDATION:
logger.info("收到 Webhook 验证请求")
+10
View File
@@ -48,5 +48,15 @@ class Event:
def message_id(self) -> str | None:
return self._d.get("id")
@property
def event_id(self) -> str | None:
"""推送信封顶层的事件 id(与 op/t/d 平级)。
非消息事件(GROUP_ADD_ROBOT 等)事件体里没有 id,被动回复的
event_id 只有这里能拿到;消息事件用 `.message_id`(即 msg_id)。
"""
id_ = self._payload.get("id")
return id_ if isinstance(id_, str) else None
def __repr__(self) -> str:
return f"<Event {self.type} {self._d}>"
-5
View File
@@ -73,11 +73,6 @@ class ValidationData(TypedDict):
event_ts: str
class AccessTokenResponse(TypedDict):
access_token: str
expires_in: int
class Intent(IntFlag):
GUILDS = 1 << 0
GUILD_MEMBERS = 1 << 1
+11 -2
View File
@@ -206,8 +206,10 @@ class Interaction:
"""互动(INTERACTION_CREATE)。
按钮点击(type=11)时 data.resolved.button_data 携带按钮的
action.data;id 可作 event_id 被动回复,并需 PUT
/interactions/{id} 回应(见 BotApi.respond_interaction)。
action.data;id 用于 PUT /interactions/{id} 回应(见
BotApi.respond_interaction)。注意被动回复的 event_id 不取这里的
id(它是裸 UUID),要取网关帧最外层的 id(Event.event_id,形如
INTERACTION_CREATE:uuid)——官方文档点名的常见坑。
"""
id: str | None = None
@@ -224,6 +226,13 @@ class Interaction:
application_id: str | None = None
timestamp: str | None = None
@property
def button_data(self) -> str:
"""被点击按钮的 action.data(即 data.resolved.button_data)。"""
resolved = (self.data or {}).get("resolved")
value = resolved.get("button_data") if isinstance(resolved, dict) else None
return value if isinstance(value, str) else ""
@dataclass(slots=True)
class ForumThread:
+26 -2
View File
@@ -1,11 +1,28 @@
"""access_token 获取与缓存:到期前直接复用,过期自动重新换取。"""
import time
from typing import Any
import ujson
from aiohttp import ClientSession
from .config import Config
from .model import AccessTokenResponse
class TokenError(RuntimeError):
"""换取 access_token 失败:携带 QQ 返回的 code/message。
与 ApiError 同构(appid/secret 错误等鉴权失败在此暴露真实原因,
而不是 KeyError: 'access_token')。
"""
def __init__(self, status: int, code: Any, message: Any) -> None:
self.status = status
self.code = code
self.message = message
super().__init__(
f"获取 access_token 失败: HTTP {status}, code={code}, message={message}"
)
class AccessToken:
@@ -28,7 +45,14 @@ class AccessToken:
f"{self._config.base_url}/app/getAppAccessToken",
json=payload,
) as resp:
data: AccessTokenResponse = await resp.json()
# 与 request() 同理:个别接口以非 JSON Content-Type 返回 JSON body
data = await resp.json(loads=ujson.loads, content_type=None)
# 失败时 QQ 返回 code/message 且 HTTP 可能仍是 200,
# 以 access_token 是否在为准判断成败,抛出真实原因
if not isinstance(data, dict) or "access_token" not in data:
code = data.get("code") if isinstance(data, dict) else None
message = data.get("message") if isinstance(data, dict) else data
raise TokenError(resp.status, code, message)
self._access_token = data["access_token"]
self._expires_at = time.time() + int(data["expires_in"])
+66 -1
View File
@@ -1,4 +1,9 @@
from qqbotsdk.api import BotApi
from aiohttp import ClientSession, ClientTimeout, web
from aiohttp.test_utils import TestServer
from qqbotsdk.api import BotApi, button, keyboard
from qqbotsdk.config import Config
from qqbotsdk.token import AccessToken
def make_api() -> tuple[BotApi, dict]:
@@ -163,3 +168,63 @@ async def test_respond_interaction():
method, path, kwargs = cap["call"]
assert (method, path) == ("PUT", "/interactions/I1")
assert kwargs["json"] == {"code": 0}
def test_button_defaults_and_overrides():
btn = button("点我", data="d1")
assert btn["render_data"] == {"label": "点我", "style": 1}
assert btn["action"] == {"type": 1, "permission": {"type": 2}, "data": "d1"}
assert len(btn["id"]) == 8 # 未指定 id 时自动生成短 id
btn2 = button("跳转", data="https://x", type=0, permission=1, style=0, id="b2")
assert btn2["id"] == "b2"
assert btn2["action"]["type"] == 0
assert btn2["action"]["permission"] == {"type": 1}
assert btn2["render_data"]["style"] == 0
def test_keyboard_rows():
b1, b2, b3 = button("a", data="1"), button("b", data="2"), button("c", data="3")
kb = keyboard(b1, [b2, b3])
rows = kb["content"]["rows"]
assert rows[0]["buttons"] == [b1] # 单按钮独占一行
assert rows[1]["buttons"] == [b2, b3] # 列表同行并排
assert len(rows) == 2
async def test_request_tolerates_non_json_content_type():
"""回应互动等接口 200 响应的 Content-Type 是 text/plain(body 为 JSON),
request 不应抛 ContentTypeError(真实 HTTP 栈回归)。"""
async def handler(request):
return web.Response(text="{}", content_type="text/plain")
app = web.Application()
app.router.add_route("PUT", "/interactions/I1", handler)
server = TestServer(app)
await server.start_server()
try:
config = Config(
app_id="id",
app_secret="s",
base_url=f"http://{server.host}:{server.port}",
timeout=ClientTimeout(total=5),
connecter="websocket",
intents_file="intents.toml",
webhook_host="0.0.0.0",
webhook_port=8080,
webhook_path="/qqbot/webhook",
)
class FixedToken(AccessToken):
async def get_access_token(self) -> str:
return "test-token"
http = ClientSession()
try:
api = BotApi(config, http, FixedToken(config, http))
assert await api.respond_interaction("I1") == {}
finally:
await http.close()
finally:
await server.close()
+39
View File
@@ -0,0 +1,39 @@
"""Event 封装属性:message_id 取自事件体 d.id,event_id 取自信封顶层 id。"""
from qqbotsdk.events import Event
def make_event() -> Event:
return Event(
{
"op": 0,
"id": "evt-1", # 信封顶层,与 op/t/d 平级
"t": "GROUP_ADD_ROBOT",
"d": {"group_openid": "G1", "op_member_openid": "M1"},
}
)
def test_event_id_from_envelope():
event = make_event()
assert event.event_id == "evt-1"
assert event.group_id == "G1"
def test_message_id_from_event_body():
event = Event(
{
"op": 0,
"id": "evt-2",
"t": "GROUP_AT_MESSAGE_CREATE",
"d": {"id": "msgid-1", "group_openid": "G1", "content": "hi"},
}
)
assert event.message_id == "msgid-1" # 消息事件回复用 msg_id
assert event.event_id == "evt-2" # 信封顶层 id 另有其值
def test_event_id_defaults_to_none():
event = Event({"op": 0, "t": "READY", "d": {}})
assert event.event_id is None
assert event.message_id is None
+22
View File
@@ -3,6 +3,7 @@ from qqbotsdk.model import Opcode
from qqbotsdk.payloads import (
C2CMessage,
GroupAtMessage,
Interaction,
Ready,
parse_event,
)
@@ -62,6 +63,27 @@ def test_parse_non_dict_d_returns_dict():
assert parse_event("GROUP_AT_MESSAGE_CREATE", d) == 42
def test_interaction_button_data():
inter = parse_event(
"INTERACTION_CREATE",
{
"id": "i1",
"chat_type": 1,
"group_openid": "G1",
"data": {"resolved": {"button_data": "btn|like"}},
},
)
assert isinstance(inter, Interaction)
assert inter.button_data == "btn|like"
def test_interaction_button_data_defaults_to_empty():
inter = Interaction(id="i2", data=None)
assert inter.button_data == ""
inter = Interaction(id="i3", data={"resolved": None})
assert inter.button_data == ""
async def test_typed_annotation_receives_dataclass():
queue = EventQueue()
emitter = EventEmitter(queue)
+64
View File
@@ -0,0 +1,64 @@
"""AccessToken:成功换取与失败时抛 TokenError(带 QQ 的 code/message)。"""
import pytest
from aiohttp import ClientTimeout
from qqbotsdk.config import Config
from qqbotsdk.token import AccessToken, TokenError
def make_token(payload) -> AccessToken:
class FakeResponse:
status = 200
def __init__(self) -> None:
self._payload = payload
async def json(self, **kwargs):
return self._payload
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
class FakeSession:
def post(self, *args, **kwargs):
return FakeResponse()
config = Config(
app_id="id",
app_secret="s",
base_url="https://x",
timeout=ClientTimeout(total=5),
connecter="websocket",
intents_file="intents.toml",
webhook_host="0.0.0.0",
webhook_port=8080,
webhook_path="/qqbot/webhook",
)
return AccessToken(config, FakeSession()) # type: ignore[arg-type]
async def test_success_caches_token():
token = make_token({"access_token": "t1", "expires_in": 7200})
assert await token.get_access_token() == "t1"
# 缓存期内直接复用,不再发请求(FakeSession 也没有第二次响应的意义)
assert await token.get_access_token() == "t1"
async def test_error_payload_raises_token_error():
token = make_token({"code": 11253, "message": "invalid appid or secret"})
with pytest.raises(TokenError) as exc_info:
await token.get_access_token()
assert exc_info.value.code == 11253
assert exc_info.value.message == "invalid appid or secret"
assert "invalid appid or secret" in str(exc_info.value)
async def test_non_dict_payload_raises_token_error():
token = make_token("gateway busy")
with pytest.raises(TokenError) as exc_info:
await token.get_access_token()
assert exc_info.value.message == "gateway busy"
Generated
+75
View File
@@ -637,6 +637,7 @@ dependencies = [
{ name = "loguru" },
{ name = "pyee" },
{ name = "python-dotenv" },
{ name = "ujson" },
]
[package.dev-dependencies]
@@ -655,6 +656,7 @@ requires-dist = [
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "pyee", specifier = ">=14.0.0" },
{ name = "python-dotenv", specifier = ">=1.2.3" },
{ name = "ujson", specifier = ">=6.0.0" },
]
[package.metadata.requires-dev]
@@ -720,6 +722,79 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" },
]
[[package]]
name = "ujson"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/64/7c/e1fa3fb70b53192436d751b5cb671f0ee960baa188b8351a7fec735223d3/ujson-6.0.0.tar.gz", hash = "sha256:80e23393feb707582e0ad495c397a4477b646d08094d2df64f7316f9fafd8aae", size = 7169158, upload-time = "2026-09-04T03:55:42.983Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/32/c67df85215ba0ebcdca8f8b1b3a856fd2434da87f84f9757e275ef99bd40/ujson-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fb37ec7d7542e2f23fd7ca8fd034c8db7221c5e86d6a6a3a170711f993eecf15", size = 55113, upload-time = "2026-09-04T03:53:55.285Z" },
{ url = "https://files.pythonhosted.org/packages/b7/a0/e5c7ae933fab41be06f0ff7976e3483519cbbf9e2492a217a5550af95c18/ujson-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad11c9153c775087d261634410da7cfaac2743d79bc9ab573177d9e3398f00c6", size = 54247, upload-time = "2026-09-04T03:53:56.383Z" },
{ url = "https://files.pythonhosted.org/packages/9a/10/0993497a08f9fcff34cbcdcd6da46491f415e711a459c5d81f594e89769c/ujson-6.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2dbe0b6d417b458164ccf1f59e081d6bd65c1fb2f626e0daeb6fb88c436f9643", size = 58497, upload-time = "2026-09-04T03:53:57.791Z" },
{ url = "https://files.pythonhosted.org/packages/70/55/06a578dd00551b10bc94d68889387e5de11977ee92cc53921eb02713146f/ujson-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:455e6ae6c925eca6358110e665a31e5bbcf0a93dfe9822a26b954c9351de2c3f", size = 52377, upload-time = "2026-09-04T03:53:58.859Z" },
{ url = "https://files.pythonhosted.org/packages/06/9a/cc0d306e93d1a8f1d48d54f52cb2cae39b56a4c990e2cd8cf328697bbc80/ujson-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5376a8c14d0eaf80789bdb10e21ae12582cdf526eb921a47f57053ef08c63f8c", size = 53718, upload-time = "2026-09-04T03:53:59.962Z" },
{ url = "https://files.pythonhosted.org/packages/15/48/0462149003b03afe83450f6bdad3ebff9ac9aee315690eb0670bf2a6e342/ujson-6.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8bd6743ad58fe6067ea1677d5df4674bd7de143b038bcd4129c3a6ced483ae8", size = 57191, upload-time = "2026-09-04T03:54:01.005Z" },
{ url = "https://files.pythonhosted.org/packages/45/ad/26f40cdffaebd1158b1083d6c89efd9e056badd706022386a0e9567ae499/ujson-6.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3967550c8952bc516c79c40726a54313aceeb3162a8d5cc655362ab83d0957c", size = 56413, upload-time = "2026-09-04T03:54:02.019Z" },
{ url = "https://files.pythonhosted.org/packages/62/60/7f0d5da6198fcad7037dafc68e6c76aebb6536b323c07a2d1f82bf692099/ujson-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:619b2152aa77c57a535e3e7eaf88ec8e25beac6d380378b2ade10362cce50f75", size = 1037442, upload-time = "2026-09-04T03:54:03.104Z" },
{ url = "https://files.pythonhosted.org/packages/83/bf/21cd9110b8b33ff1530d854f38b7bd2b8d2be41af67590fccffb418fb29f/ujson-6.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2e36269e715c8deea036d263557042e2598e79d52110233c1a623ed9e7c1cf0a", size = 1196458, upload-time = "2026-09-04T03:54:04.39Z" },
{ url = "https://files.pythonhosted.org/packages/ed/51/2e3b3a19b36862f72300a306051fb7a265ba0f5a72d6e2eebd30f2722b44/ujson-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c626f68524a19f50d9a9babc17f9c379d1b2a9f2a3da5ac3c40a205cc736259f", size = 1088718, upload-time = "2026-09-04T03:54:05.787Z" },
{ url = "https://files.pythonhosted.org/packages/6d/f5/faeb3439f844e61040dc4bd2744c58745541ec1a94e68f08994fe4f41ee1/ujson-6.0.0-cp313-cp313-win32.whl", hash = "sha256:cea0a63173e4ae98cd960f484096233da76a62550ac10c53312a69ad9f3545b1", size = 229128, upload-time = "2026-09-04T03:54:07.5Z" },
{ url = "https://files.pythonhosted.org/packages/05/19/55a89733b9078a88605f5884763e0457409fd8d04d2e47d4d761052e28d6/ujson-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:88b237680c705fd37bacbaaa335106fecb234a47e1df0737d949b8e32c7eb5f9", size = 227518, upload-time = "2026-09-04T03:54:08.653Z" },
{ url = "https://files.pythonhosted.org/packages/d5/cb/807314a66fb495d600718b11f7639af07138a25ea301b91234a18a43af59/ujson-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:ec570979304a529a8be1bf9ea28889742a2ff5de9af1c6734584dfe1645da3e6", size = 400244, upload-time = "2026-09-04T03:54:10.006Z" },
{ url = "https://files.pythonhosted.org/packages/7c/40/c22e49f786f5a0a71bae6323d0e6fa9a4a47b7b68c9f00631fdd78f147f7/ujson-6.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63eefaa34abbe14167493710619b840d3fc167ba86e5fbe0c4a5eb01686aa3a0", size = 55208, upload-time = "2026-09-04T03:54:11.403Z" },
{ url = "https://files.pythonhosted.org/packages/86/40/90a47580ae4246134a080b0f76637e038476271461d7ab227c1c4431822d/ujson-6.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af85ae40c71d422fad944aa8666d59374e4fa92f77899fce34b984037db41420", size = 54303, upload-time = "2026-09-04T03:54:12.467Z" },
{ url = "https://files.pythonhosted.org/packages/62/63/a275e218f7c5f49c0e31b446e9eb581267b7d3393d4dfbc25f397967e51b/ujson-6.0.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2145005321a4b175486dd890946b036bb8730e4e8e17744f5abce23ea014e024", size = 58509, upload-time = "2026-09-04T03:54:13.472Z" },
{ url = "https://files.pythonhosted.org/packages/cd/3b/11fc8994c579a325c5c2075f03c70c967849d2c63cf97197507f31aaa739/ujson-6.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2c670cd7aaad2a3bff450addb32b26aa831f82a8b6c2c875ec19bb282a6c45d", size = 52422, upload-time = "2026-09-04T03:54:14.475Z" },
{ url = "https://files.pythonhosted.org/packages/37/73/a7ecfa39bb08cfe57d35064b4be21712fe671bae47574a4c9901a9a1ad2a/ujson-6.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:63b56e3fcccc339e2c1332e75adc779bd145964e1a47a39a229fa01b2e25618a", size = 53707, upload-time = "2026-09-04T03:54:15.657Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c3/e6d76ff353d179dd0dca2e5df6afdd170874031eb73284acb9a327dd7f52/ujson-6.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab7b316bba31be494635dcc5db87e429f2478073d15d2c54925c32fd9e1947f4", size = 57234, upload-time = "2026-09-04T03:54:16.793Z" },
{ url = "https://files.pythonhosted.org/packages/cf/b4/c52aa5b797b76a2ca10da513010120809d70bb12acdfc27ad7875a652fee/ujson-6.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9d26982045b28db1937ac60682a9940fdb72f9cab3421a5d56c03f2207c99e9", size = 56411, upload-time = "2026-09-04T03:54:17.979Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ad/5e2dd3fbbadee85811279e57dee23f346d8cc099809c14f7bb01d1a5a879/ujson-6.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fc115cca04dbdfd98a67ec89ba5ffd8a87f3201171af54980cfd550997611c41", size = 1037485, upload-time = "2026-09-04T03:54:19.215Z" },
{ url = "https://files.pythonhosted.org/packages/b8/61/73d5ef4020716e08de4992519d090785908bf229a3d464abf0a067f06c21/ujson-6.0.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90f766c5f8e55de2fe65e4241e3e2e46ed7528e7931255a7ed0dfcb5ce622b15", size = 1196525, upload-time = "2026-09-04T03:54:20.759Z" },
{ url = "https://files.pythonhosted.org/packages/ed/4d/d63aafdf83ecb52a76ab46b0450e5431462b713e0b2576539a1b80ed6afb/ujson-6.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dfceda99f3105e9e6fce8dfd157f80894ad20247dc9ffce368c8b7883e7a2aac", size = 1088718, upload-time = "2026-09-04T03:54:22.056Z" },
{ url = "https://files.pythonhosted.org/packages/c5/92/504ccce4f8b56612dd5ebb1f221b5cb6435bc33d357dafbdedfe5b0b691f/ujson-6.0.0-cp314-cp314-win32.whl", hash = "sha256:22eafdd4f8ee6fe2db0737285c75b15f7486dc53c07b09a4b3699c92c407c3e5", size = 235844, upload-time = "2026-09-04T03:54:23.399Z" },
{ url = "https://files.pythonhosted.org/packages/e0/11/c897b08e00d9778a0dea895d5e88031180812941e3cd7fc63fa26034767a/ujson-6.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:9d522e95bffac7338178757a7931b81639b9e0f2a3ee6e8c7ffdf867f2bfed36", size = 235353, upload-time = "2026-09-04T03:54:24.587Z" },
{ url = "https://files.pythonhosted.org/packages/99/cc/69a625656d73634af2e7bb8854b05f0d47a4650954e0876e28515965a522/ujson-6.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:bc6df52a60b521c7b7d69de0c14856397d3cce1e39aa22cfe439c350d6f52524", size = 415010, upload-time = "2026-09-04T03:54:25.781Z" },
{ url = "https://files.pythonhosted.org/packages/bd/53/cdc879e035a9b67e50fa34aa13d2d9160a826801a5fcf2993f48b9768944/ujson-6.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:222389a616f6407eb40e1efa80a35c1ba468903e50a305faf425c26e3c32bdb9", size = 55660, upload-time = "2026-09-04T03:54:26.996Z" },
{ url = "https://files.pythonhosted.org/packages/51/29/33891cfee86cc13e00a1de6fa326378a637dad786078aaf56ab6336c60cf/ujson-6.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:593acfa0f36ada24e89c07147441fe364081fa1631db73ee55f40893c196e0b9", size = 54730, upload-time = "2026-09-04T03:54:28.106Z" },
{ url = "https://files.pythonhosted.org/packages/16/f6/2d4bd6fb364f8ded5840854bdda58032c8cd11614d70ce0c125a52dfb7a9/ujson-6.0.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5b3afbe992e2d1b8c1e4e7a0da2c77da23f29545e5ba695a4a9241702234f20e", size = 59301, upload-time = "2026-09-04T03:54:29.215Z" },
{ url = "https://files.pythonhosted.org/packages/61/fd/7baf38f591fd964558891a1798e9b49078558346a24020b7c27945389130/ujson-6.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:65e0e0c21ead4d0087c9c65a82eb2446c4bd51d36388d41035ce773517e7a3bf", size = 53364, upload-time = "2026-09-04T03:54:30.327Z" },
{ url = "https://files.pythonhosted.org/packages/63/c0/640ed28e4443c81e3ed9cbec2b216f4c3943388f4f45b703e8e0993c4f8f/ujson-6.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f3eff1f93d9d1f0bd5eee35883b9c71ad9befcfcd0ddc7cd5862c69fba21cf6", size = 54447, upload-time = "2026-09-04T03:54:31.527Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f7/3688adc11a3e22e4b26563256404c6557682efe62510f988bf7dfc09d8b1/ujson-6.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4579b8c96824f65888d4a615463c2dc2b7db6c6f0c7f83ece2a58714fd1a8123", size = 58263, upload-time = "2026-09-04T03:54:32.587Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c9/9ab8d5ab9ca362381d0fcc8c6e6a831e96385a908792b2378db6282a374e/ujson-6.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8af54166141d5c8ebeebc044c3569ef10edfcdf6fd8ecb487a2bf33c776ebc8f", size = 57154, upload-time = "2026-09-04T03:54:33.698Z" },
{ url = "https://files.pythonhosted.org/packages/23/01/82ed9b5594d770f6490334ce78af22c754b91b8de12efd3ddfaa1d23da9a/ujson-6.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ad8bdad17cfc64aefb049e53687ff8730a72e2c3d99edcb36001683122597846", size = 1038449, upload-time = "2026-09-04T03:54:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/5f/dc/3cea633a17cb79d8b642e06b6c07f21ac31072a4b3043cc41df74db54fa5/ujson-6.0.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0dd8981828f6b515ba5e9f2473f433aa59bebe4784182b48695b71af52033b4f", size = 1197332, upload-time = "2026-09-04T03:54:36.441Z" },
{ url = "https://files.pythonhosted.org/packages/f9/1d/3fcc1ae871d8cd6ee40ec7e92556d9641fdf248875656780c4feea33c793/ujson-6.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:97caee7e4c3e20dff9e6adca0b7443c3cf9d7546ed5d0750954c5bb5456bad86", size = 1089519, upload-time = "2026-09-04T03:54:38.866Z" },
{ url = "https://files.pythonhosted.org/packages/85/86/af921b0c127f2c2d953836abfd277178bcbdfdf72318f26b4793d04a0c9d/ujson-6.0.0-cp314-cp314t-win32.whl", hash = "sha256:3bd770b553bebc408b49d6fdb46efb1dc568368d949ac7813a07fcccaea044ae", size = 236539, upload-time = "2026-09-04T03:54:40.163Z" },
{ url = "https://files.pythonhosted.org/packages/79/12/bb371cd75bb779d3282e5c1efaeb5e20bada1faecba6d83cabdd36756031/ujson-6.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:683501475e3dfa935574bfd2b3d26f7393b4a880a745aeab63cc3d013027bba0", size = 236201, upload-time = "2026-09-04T03:54:41.568Z" },
{ url = "https://files.pythonhosted.org/packages/68/82/f301c155669dd0bec9e569ecd5013b61e88528b7587bec2457b96b7fce23/ujson-6.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e1fa46cb8ddbfba2adf8277b8225e2ebf5bae435e2251c730c17bc0020f63c5e", size = 415482, upload-time = "2026-09-04T03:54:42.809Z" },
{ url = "https://files.pythonhosted.org/packages/c2/2b/020feca4cc502b274029cd1514d428908e334a17386761d157da32447fa6/ujson-6.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:b2ab962524adb39dbad565fd259e15a1c26b8944fa978c24ed6dea5ab1eeefd0", size = 55210, upload-time = "2026-09-04T03:54:44.1Z" },
{ url = "https://files.pythonhosted.org/packages/4e/0e/1cd913419d17260f6d4c9869ab1208132b1281f4db00d3f07b664712ffdf/ujson-6.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:dae3765f731779faa947715485f6794bc5984802be4584478a3e9e5143dd62e1", size = 54370, upload-time = "2026-09-04T03:54:45.249Z" },
{ url = "https://files.pythonhosted.org/packages/0a/0e/876719d6f04bb48560806bb508a038550f6a8184558f0a7274bc3015e1fa/ujson-6.0.0-cp315-cp315-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:34c0403b485d8ddd86bd29d879cc9f72223579b57188b0a2bc07a8b06f8cfbdf", size = 55682, upload-time = "2026-09-04T03:54:46.34Z" },
{ url = "https://files.pythonhosted.org/packages/99/92/b59b4827a9c6ba0d12939b0d6e790b8629946873b7a655ff5a06735bd173/ujson-6.0.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d56340493496d50ccc41b460610c1ce6a197aac710733b5f36910e8c9f3ba6d", size = 52648, upload-time = "2026-09-04T03:54:47.641Z" },
{ url = "https://files.pythonhosted.org/packages/6c/49/3d702afd9beb434f5140b12ffdf88198c144c1de5bd17a2a3a6fd7872b22/ujson-6.0.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a38a21efd05384fb82d35bed81fac0ff6056ea39c3dee3c293885ce910879dd0", size = 54007, upload-time = "2026-09-04T03:54:48.714Z" },
{ url = "https://files.pythonhosted.org/packages/52/fb/4dd3f307f62f0b22f33b9d760efbfa7c7890a76591e6867c72bd27070966/ujson-6.0.0-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee87d8c4a4ebbef1c7cb2cf251a1d77726ef06a1597ed04d3dce92709b8fe0f1", size = 57412, upload-time = "2026-09-04T03:54:49.753Z" },
{ url = "https://files.pythonhosted.org/packages/f2/12/03ef04cde2e056f9ec699046f78bf2e8c1339ebfe0f29486098bf965e9ca/ujson-6.0.0-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:928d83b72808dc73a5df530b7fc27101052be1baf013a5dd75a1535de6cf107e", size = 56439, upload-time = "2026-09-04T03:54:50.795Z" },
{ url = "https://files.pythonhosted.org/packages/26/d6/5cd07dc0732de702101e2360b07f2841ba50a77d2d758b0042623caae049/ujson-6.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cd835565b660ca125f5895105981d691c708c15367b88a69fa4d92ddbe24504a", size = 1037774, upload-time = "2026-09-04T03:54:52.087Z" },
{ url = "https://files.pythonhosted.org/packages/bc/a3/59bcf91336ceebeb6a54716987c7069f9ddf99579488e513252300beb6ba/ujson-6.0.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:e6926204905e1a2f278bacf92ff2fe31343bcc7fb9ff08fdd42be66b3a217ef0", size = 1193839, upload-time = "2026-09-04T03:54:53.548Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1c/fb168acf568b8d1312cfb3b079ae4a91ff95130219551ce2a3fd5edf71a4/ujson-6.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7a1472649bc9ef3b9ce3ab279e9e812368bfac25210b7ec96bd544767c019577", size = 1088955, upload-time = "2026-09-04T03:54:55.287Z" },
{ url = "https://files.pythonhosted.org/packages/49/c2/6fe4524ff1edc26234f67b1dc9077e05c70dd59cdc736297dea18b171bd5/ujson-6.0.0-cp315-cp315-win32.whl", hash = "sha256:aea27aa0927b0423a0cfb167bd505c2dc59d1df65c66372204e43ba94fc964a8", size = 235843, upload-time = "2026-09-04T03:54:56.797Z" },
{ url = "https://files.pythonhosted.org/packages/26/bc/1a118013f92236150444d6ff931e78e698bf45f2bb9e9688d625970f3557/ujson-6.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:102ddbb1677540f0cae80cc36f5db9663a626c7b3bf872ed10f10fe72343a3c9", size = 235351, upload-time = "2026-09-04T03:54:58.02Z" },
{ url = "https://files.pythonhosted.org/packages/b1/db/001d7bd04cde9cd35fb0635239026ad0ae8b57cf12e770db3427dfc85217/ujson-6.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:9ef1920b423effe2837351d19a2278d7a516404a07200cca30b881077a2d7877", size = 415004, upload-time = "2026-09-04T03:54:59.741Z" },
{ url = "https://files.pythonhosted.org/packages/e9/60/5c91a9e9e7f0b433dd782c57c388f7e764f162a1de433545f30fe93f48c6/ujson-6.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:7168df25a051fd2a60f8d123b2123b60ead7c1f22cdd467ab7c2bba0fad0aec1", size = 55668, upload-time = "2026-09-04T03:55:01.139Z" },
{ url = "https://files.pythonhosted.org/packages/09/dd/1dddba1b0f74092f433e6a26ce4cb0f419a7a93575b54fcbb0c6d64e616d/ujson-6.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:987e191700873419cc23d94d4212e57a85df24eebbe9a33785907b0c99a5a57a", size = 54828, upload-time = "2026-09-04T03:55:02.353Z" },
{ url = "https://files.pythonhosted.org/packages/70/2d/6e65a3a336717d65cd8035ff870ad507b5a6375b8bc992718e744d620891/ujson-6.0.0-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:0eeef12ef46e129278b50ca4c66c6b35c318f2fd09346bacddf218ed378cc0bb", size = 56651, upload-time = "2026-09-04T03:55:03.39Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a4/89d2bfc97fd073a3fe44c90beea19eb402c48101f83d46626d4b4d32c9d4/ujson-6.0.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68d623416ad997666bd8ea899b15554462b6250e803f4ce084c7dfd06a775314", size = 53687, upload-time = "2026-09-04T03:55:04.587Z" },
{ url = "https://files.pythonhosted.org/packages/02/5b/ff1227377dbd1b1bb5834d59e3410ff27ef9c1eac1125fbb610e220f0e47/ujson-6.0.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7253ae5cac107d2940226a113165738630a98c19cdeaec1e6d6d6c3a7c307b95", size = 54873, upload-time = "2026-09-04T03:55:05.828Z" },
{ url = "https://files.pythonhosted.org/packages/55/49/f80678f440126a3bd20253b91cf5bb200f1233bc5822f90024c7c94cfcd0/ujson-6.0.0-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b6494d29f7103a97d930cbd25f23fdc4d77e145a931e743660d697a200fd831", size = 58581, upload-time = "2026-09-04T03:55:07.009Z" },
{ url = "https://files.pythonhosted.org/packages/4c/2d/742897add5ea6b4ac9262208fba4bcb463e3f1b60606b6d79f05c7f27f17/ujson-6.0.0-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d56d408ccfb9b0e5c2b4ea687396df30ca42ebe2aedac88362069620ce65402", size = 57408, upload-time = "2026-09-04T03:55:08.105Z" },
{ url = "https://files.pythonhosted.org/packages/e1/b1/8747b3acf29d6219b042e8983f840fd4866dd9c65e5da9457311d4fb4fa1/ujson-6.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:add6b3827cbd6ce068ad70b1b890d44271801386a726e2bafe5bced784466642", size = 1038904, upload-time = "2026-09-04T03:55:09.313Z" },
{ url = "https://files.pythonhosted.org/packages/3f/21/deab9b41b6a8737210cd2460054e915f10b878bfda824e2dccc3e5f0db5f/ujson-6.0.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1cda9f81e58120675dbaba7b254849ee59698e5dee83c4383a3c1a96ca92a679", size = 1194845, upload-time = "2026-09-04T03:55:10.988Z" },
{ url = "https://files.pythonhosted.org/packages/31/40/b25a5f2b7bb6a5940692dc9d10bd89dad0c1e7d5af64c473d7391ec94513/ujson-6.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:d7945560fc6ce687ea83aa0bc375aa8a1101d9eee1fcbd085c5e0a5b6c6ac8ad", size = 1089861, upload-time = "2026-09-04T03:55:12.564Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ce/baf673bd0ebe6135abb5bee5a4dcf162d2a79608bdd18fbd0e47bb7371ce/ujson-6.0.0-cp315-cp315t-win32.whl", hash = "sha256:54ab6b66fa6f67dfa8234e109df132074e155af3b299ad83aab13ba4b6db9b3f", size = 236549, upload-time = "2026-09-04T03:55:14.153Z" },
{ url = "https://files.pythonhosted.org/packages/15/e8/39a55080f06270c7fb9a9e6384a2cc8a9d24094ffe90c6447fea6724f346/ujson-6.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:801ff407fda799f4ff98d960342128b065a14113eaccfc116b50092342636861", size = 236190, upload-time = "2026-09-04T03:55:16.018Z" },
{ url = "https://files.pythonhosted.org/packages/40/76/ccb45390fb2bab53b69c7a49c0cec655a93727eeae0b3912132fa7150649/ujson-6.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a2e699d5f290f81829f42638f8bc6582e3e73452d8607edf749ad3e1843946fa", size = 415495, upload-time = "2026-09-04T03:55:17.279Z" },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"