mirror of
https://github.com/snowy-yang/qqbotsdk.git
synced 2026-10-21 11:34:48 +00:00
qqbotsdk: QQ 官方机器人 SDK(协议层/DI/业务 API/测试/文档)
This commit is contained in:
commit
0c364bf803
32 files changed
+3760
No files matched your search
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# 密钥与本地配置,绝不入库
|
||||||
|
.env
|
||||||
|
|
||||||
|
# 参考资料(botpy 官方 SDK 源码拷贝,非本 SDK 代码)
|
||||||
|
botpy_api.py
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# 工具缓存
|
||||||
|
.ruff_cache/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# 编辑器/IDE
|
||||||
|
.vscode/
|
||||||
|
.zcode/
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 snowy-yang
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# qqbotsdk
|
||||||
|
|
||||||
|
QQ 官方机器人 Python SDK(Python 3.13 / asyncio / aiohttp),支持 WebSocket 与 Webhook 两种接入方式。
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph 接入层
|
||||||
|
WS["WebsocketConnecter(ws 长连接 / 重连 / 看门狗)"]
|
||||||
|
WH["WebhookConnecter(http 回调 / 验签 / 去重)"]
|
||||||
|
end
|
||||||
|
subgraph 分发层
|
||||||
|
Q[("EventQueue")]
|
||||||
|
EE["EventEmitter(路由 / DI 注入 / 异常隔离)"]
|
||||||
|
end
|
||||||
|
subgraph 协议层
|
||||||
|
WSP["WebsocketProtocol(鉴权 / 心跳 / 序列号)"]
|
||||||
|
WHP["WebhookProtocol(op=13 验证应答)"]
|
||||||
|
end
|
||||||
|
subgraph 业务层
|
||||||
|
API["BotApi(REST + ApiError)"]
|
||||||
|
USER["用户 handler @ee.on(...)"]
|
||||||
|
end
|
||||||
|
QQ["QQ 开放平台"]
|
||||||
|
|
||||||
|
WS --> Q
|
||||||
|
WH --> Q
|
||||||
|
Q --> EE
|
||||||
|
EE <--> WSP
|
||||||
|
EE --> USER
|
||||||
|
USER --> API
|
||||||
|
API --> QQ
|
||||||
|
WS --> QQ
|
||||||
|
```
|
||||||
|
|
||||||
|
组件生命周期由 dishka 容器统一装配(APP scope,无全局单例),依赖关系、数据流与设计决策详见 [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 内置协议处理器(`ws_protocol.py` / `webhook_protocol.py`):HELLO 鉴权(IDENTIFY/RESUME 自动切换)、apscheduler 定时心跳、READY 会话捕获、INVALID_SESSION 会话失效处理
|
||||||
|
- 断线自动重连(指数退避 + 心跳看门狗),重连时自动 RESUME 续传
|
||||||
|
- Webhook 接入自带 Ed25519 验签、op=13 验证应答与 60s TTL 事件去重
|
||||||
|
- 事件订阅由项目根目录的 `intents.toml` 控制,将对应事件改为 `true` 即可
|
||||||
|
- 组件生命周期由 dishka DI 容器统一管理,全 SDK 共享一个 HTTP 连接池
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
`.env`(`APPID`/`APPSECRET` 必填,缺失启动即报错):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
APPID=你的AppID
|
||||||
|
APPSECRET=你的AppSecret
|
||||||
|
BASE_URL=https://api.bot.qq.com # 可选,默认官方地址
|
||||||
|
TIMEOUT=5000 # 可选,毫秒
|
||||||
|
CONNECTER=websocket # websocket 或 webhook
|
||||||
|
|
||||||
|
# 仅 webhook 接入时
|
||||||
|
WEBHOOK_HOST=0.0.0.0
|
||||||
|
WEBHOOK_PORT=8080
|
||||||
|
WEBHOOK_PATH=/qqbot/webhook
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
编写入口(如 `main.py`),把自己构造的 `EventEmitter` 传给 `main()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from qqbotsdk import EventEmitter, EventQueue, main
|
||||||
|
|
||||||
|
ee = EventEmitter(EventQueue())
|
||||||
|
|
||||||
|
# 方式一:裸参数(d 为原始事件数据)
|
||||||
|
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def on_group_message(d):
|
||||||
|
print("收到群消息:", d)
|
||||||
|
|
||||||
|
# 方式二:类型化 payload(dataclass,见 qqbotsdk.payloads)
|
||||||
|
from qqbotsdk.payloads import GroupAtMessage
|
||||||
|
|
||||||
|
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def on_group_message(msg: GroupAtMessage):
|
||||||
|
print(msg.group_openid, msg.user_openid, msg.content)
|
||||||
|
|
||||||
|
# 方式三:依赖注入(dishka)
|
||||||
|
from qqbotsdk import Inject
|
||||||
|
from qqbotsdk.api import BotApi
|
||||||
|
from qqbotsdk.events import Event
|
||||||
|
|
||||||
|
@ee.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def handle(event: Event, api: Inject[BotApi]):
|
||||||
|
print(event.user_id, event.group_id, event.content)
|
||||||
|
# 被动回复:带 msg_id(群聊 5 分钟/单聊 60 分钟内有效,最多分别回复 5/4 次);
|
||||||
|
# 同一条消息多次回复递增 msg_seq
|
||||||
|
await api.post_group_message(
|
||||||
|
event.group_id,
|
||||||
|
content=f"你说:{event.content}",
|
||||||
|
msg_id=event.message_id,
|
||||||
|
) # 失败抛 ApiError;富媒体先 upload_group_file 拿 file_info
|
||||||
|
|
||||||
|
# 业务事件(op=0)处理器的返回值不会回流;
|
||||||
|
# 协议层(op≠0)由 ws_protocol / webhook_protocol 的类处理,经 reply 队列发回。
|
||||||
|
# 单个 handler 抛异常不会影响其他 handler 和主循环,异常会被记录。
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(ee)
|
||||||
|
```
|
||||||
|
|
||||||
|
运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Webhook 接入:将 `.env` 中 `CONNECTER` 改为 `webhook`,运行后把
|
||||||
|
`http://<你的域名>:8080/qqbot/webhook` 配置到 QQ 开放平台回调地址(仅支持 80/443/8080/8443 端口,需公网 HTTPS)。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest # 单元测试
|
||||||
|
uv run ruff check src tests && uv run pyright
|
||||||
|
```
|
||||||
|
|
||||||
|
## 待完善
|
||||||
|
|
||||||
|
- 更多 OpenAPI 封装(按需在 `api.py` 的 `BotApi` 上加薄封装即可)
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# qqbotsdk 架构
|
||||||
|
|
||||||
|
本文描述 SDK 的分层结构、数据流与扩展点。阅读对象:维护者与想深入定制的使用者。
|
||||||
|
|
||||||
|
## 总览
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph 接入层["接入层 connecter.py"]
|
||||||
|
WS["WebsocketConnecter\ngateway握手 + ws长连接\n指数退避重连 + 心跳看门狗"]
|
||||||
|
WH["WebhookConnecter\naiohttp server\n验签 + TTL去重"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 分发层["分发层 emitter.py"]
|
||||||
|
Q[("EventQueue\nmsg_queue / reply_queue")]
|
||||||
|
EE["EventEmitter\nop/t 路由 + DI 注入\nhandler 异常隔离"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 协议层["协议层 BaseProtocol"]
|
||||||
|
WSP["WebsocketProtocol\nIDENTIFY/RESUME\napscheduler 心跳\ns 序列号跟踪"]
|
||||||
|
WHP["WebhookProtocol\nop=13 验证应答"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 业务层["业务层"]
|
||||||
|
API["BotApi\nREST 封装 + ApiError"]
|
||||||
|
USER["用户 handler\n@ee.on(...)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 基础设施["基础设施"]
|
||||||
|
CFG["Config\n.env 一次性读齐"]
|
||||||
|
HTTP[("ClientSession\n共享连接池")]
|
||||||
|
TK["AccessToken\n缓存 + 自动续期"]
|
||||||
|
CRY["crypto\nEd25519 验签/签名"]
|
||||||
|
end
|
||||||
|
|
||||||
|
QQ["QQ 开放平台"]
|
||||||
|
|
||||||
|
WS -- "事件 payload" --> Q
|
||||||
|
WH -- "事件 payload" --> Q
|
||||||
|
Q -- "dispatch 泵" --> EE
|
||||||
|
EE -- "on_payload" --> WSP
|
||||||
|
WSP -- "IDENTIFY/HEARTBEAT 等\npayload 返回值" --> Q
|
||||||
|
EE -- "Event 注入" --> USER
|
||||||
|
USER -- "Inject[BotApi]" --> API
|
||||||
|
API --> QQ
|
||||||
|
WS --> QQ
|
||||||
|
TK --> HTTP
|
||||||
|
API --> TK
|
||||||
|
WSP --> TK
|
||||||
|
WHP --> CRY
|
||||||
|
WH --> CRY
|
||||||
|
CFG -. "构造注入" .-> WS & WH & API & TK & WSP & WHP
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流(WebSocket 接入为例)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant QQ as QQ 开放平台
|
||||||
|
participant WS as WebsocketConnecter
|
||||||
|
participant Q as EventQueue
|
||||||
|
participant EE as EventEmitter
|
||||||
|
participant P as WebsocketProtocol
|
||||||
|
participant H as 用户 handler
|
||||||
|
|
||||||
|
QQ->>WS: HELLO (op=10)
|
||||||
|
WS->>Q: put_event
|
||||||
|
Q->>EE: dispatch 泵
|
||||||
|
EE->>P: on_payload(记 s 序列号)
|
||||||
|
EE->>P: 路由到 HELLO handler
|
||||||
|
P-->>Q: IDENTIFY/RESUME payload → reply_queue
|
||||||
|
WS->>QQ: send(过滤 NOT_SET 字段)
|
||||||
|
|
||||||
|
loop 每次心跳间隔
|
||||||
|
P->>Q: HEARTBEAT payload(apscheduler 定时)
|
||||||
|
WS->>QQ: send
|
||||||
|
QQ->>WS: HEARTBEAT_ACK(刷新看门狗时钟)
|
||||||
|
end
|
||||||
|
|
||||||
|
QQ->>WS: DISPATCH 事件 (op=0, t=GROUP_AT_MESSAGE_CREATE)
|
||||||
|
WS->>Q: put_event
|
||||||
|
Q->>EE: dispatch 泵
|
||||||
|
EE->>H: 开 REQUEST scope,注入 Event/Inject[BotApi]
|
||||||
|
H->>EE: 返回值不回流(op=0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 分层规则
|
||||||
|
|
||||||
|
依赖方向自上而下,下层不认识上层:
|
||||||
|
|
||||||
|
| 层 | 模块 | 职责 | 不允许出现 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 接入层 | `connecter.py` | 传输细节:握手、重连、验签、去重 | 事件业务语义、handler 调用 |
|
||||||
|
| 分发层 | `emitter.py` | 路由、DI 注入、异常隔离、应答回流 | 传输细节、具体协议状态机 |
|
||||||
|
| 协议层 | `ws_protocol.py` `webhook_protocol.py` | 鉴权/心跳/验证等协议状态机 | 传输细节(只经队列收发) |
|
||||||
|
| 业务层 | `api.py` `payloads.py` + 用户代码 | OpenAPI 调用、payload 解析与机器人逻辑 | 协议细节 |
|
||||||
|
|
||||||
|
跨层共享的最小公共物:`model.py`(Opcode/Payload/Intent)、`queue.py`(事件通道)、`config.py`、`session.py`(ws 会话状态)、`payloads.py`(事件 dataclass 与解析,被 `events.py`/`emitter.py` 使用)。
|
||||||
|
|
||||||
|
## 生命周期与依赖装配
|
||||||
|
|
||||||
|
- 全部组件由 dishka 以 **APP scope** 构造注入(`di.py`),无任何全局单例。
|
||||||
|
- 唯一的 `ClientSession` 以 async-generator provider 注册,`container.close()` 时统一释放。
|
||||||
|
- 接入方式的选择收敛在 `ADAPTERS` 表:`make_container()` 按 `CONNECTER` 环境变量动态 `provider.provide(实现类, provides=抽象)`,主体代码只依赖 `BaseProtocol`/`Connecter` 抽象。
|
||||||
|
- 用户在 `main(ee)` 之前构造的 `EventEmitter` 会被容器复用(连同它的队列),保证 handler 注册在主体实际使用的实例上。
|
||||||
|
|
||||||
|
## 关键设计决策
|
||||||
|
|
||||||
|
1. **管线而非回调直调**:事件一律经 `EventQueue` 异步流转,接入与分发两侧解耦——Webhook handler 里不需要知道 ws 的存在,反之亦然。代价是多一跳队列延迟(可忽略)。
|
||||||
|
2. **协议层状态机独立**(`BaseProtocol.register/on_payload`):emitter 不认识"心跳""序列号"这些 ws 概念;协议类通过返回 payload dict 经 reply 队列外发。
|
||||||
|
3. **看门狗两层互补**(见 `connecter.py` 模块注释):静默超时抓连接彻底失联,ACK 期限检查抓"有事件流但心跳已死"。阈值 `_SILENCE_FACTOR=1.1`、`_ACK_FACTOR=1.2` 是按 ACK 自然节奏校准的,**不能压到 1× 整**,否则正常抖动会周期性误杀(误杀由 RESUME 兜底,不丢消息)。
|
||||||
|
4. **handler 异常就地隔离**:`_invoke`/`_call_direct` 捕获记日志,`dispatch` 主循环另有兜底;单条事件的单个 handler 失败不影响进程。
|
||||||
|
5. **注入约定**:handler 参数标注 `Inject[T]`(或裸标 `Event`/`Session`/`BotApi`)→ 从 dishka scope 解析;标注 `payloads.py` 里的 dataclass 类型 → 拿到解析后的 payload 对象(无容器路径同样生效);未标注或解析失败 → 回退传原始 `d`,保持旧处理器兼容。注意 `Inject` 不能改成 PEP 695 懒别名(`get_type_hints` 不解包)。
|
||||||
|
|
||||||
|
## 扩展点
|
||||||
|
|
||||||
|
- **新增接入方式**(如轮询/其他平台):实现 `Connecter.run()` 与 `BaseProtocol.register()`,在 `di.ADAPTERS` 注册一行,`Config.load` 的白名单加一个名字。
|
||||||
|
- **新增业务 API**:在 `api.py` 的 `BotApi` 上加方法(或独立模块组合 `BotApi`),全部自动获得鉴权、超时与错误处理。群/单聊消息经 `_post_message` 统一处理 msg_id/msg_seq/media 语义;已有方法覆盖:单聊/群聊消息与富媒体上传、频道消息/撤回/表态、公告、精华消息、日程、禁言、频道信息查询(guild/channels/roles/member)。
|
||||||
|
- **新增事件类型 dataclass**:在 `payloads.py` 定义 dataclass 并登记 `EVENT_TYPES`,handler 即可按类型标注拿解析结果;未收录事件回退传 dict。当前已覆盖 intents.toml 的全部事件组(消息、成员变动、审核、表态、互动、论坛、音频)。
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
`tests/` 覆盖:
|
||||||
|
|
||||||
|
- 纯逻辑:crypto 往返、intents 解析、Config 校验、`_route` 路由边界、webhook 去重、序列号单调性、handler 异常隔离与 reply 回流、payload dataclass 解析、BotApi 各业务方法的拼参(Recording 子类截获 request)。
|
||||||
|
- **网络集成**(`test_ws_integration.py` / `test_webhook_integration.py`):
|
||||||
|
- WebSocket:aiohttp 假网关(token 接口 + gateway + ws 协议行为)端到端跑通 HELLO → IDENTIFY → READY 会话捕获 → apscheduler 心跳/ACK → 业务事件 dataclass 分发;
|
||||||
|
- Webhook:真实起 `WebhookConnecter` 的 http server(TestServer),覆盖 op=13 验证应答(签名内容正确性)、合法签名放行分发、坏签名 401、同 id 去重。
|
||||||
|
|
||||||
|
两套集成测试验证的都是真实连接路径,改接入层/协议层后跑 `uv run pytest` 即可回归。
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# =============================================
|
||||||
|
# 事件监听配置(TOML)
|
||||||
|
# 修改 true 表示监听该事件,false 表示不监听
|
||||||
|
# 分组名与位掩码对应关系见注释
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# GUILDS (1 << 0)
|
||||||
|
[GUILDS]
|
||||||
|
GUILD_CREATE = false # 当机器人加入新guild时
|
||||||
|
GUILD_UPDATE = false # 当guild资料发生变更时
|
||||||
|
GUILD_DELETE = false # 当机器人退出guild时
|
||||||
|
CHANNEL_CREATE = false # 当channel被创建时
|
||||||
|
CHANNEL_UPDATE = false # 当channel被更新时
|
||||||
|
CHANNEL_DELETE = false # 当channel被删除时
|
||||||
|
|
||||||
|
# GUILD_MEMBERS (1 << 1)
|
||||||
|
[GUILD_MEMBERS]
|
||||||
|
GUILD_MEMBER_ADD = false # 当成员加入时
|
||||||
|
GUILD_MEMBER_UPDATE = false # 当成员资料变更时
|
||||||
|
GUILD_MEMBER_REMOVE = false # 当成员被移除时
|
||||||
|
|
||||||
|
# GUILD_MESSAGES (1 << 9) 消息事件,仅 *私域* 机器人能够设置此 intents。
|
||||||
|
[GUILD_MESSAGES]
|
||||||
|
MESSAGE_CREATE = false # 发送消息事件(所有消息)
|
||||||
|
MESSAGE_DELETE = false # 删除(撤回)消息事件
|
||||||
|
|
||||||
|
# GUILD_MESSAGE_REACTIONS (1 << 10)
|
||||||
|
[GUILD_MESSAGE_REACTIONS]
|
||||||
|
MESSAGE_REACTION_ADD = false # 为消息添加表情表态
|
||||||
|
MESSAGE_REACTION_REMOVE = false # 为消息删除表情表态
|
||||||
|
|
||||||
|
# DIRECT_MESSAGE (1 << 12)
|
||||||
|
[DIRECT_MESSAGE]
|
||||||
|
DIRECT_MESSAGE_CREATE = false # 当收到用户发给机器人的私信消息时
|
||||||
|
DIRECT_MESSAGE_DELETE = false # 删除(撤回)消息事件
|
||||||
|
|
||||||
|
# GROUP_AND_C2C_EVENT (1 << 25)
|
||||||
|
[GROUP_AND_C2C_EVENT]
|
||||||
|
C2C_MESSAGE_CREATE = true # 用户单聊发消息给机器人时候
|
||||||
|
FRIEND_ADD = true # 用户添加使用机器人
|
||||||
|
FRIEND_DEL = true # 用户删除机器人
|
||||||
|
C2C_MSG_REJECT = true # 用户在机器人资料卡手动关闭"主动消息"推送
|
||||||
|
C2C_MSG_RECEIVE = true # 用户在机器人资料卡手动开启"主动消息"推送
|
||||||
|
GROUP_AT_MESSAGE_CREATE = true # 用户在群里@机器人时收到的消息
|
||||||
|
GROUP_ADD_ROBOT = true # 机器人被添加到群聊
|
||||||
|
GROUP_DEL_ROBOT = true # 机器人被移出群聊
|
||||||
|
GROUP_MSG_REJECT = true # 群管理员主动关闭通知
|
||||||
|
GROUP_MSG_RECEIVE = true # 群管理员主动开启通知
|
||||||
|
|
||||||
|
# INTERACTION (1 << 26)
|
||||||
|
[INTERACTION]
|
||||||
|
INTERACTION_CREATE = false # 互动事件创建时
|
||||||
|
|
||||||
|
# MESSAGE_AUDIT (1 << 27)
|
||||||
|
[MESSAGE_AUDIT]
|
||||||
|
MESSAGE_AUDIT_PASS = false # 消息审核通过
|
||||||
|
MESSAGE_AUDIT_REJECT = false # 消息审核不通过
|
||||||
|
|
||||||
|
# FORUMS_EVENT (1 << 28) 论坛事件,仅 *私域* 机器人能够设置此 intents。
|
||||||
|
[FORUMS_EVENT]
|
||||||
|
FORUM_THREAD_CREATE = false # 当用户创建主题时
|
||||||
|
FORUM_THREAD_UPDATE = false # 当用户更新主题时
|
||||||
|
FORUM_THREAD_DELETE = false # 当用户删除主题时
|
||||||
|
FORUM_POST_CREATE = false # 当用户创建帖子时
|
||||||
|
FORUM_POST_DELETE = false # 当用户删除帖子时
|
||||||
|
FORUM_REPLY_CREATE = false # 当用户回复评论时
|
||||||
|
FORUM_REPLY_DELETE = false # 当用户回复评论时
|
||||||
|
FORUM_PUBLISH_AUDIT_RESULT = false # 当用户发表审核通过时
|
||||||
|
|
||||||
|
# AUDIO_ACTION (1 << 29)
|
||||||
|
[AUDIO_ACTION]
|
||||||
|
AUDIO_START = false # 音频开始播放时
|
||||||
|
AUDIO_FINISH = false # 音频播放结束时
|
||||||
|
AUDIO_ON_MIC = false # 上麦时
|
||||||
|
AUDIO_OFF_MIC = false # 下麦时
|
||||||
|
|
||||||
|
# PUBLIC_GUILD_MESSAGES (1 << 30) 消息事件,此为公域的消息事件
|
||||||
|
[PUBLIC_GUILD_MESSAGES]
|
||||||
|
AT_MESSAGE_CREATE = false # 当收到@机器人的消息时
|
||||||
|
PUBLIC_MESSAGE_DELETE = false # 当频道的消息被删除时
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
[project]
|
||||||
|
name = "qqbotsdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "QQ 官方机器人 Python SDK(WebSocket / Webhook)"
|
||||||
|
license = "MIT"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "snowy-yang", email = "102860110+snowy-yang@users.noreply.github.com" },
|
||||||
|
]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"aiohttp>=3.14.3",
|
||||||
|
"apscheduler>=3.11.3",
|
||||||
|
"cryptography>=50.0.1",
|
||||||
|
"dishka>=1.10.1",
|
||||||
|
"loguru>=0.7.3",
|
||||||
|
"pyee>=14.0.0",
|
||||||
|
"python-dotenv>=1.2.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
qqbotsdk = "qqbotsdk:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["uv_build>=0.12.7,<0.13.0"]
|
||||||
|
build-backend = "uv_build"
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = ["pyright>=1.1.411", "pytest>=8.3.0", "pytest-asyncio>=0.24.0", "ruff>=0.16.5"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py313"
|
||||||
|
line-length = 88
|
||||||
|
indent-width = 4
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"F", # Pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"N", # pep8-naming
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"SIM", # flake8-simplify
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long
|
||||||
|
"B008", # do not perform function calls in argument defaults
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
known-first-party = ["qqbotsdk"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/**/*.py" = ["S"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
line-ending = "auto"
|
||||||
|
|
||||||
|
[tool.pyright]
|
||||||
|
typeCheckingMode = "standard"
|
||||||
|
pythonVersion = "3.13"
|
||||||
|
include = ["src"]
|
||||||
|
exclude = ["**/__pycache__", "**/.*", "**/node_modules", ".venv"]
|
||||||
|
reportMissingImports = true
|
||||||
|
reportMissingTypeStubs = false
|
||||||
|
reportUnknownParameterType = false
|
||||||
|
reportUnknownVariableType = false
|
||||||
|
reportUnknownMemberType = false
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""qqbotsdk:QQ 官方机器人 SDK 入口。
|
||||||
|
|
||||||
|
用法:构造 `EventEmitter(EventQueue())` 注册 handler,交给 `main(ee)`
|
||||||
|
启动;连接方式由 .env 的 CONNECTER 决定(websocket / webhook)。
|
||||||
|
架构详情见 docs/ARCHITECTURE.md。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .connecter import Connecter
|
||||||
|
from .di import Inject as Inject
|
||||||
|
from .di import make_container
|
||||||
|
from .emitter import BaseProtocol as BaseProtocol
|
||||||
|
from .emitter import EventEmitter as EventEmitter
|
||||||
|
from .queue import EventQueue as EventQueue
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
async def run_loop(emitter: EventEmitter | None = None) -> None:
|
||||||
|
"""启动 SDK。可传入外部构造的 EventEmitter 以注册 handler;
|
||||||
|
不传时由容器自建。"""
|
||||||
|
container = make_container(emitter)
|
||||||
|
try:
|
||||||
|
emitter = await container.get(EventEmitter)
|
||||||
|
emitter.use_container(container)
|
||||||
|
emitter.register_protocol(await container.get(BaseProtocol))
|
||||||
|
connecter = await container.get(Connecter)
|
||||||
|
|
||||||
|
logger.info(f"qqbotsdk 启动,连接方式: {Config.load().connecter}")
|
||||||
|
await asyncio.gather(emitter.dispatch(), connecter.run())
|
||||||
|
finally:
|
||||||
|
# 容器关闭时统一释放共享的 HTTP 会话等 APP 级资源
|
||||||
|
await container.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main(emitter: EventEmitter | None = None) -> None:
|
||||||
|
try:
|
||||||
|
asyncio.run(run_loop(emitter))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logger.info("qqbotsdk 已退出")
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
"""OpenAPI 客户端:自动附带鉴权头,HTTP ≥400 抛 ApiError。
|
||||||
|
|
||||||
|
消息接口按接入面分组:v2 群/单聊(openid)、频道(channel_id)。
|
||||||
|
被动回复规则(官方文档 2026-09 核对):携带 msg_id(或 event_id,
|
||||||
|
二选一)为被动回复——单聊 60 分钟内最多 4 次,群聊 5 分钟内最多 5 次;
|
||||||
|
同一条消息多次回复需递增 msg_seq(相同 msg_id+msg_seq 会发送失败);
|
||||||
|
不携带则为主动消息(另有频控,群/单聊每日每对象 1000 条)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .token import AccessToken
|
||||||
|
|
||||||
|
# msg_type 取值
|
||||||
|
MSG_TYPE_TEXT = 0
|
||||||
|
MSG_TYPE_MARKDOWN = 2
|
||||||
|
MSG_TYPE_MEDIA = 7
|
||||||
|
# file_type 取值(富媒体上传)
|
||||||
|
FILE_TYPE_IMAGE = 1
|
||||||
|
FILE_TYPE_VIDEO = 2
|
||||||
|
FILE_TYPE_VOICE = 3
|
||||||
|
FILE_TYPE_FILE = 4
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(RuntimeError):
|
||||||
|
"""OpenAPI 调用失败:HTTP 非 2xx 或业务 code 非 0。"""
|
||||||
|
|
||||||
|
def __init__(self, status: int, code: Any, message: Any) -> None:
|
||||||
|
self.status = status
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
super().__init__(f"API 调用失败: HTTP {status}, code={code}, message={message}")
|
||||||
|
|
||||||
|
|
||||||
|
class BotApi:
|
||||||
|
"""通用 REST 封装;具体的业务接口在此之上做薄封装。"""
|
||||||
|
|
||||||
|
def __init__(self, config: Config, http: ClientSession, token: AccessToken) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._http = http
|
||||||
|
self._token = token
|
||||||
|
|
||||||
|
async def request(self, method: str, path: str, **kwargs) -> Any:
|
||||||
|
access_token = await self._token.get_access_token()
|
||||||
|
headers = {"Authorization": f"QQBot {access_token}"}
|
||||||
|
async with self._http.request(
|
||||||
|
method,
|
||||||
|
f"{self._config.base_url}{path}",
|
||||||
|
headers=headers,
|
||||||
|
**kwargs,
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
if resp.status >= 400:
|
||||||
|
logger.warning(f"{method} {path} 失败: {data}")
|
||||||
|
code = data.get("code") if isinstance(data, dict) else None
|
||||||
|
message = data.get("message") if isinstance(data, dict) else data
|
||||||
|
raise ApiError(resp.status, code, message)
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def get(self, path: str, **kwargs) -> Any:
|
||||||
|
return await self.request("GET", path, **kwargs)
|
||||||
|
|
||||||
|
async def post(self, path: str, **kwargs) -> Any:
|
||||||
|
return await self.request("POST", path, **kwargs)
|
||||||
|
|
||||||
|
async def delete(self, path: str, **kwargs) -> Any:
|
||||||
|
return await self.request("DELETE", path, **kwargs)
|
||||||
|
|
||||||
|
async def put(self, path: str, **kwargs) -> Any:
|
||||||
|
return await self.request("PUT", path, **kwargs)
|
||||||
|
|
||||||
|
async def patch(self, path: str, **kwargs) -> Any:
|
||||||
|
return await self.request("PATCH", path, **kwargs)
|
||||||
|
|
||||||
|
# ---------- 通用 ----------
|
||||||
|
|
||||||
|
async def me(self) -> dict:
|
||||||
|
"""机器人自身信息(GET /users/@me)。"""
|
||||||
|
return await self.get("/users/@me")
|
||||||
|
|
||||||
|
# ---------- v2 单聊(C2C) ----------
|
||||||
|
|
||||||
|
async def post_c2c_message(
|
||||||
|
self,
|
||||||
|
openid: str,
|
||||||
|
content: str = "",
|
||||||
|
msg_id: str | None = None,
|
||||||
|
msg_seq: int = 1,
|
||||||
|
media: dict | None = None,
|
||||||
|
event_id: str | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""发单聊消息(POST /v2/users/{openid}/messages)。
|
||||||
|
|
||||||
|
media 为富媒体上传接口返回的 {"file_info": ...} 结构,
|
||||||
|
此时 content 作媒体下方文本。msg_id(回复消息)与 event_id
|
||||||
|
(响应事件)二选一,均为被动回复。
|
||||||
|
"""
|
||||||
|
return await self._post_message(
|
||||||
|
f"/v2/users/{openid}/messages",
|
||||||
|
content=content,
|
||||||
|
msg_id=msg_id,
|
||||||
|
msg_seq=msg_seq,
|
||||||
|
media=media,
|
||||||
|
event_id=event_id,
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def upload_c2c_file(
|
||||||
|
self, openid: str, file_type: int, url: str, srv_send_msg: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""上传单聊富媒体(POST /v2/users/{openid}/files)。
|
||||||
|
|
||||||
|
返回含 file_info,可传入 post_c2c_message 的 media 直接发送,
|
||||||
|
或 srv_send_msg=True 直接作为消息发出。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/users/{openid}/files",
|
||||||
|
json={"file_type": file_type, "url": url, "srv_send_msg": srv_send_msg},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- v2 群聊 ----------
|
||||||
|
|
||||||
|
async def post_group_message(
|
||||||
|
self,
|
||||||
|
group_openid: str,
|
||||||
|
content: str = "",
|
||||||
|
msg_id: str | None = None,
|
||||||
|
msg_seq: int = 1,
|
||||||
|
media: dict | None = None,
|
||||||
|
event_id: str | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""发群消息(POST /v2/groups/{group_openid}/messages)。
|
||||||
|
|
||||||
|
msg_id(回复消息)与 event_id(响应 GROUP_ADD_ROBOT /
|
||||||
|
GROUP_MSG_RECEIVE / INTERACTION_CREATE 等事件)二选一。
|
||||||
|
"""
|
||||||
|
return await self._post_message(
|
||||||
|
f"/v2/groups/{group_openid}/messages",
|
||||||
|
content=content,
|
||||||
|
msg_id=msg_id,
|
||||||
|
msg_seq=msg_seq,
|
||||||
|
media=media,
|
||||||
|
event_id=event_id,
|
||||||
|
**extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def upload_group_file(
|
||||||
|
self, group_openid: str, file_type: int, url: str, srv_send_msg: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""上传群富媒体(POST /v2/groups/{group_openid}/files)。"""
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/{group_openid}/files",
|
||||||
|
json={"file_type": file_type, "url": url, "srv_send_msg": srv_send_msg},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _post_message(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
content: str,
|
||||||
|
msg_id: str | None,
|
||||||
|
msg_seq: int,
|
||||||
|
media: dict | None,
|
||||||
|
event_id: str | None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
body: dict[str, Any] = {"msg_type": MSG_TYPE_MEDIA if media else MSG_TYPE_TEXT}
|
||||||
|
if content:
|
||||||
|
body["content"] = content
|
||||||
|
if msg_id:
|
||||||
|
body["msg_id"] = msg_id
|
||||||
|
body["msg_seq"] = msg_seq
|
||||||
|
elif event_id:
|
||||||
|
body["event_id"] = event_id
|
||||||
|
body["msg_seq"] = msg_seq
|
||||||
|
if media is not None:
|
||||||
|
body["media"] = media
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post(path, json=body)
|
||||||
|
|
||||||
|
async def _withdraw_v2_message(self, path: str, message_id: str) -> dict:
|
||||||
|
# 发送超过 2 分钟的消息不可撤回
|
||||||
|
return await self.delete(f"{path}/messages/{message_id}")
|
||||||
|
|
||||||
|
async def withdraw_group_message(self, group_openid: str, message_id: str) -> dict:
|
||||||
|
"""撤回群消息(DELETE /v2/groups/{group_openid}/messages/{message_id})。
|
||||||
|
|
||||||
|
群管理员可撤回自己与普通成员的消息;普通成员仅能撤回自己的。
|
||||||
|
"""
|
||||||
|
return await self._withdraw_v2_message(f"/v2/groups/{group_openid}", message_id)
|
||||||
|
|
||||||
|
async def withdraw_c2c_message(self, openid: str, message_id: str) -> dict:
|
||||||
|
"""撤回单聊消息(DELETE /v2/users/{openid}/messages/{message_id})。"""
|
||||||
|
return await self._withdraw_v2_message(f"/v2/users/{openid}", message_id)
|
||||||
|
|
||||||
|
async def mute_group_member(self, group_openid: str, mute_expire_at: str) -> dict:
|
||||||
|
"""群成员禁言(POST /v2/groups/{group_openid}/restrict_chat_setting)。
|
||||||
|
|
||||||
|
mute_expire_at 为禁言到期时间(RFC3339 格式,如
|
||||||
|
"2026-01-01T12:00:00+08:00");传当前时间之前即解除禁言。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/v2/groups/{group_openid}/restrict_chat_setting",
|
||||||
|
json={"mute_expire_at": mute_expire_at},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 频道 ----------
|
||||||
|
|
||||||
|
async def post_channel_message(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
content: str = "",
|
||||||
|
msg_id: str | None = None,
|
||||||
|
msg_seq: int = 1,
|
||||||
|
image: str | None = None,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""发频道消息(POST /channels/{channel_id}/messages)。
|
||||||
|
|
||||||
|
image 为图片 URL;其余字段(embed/ark/markdown 等)经 extra 透传。
|
||||||
|
"""
|
||||||
|
body: dict[str, Any] = {"msg_seq": msg_seq}
|
||||||
|
if content:
|
||||||
|
body["content"] = content
|
||||||
|
if msg_id:
|
||||||
|
body["msg_id"] = msg_id
|
||||||
|
if image:
|
||||||
|
body["image"] = image
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post(f"/channels/{channel_id}/messages", json=body)
|
||||||
|
|
||||||
|
async def withdraw_channel_message(
|
||||||
|
self, channel_id: str, message_id: str, hide_tip: bool = False
|
||||||
|
) -> dict:
|
||||||
|
"""撤回频道消息(DELETE /channels/{channel_id}/messages/{message_id})。
|
||||||
|
|
||||||
|
hide_tip 控制是否隐藏撤回提示小灰条(官方参数名为 hidetip);
|
||||||
|
仅私域机器人可用。
|
||||||
|
"""
|
||||||
|
return await self.delete(
|
||||||
|
f"/channels/{channel_id}/messages/{message_id}",
|
||||||
|
params={"hidetip": str(hide_tip).lower()},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def put_channel_reaction(
|
||||||
|
self, channel_id: str, message_id: str, emoji_type: int, emoji_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""对频道消息表态(PUT .../reactions/{type}/{emoji_id})。
|
||||||
|
|
||||||
|
emoji_type=1 为系统表情,emoji_id 为系统表情序号(字符串)。
|
||||||
|
"""
|
||||||
|
return await self.put(
|
||||||
|
f"/channels/{channel_id}/messages/{message_id}"
|
||||||
|
f"/reactions/{emoji_type}/{emoji_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_channel_reaction(
|
||||||
|
self, channel_id: str, message_id: str, emoji_type: int, emoji_id: str
|
||||||
|
) -> dict:
|
||||||
|
"""删除自己对该消息的表态(DELETE .../reactions/{type}/{emoji_id})。"""
|
||||||
|
return await self.delete(
|
||||||
|
f"/channels/{channel_id}/messages/{message_id}"
|
||||||
|
f"/reactions/{emoji_type}/{emoji_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 频道管理 ----------
|
||||||
|
|
||||||
|
async def create_guild_announce(
|
||||||
|
self,
|
||||||
|
guild_id: str,
|
||||||
|
channel_id: str,
|
||||||
|
message_id: str,
|
||||||
|
announces_type: int = 0,
|
||||||
|
**extra: Any,
|
||||||
|
) -> dict:
|
||||||
|
"""创建频道公告(POST /guilds/{guild_id}/announces)。
|
||||||
|
|
||||||
|
announces_type: 0 成员公告(默认)、1 欢迎公告;
|
||||||
|
recommend_channels 等经 extra 透传(与消息类公告互斥)。
|
||||||
|
"""
|
||||||
|
body = {
|
||||||
|
"channel_id": channel_id,
|
||||||
|
"message_id": message_id,
|
||||||
|
"announces_type": announces_type,
|
||||||
|
}
|
||||||
|
body.update(extra)
|
||||||
|
return await self.post(f"/guilds/{guild_id}/announces", json=body)
|
||||||
|
|
||||||
|
async def delete_guild_announce(self, guild_id: str, message_id: str) -> dict:
|
||||||
|
"""删除频道公告(DELETE /guilds/{guild_id}/announces/{message_id})。
|
||||||
|
|
||||||
|
删除推荐子频道类型的公告需传 message_id="all"。
|
||||||
|
"""
|
||||||
|
return await self.delete(f"/guilds/{guild_id}/announces/{message_id}")
|
||||||
|
|
||||||
|
async def pin_channel_message(self, channel_id: str, message_id: str) -> dict:
|
||||||
|
"""添加精华消息(PUT /channels/{channel_id}/pins/{message_id})。"""
|
||||||
|
return await self.put(f"/channels/{channel_id}/pins/{message_id}")
|
||||||
|
|
||||||
|
async def unpin_channel_message(self, channel_id: str, message_id: str) -> dict:
|
||||||
|
"""移出精华消息(DELETE /channels/{channel_id}/pins/{message_id})。"""
|
||||||
|
return await self.delete(f"/channels/{channel_id}/pins/{message_id}")
|
||||||
|
|
||||||
|
async def get_channel_pins(self, channel_id: str) -> dict:
|
||||||
|
"""获取精华消息列表(GET /channels/{channel_id}/pins)。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/pins")
|
||||||
|
|
||||||
|
async def get_channel_schedules(
|
||||||
|
self, channel_id: str, since: int | None = None
|
||||||
|
) -> list | dict:
|
||||||
|
"""获取子频道日程列表(GET /channels/{channel_id}/schedules)。
|
||||||
|
|
||||||
|
默认返回当天日程;传 since(毫秒级时间戳)返回结束时间在其后的日程。
|
||||||
|
"""
|
||||||
|
params = {"since": str(since)} if since is not None else None
|
||||||
|
return await self.get(f"/channels/{channel_id}/schedules", params=params)
|
||||||
|
|
||||||
|
async def get_channel_schedule(self, channel_id: str, schedule_id: str) -> dict:
|
||||||
|
"""获取单个日程(GET /channels/{channel_id}/schedules/{schedule_id})。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}/schedules/{schedule_id}")
|
||||||
|
|
||||||
|
async def create_channel_schedule(
|
||||||
|
self,
|
||||||
|
channel_id: str,
|
||||||
|
name: str,
|
||||||
|
start_timestamp: str,
|
||||||
|
end_timestamp: str,
|
||||||
|
jump_channel_id: str,
|
||||||
|
remind_type: str = "0",
|
||||||
|
description: str = "",
|
||||||
|
) -> dict:
|
||||||
|
"""创建日程(POST /channels/{channel_id}/schedules)。
|
||||||
|
|
||||||
|
时间为毫秒级时间戳字符串;remind_type: "0" 不提醒 / "1" 开始时提醒
|
||||||
|
(官方字段为字符串)。请求体按官方约定包在 "schedule" 字段里。
|
||||||
|
需管理频道权限;单管理员每天限 10 次。
|
||||||
|
"""
|
||||||
|
return await self.post(
|
||||||
|
f"/channels/{channel_id}/schedules",
|
||||||
|
json={
|
||||||
|
"schedule": {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"start_timestamp": start_timestamp,
|
||||||
|
"end_timestamp": end_timestamp,
|
||||||
|
"jump_channel_id": jump_channel_id,
|
||||||
|
"remind_type": remind_type,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update_channel_schedule(
|
||||||
|
self, channel_id: str, schedule_id: str, **fields: Any
|
||||||
|
) -> dict:
|
||||||
|
"""修改日程(PATCH /channels/{channel_id}/schedules/{schedule_id})。
|
||||||
|
|
||||||
|
schedule 内的字段(name/start_timestamp/...)经 fields 透传。
|
||||||
|
"""
|
||||||
|
return await self.patch(
|
||||||
|
f"/channels/{channel_id}/schedules/{schedule_id}",
|
||||||
|
json={"schedule": {"id": schedule_id, **fields}},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_channel_schedule(self, channel_id: str, schedule_id: str) -> dict:
|
||||||
|
"""删除日程(DELETE /channels/{channel_id}/schedules/{schedule_id})。"""
|
||||||
|
return await self.delete(f"/channels/{channel_id}/schedules/{schedule_id}")
|
||||||
|
|
||||||
|
async def mute_guild_member(
|
||||||
|
self, guild_id: str, user_id: str, mute_seconds: int = 0
|
||||||
|
) -> dict:
|
||||||
|
"""禁言/解禁频道成员(PATCH /guilds/{guild_id}/members/{user_id}/mute)。
|
||||||
|
|
||||||
|
mute_seconds 为禁言时长(秒),传 0 解除禁言;上限 28 天。
|
||||||
|
"""
|
||||||
|
return await self.patch(
|
||||||
|
f"/guilds/{guild_id}/members/{user_id}/mute",
|
||||||
|
json={"mute_seconds": str(mute_seconds)},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 频道信息查询 ----------
|
||||||
|
|
||||||
|
async def get_guild(self, guild_id: str) -> dict:
|
||||||
|
"""获取频道信息(GET /guilds/{guild_id})。"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}")
|
||||||
|
|
||||||
|
async def get_guild_channels(self, guild_id: str) -> list | dict:
|
||||||
|
"""获取频道下的子频道列表(GET /channels/{guild_id}/channels)。"""
|
||||||
|
return await self.get(f"/channels/{guild_id}/channels")
|
||||||
|
|
||||||
|
async def get_channel(self, channel_id: str) -> dict:
|
||||||
|
"""获取子频道信息(GET /channels/{channel_id})。"""
|
||||||
|
return await self.get(f"/channels/{channel_id}")
|
||||||
|
|
||||||
|
async def get_guild_roles(self, guild_id: str) -> dict:
|
||||||
|
"""获取频道身份组列表(GET /guilds/{guild_id}/roles)。"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/roles")
|
||||||
|
|
||||||
|
async def get_guild_member(self, guild_id: str, user_id: str) -> dict:
|
||||||
|
"""获取成员详情(GET /guilds/{guild_id}/members/{user_id})。"""
|
||||||
|
return await self.get(f"/guilds/{guild_id}/members/{user_id}")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""全局配置:启动时从环境变量一次性读齐(校验缺失/非法值),经 DI 注入各组件。"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from aiohttp import ClientTimeout
|
||||||
|
|
||||||
|
|
||||||
|
def _required_env(name: str) -> str:
|
||||||
|
value = os.getenv(name, "")
|
||||||
|
if not value:
|
||||||
|
raise ValueError(f"缺少必需的环境变量 {name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Config:
|
||||||
|
"""SDK 全局配置:启动时从环境变量一次性读齐,经 DI 注入各组件。"""
|
||||||
|
|
||||||
|
app_id: str
|
||||||
|
app_secret: str
|
||||||
|
base_url: str
|
||||||
|
timeout: ClientTimeout
|
||||||
|
connecter: str
|
||||||
|
intents_file: str
|
||||||
|
webhook_host: str
|
||||||
|
webhook_port: int
|
||||||
|
webhook_path: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls) -> "Config":
|
||||||
|
connecter = os.getenv("CONNECTER", "websocket")
|
||||||
|
if connecter not in ("websocket", "webhook"):
|
||||||
|
raise ValueError(f"未知的接入方式: {connecter}")
|
||||||
|
# TIMEOUT 以毫秒配置
|
||||||
|
timeout = float(os.getenv("TIMEOUT", "5000")) / 1000
|
||||||
|
return cls(
|
||||||
|
app_id=_required_env("APPID"),
|
||||||
|
app_secret=_required_env("APPSECRET"),
|
||||||
|
base_url=os.getenv("BASE_URL", "https://api.bot.qq.com"),
|
||||||
|
timeout=ClientTimeout(total=timeout),
|
||||||
|
connecter=connecter,
|
||||||
|
intents_file=os.getenv("INTENTS_FILE", "intents.toml"),
|
||||||
|
webhook_host=os.getenv("WEBHOOK_HOST", "0.0.0.0"),
|
||||||
|
webhook_port=int(os.getenv("WEBHOOK_PORT", "8080")),
|
||||||
|
webhook_path=os.getenv("WEBHOOK_PATH", "/qqbot/webhook"),
|
||||||
|
)
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
"""连接适配器:把两种接入方式(WebSocket / Webhook)归一为事件流入、reply 队列流出。
|
||||||
|
|
||||||
|
- WebsocketConnecter:gateway 握手 + ws 长连接,指数退避重连;
|
||||||
|
看门狗两层互补——receive 的静默超时抓"连接彻底静默",
|
||||||
|
收到消息时的 ACK 期限检查抓"有事件流但心跳已死";
|
||||||
|
- WebhookConnecter:aiohttp server 接收回调,验签失败 401,
|
||||||
|
按 payload id 做 60s TTL 去重,正常推送进事件队列。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from time import monotonic
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from aiohttp import WSMsgType, web
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .crypto import verify_sig
|
||||||
|
from .emitter import EventEmitter
|
||||||
|
from .model import NOT_SET, Opcode, Payload
|
||||||
|
from .queue import EventQueue
|
||||||
|
from .token import AccessToken
|
||||||
|
|
||||||
|
|
||||||
|
class Connecter(Protocol):
|
||||||
|
async def run(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitError(RuntimeError):
|
||||||
|
"""gateway 接口触发频率限制(code=100017)。"""
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketConnecter:
|
||||||
|
# 看门狗阈值(× 心跳间隔):贴着 ACK 的自然节奏(1×)跑,误杀由 RESUME 兜底不丢消息;
|
||||||
|
# 不能压到 1× 整——ACK 的自然间隔就是 1×,正常抖动会贴边误杀;TCP 下丢包表现为延迟而非静默
|
||||||
|
_SILENCE_FACTOR = 1.1
|
||||||
|
_ACK_FACTOR = 1.2
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
http: aiohttp.ClientSession,
|
||||||
|
token: AccessToken,
|
||||||
|
queue: EventQueue,
|
||||||
|
) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._http = http
|
||||||
|
self._token = token
|
||||||
|
self._queue = queue
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
backoff: float = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await self._connect()
|
||||||
|
backoff = 1
|
||||||
|
except RateLimitError:
|
||||||
|
logger.warning("gateway 触发频率限制")
|
||||||
|
backoff = 60
|
||||||
|
except TimeoutError:
|
||||||
|
logger.warning("心跳超时,触发看门狗重连")
|
||||||
|
except (aiohttp.ClientError, RuntimeError) as e:
|
||||||
|
logger.warning(f"WebSocket 连接异常: {e}")
|
||||||
|
logger.info(f"{backoff}s 后重连")
|
||||||
|
await asyncio.sleep(backoff)
|
||||||
|
backoff = min(backoff * 2, 60)
|
||||||
|
|
||||||
|
async def _connect(self) -> None:
|
||||||
|
access_token = await self._token.get_access_token()
|
||||||
|
headers = {"Authorization": f"QQBot {access_token}"}
|
||||||
|
|
||||||
|
async with self._http.get(
|
||||||
|
f"{self._config.base_url}/gateway", headers=headers
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
|
||||||
|
if data.get("code") == 100017:
|
||||||
|
raise RateLimitError
|
||||||
|
gateway = data.get("url")
|
||||||
|
if not gateway:
|
||||||
|
raise RuntimeError(f"获取 gateway 失败: {data}")
|
||||||
|
|
||||||
|
async with self._http.ws_connect(gateway, headers=headers) as ws:
|
||||||
|
logger.info("WebSocket 已连接")
|
||||||
|
receive = asyncio.create_task(self.receive_helper(ws))
|
||||||
|
reply = asyncio.create_task(self.reply_helper(ws))
|
||||||
|
try:
|
||||||
|
await receive
|
||||||
|
finally:
|
||||||
|
reply.cancel()
|
||||||
|
logger.info("WebSocket 已断开")
|
||||||
|
|
||||||
|
async def receive_helper(self, ws: aiohttp.ClientWebSocketResponse) -> None:
|
||||||
|
interval: float = 0
|
||||||
|
last_ack = monotonic()
|
||||||
|
while not ws.closed:
|
||||||
|
# HELLO 前不设超时;之后按看门狗阈值等待任何消息
|
||||||
|
msg = await ws.receive(timeout=interval * self._SILENCE_FACTOR or None)
|
||||||
|
if msg.type != WSMsgType.TEXT:
|
||||||
|
logger.warning(f"WebSocket 收到非文本帧,连接终止: {msg.type}")
|
||||||
|
break
|
||||||
|
data: Payload = msg.json()
|
||||||
|
op = data.get("op")
|
||||||
|
if op == Opcode.HELLO:
|
||||||
|
d = data.get("d")
|
||||||
|
if isinstance(d, dict):
|
||||||
|
interval = d.get("heartbeat_interval", 41_250) / 1000
|
||||||
|
elif op == Opcode.HEARTBEAT_ACK:
|
||||||
|
last_ack = monotonic()
|
||||||
|
await self._queue.put_event(data)
|
||||||
|
if op in (Opcode.RECONNECT, Opcode.INVALID_SESSION):
|
||||||
|
break
|
||||||
|
if monotonic() - last_ack > interval * self._ACK_FACTOR:
|
||||||
|
raise TimeoutError
|
||||||
|
|
||||||
|
async def reply_helper(self, ws: aiohttp.ClientWebSocketResponse) -> None:
|
||||||
|
while not ws.closed:
|
||||||
|
payload: Payload = await self._queue.get_reply()
|
||||||
|
body = {k: v for k, v in payload.items() if v != NOT_SET}
|
||||||
|
try:
|
||||||
|
await ws.send_json(body)
|
||||||
|
except (aiohttp.ClientError, ConnectionError) as e:
|
||||||
|
logger.warning(f"WebSocket 发送失败,丢弃该帧: {body} ({e})")
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookConnecter:
|
||||||
|
_DEDUP_TTL = 60
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, config: Config, emitter: EventEmitter, queue: EventQueue
|
||||||
|
) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._emitter = emitter
|
||||||
|
self._queue = queue
|
||||||
|
self._seen: dict[str, float] = {}
|
||||||
|
|
||||||
|
def _make_app(self) -> web.Application:
|
||||||
|
app = web.Application()
|
||||||
|
app.add_routes([web.post(self._config.webhook_path, self._handle)])
|
||||||
|
return app
|
||||||
|
|
||||||
|
async def run(self) -> None:
|
||||||
|
runner = web.AppRunner(self._make_app())
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, self._config.webhook_host, self._config.webhook_port)
|
||||||
|
await site.start()
|
||||||
|
logger.info(
|
||||||
|
f"Webhook 回调服务已启动: "
|
||||||
|
f"http://{self._config.webhook_host}:{self._config.webhook_port}"
|
||||||
|
f"{self._config.webhook_path}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await asyncio.Event().wait()
|
||||||
|
finally:
|
||||||
|
await runner.cleanup()
|
||||||
|
|
||||||
|
def _duplicated(self, event_id: str | None) -> bool:
|
||||||
|
if not event_id:
|
||||||
|
return False
|
||||||
|
now = monotonic()
|
||||||
|
for key in [k for k, ts in self._seen.items() if now - ts > self._DEDUP_TTL]:
|
||||||
|
del self._seen[key]
|
||||||
|
if event_id in self._seen:
|
||||||
|
return True
|
||||||
|
self._seen[event_id] = now
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _handle(self, request: web.Request) -> web.Response:
|
||||||
|
body = await request.read()
|
||||||
|
payload: Payload = json.loads(body)
|
||||||
|
|
||||||
|
if payload.get("op") == Opcode.VALIDATION:
|
||||||
|
logger.info("收到 Webhook 验证请求")
|
||||||
|
ret = await self._emitter.handle(payload)
|
||||||
|
if ret is None:
|
||||||
|
return web.json_response(
|
||||||
|
{"error": "missing validation handler"}, status=500
|
||||||
|
)
|
||||||
|
return web.json_response(ret)
|
||||||
|
|
||||||
|
timestamp = request.headers.get("X-Signature-Timestamp", "")
|
||||||
|
signature = request.headers.get("X-Signature-Ed25519", "")
|
||||||
|
if not verify_sig(self._config.app_secret, timestamp, signature, body):
|
||||||
|
logger.warning("Webhook 验签失败")
|
||||||
|
return web.json_response({"error": "invalid signature"}, status=401)
|
||||||
|
|
||||||
|
event_id = payload.get("id")
|
||||||
|
if isinstance(event_id, str) and self._duplicated(event_id):
|
||||||
|
logger.info(f"Webhook 重复推送已跳过: {event_id}")
|
||||||
|
return web.json_response({"opcode": 12})
|
||||||
|
|
||||||
|
await self._queue.put_event(payload)
|
||||||
|
return web.json_response({"opcode": 12})
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Ed25519 签名工具:QQ 开放平台 webhook 验签/验证应答所用。
|
||||||
|
|
||||||
|
seed 派生规则(官方约定):AppSecret 的 utf8 字节反复倍增补足 32 字节。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
_ED25519_SEED_SIZE = 32
|
||||||
|
|
||||||
|
|
||||||
|
def build_seed(secret: str) -> bytes:
|
||||||
|
seed = secret.encode("utf-8")
|
||||||
|
while len(seed) < _ED25519_SEED_SIZE:
|
||||||
|
seed *= 2
|
||||||
|
return seed[:_ED25519_SEED_SIZE]
|
||||||
|
|
||||||
|
|
||||||
|
def sign_msg(secret: str, msg: str) -> str:
|
||||||
|
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(build_seed(secret))
|
||||||
|
return private_key.sign(msg.encode("utf-8")).hex()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_sig(secret: str, timestamp: str, signature: str, body: bytes) -> bool:
|
||||||
|
try:
|
||||||
|
sig = bytes.fromhex(signature)
|
||||||
|
private_key = ed25519.Ed25519PrivateKey.from_private_bytes(build_seed(secret))
|
||||||
|
private_key.public_key().verify(sig, timestamp.encode("utf-8") + body)
|
||||||
|
return True
|
||||||
|
except (InvalidSignature, ValueError):
|
||||||
|
return False
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""依赖装配:dishka 容器的 provider 定义与 ADAPTERS 接线表。
|
||||||
|
|
||||||
|
所有组件均为 APP scope 单例;按 CONNECTER 环境变量动态 provide
|
||||||
|
具体的连接适配器与协议处理器。业务代码注入用 `Inject[T]`(FromDishka
|
||||||
|
别名),裸标注 `Event`/`Session`/`BotApi` 也可直接解析。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
from dishka import (
|
||||||
|
AsyncContainer,
|
||||||
|
FromDishka,
|
||||||
|
Provider,
|
||||||
|
Scope,
|
||||||
|
from_context,
|
||||||
|
make_async_container,
|
||||||
|
provide,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .api import BotApi
|
||||||
|
from .config import Config
|
||||||
|
from .connecter import Connecter, WebhookConnecter, WebsocketConnecter
|
||||||
|
from .emitter import BaseProtocol, EventEmitter
|
||||||
|
from .events import Event
|
||||||
|
from .queue import EventQueue
|
||||||
|
from .session import Session
|
||||||
|
from .token import AccessToken
|
||||||
|
from .webhook_protocol import WebhookProtocol
|
||||||
|
from .ws_protocol import WebsocketProtocol
|
||||||
|
|
||||||
|
Inject = FromDishka
|
||||||
|
|
||||||
|
# 接入方式 → (连接适配器, 协议处理器),均由 dishka 按构造签名自动装配
|
||||||
|
ADAPTERS: dict[str, tuple[type[Connecter], type[BaseProtocol]]] = {
|
||||||
|
"websocket": (WebsocketConnecter, WebsocketProtocol),
|
||||||
|
"webhook": (WebhookConnecter, WebhookProtocol),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AppProvider(Provider):
|
||||||
|
event = from_context(Event, scope=Scope.REQUEST)
|
||||||
|
|
||||||
|
@provide(scope=Scope.APP)
|
||||||
|
def get_config(self) -> Config:
|
||||||
|
return Config.load()
|
||||||
|
|
||||||
|
@provide(scope=Scope.APP)
|
||||||
|
def get_session(self) -> Session:
|
||||||
|
return Session()
|
||||||
|
|
||||||
|
@provide(scope=Scope.APP)
|
||||||
|
def get_token(self, config: Config, http: ClientSession) -> AccessToken:
|
||||||
|
return AccessToken(config, http)
|
||||||
|
|
||||||
|
@provide(scope=Scope.APP)
|
||||||
|
def get_api(
|
||||||
|
self, config: Config, http: ClientSession, token: AccessToken
|
||||||
|
) -> BotApi:
|
||||||
|
return BotApi(config, http, token)
|
||||||
|
|
||||||
|
@provide(scope=Scope.APP)
|
||||||
|
async def get_http(self, config: Config) -> AsyncIterator[ClientSession]:
|
||||||
|
# 全 SDK 共享一个连接池,随容器关闭
|
||||||
|
session = ClientSession(timeout=config.timeout)
|
||||||
|
yield session
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
def make_container(emitter: EventEmitter | None = None) -> AsyncContainer:
|
||||||
|
"""构建 DI 容器。传入外部构造的 EventEmitter 时,容器复用该实例及其队列,
|
||||||
|
保证用户在 main() 之前注册的 handler 与主体是同一个 emitter。"""
|
||||||
|
if emitter is None:
|
||||||
|
emitter = EventEmitter(EventQueue())
|
||||||
|
|
||||||
|
provider = AppProvider()
|
||||||
|
provider.provide(lambda: emitter, provides=EventEmitter, scope=Scope.APP)
|
||||||
|
provider.provide(lambda: emitter.queue, provides=EventQueue, scope=Scope.APP)
|
||||||
|
|
||||||
|
connecter_cls, protocol_cls = ADAPTERS[Config.load().connecter]
|
||||||
|
provider.provide(protocol_cls, provides=BaseProtocol, scope=Scope.APP)
|
||||||
|
provider.provide(connecter_cls, provides=Connecter, scope=Scope.APP)
|
||||||
|
return make_async_container(provider)
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""事件分发主体:路由 payload → 触发 handler → 协议层应答回流。
|
||||||
|
|
||||||
|
- `dispatch()` 泵循环消费 EventQueue.msg_queue,`emit()` 按
|
||||||
|
op(DISPATCH 用 t 名、其余用 Opcode 名)路由到 `@on` 注册的 handler;
|
||||||
|
- DI 容器在位时按 handler 参数标注注入(`Inject[T]`/`Event` 等,
|
||||||
|
解析失败回退传原始 d),否则直传 d;
|
||||||
|
- 业务 handler(op=0)返回值不回流;协议层(op≠0)返回的 payload
|
||||||
|
进 reply 队列由 ws 发出。handler 异常就地隔离,不影响其他 handler。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from typing import Any, TypeVar, cast, get_args, get_type_hints, overload
|
||||||
|
|
||||||
|
from dishka import AsyncContainer
|
||||||
|
from dishka.exceptions import NoFactoryError
|
||||||
|
from loguru import logger
|
||||||
|
from pyee.asyncio import AsyncIOEventEmitter
|
||||||
|
|
||||||
|
from .events import Event
|
||||||
|
from .model import Opcode, Payload
|
||||||
|
from .payloads import PAYLOAD_TYPES
|
||||||
|
from .queue import EventQueue
|
||||||
|
|
||||||
|
Handler = TypeVar("Handler", bound=Callable[..., Awaitable[Any]])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseProtocol(ABC):
|
||||||
|
"""协议层处理器接口:具体接入方式(websocket/webhook)实现后注册到主体。"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def register(self, emitter: "EventEmitter") -> None: ...
|
||||||
|
|
||||||
|
def on_payload(self, payload: Payload) -> None: # noqa: B027
|
||||||
|
"""路由后的每条 payload 回调,供协议类跟踪协议级状态(如 s 序列号)。"""
|
||||||
|
|
||||||
|
|
||||||
|
class EventEmitter:
|
||||||
|
def __init__(self, queue: EventQueue) -> None:
|
||||||
|
self._ee = AsyncIOEventEmitter()
|
||||||
|
self._queue = queue
|
||||||
|
self._container: AsyncContainer | None = None
|
||||||
|
self._protocol: BaseProtocol | None = None
|
||||||
|
self._hints: dict[Callable[..., Any], dict[str, Any]] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def queue(self) -> EventQueue:
|
||||||
|
"""本 emitter 消费的事件队列;外部构造 emitter 时由容器复用同一实例。"""
|
||||||
|
return self._queue
|
||||||
|
|
||||||
|
def use_container(self, container: AsyncContainer) -> None:
|
||||||
|
self._container = container
|
||||||
|
|
||||||
|
def register_protocol(self, protocol: BaseProtocol) -> None:
|
||||||
|
self._protocol = protocol
|
||||||
|
protocol.register(self)
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def on(self, event: Opcode | str) -> Callable[[Handler], Handler]: ...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def on(self, event: Opcode | str, fn: Handler) -> Handler: ...
|
||||||
|
|
||||||
|
def on(
|
||||||
|
self, event: Opcode | str, fn: Handler | None = None
|
||||||
|
) -> Handler | Callable[[Handler], Handler]:
|
||||||
|
name = event.name if isinstance(event, Opcode) else event
|
||||||
|
|
||||||
|
def decorator(fn: Handler) -> Handler:
|
||||||
|
self._ee.on(name, fn)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
if fn is None:
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
self._ee.on(name, fn)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
async def dispatch(self) -> None:
|
||||||
|
while True:
|
||||||
|
data = await self._queue.get_event()
|
||||||
|
try:
|
||||||
|
await self.emit(data)
|
||||||
|
except Exception as e:
|
||||||
|
# dispatch 是主循环,单条事件路由失败不能带崩整个进程
|
||||||
|
logger.exception(f"事件分发异常,已跳过: {data} ({e})")
|
||||||
|
|
||||||
|
def _route(self, data: Payload) -> tuple[int, str, Event] | None:
|
||||||
|
op = data.get("op")
|
||||||
|
if not isinstance(op, int):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 线上 payload 反序列化出来是 int,Opcode(9) 取名供非 DISPATCH 事件路由
|
||||||
|
event_name: str | None
|
||||||
|
if op == Opcode.DISPATCH:
|
||||||
|
t = data.get("t")
|
||||||
|
event_name = t if isinstance(t, str) else None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
event_name = Opcode(op).name
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if not isinstance(event_name, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return op, event_name, Event(data)
|
||||||
|
|
||||||
|
async def emit(self, data: Payload) -> None:
|
||||||
|
routed = self._route(data)
|
||||||
|
if routed is None:
|
||||||
|
logger.warning(f"无法确定事件名,已跳过: {data}")
|
||||||
|
return
|
||||||
|
op, event_name, event = routed
|
||||||
|
|
||||||
|
if self._protocol is not None:
|
||||||
|
self._protocol.on_payload(data)
|
||||||
|
|
||||||
|
listeners = self._ee.listeners(event_name)
|
||||||
|
if not listeners:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._container is None:
|
||||||
|
for fn in listeners:
|
||||||
|
await self._call_direct(fn, event, reply=op != Opcode.DISPATCH)
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self._container({Event: event}) as scope:
|
||||||
|
for fn in listeners:
|
||||||
|
await self._invoke(scope, fn, event, reply=op != Opcode.DISPATCH)
|
||||||
|
|
||||||
|
async def handle(self, data: Payload) -> Any:
|
||||||
|
"""同步分发事件并返回首个处理器的非空返回值,用于 op=13 等需要应答的协议事件。"""
|
||||||
|
routed = self._route(data)
|
||||||
|
if routed is None:
|
||||||
|
return None
|
||||||
|
_, event_name, event = routed
|
||||||
|
|
||||||
|
for fn in self._ee.listeners(event_name):
|
||||||
|
if self._container is None:
|
||||||
|
args = self._direct_args(fn, event)
|
||||||
|
ret = await fn(*args)
|
||||||
|
else:
|
||||||
|
async with self._container({Event: event}) as scope:
|
||||||
|
args, kwargs = await self._resolve_params(scope, fn, event)
|
||||||
|
ret = await fn(*args, **kwargs)
|
||||||
|
if ret is not None:
|
||||||
|
return ret
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _direct_args(
|
||||||
|
self, fn: Callable[..., Awaitable[Any]], event: Event
|
||||||
|
) -> list[Any]:
|
||||||
|
"""无 DI 容器时的参数解析:Event → 事件对象,payload 类型 → dataclass,其余 → 原始 d。"""
|
||||||
|
hints = self._hints.get(fn)
|
||||||
|
if hints is None:
|
||||||
|
hints = get_type_hints(fn, include_extras=True)
|
||||||
|
self._hints[fn] = hints
|
||||||
|
|
||||||
|
args: list[Any] = []
|
||||||
|
for name in inspect.signature(fn).parameters:
|
||||||
|
hint = hints.get(name)
|
||||||
|
if hint in PAYLOAD_TYPES:
|
||||||
|
args.append(event.typed)
|
||||||
|
elif hint is Event:
|
||||||
|
args.append(event)
|
||||||
|
else:
|
||||||
|
args.append(event.data)
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def _call_direct(
|
||||||
|
self, fn: Callable[..., Awaitable[Any]], event: Event, reply: bool
|
||||||
|
) -> None:
|
||||||
|
# 用户 handler 的异常在这里就地隔离(无 DI 容器的直连路径)
|
||||||
|
try:
|
||||||
|
ret: Any = await fn(*self._direct_args(fn, event))
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"事件处理器 {getattr(fn, '__qualname__', fn)} 异常: {e}")
|
||||||
|
return
|
||||||
|
if reply and isinstance(ret, dict):
|
||||||
|
await self._queue.put_reply(cast(Payload, ret))
|
||||||
|
|
||||||
|
async def _invoke(
|
||||||
|
self,
|
||||||
|
scope: AsyncContainer,
|
||||||
|
fn: Callable[..., Awaitable[Any]],
|
||||||
|
event: Event,
|
||||||
|
reply: bool,
|
||||||
|
) -> None:
|
||||||
|
# 用户 handler 的异常在这里就地隔离,不影响同一事件的其他 handler
|
||||||
|
try:
|
||||||
|
args, kwargs = await self._resolve_params(scope, fn, event)
|
||||||
|
ret: Any = await fn(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"事件处理器 {getattr(fn, '__qualname__', fn)} 异常: {e}")
|
||||||
|
return
|
||||||
|
if reply and isinstance(ret, dict):
|
||||||
|
await self._queue.put_reply(cast(Payload, ret))
|
||||||
|
|
||||||
|
async def _resolve_params(
|
||||||
|
self,
|
||||||
|
scope: AsyncContainer,
|
||||||
|
fn: Callable[..., Awaitable[Any]],
|
||||||
|
event: Event,
|
||||||
|
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||||
|
hints = self._hints.get(fn)
|
||||||
|
if hints is None:
|
||||||
|
hints = get_type_hints(fn, include_extras=True)
|
||||||
|
self._hints[fn] = hints
|
||||||
|
|
||||||
|
positional: list[Any] = []
|
||||||
|
kwargs: dict[str, Any] = {}
|
||||||
|
for name in inspect.signature(fn).parameters:
|
||||||
|
hint = hints.get(name)
|
||||||
|
if hint is None:
|
||||||
|
positional.append(event.data)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if hint in PAYLOAD_TYPES:
|
||||||
|
kwargs[name] = event.typed
|
||||||
|
continue
|
||||||
|
if hint is Event:
|
||||||
|
kwargs[name] = event
|
||||||
|
continue
|
||||||
|
|
||||||
|
target = get_args(hint)[0] if hasattr(hint, "__metadata__") else hint
|
||||||
|
try:
|
||||||
|
kwargs[name] = await scope.get(target)
|
||||||
|
except NoFactoryError:
|
||||||
|
positional.append(event.data)
|
||||||
|
return tuple(positional), kwargs
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""事件对象:对线上 payload 的轻量封装,供业务 handler 经 DI 注入使用。"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .model import Payload
|
||||||
|
from .payloads import parse_event
|
||||||
|
|
||||||
|
|
||||||
|
class Event:
|
||||||
|
"""一条 DISPATCH 事件;属性取值兼容群(openid)与频道两套字段名。"""
|
||||||
|
|
||||||
|
def __init__(self, payload: Payload) -> None:
|
||||||
|
self._payload = payload
|
||||||
|
t = payload.get("t")
|
||||||
|
self.type: str | None = t if isinstance(t, str) else None
|
||||||
|
d = payload.get("d")
|
||||||
|
self._d: dict[str, Any] = dict(d) if isinstance(d, dict) else {}
|
||||||
|
self._typed: object | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def raw(self) -> Payload:
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> dict[str, Any]:
|
||||||
|
return self._d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def typed(self) -> object:
|
||||||
|
"""d 的 dataclass 解析结果(见 payloads.py);未知事件类型为 dict。"""
|
||||||
|
if self._typed is None:
|
||||||
|
self._typed = parse_event(self.type, self._d)
|
||||||
|
return self._typed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_id(self) -> str | None:
|
||||||
|
return self._d.get("user_id") or self._d.get("from_user_id")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def group_id(self) -> str | None:
|
||||||
|
return self._d.get("group_openid")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def content(self) -> str | None:
|
||||||
|
return self._d.get("content")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message_id(self) -> str | None:
|
||||||
|
return self._d.get("id")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<Event {self.type} {self._d}>"
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
"""协议模型:Opcode、payload 及各事件 d 字段的 TypedDict、Intent 位掩码。
|
||||||
|
|
||||||
|
Payload 里 `NOT_SET` 哨兵标记"外发时应省略"的字段,由
|
||||||
|
WebsocketConnecter.reply_helper 统一过滤。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from enum import IntEnum, IntFlag
|
||||||
|
from typing import Any, Literal, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class MissingType:
|
||||||
|
def __repr__(self) -> Literal["<NotSet>"]:
|
||||||
|
return "<NotSet>"
|
||||||
|
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
NOT_SET = MissingType()
|
||||||
|
type NoneType = None | MissingType
|
||||||
|
|
||||||
|
type DispatchData = Any
|
||||||
|
|
||||||
|
|
||||||
|
class Opcode(IntEnum):
|
||||||
|
DISPATCH = 0
|
||||||
|
HEARTBEAT = 1
|
||||||
|
IDENTIFY = 2
|
||||||
|
RESUME = 6
|
||||||
|
RECONNECT = 7
|
||||||
|
INVALID_SESSION = 9
|
||||||
|
HELLO = 10
|
||||||
|
HEARTBEAT_ACK = 11
|
||||||
|
VALIDATION = 13
|
||||||
|
|
||||||
|
|
||||||
|
class Payload(TypedDict, total=False):
|
||||||
|
id: str | NoneType
|
||||||
|
# 线上 payload 反序列化出来是 int,这里按实际类型声明
|
||||||
|
op: int | NoneType
|
||||||
|
d: "HelloData | IdentifyData | ResumeData | ValidationData | DispatchData | int | NoneType"
|
||||||
|
s: int | NoneType
|
||||||
|
t: str | NoneType
|
||||||
|
|
||||||
|
|
||||||
|
class HelloData(TypedDict):
|
||||||
|
heartbeat_interval: int # ms
|
||||||
|
|
||||||
|
|
||||||
|
class IdentifyData(TypedDict):
|
||||||
|
token: str
|
||||||
|
intents: int
|
||||||
|
shard: tuple[int, int]
|
||||||
|
properties: dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class ResumeData(TypedDict):
|
||||||
|
token: str
|
||||||
|
session_id: str
|
||||||
|
seq: int
|
||||||
|
|
||||||
|
|
||||||
|
class ReadyData(TypedDict, total=False):
|
||||||
|
version: int
|
||||||
|
session_id: str
|
||||||
|
user: dict[str, Any]
|
||||||
|
shard: tuple[int, int]
|
||||||
|
intents: int
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationData(TypedDict):
|
||||||
|
plain_token: str
|
||||||
|
event_ts: str
|
||||||
|
|
||||||
|
|
||||||
|
class AccessTokenResponse(TypedDict):
|
||||||
|
access_token: str
|
||||||
|
expires_in: int
|
||||||
|
|
||||||
|
|
||||||
|
class Intent(IntFlag):
|
||||||
|
GUILDS = 1 << 0
|
||||||
|
GUILD_MEMBERS = 1 << 1
|
||||||
|
GUILD_MESSAGES = 1 << 9
|
||||||
|
GUILD_MESSAGE_REACTIONS = 1 << 10
|
||||||
|
DIRECT_MESSAGE = 1 << 12
|
||||||
|
GROUP_AND_C2C_EVENT = 1 << 25
|
||||||
|
INTERACTION = 1 << 26
|
||||||
|
MESSAGE_AUDIT = 1 << 27
|
||||||
|
FORUMS_EVENT = 1 << 28
|
||||||
|
AUDIO_ACTION = 1 << 29
|
||||||
|
PUBLIC_GUILD_MESSAGES = 1 << 30
|
||||||
|
|
||||||
|
|
||||||
|
def payload_of(op: Opcode, d: Any = NOT_SET) -> Payload:
|
||||||
|
return {"id": NOT_SET, "op": op, "d": d, "s": NOT_SET, "t": NOT_SET}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
"""事件 payload 的 dataclass 定义与解析。
|
||||||
|
|
||||||
|
handler 直接标注具体类型即可拿到解析后的对象(如
|
||||||
|
`async def on_msg(msg: GroupAtMessage)`);未收录的事件类型
|
||||||
|
由 emitter 回退传原始 dict,新增事件只需在这里加 dataclass
|
||||||
|
并登记到 EVENT_TYPES。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, fields
|
||||||
|
from types import UnionType
|
||||||
|
from typing import Union, get_args, get_origin
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Author:
|
||||||
|
"""消息作者。群场景是 member_openid/user_openid,频道场景是 id/username。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
user_openid: str | None = None
|
||||||
|
member_openid: str | None = None
|
||||||
|
username: str | None = None
|
||||||
|
bot: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Ready:
|
||||||
|
version: int | None = None
|
||||||
|
session_id: str | None = None
|
||||||
|
user: dict | None = None
|
||||||
|
shard: list[int] | None = None
|
||||||
|
intents: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupAtMessage:
|
||||||
|
"""群里 @机器人的消息(GROUP_AT_MESSAGE_CREATE)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
content: str | None = None
|
||||||
|
group_openid: str | None = None
|
||||||
|
author: Author | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_openid(self) -> str | None:
|
||||||
|
return self.author.member_openid if self.author else None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class C2CMessage:
|
||||||
|
"""用户单聊消息(C2C_MESSAGE_CREATE)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
content: str | None = None
|
||||||
|
author: Author | None = None
|
||||||
|
attachments: list[dict] | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_openid(self) -> str | None:
|
||||||
|
return self.author.user_openid if self.author else None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class FriendAdd:
|
||||||
|
openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class FriendDel:
|
||||||
|
openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupAddRobot:
|
||||||
|
group_openid: str | None = None
|
||||||
|
op_member_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupDelRobot:
|
||||||
|
group_openid: str | None = None
|
||||||
|
op_member_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class C2CMsgReject:
|
||||||
|
user_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
event_status: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class C2CMsgReceive:
|
||||||
|
user_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
event_status: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupMsgReject:
|
||||||
|
group_openid: str | None = None
|
||||||
|
group_manager_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GroupMsgReceive:
|
||||||
|
group_openid: str | None = None
|
||||||
|
group_manager_openid: str | None = None
|
||||||
|
event_ts: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class AtMessage:
|
||||||
|
"""频道 @机器人消息(AT_MESSAGE_CREATE / PUBLIC 与私域共用结构)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
content: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
guild_id: str | None = None
|
||||||
|
author: Author | None = None
|
||||||
|
member: dict | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DirectMessage:
|
||||||
|
"""私信消息(DIRECT_MESSAGE_CREATE)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
content: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
guild_id: str | None = None
|
||||||
|
author: Author | None = None
|
||||||
|
member: dict | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Guild:
|
||||||
|
"""频道服务器(GUILD_CREATE/UPDATE/DELETE,d 复用同一结构)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
name: str | None = None
|
||||||
|
icon: str | None = None
|
||||||
|
owner_id: str | None = None
|
||||||
|
joined_at: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Channel:
|
||||||
|
"""子频道(CHANNEL_CREATE/UPDATE/DELETE,d 复用同一结构)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
guild_id: str | None = None
|
||||||
|
name: str | None = None
|
||||||
|
channel_type: int | None = None
|
||||||
|
sub_type: int | None = None
|
||||||
|
position: int | None = None
|
||||||
|
parent_id: str | None = None
|
||||||
|
owner_id: str | None = None
|
||||||
|
private_type: int | None = None
|
||||||
|
speak_permission: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class MessageAudit:
|
||||||
|
"""消息审核结果(MESSAGE_AUDIT_PASS/REJECT)。"""
|
||||||
|
|
||||||
|
audit_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
guild_id: str | None = None
|
||||||
|
author_id: str | None = None
|
||||||
|
message_info: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class MessageReaction:
|
||||||
|
"""表情表态(MESSAGE_REACTION_ADD/REMOVE)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
user_id: str | None = None
|
||||||
|
message_id: str | None = None
|
||||||
|
emoji: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class MessageDelete:
|
||||||
|
"""消息删除(MESSAGE_DELETE / PUBLIC_MESSAGE_DELETE)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
id: str | None = None
|
||||||
|
message: dict | None = None
|
||||||
|
op_user: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class Interaction:
|
||||||
|
"""互动(INTERACTION_CREATE)。"""
|
||||||
|
|
||||||
|
id: str | None = None
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
type: int | None = None
|
||||||
|
data: dict | None = None
|
||||||
|
version: int | None = None
|
||||||
|
application_id: str | None = None
|
||||||
|
group_member: dict | None = None
|
||||||
|
c2c_member: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ForumThread:
|
||||||
|
"""论坛主题(FORUM_THREAD_CREATE/UPDATE/DELETE)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
author_id: str | None = None
|
||||||
|
thread_info: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ForumPost:
|
||||||
|
"""论坛帖子(FORUM_POST_CREATE/DELETE)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
author_id: str | None = None
|
||||||
|
post_info: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class ForumReply:
|
||||||
|
"""论坛评论(FORUM_REPLY_CREATE/DELETE)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
author_id: str | None = None
|
||||||
|
reply_info: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class AudioAction:
|
||||||
|
"""音频动作(AUDIO_START/FINISH/ON_MIC/OFF_MIC)。"""
|
||||||
|
|
||||||
|
guild_id: str | None = None
|
||||||
|
channel_id: str | None = None
|
||||||
|
audio_info: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# 事件名(t)→ payload 类型
|
||||||
|
EVENT_TYPES: dict[str, type] = {
|
||||||
|
"READY": Ready,
|
||||||
|
"GROUP_AT_MESSAGE_CREATE": GroupAtMessage,
|
||||||
|
"C2C_MESSAGE_CREATE": C2CMessage,
|
||||||
|
"FRIEND_ADD": FriendAdd,
|
||||||
|
"FRIEND_DEL": FriendDel,
|
||||||
|
"GROUP_ADD_ROBOT": GroupAddRobot,
|
||||||
|
"GROUP_DEL_ROBOT": GroupDelRobot,
|
||||||
|
"C2C_MSG_REJECT": C2CMsgReject,
|
||||||
|
"C2C_MSG_RECEIVE": C2CMsgReceive,
|
||||||
|
"GROUP_MSG_REJECT": GroupMsgReject,
|
||||||
|
"GROUP_MSG_RECEIVE": GroupMsgReceive,
|
||||||
|
"AT_MESSAGE_CREATE": AtMessage,
|
||||||
|
"DIRECT_MESSAGE_CREATE": DirectMessage,
|
||||||
|
"GUILD_CREATE": Guild,
|
||||||
|
"GUILD_UPDATE": Guild,
|
||||||
|
"GUILD_DELETE": Guild,
|
||||||
|
"CHANNEL_CREATE": Channel,
|
||||||
|
"CHANNEL_UPDATE": Channel,
|
||||||
|
"CHANNEL_DELETE": Channel,
|
||||||
|
"MESSAGE_AUDIT_PASS": MessageAudit,
|
||||||
|
"MESSAGE_AUDIT_REJECT": MessageAudit,
|
||||||
|
"MESSAGE_REACTION_ADD": MessageReaction,
|
||||||
|
"MESSAGE_REACTION_REMOVE": MessageReaction,
|
||||||
|
"MESSAGE_DELETE": MessageDelete,
|
||||||
|
"PUBLIC_MESSAGE_DELETE": MessageDelete,
|
||||||
|
"INTERACTION_CREATE": Interaction,
|
||||||
|
"FORUM_THREAD_CREATE": ForumThread,
|
||||||
|
"FORUM_THREAD_UPDATE": ForumThread,
|
||||||
|
"FORUM_THREAD_DELETE": ForumThread,
|
||||||
|
"FORUM_POST_CREATE": ForumPost,
|
||||||
|
"FORUM_POST_DELETE": ForumPost,
|
||||||
|
"FORUM_REPLY_CREATE": ForumReply,
|
||||||
|
"FORUM_REPLY_DELETE": ForumReply,
|
||||||
|
"AUDIO_START": AudioAction,
|
||||||
|
"AUDIO_FINISH": AudioAction,
|
||||||
|
"AUDIO_ON_MIC": AudioAction,
|
||||||
|
"AUDIO_OFF_MIC": AudioAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 全部已知 payload 类型,供 emitter 按参数标注识别
|
||||||
|
PAYLOAD_TYPES: frozenset[type] = frozenset(EVENT_TYPES.values())
|
||||||
|
|
||||||
|
|
||||||
|
def _is_author_type(annotation: object) -> bool:
|
||||||
|
if annotation is Author:
|
||||||
|
return True
|
||||||
|
origin = get_origin(annotation)
|
||||||
|
if origin is Union or origin is UnionType:
|
||||||
|
return Author in get_args(annotation)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _build(cls: type, d: dict) -> object:
|
||||||
|
kwargs = {}
|
||||||
|
for f in fields(cls):
|
||||||
|
if f.name not in d:
|
||||||
|
continue
|
||||||
|
value = d[f.name]
|
||||||
|
if isinstance(value, dict) and _is_author_type(f.type):
|
||||||
|
value = _build(Author, value)
|
||||||
|
kwargs[f.name] = value
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_event(t: str | None, d: dict) -> object:
|
||||||
|
"""把事件 d 解析为对应 dataclass;未收录的事件类型原样返回 dict。"""
|
||||||
|
if t and (cls := EVENT_TYPES.get(t)) and isinstance(d, dict):
|
||||||
|
return _build(cls, d)
|
||||||
|
return d
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""事件/应答队列:连接层与分发层之间唯一的跨任务通道。"""
|
||||||
|
|
||||||
|
from asyncio import Queue
|
||||||
|
|
||||||
|
from .model import Payload
|
||||||
|
|
||||||
|
|
||||||
|
class EventQueue:
|
||||||
|
"""连接层与分发层之间的事件/应答通道。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.msg_queue: Queue[Payload] = Queue()
|
||||||
|
self.reply_queue: Queue[Payload] = Queue()
|
||||||
|
|
||||||
|
async def put_event(self, item: Payload) -> None:
|
||||||
|
return await self.msg_queue.put(item)
|
||||||
|
|
||||||
|
async def get_event(self) -> Payload:
|
||||||
|
return await self.msg_queue.get()
|
||||||
|
|
||||||
|
async def put_reply(self, item: Payload) -> None:
|
||||||
|
return await self.reply_queue.put(item)
|
||||||
|
|
||||||
|
async def get_reply(self) -> Payload:
|
||||||
|
return await self.reply_queue.get()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""WebSocket 会话状态:session_id 与单调递增的 s 序列号。"""
|
||||||
|
|
||||||
|
|
||||||
|
class Session:
|
||||||
|
"""RESUME 续传所需的状态载体,由 DI 容器以 APP 单例持有。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.session_id: str | None = None
|
||||||
|
self._sequence_id: int | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sequence_id(self) -> int | None:
|
||||||
|
return self._sequence_id
|
||||||
|
|
||||||
|
def update_sequence(self, value: int) -> None:
|
||||||
|
"""只接受不回退的序列号,乱序/重放的旧值会被忽略。"""
|
||||||
|
current = self._sequence_id or 0
|
||||||
|
if value >= current:
|
||||||
|
self._sequence_id = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def seq(self) -> int:
|
||||||
|
"""RESUME 时使用的序列号,无历史序列号时为 0。"""
|
||||||
|
return self._sequence_id or 0
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""access_token 获取与缓存:到期前直接复用,过期自动重新换取。"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .model import AccessTokenResponse
|
||||||
|
|
||||||
|
|
||||||
|
class AccessToken:
|
||||||
|
def __init__(self, config: Config, http: ClientSession) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._http = http
|
||||||
|
self._access_token = ""
|
||||||
|
self._expires_at = 0.0
|
||||||
|
|
||||||
|
async def get_access_token(self) -> str:
|
||||||
|
if time.time() < self._expires_at:
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"appId": self._config.app_id,
|
||||||
|
"clientSecret": self._config.app_secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
async with self._http.post(
|
||||||
|
f"{self._config.base_url}/app/getAppAccessToken",
|
||||||
|
json=payload,
|
||||||
|
) as resp:
|
||||||
|
data: AccessTokenResponse = await resp.json()
|
||||||
|
self._access_token = data["access_token"]
|
||||||
|
self._expires_at = time.time() + int(data["expires_in"])
|
||||||
|
|
||||||
|
return self._access_token
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""Webhook 接入的协议层:op=13 验证请求应答(签名 event_ts + plain_token)。"""
|
||||||
|
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .crypto import sign_msg
|
||||||
|
from .emitter import BaseProtocol, EventEmitter
|
||||||
|
from .model import Opcode, ValidationData
|
||||||
|
from .queue import EventQueue
|
||||||
|
from .session import Session
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookProtocol(BaseProtocol):
|
||||||
|
# 构造签名与 WebsocketProtocol 保持一致,供 ADAPTERS 统一实例化
|
||||||
|
def __init__(self, config: Config, queue: EventQueue, session: Session) -> None:
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
@override
|
||||||
|
def register(self, emitter: EventEmitter) -> None:
|
||||||
|
emitter.on(Opcode.VALIDATION)(self.validation)
|
||||||
|
|
||||||
|
async def validation(self, data: ValidationData) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"plain_token": data["plain_token"],
|
||||||
|
"signature": sign_msg(
|
||||||
|
self._config.app_secret,
|
||||||
|
data["event_ts"] + data["plain_token"],
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""WebSocket 接入的协议层:IDENTIFY/RESUME 鉴权、定时心跳、序列号跟踪。
|
||||||
|
|
||||||
|
识别 HELLO 后启动 apscheduler 心跳;断线重连时若已有 session_id
|
||||||
|
则发 RESUME 续传,收到不可恢复的 INVALID_SESSION 才清空会话重新 IDENTIFY。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from tomllib import load
|
||||||
|
from typing import override
|
||||||
|
|
||||||
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
from .emitter import BaseProtocol, EventEmitter
|
||||||
|
from .model import (
|
||||||
|
HelloData,
|
||||||
|
IdentifyData,
|
||||||
|
Intent,
|
||||||
|
Opcode,
|
||||||
|
Payload,
|
||||||
|
ResumeData,
|
||||||
|
payload_of,
|
||||||
|
)
|
||||||
|
from .payloads import Ready
|
||||||
|
from .queue import EventQueue
|
||||||
|
from .session import Session
|
||||||
|
from .token import AccessToken
|
||||||
|
|
||||||
|
# TOML 分组名即 Intent 枚举成员名
|
||||||
|
INTENT_GROUPS: dict[str, Intent] = {name: Intent[name] for name in Intent.__members__}
|
||||||
|
|
||||||
|
|
||||||
|
def get_intents(path: str) -> int:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
config = load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.warning(f"未找到 {path},默认不监听任何事件")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
intents = 0
|
||||||
|
for group, events in config.items():
|
||||||
|
if intent := INTENT_GROUPS.get(group):
|
||||||
|
if isinstance(events, dict) and any(events.values()):
|
||||||
|
intents |= intent
|
||||||
|
else:
|
||||||
|
logger.warning(f"未知的 intents 分组: {group}")
|
||||||
|
return intents
|
||||||
|
|
||||||
|
|
||||||
|
class WebsocketProtocol(BaseProtocol):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
config: Config,
|
||||||
|
queue: EventQueue,
|
||||||
|
session: Session,
|
||||||
|
token: AccessToken,
|
||||||
|
) -> None:
|
||||||
|
self._config = config
|
||||||
|
self._queue = queue
|
||||||
|
self._session = session
|
||||||
|
self._token = token
|
||||||
|
self._scheduler = AsyncIOScheduler()
|
||||||
|
self._heartbeat_job_id = "heartbeat"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def register(self, emitter: EventEmitter) -> None:
|
||||||
|
emitter.on(Opcode.HELLO)(self.identify)
|
||||||
|
emitter.on(Opcode.HEARTBEAT)(self.heartbeat)
|
||||||
|
emitter.on("READY")(self.ready)
|
||||||
|
emitter.on(Opcode.INVALID_SESSION)(self.invalid_session)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def on_payload(self, payload: Payload) -> None:
|
||||||
|
seq = payload.get("s")
|
||||||
|
if isinstance(seq, int):
|
||||||
|
self._session.update_sequence(seq)
|
||||||
|
|
||||||
|
async def identify(self, data: HelloData) -> Payload:
|
||||||
|
if not self._scheduler.running:
|
||||||
|
self._scheduler.add_job(
|
||||||
|
self.send_heartbeat,
|
||||||
|
"interval",
|
||||||
|
seconds=data["heartbeat_interval"] / 1000,
|
||||||
|
id=self._heartbeat_job_id,
|
||||||
|
)
|
||||||
|
self._scheduler.start()
|
||||||
|
logger.info(f"心跳任务已启动,间隔 {data['heartbeat_interval']}ms")
|
||||||
|
|
||||||
|
access_token = await self._token.get_access_token()
|
||||||
|
|
||||||
|
d: ResumeData | IdentifyData
|
||||||
|
if self._session.session_id:
|
||||||
|
op = Opcode.RESUME
|
||||||
|
d = {
|
||||||
|
"token": f"QQBot {access_token}",
|
||||||
|
"session_id": self._session.session_id,
|
||||||
|
"seq": self._session.seq,
|
||||||
|
}
|
||||||
|
logger.info("发送 RESUME 恢复会话")
|
||||||
|
else:
|
||||||
|
op = Opcode.IDENTIFY
|
||||||
|
d = {
|
||||||
|
"token": f"QQBot {access_token}",
|
||||||
|
"intents": get_intents(self._config.intents_file),
|
||||||
|
"shard": (0, 1),
|
||||||
|
"properties": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload_of(op, d)
|
||||||
|
|
||||||
|
async def heartbeat(self, _) -> Payload:
|
||||||
|
return payload_of(Opcode.HEARTBEAT, self._session.sequence_id)
|
||||||
|
|
||||||
|
async def send_heartbeat(self) -> None:
|
||||||
|
await self._queue.put_reply(await self.heartbeat(None))
|
||||||
|
|
||||||
|
async def ready(self, ready: Ready) -> None:
|
||||||
|
self._session.session_id = ready.session_id
|
||||||
|
logger.info("会话已建立(READY)")
|
||||||
|
|
||||||
|
async def invalid_session(self, resumable: bool) -> None:
|
||||||
|
if not resumable:
|
||||||
|
self._session.session_id = None
|
||||||
|
logger.warning("会话已失效,下次连接将重新 IDENTIFY")
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
from qqbotsdk.api import BotApi
|
||||||
|
|
||||||
|
|
||||||
|
def make_api() -> tuple[BotApi, dict]:
|
||||||
|
"""替换掉 request 记录入参,只测业务方法拼参。"""
|
||||||
|
captured: dict = {}
|
||||||
|
|
||||||
|
class Recording(BotApi):
|
||||||
|
async def request(self, method, path, **kwargs): # type: ignore[override]
|
||||||
|
captured["call"] = (method, path, kwargs)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return Recording.__new__(Recording), captured
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_group_message_passive():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.post_group_message("G1", content="hi", msg_id="m1", msg_seq=2)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/messages")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"msg_type": 0,
|
||||||
|
"content": "hi",
|
||||||
|
"msg_id": "m1",
|
||||||
|
"msg_seq": 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_c2c_message_active_no_msg_seq():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.post_c2c_message("U1", content="yo")
|
||||||
|
_, path, kwargs = cap["call"]
|
||||||
|
assert path == "/v2/users/U1/messages"
|
||||||
|
assert kwargs["json"] == {"msg_type": 0, "content": "yo"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_post_message_with_media_sets_msg_type():
|
||||||
|
api, cap = make_api()
|
||||||
|
media = {"file_info": "f1"}
|
||||||
|
await api.post_group_message("G1", content="看图", msg_id="m1", media=media)
|
||||||
|
body = cap["call"][2]["json"]
|
||||||
|
assert body["msg_type"] == 7
|
||||||
|
assert body["media"] == media
|
||||||
|
|
||||||
|
|
||||||
|
async def test_upload_group_file():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.upload_group_file("G1", file_type=1, url="http://x/img.png")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/files")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"file_type": 1,
|
||||||
|
"url": "http://x/img.png",
|
||||||
|
"srv_send_msg": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_channel_message_and_reaction():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.post_channel_message("C1", content="hi", msg_id="m1", image="http://i")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/channels/C1/messages")
|
||||||
|
assert kwargs["json"] == {
|
||||||
|
"msg_seq": 1,
|
||||||
|
"content": "hi",
|
||||||
|
"msg_id": "m1",
|
||||||
|
"image": "http://i",
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.put_channel_reaction("C1", "m1", emoji_type=1, emoji_id="4")
|
||||||
|
assert cap["call"][:2] == ("PUT", "/channels/C1/messages/m1/reactions/1/4")
|
||||||
|
|
||||||
|
await api.delete_channel_reaction("C1", "m1", emoji_type=1, emoji_id="4")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/channels/C1/messages/m1/reactions/1/4")
|
||||||
|
|
||||||
|
await api.withdraw_channel_message("C1", "m1", hide_tip=True)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("DELETE", "/channels/C1/messages/m1")
|
||||||
|
assert kwargs["params"] == {"hidetip": "true"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_withdraw_v2_messages():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.withdraw_group_message("G1", "ROBOT1.0_x")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/v2/groups/G1/messages/ROBOT1.0_x")
|
||||||
|
await api.withdraw_c2c_message("U1", "ROBOT1.0_y")
|
||||||
|
assert cap["call"][:2] == ("DELETE", "/v2/users/U1/messages/ROBOT1.0_y")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_event_id_passive_reply():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.post_group_message("G1", content="hi", event_id="e1")
|
||||||
|
body = cap["call"][2]["json"]
|
||||||
|
assert body["event_id"] == "e1"
|
||||||
|
assert "msg_id" not in body
|
||||||
|
assert body["msg_seq"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mute_and_schedule_official_field_names():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.mute_guild_member("GID", "UID", mute_seconds=3600)
|
||||||
|
assert cap["call"][2]["json"] == {"mute_seconds": "3600"}
|
||||||
|
|
||||||
|
await api.create_channel_schedule(
|
||||||
|
"C1", "活动", "1642076453000", "1642083653000", "C2"
|
||||||
|
)
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/channels/C1/schedules")
|
||||||
|
info = kwargs["json"]["schedule"]
|
||||||
|
assert info["remind_type"] == "0"
|
||||||
|
assert "schedule_info" not in kwargs["json"]
|
||||||
|
|
||||||
|
await api.mute_group_member("G1", "2026-01-01T12:00:00+08:00")
|
||||||
|
method, path, kwargs = cap["call"]
|
||||||
|
assert (method, path) == ("POST", "/v2/groups/G1/restrict_chat_setting")
|
||||||
|
assert kwargs["json"] == {"mute_expire_at": "2026-01-01T12:00:00+08:00"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_me():
|
||||||
|
api, cap = make_api()
|
||||||
|
await api.me()
|
||||||
|
assert cap["call"][:2] == ("GET", "/users/@me")
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
from qqbotsdk.crypto import build_seed, sign_msg, verify_sig
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_seed_pads_to_32_bytes():
|
||||||
|
assert len(build_seed("abc")) == 32
|
||||||
|
assert len(build_seed("x" * 32)) == 32
|
||||||
|
assert len(build_seed("x" * 40)) == 32
|
||||||
|
# 同一 secret 展开结果稳定
|
||||||
|
assert build_seed("abc") == build_seed("abc")
|
||||||
|
|
||||||
|
|
||||||
|
def test_sign_and_verify_roundtrip():
|
||||||
|
secret = "test-secret"
|
||||||
|
timestamp = "1726000000"
|
||||||
|
plain_token = "plain_token"
|
||||||
|
sig = sign_msg(secret, timestamp + plain_token)
|
||||||
|
# sign_msg 产出 hex 编码的 Ed25519 签名(64 字节 = 128 hex 字符)
|
||||||
|
assert len(sig) == 128
|
||||||
|
assert verify_sig(secret, timestamp, sig, plain_token.encode())
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_sig_accepts_valid_signature():
|
||||||
|
secret = "test-secret"
|
||||||
|
timestamp = "1726000000"
|
||||||
|
body = b'{"plain_token":"abc"}'
|
||||||
|
# 签名内容 = timestamp + body
|
||||||
|
key = ed25519.Ed25519PrivateKey.from_private_bytes(build_seed(secret))
|
||||||
|
signature = key.sign(timestamp.encode() + body).hex()
|
||||||
|
assert verify_sig(secret, timestamp, signature, body)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_sig_rejects_tampered_body():
|
||||||
|
secret = "test-secret"
|
||||||
|
timestamp = "1726000000"
|
||||||
|
body = b'{"plain_token":"abc"}'
|
||||||
|
sig = sign_msg(secret, timestamp + body.decode())
|
||||||
|
assert not verify_sig(secret, timestamp, sig, b'{"plain_token":"xyz"}')
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_sig_rejects_bad_hex():
|
||||||
|
assert not verify_sig("secret", "123", "not-hex", b"body")
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from qqbotsdk.config import Config
|
||||||
|
from qqbotsdk.connecter import WebhookConnecter
|
||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
|
||||||
|
|
||||||
|
def make_connecter() -> WebhookConnecter:
|
||||||
|
config = Config(
|
||||||
|
app_id="app",
|
||||||
|
app_secret="secret",
|
||||||
|
base_url="https://api.bot.qq.com",
|
||||||
|
timeout=None, # type: ignore[arg-type]
|
||||||
|
connecter="webhook",
|
||||||
|
intents_file="intents.toml",
|
||||||
|
webhook_host="0.0.0.0",
|
||||||
|
webhook_port=8080,
|
||||||
|
webhook_path="/qqbot/webhook",
|
||||||
|
)
|
||||||
|
return WebhookConnecter(config, EventEmitter(EventQueue()), EventQueue())
|
||||||
|
|
||||||
|
|
||||||
|
def test_first_push_is_not_duplicate():
|
||||||
|
c = make_connecter()
|
||||||
|
assert not c._duplicated("evt-1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_id_within_ttl_is_duplicate():
|
||||||
|
c = make_connecter()
|
||||||
|
assert not c._duplicated("evt-1")
|
||||||
|
assert c._duplicated("evt-1")
|
||||||
|
|
||||||
|
|
||||||
|
def test_none_or_empty_id_never_duplicates():
|
||||||
|
c = make_connecter()
|
||||||
|
assert not c._duplicated(None)
|
||||||
|
assert not c._duplicated("")
|
||||||
|
assert not c._duplicated(None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_expired_entries_are_evicted():
|
||||||
|
c = make_connecter()
|
||||||
|
assert not c._duplicated("evt-1")
|
||||||
|
# 人为把记录改成过期
|
||||||
|
for key in list(c._seen):
|
||||||
|
c._seen[key] -= c._DEDUP_TTL + 1
|
||||||
|
assert not c._duplicated("evt-1")
|
||||||
|
assert "evt-1" in c._seen # 以新时间戳重新记录
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.model import Opcode, payload_of
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
from qqbotsdk.session import Session
|
||||||
|
|
||||||
|
|
||||||
|
def test_sequence_only_moves_forward():
|
||||||
|
s = Session()
|
||||||
|
assert s.sequence_id is None
|
||||||
|
s.update_sequence(5)
|
||||||
|
s.update_sequence(3) # 回退被忽略
|
||||||
|
assert s.sequence_id == 5
|
||||||
|
s.update_sequence(7)
|
||||||
|
assert s.sequence_id == 7
|
||||||
|
assert s.seq == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_seq_defaults_to_zero():
|
||||||
|
s = Session()
|
||||||
|
assert s.seq == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_handler_exception_is_isolated():
|
||||||
|
queue = EventQueue()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
called: list[str] = []
|
||||||
|
|
||||||
|
@emitter.on("READY")
|
||||||
|
async def bad(_):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
@emitter.on("READY")
|
||||||
|
async def good(_):
|
||||||
|
called.append("ok")
|
||||||
|
|
||||||
|
await emitter.emit({"op": Opcode.DISPATCH, "t": "READY", "d": {}})
|
||||||
|
assert called == ["ok"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reply_payload_is_enqueued():
|
||||||
|
queue = EventQueue()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
|
||||||
|
@emitter.on(Opcode.HEARTBEAT)
|
||||||
|
async def beat(_):
|
||||||
|
return payload_of(Opcode.HEARTBEAT, 42)
|
||||||
|
|
||||||
|
await emitter.emit({"op": Opcode.HEARTBEAT, "d": 0})
|
||||||
|
reply = await queue.get_reply()
|
||||||
|
assert reply["op"] == Opcode.HEARTBEAT
|
||||||
|
assert reply["d"] == 42
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from qqbotsdk.config import Config
|
||||||
|
from qqbotsdk.ws_protocol import get_intents
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def env(monkeypatch):
|
||||||
|
monkeypatch.setenv("APPID", "123")
|
||||||
|
monkeypatch.setenv("APPSECRET", "s")
|
||||||
|
monkeypatch.delenv("CONNECTER", raising=False)
|
||||||
|
monkeypatch.delenv("BASE_URL", raising=False)
|
||||||
|
|
||||||
|
|
||||||
|
def write(tmp_path, content: str) -> str:
|
||||||
|
path = tmp_path / "intents.toml"
|
||||||
|
path.write_text(content, encoding="utf-8")
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_file_returns_zero(tmp_path):
|
||||||
|
assert get_intents(str(tmp_path / "nope.toml")) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_enabled_group_sets_bit(tmp_path):
|
||||||
|
path = write(tmp_path, "[GROUP_AND_C2C_EVENT]\nC2C_MESSAGE_CREATE = true\n")
|
||||||
|
assert get_intents(path) & (1 << 25)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_disabled_returns_zero(tmp_path):
|
||||||
|
path = write(tmp_path, "[GUILDS]\nGUILD_CREATE = false\n")
|
||||||
|
assert get_intents(path) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_group_is_skipped(tmp_path):
|
||||||
|
path = write(
|
||||||
|
tmp_path, "[NOT_A_GROUP]\nFOO = true\n\n[GUILDS]\nGUILD_CREATE = true\n"
|
||||||
|
)
|
||||||
|
assert get_intents(path) == (1 << 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_load_defaults():
|
||||||
|
config = Config.load()
|
||||||
|
assert config.app_id == "123"
|
||||||
|
assert config.app_secret == "s"
|
||||||
|
assert config.connecter == "websocket"
|
||||||
|
assert config.base_url == "https://api.bot.qq.com"
|
||||||
|
assert config.webhook_port == 8080
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_requires_appid(monkeypatch):
|
||||||
|
monkeypatch.delenv("APPID", raising=False)
|
||||||
|
with pytest.raises(ValueError, match="APPID"):
|
||||||
|
Config.load()
|
||||||
|
|
||||||
|
|
||||||
|
def test_config_rejects_unknown_connecter(monkeypatch):
|
||||||
|
monkeypatch.setenv("CONNECTER", "carrier-pigeon")
|
||||||
|
with pytest.raises(ValueError, match="carrier-pigeon"):
|
||||||
|
Config.load()
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.model import Opcode
|
||||||
|
from qqbotsdk.payloads import (
|
||||||
|
C2CMessage,
|
||||||
|
GroupAtMessage,
|
||||||
|
Ready,
|
||||||
|
parse_event,
|
||||||
|
)
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_group_at_message():
|
||||||
|
msg = parse_event(
|
||||||
|
"GROUP_AT_MESSAGE_CREATE",
|
||||||
|
{
|
||||||
|
"author": {"id": None, "member_openid": "M1"},
|
||||||
|
"content": "hi",
|
||||||
|
"group_openid": "G1",
|
||||||
|
"id": "msgid-1",
|
||||||
|
"timestamp": "2026-01-01",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert isinstance(msg, GroupAtMessage)
|
||||||
|
assert msg.group_openid == "G1"
|
||||||
|
assert msg.user_openid == "M1"
|
||||||
|
assert msg.id == "msgid-1"
|
||||||
|
assert msg.author is not None and msg.author.member_openid == "M1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_c2c_message():
|
||||||
|
msg = parse_event(
|
||||||
|
"C2C_MESSAGE_CREATE",
|
||||||
|
{"author": {"user_openid": "U1"}, "content": "hello", "id": "m2"},
|
||||||
|
)
|
||||||
|
assert isinstance(msg, C2CMessage)
|
||||||
|
assert msg.user_openid == "U1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_ready():
|
||||||
|
ready = parse_event(
|
||||||
|
"READY", {"session_id": "s1", "version": 1, "user": {"id": "b"}}
|
||||||
|
)
|
||||||
|
assert isinstance(ready, Ready)
|
||||||
|
assert ready.session_id == "s1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_unknown_type_returns_dict():
|
||||||
|
d = {"whatever": 1}
|
||||||
|
assert parse_event("SOME_FUTURE_EVENT", d) is d
|
||||||
|
assert parse_event(None, d) is d
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_ignores_extra_and_missing_fields():
|
||||||
|
msg = parse_event("GROUP_AT_MESSAGE_CREATE", {"content": "x", "future_field": 9})
|
||||||
|
assert isinstance(msg, GroupAtMessage)
|
||||||
|
assert msg.content == "x"
|
||||||
|
assert msg.group_openid is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_non_dict_d_returns_dict():
|
||||||
|
d = 42 # 心跳等 opcode 的 d 是数字
|
||||||
|
assert parse_event("GROUP_AT_MESSAGE_CREATE", d) == 42
|
||||||
|
|
||||||
|
|
||||||
|
async def test_typed_annotation_receives_dataclass():
|
||||||
|
queue = EventQueue()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
received: list[GroupAtMessage] = []
|
||||||
|
|
||||||
|
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def handler(msg: GroupAtMessage):
|
||||||
|
received.append(msg)
|
||||||
|
|
||||||
|
await emitter.emit(
|
||||||
|
{
|
||||||
|
"op": Opcode.DISPATCH,
|
||||||
|
"t": "GROUP_AT_MESSAGE_CREATE",
|
||||||
|
"d": {"content": "hi", "group_openid": "G1"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert len(received) == 1
|
||||||
|
assert received[0].content == "hi"
|
||||||
|
assert received[0].group_openid == "G1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_untyped_annotation_still_receives_raw_dict():
|
||||||
|
queue = EventQueue()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
received: list[dict] = []
|
||||||
|
|
||||||
|
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def handler(d):
|
||||||
|
received.append(d)
|
||||||
|
|
||||||
|
await emitter.emit(
|
||||||
|
{"op": Opcode.DISPATCH, "t": "GROUP_AT_MESSAGE_CREATE", "d": {"content": "x"}}
|
||||||
|
)
|
||||||
|
assert received == [{"content": "x"}]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.model import Opcode
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def emitter() -> EventEmitter:
|
||||||
|
return EventEmitter(EventQueue())
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_dispatch_uses_t(emitter: EventEmitter):
|
||||||
|
routed = emitter._route({"op": Opcode.DISPATCH, "t": "READY", "d": {}})
|
||||||
|
assert routed is not None
|
||||||
|
op, name, event = routed
|
||||||
|
assert op == Opcode.DISPATCH
|
||||||
|
assert name == "READY"
|
||||||
|
assert event.type == "READY"
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_non_dispatch_uses_opcode_name(emitter: EventEmitter):
|
||||||
|
routed = emitter._route({"op": Opcode.HELLO, "d": {"heartbeat_interval": 1}})
|
||||||
|
assert routed is not None
|
||||||
|
assert routed[1] == "HELLO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_rejects_missing_op(emitter: EventEmitter):
|
||||||
|
assert emitter._route({"d": {}}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_rejects_unknown_opcode(emitter: EventEmitter):
|
||||||
|
assert emitter._route({"op": 999}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_rejects_dispatch_without_t(emitter: EventEmitter):
|
||||||
|
assert emitter._route({"op": Opcode.DISPATCH, "d": {}}) is None
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""Webhook 接入的端到端集成测试:真实起 WebhookConnecter 的 http server,
|
||||||
|
覆盖 op=13 验证应答、合法签名放行、坏签名 401、按 id 去重。"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from aiohttp.test_utils import TestClient, TestServer
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||||
|
|
||||||
|
from qqbotsdk.config import Config
|
||||||
|
from qqbotsdk.connecter import WebhookConnecter
|
||||||
|
from qqbotsdk.crypto import build_seed
|
||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.payloads import GroupAtMessage
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
from qqbotsdk.session import Session
|
||||||
|
from qqbotsdk.webhook_protocol import WebhookProtocol
|
||||||
|
|
||||||
|
SECRET = "webhook-test-secret"
|
||||||
|
|
||||||
|
|
||||||
|
def make_config(port: int) -> Config:
|
||||||
|
return Config(
|
||||||
|
app_id="app",
|
||||||
|
app_secret=SECRET,
|
||||||
|
base_url="https://api.bot.qq.com",
|
||||||
|
timeout=None, # type: ignore[arg-type]
|
||||||
|
connecter="webhook",
|
||||||
|
intents_file="no-such-intents.toml",
|
||||||
|
webhook_host="127.0.0.1",
|
||||||
|
webhook_port=port,
|
||||||
|
webhook_path="/qqbot/webhook",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sign(secret: str, timestamp: str, body: bytes) -> str:
|
||||||
|
key = ed25519.Ed25519PrivateKey.from_private_bytes(build_seed(secret))
|
||||||
|
return key.sign(timestamp.encode() + body).hex()
|
||||||
|
|
||||||
|
|
||||||
|
def event_body(event_id: str, content: str) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"op": 0,
|
||||||
|
"id": event_id,
|
||||||
|
"t": "GROUP_AT_MESSAGE_CREATE",
|
||||||
|
"d": {"content": content, "group_openid": "G1"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_webhook_end_to_end():
|
||||||
|
config = make_config(0)
|
||||||
|
queue = EventQueue()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
emitter.register_protocol(WebhookProtocol(config, queue, Session()))
|
||||||
|
connecter = WebhookConnecter(config, emitter, queue)
|
||||||
|
|
||||||
|
received: list[GroupAtMessage] = []
|
||||||
|
|
||||||
|
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def on_msg(msg: GroupAtMessage) -> None:
|
||||||
|
received.append(msg)
|
||||||
|
|
||||||
|
dispatch_task = asyncio.create_task(emitter.dispatch())
|
||||||
|
server = TestServer(connecter._make_app())
|
||||||
|
client = TestClient(server)
|
||||||
|
await client.start_server()
|
||||||
|
try:
|
||||||
|
# 1. op=13 验证请求(不走验签),应答 plain_token + 对 event_ts+plain_token 的签名
|
||||||
|
resp = await client.post(
|
||||||
|
"/qqbot/webhook",
|
||||||
|
json={"op": 13, "d": {"plain_token": "pt", "event_ts": "1726000000"}},
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
ret = await resp.json()
|
||||||
|
assert ret["plain_token"] == "pt"
|
||||||
|
assert sign(SECRET, "1726000000", b"pt") == ret["signature"]
|
||||||
|
|
||||||
|
# 2. 合法签名的事件推送 → 放行 → handler 收到 dataclass
|
||||||
|
body = json.dumps(event_body("evt-1", "hello")).encode()
|
||||||
|
resp = await client.post(
|
||||||
|
"/qqbot/webhook",
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"X-Signature-Timestamp": "1726000001",
|
||||||
|
"X-Signature-Ed25519": sign(SECRET, "1726000001", body),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
assert await resp.json() == {"opcode": 12}
|
||||||
|
for _ in range(50):
|
||||||
|
if received:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.02)
|
||||||
|
assert len(received) == 1
|
||||||
|
assert received[0].content == "hello"
|
||||||
|
|
||||||
|
# 3. 坏签名 → 401,事件不入队
|
||||||
|
resp = await client.post(
|
||||||
|
"/qqbot/webhook",
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"X-Signature-Timestamp": "1726000002",
|
||||||
|
"X-Signature-Ed25519": "00" * 64,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status == 401
|
||||||
|
|
||||||
|
# 4. 同 id 重复推送 → 去重跳过,handler 不再收到
|
||||||
|
resp = await client.post(
|
||||||
|
"/qqbot/webhook",
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"X-Signature-Timestamp": "1726000001",
|
||||||
|
"X-Signature-Ed25519": sign(SECRET, "1726000001", body),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status == 200
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
assert len(received) == 1
|
||||||
|
finally:
|
||||||
|
await client.close()
|
||||||
|
dispatch_task.cancel()
|
||||||
|
await asyncio.gather(dispatch_task, return_exceptions=True)
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""WebSocket 接入的端到端集成测试:起一个假网关(token 接口 + gateway + ws),
|
||||||
|
真实跑通 HELLO → IDENTIFY → READY → 心跳 ACK → 业务事件分发的完整时序。"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from aiohttp import ClientSession, WSMsgType, web
|
||||||
|
|
||||||
|
from qqbotsdk.config import Config
|
||||||
|
from qqbotsdk.connecter import WebsocketConnecter
|
||||||
|
from qqbotsdk.emitter import EventEmitter
|
||||||
|
from qqbotsdk.payloads import GroupAtMessage
|
||||||
|
from qqbotsdk.queue import EventQueue
|
||||||
|
from qqbotsdk.session import Session
|
||||||
|
from qqbotsdk.token import AccessToken
|
||||||
|
from qqbotsdk.ws_protocol import WebsocketProtocol
|
||||||
|
|
||||||
|
|
||||||
|
class FakeGateway:
|
||||||
|
"""模拟 QQ 开放平台的鉴权、gateway 与 ws 协议行为。"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.identifies: list[dict] = []
|
||||||
|
self.heartbeats = 0
|
||||||
|
self.runner: web.AppRunner | None = None
|
||||||
|
self.port = 0
|
||||||
|
# 测试用同步点
|
||||||
|
self.ready_sent = asyncio.Event()
|
||||||
|
self.got_dispatch = asyncio.Event()
|
||||||
|
|
||||||
|
async def start(self) -> None:
|
||||||
|
app = web.Application()
|
||||||
|
app.router.add_post("/app/getAppAccessToken", self._token)
|
||||||
|
app.router.add_get("/gateway", self._gateway)
|
||||||
|
app.router.add_get("/ws", self._ws)
|
||||||
|
self.runner = web.AppRunner(app)
|
||||||
|
await self.runner.setup()
|
||||||
|
site = web.TCPSite(self.runner, "127.0.0.1", 0)
|
||||||
|
await site.start()
|
||||||
|
assert site._server is not None
|
||||||
|
self.port = site._server.sockets[0].getsockname()[1]
|
||||||
|
|
||||||
|
async def stop(self) -> None:
|
||||||
|
assert self.runner is not None
|
||||||
|
await self.runner.cleanup()
|
||||||
|
|
||||||
|
async def _token(self, request: web.Request) -> web.Response:
|
||||||
|
return web.json_response({"access_token": "test-token", "expires_in": 9999999})
|
||||||
|
|
||||||
|
async def _gateway(self, request: web.Request) -> web.Response:
|
||||||
|
return web.json_response({"url": f"http://127.0.0.1:{self.port}/ws"})
|
||||||
|
|
||||||
|
async def _ws(self, request: web.Request) -> web.WebSocketResponse:
|
||||||
|
ws = web.WebSocketResponse()
|
||||||
|
await ws.prepare(request)
|
||||||
|
|
||||||
|
await ws.send_json({"op": 10, "d": {"heartbeat_interval": 300}})
|
||||||
|
s = 0
|
||||||
|
|
||||||
|
async for msg in ws:
|
||||||
|
if msg.type != WSMsgType.TEXT:
|
||||||
|
break
|
||||||
|
payload: dict[str, Any] = json.loads(msg.data)
|
||||||
|
op = payload.get("op")
|
||||||
|
if op == 2: # IDENTIFY
|
||||||
|
self.identifies.append(payload)
|
||||||
|
s += 1
|
||||||
|
await ws.send_json(
|
||||||
|
{
|
||||||
|
"op": 0,
|
||||||
|
"s": s,
|
||||||
|
"t": "READY",
|
||||||
|
"d": {"session_id": "S1", "user": {"id": "bot"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.ready_sent.set()
|
||||||
|
# READY 之后立刻推一条业务事件
|
||||||
|
s += 1
|
||||||
|
await ws.send_json(
|
||||||
|
{
|
||||||
|
"op": 0,
|
||||||
|
"s": s,
|
||||||
|
"t": "GROUP_AT_MESSAGE_CREATE",
|
||||||
|
"d": {"content": "ping", "group_openid": "G1"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif op == 1: # HEARTBEAT
|
||||||
|
self.heartbeats += 1
|
||||||
|
await ws.send_json({"op": 11})
|
||||||
|
return ws
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_websocket_end_to_end():
|
||||||
|
gw = FakeGateway()
|
||||||
|
await gw.start()
|
||||||
|
try:
|
||||||
|
config = Config(
|
||||||
|
app_id="app",
|
||||||
|
app_secret="secret",
|
||||||
|
base_url=f"http://127.0.0.1:{gw.port}",
|
||||||
|
timeout=None, # type: ignore[arg-type]
|
||||||
|
connecter="websocket",
|
||||||
|
intents_file="no-such-intents.toml", # 测试环境无配置,intents=0
|
||||||
|
webhook_host="127.0.0.1",
|
||||||
|
webhook_port=0,
|
||||||
|
webhook_path="/x",
|
||||||
|
)
|
||||||
|
|
||||||
|
http = ClientSession(timeout=config.timeout)
|
||||||
|
token = AccessToken(config, http)
|
||||||
|
queue = EventQueue()
|
||||||
|
session = Session()
|
||||||
|
emitter = EventEmitter(queue)
|
||||||
|
protocol = WebsocketProtocol(config, queue, session, token)
|
||||||
|
emitter.register_protocol(protocol)
|
||||||
|
connecter = WebsocketConnecter(config, http, token, queue)
|
||||||
|
|
||||||
|
received: list[GroupAtMessage] = []
|
||||||
|
|
||||||
|
@emitter.on("GROUP_AT_MESSAGE_CREATE")
|
||||||
|
async def on_msg(msg: GroupAtMessage) -> None:
|
||||||
|
received.append(msg)
|
||||||
|
gw.got_dispatch.set()
|
||||||
|
|
||||||
|
dispatch_task = asyncio.create_task(emitter.dispatch())
|
||||||
|
connect_task = asyncio.create_task(connecter.run())
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(gw.ready_sent.wait(), timeout=10)
|
||||||
|
await asyncio.wait_for(gw.got_dispatch.wait(), timeout=10)
|
||||||
|
await asyncio.sleep(0.5) # 留出至少一个心跳周期(300ms)
|
||||||
|
|
||||||
|
# READY 已被协议层捕获,session_id 用于后续 RESUME
|
||||||
|
assert session.session_id == "S1"
|
||||||
|
# 心跳定时器真实运行,服务端收到了 HEARTBEAT
|
||||||
|
assert gw.heartbeats >= 1
|
||||||
|
# IDENTIFY 形态正确(token 前缀 / shard / intents)
|
||||||
|
identify = gw.identifies[0]
|
||||||
|
assert identify["op"] == 2
|
||||||
|
assert identify["d"]["token"] == "QQBot test-token"
|
||||||
|
assert identify["d"]["intents"] == 0
|
||||||
|
assert identify["d"]["shard"] == [0, 1]
|
||||||
|
# 业务事件以 dataclass 形式分发到 handler
|
||||||
|
assert len(received) == 1
|
||||||
|
assert received[0].content == "ping"
|
||||||
|
assert received[0].group_openid == "G1"
|
||||||
|
finally:
|
||||||
|
dispatch_task.cancel()
|
||||||
|
connect_task.cancel()
|
||||||
|
await asyncio.gather(dispatch_task, connect_task, return_exceptions=True)
|
||||||
|
await http.close()
|
||||||
|
finally:
|
||||||
|
await gw.stop()
|
||||||
@@ -0,0 +1,806 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohappyeyeballs"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiohttp"
|
||||||
|
version = "3.14.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohappyeyeballs" },
|
||||||
|
{ name = "aiosignal" },
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "frozenlist" },
|
||||||
|
{ name = "multidict" },
|
||||||
|
{ name = "propcache" },
|
||||||
|
{ name = "yarl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiosignal"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "frozenlist" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "apscheduler"
|
||||||
|
version = "3.11.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzlocal" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/6b/eeff360196bb20b312c9e762a820fd1b2c6d809466c755ef57863478e454/apscheduler-3.11.3.tar.gz", hash = "sha256:cd2fcc9330039a81a5893472ad49facf23a6d5604cbe1d918c835c6de7834d5a", size = 110312, upload-time = "2026-06-28T19:39:22.493Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/c9/8638db32514dbb9157b3d82680c6faea89283523edf9ed2415ea3884f2ae/apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30", size = 66024, upload-time = "2026-06-28T19:39:20.982Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "26.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "50.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dishka"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/97/da/b75b5fe665d16725e1d46e291c8dd4b0ed1731c7964f41087fda92f03d40/dishka-1.10.1.tar.gz", hash = "sha256:51fb624b9acc948b90f57128460061d1f3a1780121fe1c7d14c565e8bbff2083", size = 282008, upload-time = "2026-04-25T12:07:57.587Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/08/dd3259a319ba45c48d975caf237c830c7873a203288cb4ab186cc5bef585/dishka-1.10.1-py3-none-any.whl", hash = "sha256:84f40c1115eb391ce3fb558fd7946c5bcaf6361c35b77b7a185b7697a5720ec7", size = 120062, upload-time = "2026-04-25T12:07:59.153Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "frozenlist"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.19"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loguru"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "multidict"
|
||||||
|
version = "6.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nodeenv"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "propcache"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyee"
|
||||||
|
version = "14.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/1d/f1/fdedc2c75c3e31a330659c85e5793bb18b3397981fbf0844c6dee5b18926/pyee-14.0.0.tar.gz", hash = "sha256:76dd0f4314ecd27f02dc73589dea7fd3853f9b6176d8ef9b122860657e3602de", size = 98760, upload-time = "2026-08-13T04:26:11.021Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/12/5347938b1f9a6453f0dbdfcc3e2388a1320ef9b9ec17fbefbc4ab647ea98/pyee-14.0.0-py3-none-any.whl", hash = "sha256:3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d", size = 15553, upload-time = "2026-08-13T04:26:09.916Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyright"
|
||||||
|
version = "1.1.411"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "nodeenv" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qqbotsdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "apscheduler" },
|
||||||
|
{ name = "cryptography" },
|
||||||
|
{ name = "dishka" },
|
||||||
|
{ name = "loguru" },
|
||||||
|
{ name = "pyee" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyright" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "aiohttp", specifier = ">=3.14.3" },
|
||||||
|
{ name = "apscheduler", specifier = ">=3.11.3" },
|
||||||
|
{ name = "cryptography", specifier = ">=50.0.1" },
|
||||||
|
{ name = "dishka", specifier = ">=1.10.1" },
|
||||||
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
|
{ name = "pyee", specifier = ">=14.0.0" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.2.3" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "pyright", specifier = ">=1.1.411" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.0" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=0.24.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.16.5" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.16.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" }
|
||||||
|
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 = "win32-setctime"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yarl"
|
||||||
|
version = "1.24.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "multidict" },
|
||||||
|
{ name = "propcache" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" },
|
||||||
|
]
|
||||||
Reference in new issue
Block a user