Compare commits

19 Commits
Author SHA1 Message Date
chakcy 793c842bbf modify: listen_scheduler 更新 2026-01-10 18:14:20 +08:00
chakcy 14b3ea707f modify: uv.lock 2026-01-06 13:55:00 +08:00
chakcy 45bae10ceb modify: 说明wd 2026-01-05 20:33:18 +08:00
chakcy 38c0b6cea3 modify: 批量入队 2025-12-15 08:37:09 +08:00
chakcy bfa97e760e modify: 优化清理调度器 2025-11-07 19:38:09 +08:00
chakcy 45817e5542 modify: 将 QApplication 实例初始化调整到调度器 start 阶段 2025-11-06 10:16:30 +08:00
chakcy 05ed2745b0 modify: 修改重试逻辑 2025-11-05 13:05:57 +08:00
chakcy 8745db8ab1 版本更新 2025-11-04 11:26:54 +08:00
chakcy fa5db8e573 modify: qt调度器 2025-11-04 11:17:59 +08:00
chakcy ac67b60d14 modify:qt 调度器 2025-11-02 14:21:30 +08:00
chakcy 58f4ad84ee modify: pyqt6调度器 2025-10-28 15:08:32 +08:00
chakcy 6bd1e28815 modify: pyside6 调度器 2025-10-26 21:27:49 +08:00
chakcy c6bcb64c61 modify: 修改支持版本 2025-10-19 21:23:55 +08:00
chakcy 0c8c6ffbc2 添加日志 2025-10-11 14:20:39 +08:00
chakcy e6291fe3a4 modify 版本更新 0.2.0 2025-09-27 21:18:06 +08:00
chakcy cfc4028a51 modify: 代码优化 2025-09-25 21:53:58 +08:00
chakcy bcb3304338 删除 src/queue_sqlite/core/core.pyd 2025-09-23 10:37:52 +08:00
chakcy 80b1c89014 modify: 修改 QueueOperation 2025-09-23 10:34:58 +08:00
chakcy 7189d6e2a4 modify: 协程调度器 2025-09-20 19:07:52 +08:00
68 changed files with 6472 additions and 1156 deletions
+10 -2
View File
@@ -5,12 +5,12 @@ build/
dist/ dist/
wheels/ wheels/
*.egg-info *.egg-info
.python-version
# Virtual environments # Virtual environments
.venv .venv
output.md output.md
.arts .arts
uv.lock
cache/ cache/
# IDE files # IDE files
@@ -18,4 +18,12 @@ cache/
.vscode/ .vscode/
# test files # test files
.pytest_cache/ .pytest_cache/
# dynamic library
*.so
*.dll
*.pyd
# log
log/
+1 -1
View File
@@ -1 +1 @@
3.13 3.12
+289 -168
View File
@@ -1,225 +1,346 @@
# SQLite 任务队列系统 # Queue SQLite - 高性能 SQLite 任务队列系统
这个项目是一个基于 SQLite 的高性能任务队列系统,使用 Rust 和 Python 混合编程实现。系统提供了任务调度、消息队列管理和任务生命周期管理的完整解决方案。 ![python-3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)
![rust-1.65+](https://img.shields.io/badge/rust-1.65+-red.svg)
![license-MIT](https://img.shields.io/badge/license-MIT-green.svg)
![version-0.2.1](https://img.shields.io/badge/version-0.2.1-orange.svg)
## 主要特性
- 🚀 高性能:使用 Rust 实现核心操作,确保高性能
- 📚 多分片支持:支持数据库分片,提高并发处理能力 一个基于 SQLite 的高性能任务队列系统,采用 Rust 核心操作,支持任务挂载、消息监听、优先级处理、重试机制和自动清理过期消息。适合构建可靠、可扩展的后台任务处理系统。
- ⏱️ 智能调度:提供接收调度器、任务调度器和清理调度器 ## 🌟 特性
- 🔒 任务生命周期管理:支持任务状态跟踪、重试机制和过期处理 ### 核心优势
- 📊 监控支持:内置资源监控功能 - 🚀 高性能:Rust 核心提供毫秒级任务处理
- 💾 持久化存储:基于 SQLite 的可靠消息存储
- 🔄 多调度器支持:标准、异步、Qt 三种调度模式
- 🎯 智能分片:自动哈希分片,支持横向扩展
- 📊 全面监控:内置资源使用监控和队列状态查看
- 🧩 任务挂载系统:通过装饰器轻松添加新任务 ### 功能亮点
## 项目结构 - ✅ 任务装饰器:使用 @task 装饰器轻松注册任务
``` - ✅ 监听装饰器:使用 @listener 装饰器实现数据变更监听
src/ - ✅ 优先级队列:支持 LOW/NORMAL/HIGH/URGENT 四级优先级
├── core/ # Rust 核心实现 - ✅ 重试机制:可配置的最大重试次数和延迟重试
│ ├── lib.rs # 主模块 - ✅ 过期清理:自动清理过期和完成的消息
│ ├── queue_operation.rs # 队列操作实现 - ✅ 批量操作:支持消息批量入队和处理
│ └── task_mounter.rs # 任务挂载实现 - ✅ 异步支持:原生支持 async/await 异步任务
│ - ✅ Qt 集成:可选 Qt 调度器用于 GUI 应用
├── queue_sqlite/ # Python 实现
│ ├── constant/ # 常量定义(消息优先级、状态、类型) ## 📦 安装
│ ├── core/ # 核心接口
│ ├── model/ # 数据模型 ### 前置要求
│ ├── queue_operation/ # 队列操作封装
│ ├── scheduler/ # 调度器实现 - Python 3.11+
│ └── task_cycle/ # 任务生命周期管理 - Rust 1.65+ (用于编译核心扩展)
│ - SQLite 3.35+
tests/ # 测试代码
### 安装方式
#### 方式一:从源码安装(推荐)
```shell
# 克隆仓库
git clone https://github.com/chakcy/queue_sqlite.git
cd queue_sqlite
# 安装 Rust(如果未安装)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 安装 Python 依赖
pip install -r requirements.txt
# 安装开发模式
pip install -e .
``` ```
## 核心组件 #### 方式二:从 PyPI 安装
1. 消息模型 (MessageItem) ```shell
pip install queue-sqlite
定义了任务消息的数据结构,包含: ```
- 消息ID、类型、状态 ## 🚀 快速开始
- 内容、创建时间、更新时间
- 优先级、来源、目标
- 重试次数、过期时间
- 标签和元数据
2. 队列操作 (QueueOperation)
提供对 SQLite 队列的基本操作:
- 初始化数据库
- 入队和出队操作
- 获取队列长度和已完成消息
- 更新状态和结果
- 删除消息和清理过期消息
3. 调度系统
包含三个主要调度器:
***接收调度器 (ReceiveScheduler)***
- 处理消息发送
- 管理回调函数
- 接收已完成消息
***任务调度器 (TaskScheduler)***
- 从队列中获取任务
- 调用任务函数
- 更新任务状态和结果
***清理调度器 (CleanupScheduler)***
- 清理过期消息
- 删除旧消息(默认清理7天前的消息)
4. 任务挂载系统 (TaskMounter)
提供装饰器挂载任务函数:
```python
@TaskMounter.task(meta={"task_name": "example"})
def example_task(message_item: MessageItem):
# 任务逻辑
return result
```
## 使用示例
### 基本使用 ### 基本使用
```python ```python
from queue_sqlite.scheduler import QueueScheduler from queue_sqlite.scheduler import QueueScheduler
from queue_sqlite.model import MessageItem from queue_sqlite.model import MessageItem
from queue_sqlite.constant import MessagePriority
from queue_sqlite.mounter import task
# 初始化调度器
scheduler = QueueScheduler(
receive_thread_num=4,
task_thread_num=8,
shard_num=12
)
# 启动调度器 # 1. 注册任务
scheduler.start_queue_scheduler() @task(meta={"max_retries": 3, "delay": 1})
def process_image(message_item):
"""处理图片任务"""
data = message_item.content
# 处理逻辑
return {"status": "success", "processed": data["image_id"]}
# 创建消息
message = MessageItem(
content={"data": "example"},
destination="task_name"
)
# 定义回调函数 # 2. 创建调度器
def callback(message_item): scheduler = QueueScheduler(scheduler_type="standard")
print(f"任务完成: {message_item.id}")
# 发送消息 # 3. 启动调度器
scheduler.send_message(message, callback) scheduler.start()
# ... 程序运行 ... # 4. 发送任务
for i in range(10):
message = MessageItem(
content={"image_id": i, "path": f"/images/{i}.jpg"},
destination="process_image", # 任务函数名
priority=MessagePriority.HIGH, # HIGH 优先级
tags="image_processing",
)
# 停止调度器 def callback(result_message):
scheduler.stop_queue_scheduler() print(f"任务完成: {result_message.id}, 结果: {result_message.result}")
scheduler.send_message(message, callback)
# 5. 等待任务完成
import time
while scheduler.queue_operation.get_queue_length() > 0:
print(f"剩余任务: {scheduler.queue_operation.get_queue_length()}")
time.sleep(1)
# 6. 停止调度器
scheduler.stop()
``` ```
### 压力测试 ### 异步任务示例
```python ```python
# tests/test_stress.py import asyncio
class TestStress: from queue_sqlite.scheduler import QueueScheduler
@classmethod from queue_sqlite.model import MessageItem
def test_stress(cls): from queue_sqlite.mounter import task
TASK_NUM = 10000
scheduler = QueueScheduler(receive_thread_num=4, task_thread_num=8, shard_num=12)
scheduler.start_queue_scheduler() @task(meta={"name": "async_processor", "max_retries": 2})
async def async_data_fetcher(message_item):
# 发送大量任务 """异步数据获取任务"""
for i in range(TASK_NUM): url = message_item.content["url"]
message = MessageItem(content={"num": i}, destination="example") # 模拟异步 HTTP 请求
scheduler.send_message(message, cls._callback) await asyncio.sleep(0.5)
return {"url": url, "data": "fetched", "status": 200}
# 等待所有任务完成
while scheduler.queue_operation.get_queue_length() > 0:
time.sleep(0.5) async def main():
scheduler = QueueScheduler(scheduler_type="async")
scheduler.stop_queue_scheduler() scheduler.start()
# 发送异步任务
message = MessageItem(
content={"url": "https://api.example.com/data"},
destination="async_data_fetcher",
)
scheduler.send_message(message, lambda m: print(f"完成: {m.id}"))
await asyncio.sleep(5)
scheduler.stop()
asyncio.run(main())
``` ```
## 安装与运行 ### 数据监听示例
### 前提条件 ```python
Python 3.7+ from queue_sqlite import QueueScheduler
from queue_sqlite.mounter import listener
Rust 工具链 # 注册监听器
@listener()
def user_activity_log(data):
"""监听用户活动数据"""
print(f"用户活动: {data}")
SQLite 开发文件 @listener()
def system_alert(data):
"""监听系统告警"""
print(f"系统告警: {data}")
### 安装步骤 # 创建调度器
1. 克隆仓库: scheduler = QueueScheduler()
scheduler.start()
```bash # 更新监听数据(会自动触发监听函数)
git clone https://gitee.com/cai-xinpenge/queue_sqlite.git scheduler.update_listen_data("user_activity_log", "用户登录")
cd sqlite-task-queue scheduler.update_listen_data("user_activity_log", "用户购买")
``` scheduler.update_listen_data("system_alert", "CPU使用率过高")
```
2. 安装 Python 依赖: ## ⚙️ 配置选项
```bash ### 调度器配置
pip install -r requirements.txt
```
3. 构建 Rust 核心模块: ```python
from queue_sqlite import SchedulerConfig, QueueScheduler
```bash config = SchedulerConfig(
cd src/core receive_thread_num=2, # 接收线程数
maturin develop --release task_thread_num=8, # 任务执行线程数
``` shard_num=4, # 数据库分片数
将会在 `src/core/target/release` 目录下生成 `core.dll` 或 `core.so` 文件。 queue_name="production", # 队列名称
将该文件复制到 `queue_sqlite/core` 目录下(dll文件需改名为pyd后缀)。 meta={"app": "myapp"} # 自定义元数据
)
4. 运行测试: scheduler = QueueScheduler(
scheduler_type="standard", # standard | async | qt
config=config
)
```
```bash ### 消息配置
pytest tests/
```
5. 性能指标 ```python
from queue_sqlite import MessageItem
from queue_sqlite.constant import MessagePriority, MessageType
from datetime import datetime, timedelta
在标准开发机器上(8核CPU,16GB内存): message = MessageItem(
# 必需字段
content={"data": "任务数据"},
destination="task_function_name",
# 可选字段
id="custom-uuid", # 默认自动生成
type=MessageType.TASK,
priority=MessagePriority.HIGH,
source="web_api",
tags="urgent,processing",
# 时间控制
expire_time=datetime.now() + timedelta(hours=1), # 1小时后过期
retry_count=0,
# 自定义元数据
metadata={"user_id": 123, "request_id": "abc123"}
)
```
可处理 10,000+ 任务(斐波那契数列前500项计算)/分钟 ## 📊 系统架构
平均任务延迟 < 50ms ### 架构图
CPU 使用率 < 60% ```text
┌─────────────────────────────────────────────────────────┐
│ Python application |
│ ┌─────────────┐ ┌─────────────┐ ┌────────────────┐ │
│ │ @task │ │ @listener │ │ QueueScheduler │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Python Service │
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ TaskMounter │ │ TaskCycle │ │ Schedulers │ │
│ │ ListenMounter│ │ AsyncCycle │ │ │ │
│ └──────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ Rust core │
│ ┌─────────────────────────────────────────┐ │
│ │ queue_sqlite_core │ │
│ │ • shared sqlite database │ │
│ │ • SQLite Optimization │ │
│ │ • Connection pool │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌─────────────────────────────────────────────────────────┐
│ SQLite database │
│ ┌───────────────────────────────────────────────┐ │
│ │ shared database (cache/queue_name/) │ │
│ │ • queue_shard_0.db │ │
│ │ • queue_shard_1.db │ │
│ │ • listen.db │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
内存占用 < 500MB ### 组件说明
### 贡献指南 1. **MessageIte**: 核心数据模型,包含消息的所有属性和方法
2. **TaskMounter**: 任务过载器,通过装饰器注册任务函数
3. **ListenMounter**:监听挂载器,通过装饰器注册监听函数
4. **TaskCycle**:任务生命周期管理器,处理重试和状态更新
5. **QueueScheduler**:统一调度器接口,支持三种实现:
- **StandardQueueScheduler**:统一调度器接口,支持三种实现:
- **AsyncQueueScheduler**:异步/等待实现
- **QtQueueScheduler**:Qt 线程池实现(GUI应用)
6. **CleanupScheduler**:自动清理过期消息
7. **ShardedQueueOperation**:Rust 实现的高性能分片队列操作
欢迎贡献代码!请遵循以下步骤: ## 🧪 测试
1. Fork 仓库 ### 运行测试套件
创建新分支 (git checkout -b feature/your-feature) ```bash
# 运行所有测试
python -m -v -s pytest tests/
提交更改 (git commit -am 'Add some feature') # 运行特定测试
python -m pytest tests/test_stress.py -v
python -m pytest tests/test_async_scheduler.py -v
```
推送到分支 (git push origin feature/your-feature) ### 性能测试示例
2. 创建 Pull Request ```python
from tests.test_stress import TestStress
### 许可证 # 压力测试:处理 10000 个任务
TestStress.test_stress()
本项目采用 MIT 许可证。 # 异步调度器测试
from tests.test_async_scheduler import TestAsyncScheduler
TestAsyncScheduler.test_async_scheduler()
```
## 📈 性能指标
### 基准测试结果
| 指标 | 标准调度器 | 异步调度器 | Qt 调度器 |
| --- | ---------- | ---------- | --------- |
| 单核 QPS | 5,000+ | 8,000+ | 6,000+ |
| 内存占用 | 50-100MB | 60-120MB | 70-150MB |
| 延迟(p95) | <50ms | <30ms | <40ms |
| 最大并发 | 1,000+ | 2,000+ | 1500+ |
### 扩展性测试
- 10 分片:支持 50,000+ 并发任务
- 自动负载均衡:分片间任务均匀分布
- 线性扩展:增加分片数可线性提升吞吐量
## 📄 许可证
本项目采用 MIT 许可证 - 查看 LICENSE 文件了解详情。
## 📞 联系与支持
- **作者**: chakcy
- **邮箱**: 947105045@qq.com
## 🙏 致谢
感谢以下开源项目:
- [SQLite](https://www.sqlite.org/) - 轻量级嵌入式数据库
- [PyO3](https://pyo3.rs/) - Rust-Python 绑定
- [r2d2](https://github.com/sfackler/r2d2) - Rust 数据库连接池
---
**Queue SQLite** - 为您的应用提供可靠、高效的任务队列解决方案。
+19 -14
View File
@@ -6,22 +6,25 @@ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3",
"Topic :: System :: Distributed Computing", "Topic :: System :: Distributed Computing",
"Typing :: Typed", "Typing :: Typed",
"Operating System :: Microsoft :: Windows", "Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
] ]
dependencies = [] dependencies = [
"queue-sqlite-core>=0.2.0",
]
description = "A high-performance, SQLite-based distributed task queue system with Rust-backed core operations. Supports task mounting, message listening, priority handling, retry mechanisms, and automatic cleanup of expired messages. Ideal for building reliable, scalable background task processing systems." description = "A high-performance, SQLite-based distributed task queue system with Rust-backed core operations. Supports task mounting, message listening, priority handling, retry mechanisms, and automatic cleanup of expired messages. Ideal for building reliable, scalable background task processing systems."
name = "queue-sqlite" name = "queue-sqlite"
readme = "README.md" readme = "README.md"
requires-python = ">=3.7" requires-python = ">=3.11"
version = "0.1.0" version = "0.2.2"
[project.optional-dependencies] [project.optional-dependencies]
pyside6 = ["PySide6"] pyqt5 = ["qtpy", "PyQt5"]
pyqt6 = ["PyQt6"] pyqt6 = ["qtpy", "PyQt6"]
pyside6 = ["qtpy", "PySide6"]
[[tool.uv.index]] [[tool.uv.index]]
default = true default = true
@@ -33,17 +36,19 @@ requires = ["hatchling"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"maturin>=1.9.4", "maturin>=1.9.4",
"psutil>=7.0.0", "psutil>=7.0.0",
"pytest>=7.4.4", "pytest>=7.4.4",
"twine>=4.0.2", "twine>=4.0.2",
] ]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/queue_sqlite"] packages = ["src/queue_sqlite"]
platforms = ["windows_x86_64", "linux_x86_64"]
[tool.hatch.build] [tool.uv.workspace]
artifacts = [ members = [
"src/queue_sqlite/core/*.pyd", "src/queue_sqlite_core",
] ]
[tool.uv.sources]
queue-sqlite-core = { workspace = true }
+1
View File
@@ -1,5 +1,6 @@
import os import os
import shutil import shutil
# 需要遍历的目录 # 需要遍历的目录
root_dir = "./" root_dir = "./"
# 遍历目录 # 遍历目录
+15 -13
View File
@@ -1,27 +1,29 @@
import os import os
import sys import sys
def generate_markdown_from_py_files(directory, output_file): def generate_markdown_from_py_files(directory, output_file):
with open(output_file, 'w', encoding='utf-8') as md_file: with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory): for root, dirs, files in os.walk(directory):
# 排除 venv 目录 # 排除 venv 目录
dirs[:] = [d for d in dirs if d != '.venv'] dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != '.vscode'] dirs[:] = [d for d in dirs if d != ".vscode"]
dirs[:] = [d for d in dirs if d != 'scripts'] dirs[:] = [d for d in dirs if d != "scripts"]
dirs[:] = [d for d in dirs if d != 'build'] dirs[:] = [d for d in dirs if d != "build"]
for file in files: for file in files:
if file.endswith('.py') or file.endswith('.rs'): if file.endswith(".py") or file.endswith(".rs"):
file_path = os.path.join(root, file) file_path = os.path.join(root, file)
md_file.write(f'`{file_path}`\n') md_file.write(f"`{file_path}`\n")
md_file.write('```python\n') md_file.write("```python\n")
with open(file_path, 'r', encoding='utf-8') as py_file: with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read()) md_file.write(py_file.read())
md_file.write('\n```\n\n') md_file.write("\n```\n\n")
if __name__ == "__main__": if __name__ == "__main__":
# 指定目录和输出文件名 # 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录 target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = 'output.md' # 输出的 Markdown 文件名 output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file) generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f'Markdown 文件已生成:{output_markdown_file}') print(f"Markdown 文件已生成:{output_markdown_file}")
-507
View File
@@ -1,507 +0,0 @@
use chrono::{DateTime, Duration, Utc};
use pyo3::prelude::*;
use pyo3::types::*;
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::params;
#[pyclass]
pub struct QueueOperation {
pool: Pool<SqliteConnectionManager>,
}
#[pymethods]
impl QueueOperation {
#[new]
pub fn new(queue_path: String) -> Self {
let manager = SqliteConnectionManager::file(queue_path.as_str());
let pool = Pool::new(manager).unwrap();
QueueOperation { pool }
}
pub fn init_db(&self) -> PyResult<()> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
status INTEGER NOT NULL,
content TEXT NOT NULL,
createtime DATETIME NOT NULL,
updatetime DATETIME NOT NULL,
result TEXT DEFAULT NULL,
priority INTEGER NOT NULL,
source TEXT NOT NULL,
destination TEXT NOT NULL,
retry_count INTEGER NOT NULL,
expire_time DATETIME DEFAULT NULL,
tags TEXT DEFAULT NULL,
metadata TEXT DEFAULT NULL,
is_deleted INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_status ON messages(status);
CREATE INDEX IF NOT EXISTS idx_priority ON messages(priority);
CREATE INDEX IF NOT EXISTS idx_dequeue ON messages(
status,
priority DESC,
createtime ASC
) WHERE is_deleted = 0 AND status = 0;
",
params![],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let pragmas = [
"PRAGMA journal_mode=WAL",
"PRAGMA synchronous=NORMAL",
"PRAGMA cache_size=-20000",
"PRAGMA mmap_size=1073741824",
"PRAGMA temp_store=MEMORY",
"PRAGMA busy_timeout=5000",
];
for pragma in pragmas.iter() {
let mut stmt = conn
.prepare(pragma)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut rows = stmt
.query([])
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
while let Some(_) = rows
.next()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?
{
}
}
Ok(())
}
pub fn enqueue<'py>(
&self,
py: Python<'py>,
message: &Bound<'py, PyDict>,
) -> PyResult<Bound<'py, PyString>> {
let mut conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let get_field = |key: &str| -> PyResult<_> {
message.get_item(key)?.ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!("{} not found", key))
})
};
let id = get_field("id")?.extract::<String>()?;
let type_ = get_field("type")?.extract::<String>()?;
let status = get_field("status")?.extract::<i32>()?;
let content = get_field("content")?.extract::<String>()?;
let createtime = get_field("createtime")?.extract::<String>()?;
let updatetime = get_field("updatetime")?.extract::<String>()?;
let result = get_field("result")?.extract::<String>()?;
let priority = get_field("priority")?.extract::<i32>()?;
let source = get_field("source")?.extract::<String>()?;
let destination = get_field("destination")?.extract::<String>()?;
let retry_count = get_field("retry_count")?.extract::<i32>()?;
let expire_time = get_field("expire_time")?.extract::<String>()?;
let tags = get_field("tags")?.extract::<String>()?;
let metadata = get_field("metadata")?.extract::<String>()?;
let tx = conn
.transaction()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
tx.execute(
"
INSERT INTO messages (
id, type, status, content, createtime,
updatetime, result, priority, source,
destination, retry_count, expire_time, tags, metadata
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
",
params![
id,
type_,
status,
content,
createtime,
updatetime,
result,
priority,
source,
destination,
retry_count,
expire_time,
tags,
metadata
],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
tx.commit()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(Bound::from(PyString::new(py, &id)))
}
pub fn dequeue<'py>(&self, py: Python<'py>, size: i32) -> PyResult<Bound<'py, PyList>> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let now: DateTime<Utc> = Utc::now();
let iso_format = now.to_rfc3339();
// Fixed SQL query with proper subquery alias
let mut stmt = conn
.prepare(
"UPDATE messages
SET status = 1
WHERE id IN (
SELECT id
FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY priority DESC, createtime ASC) AS rn
FROM messages
WHERE is_deleted = 0
AND status = 0
AND (expire_time IS NULL OR expire_time > ?1)
) AS sub
WHERE rn <= ?2
)
RETURNING *;",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let rows = PyList::empty(py);
let mapped_rows = stmt
.query_map(params![iso_format, size], |row| {
let dict = PyDict::new(py);
// Helper to convert PyErr to rusqlite::Error
let to_rusqlite_error =
|e: PyErr| rusqlite::Error::ToSqlConversionFailure(Box::new(e));
// Extract and set each field with proper error handling
dict.set_item("id", row.get::<_, String>("id")?)
.map_err(to_rusqlite_error)?;
dict.set_item("type", row.get::<_, String>("type")?)
.map_err(to_rusqlite_error)?;
dict.set_item("status", row.get::<_, i32>("status")?)
.map_err(to_rusqlite_error)?;
dict.set_item("content", row.get::<_, String>("content")?)
.map_err(to_rusqlite_error)?;
dict.set_item("createtime", row.get::<_, String>("createtime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("updatetime", row.get::<_, String>("updatetime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("result", row.get::<_, Option<String>>("result")?)
.map_err(to_rusqlite_error)?;
dict.set_item("priority", row.get::<_, i32>("priority")?)
.map_err(to_rusqlite_error)?;
dict.set_item("source", row.get::<_, Option<String>>("source")?)
.map_err(to_rusqlite_error)?;
dict.set_item("destination", row.get::<_, Option<String>>("destination")?)
.map_err(to_rusqlite_error)?;
dict.set_item("retry_count", row.get::<_, i32>("retry_count")?)
.map_err(to_rusqlite_error)?;
dict.set_item("expire_time", row.get::<_, Option<String>>("expire_time")?)
.map_err(to_rusqlite_error)?;
dict.set_item("tags", row.get::<_, Option<String>>("tags")?)
.map_err(to_rusqlite_error)?;
dict.set_item("metadata", row.get::<_, Option<String>>("metadata")?)
.map_err(to_rusqlite_error)?;
// Return the dictionary as Py<PyDict>
Ok(dict)
})
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
for row in mapped_rows {
let dict =
row.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
rows.append(dict)?;
}
Ok(rows.into())
}
pub fn get_queue_length(&self) -> PyResult<i32> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let now = Utc::now().to_rfc3339();
// Using query_row for single result handling
let count: i32 = conn
.query_row(
"SELECT COUNT(*) FROM messages
WHERE is_deleted = 0
AND status = ?1
AND (expire_time IS NULL OR expire_time > ?2)",
params![0, now],
|row| row.get(0),
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(count)
}
pub fn get_completed_messages<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut stmt = conn
.prepare(
"SELECT
*
FROM
messages
WHERE
is_deleted = 0
AND (status = ?1 OR status = ?2)",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let rows = PyList::empty(py);
let mapped_rows = stmt
.query_map(params![2, 3], |row| {
let dict = PyDict::new(py);
// Helper to convert PyErr to rusqlite::Error
let to_rusqlite_error =
|e: PyErr| rusqlite::Error::ToSqlConversionFailure(Box::new(e));
// Extract and set each field with proper error handling
dict.set_item("id", row.get::<_, String>("id")?)
.map_err(to_rusqlite_error)?;
dict.set_item("type", row.get::<_, String>("type")?)
.map_err(to_rusqlite_error)?;
dict.set_item("status", row.get::<_, i32>("status")?)
.map_err(to_rusqlite_error)?;
dict.set_item("content", row.get::<_, String>("content")?)
.map_err(to_rusqlite_error)?;
dict.set_item("createtime", row.get::<_, String>("createtime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("updatetime", row.get::<_, String>("updatetime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("result", row.get::<_, Option<String>>("result")?)
.map_err(to_rusqlite_error)?;
dict.set_item("priority", row.get::<_, i32>("priority")?)
.map_err(to_rusqlite_error)?;
dict.set_item("source", row.get::<_, Option<String>>("source")?)
.map_err(to_rusqlite_error)?;
dict.set_item("destination", row.get::<_, Option<String>>("destination")?)
.map_err(to_rusqlite_error)?;
dict.set_item("retry_count", row.get::<_, i32>("retry_count")?)
.map_err(to_rusqlite_error)?;
dict.set_item("expire_time", row.get::<_, Option<String>>("expire_time")?)
.map_err(to_rusqlite_error)?;
dict.set_item("tags", row.get::<_, Option<String>>("tags")?)
.map_err(to_rusqlite_error)?;
dict.set_item("metadata", row.get::<_, Option<String>>("metadata")?)
.map_err(to_rusqlite_error)?;
// Return the dictionary as Py<PyDict>
Ok(dict)
})
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
for row in mapped_rows {
let dict =
row.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
rows.append(dict)?;
}
Ok(rows.into())
}
pub fn get_result<'py>(&self, py: Python<'py>, id: String) -> PyResult<Bound<'py, PyDict>> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut stmt = conn
.prepare(
"SELECT result, status FROM messages WHERE id = ?1 AND (status = ?2 OR status = ?3)",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut mapped_rows = stmt
.query_map(params![&id, 2, 3], |row| {
let dict = PyDict::new(py);
// Helper to convert PyErr to rusqlite::Error
let to_rusqlite_error =
|e: PyErr| rusqlite::Error::ToSqlConversionFailure(Box::new(e));
// Extract and set each field with proper error handling
dict.set_item("id", row.get::<_, String>("id")?)
.map_err(to_rusqlite_error)?;
dict.set_item("type", row.get::<_, String>("type")?)
.map_err(to_rusqlite_error)?;
dict.set_item("status", row.get::<_, i32>("status")?)
.map_err(to_rusqlite_error)?;
dict.set_item("content", row.get::<_, String>("content")?)
.map_err(to_rusqlite_error)?;
dict.set_item("createtime", row.get::<_, String>("createtime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("updatetime", row.get::<_, String>("updatetime")?)
.map_err(to_rusqlite_error)?;
dict.set_item("result", row.get::<_, Option<String>>("result")?)
.map_err(to_rusqlite_error)?;
dict.set_item("priority", row.get::<_, i32>("priority")?)
.map_err(to_rusqlite_error)?;
dict.set_item("source", row.get::<_, Option<String>>("source")?)
.map_err(to_rusqlite_error)?;
dict.set_item("destination", row.get::<_, Option<String>>("destination")?)
.map_err(to_rusqlite_error)?;
dict.set_item("retry_count", row.get::<_, i32>("retry_count")?)
.map_err(to_rusqlite_error)?;
dict.set_item("expire_time", row.get::<_, Option<String>>("expire_time")?)
.map_err(to_rusqlite_error)?;
dict.set_item("tags", row.get::<_, Option<String>>("tags")?)
.map_err(to_rusqlite_error)?;
dict.set_item("metadata", row.get::<_, Option<String>>("metadata")?)
.map_err(to_rusqlite_error)?;
// Return the dictionary as Py<PyDict>
Ok(dict)
})
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let dict = mapped_rows
.next()
.transpose()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(dict.unwrap().into())
}
pub fn update_status(&self, id: String, status: i32) -> PyResult<()> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
UPDATE messages SET status = ?1 WHERE id = ?2
",
params![status, id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
pub fn update_result(&self, id: String, result: String) -> PyResult<()> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
UPDATE messages SET result = ?1 WHERE id = ?2
",
params![result, id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
pub fn delete_message(&self, id: String) -> PyResult<()> {
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
UPDATE messages SET is_deleted = 1 WHERE id = ?1
",
params![id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
pub fn clean_expired_messages(&self) -> PyResult<()> {
let now: DateTime<Utc> = Utc::now();
let iso_format = now.to_rfc3339();
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
UPDATE messages
SET is_deleted = 1
WHERE is_deleted = 0
AND expire_time IS NOT NULL
AND expire_time < ?1
",
params![iso_format],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
pub fn clean_old_messages(&self, days: i64) -> PyResult<()> {
// 获取七天前的时间
let now: DateTime<Utc> = Utc::now();
let mut old_time = now - Duration::days(days);
let iso_format = old_time.to_rfc3339();
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
UPDATE messages
SET is_deleted = 1
WHERE is_deleted = 0
AND status IN (?1, ?2)
AND updatetime < ?3
",
params![2, 3, iso_format],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 17:05:40
@Author : chakcy
@Email : 947105045@qq.com
@License : (C)Copyright 2020-2025, chakcy
@package : queue_sqlite
@description : A high-performance, SQLite-based distributed task queue system with Rust-backed core operations. Supports task mounting, message listening, priority handling, retry mechanisms, and automatic cleanup of expired messages. Ideal for building reliable, scalable background task processing systems.
"""
from . import constant
from . import cycle
from . import model
from . import mounter
from . import queue_operation
from . import scheduler
import logging
import os
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
# 设置 error 级别日志写入 error.log 文件
if not os.path.exists("log"):
os.mkdir("log")
error_log_handler = logging.FileHandler("log/error.log", encoding="utf-8")
error_log_handler.setLevel(logging.ERROR)
error_log_handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
)
logging.getLogger().addHandler(error_log_handler)
__title__ = "queue_sqlite"
__version__ = "0.2.2"
__author__ = "chakcy"
__email__ = "947105045@qq.com"
__license__ = "MIT"
__copyright__ = "Copyright 2020-2025, chakcy"
__all__ = ["constant", "cycle", "model", "mounter", "queue_operation", "scheduler"]
__version_info__ = tuple(int(i) for i in __version__.split("."))
__description__ = "A high-performance, SQLite-based distributed task queue system with Rust-backed core operations. Supports task mounting, message listening, priority handling, retry mechanisms, and automatic cleanup of expired messages. Ideal for building reliable, scalable background task processing systems."
def get_info():
return {
"name": __title__,
"version": __version__,
"author": __author__,
"email": __email__,
"license": __license__,
"copyright": __copyright__,
"description": __description__,
}
+12 -1
View File
@@ -1,5 +1,16 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 16:56:34
@Author : chakcy
@Email : 947105045@qq.com
@description : 常量模块
"""
from .message_priority import MessagePriority from .message_priority import MessagePriority
from .message_status import MessageStatus from .message_status import MessageStatus
from .message_type import MessageType from .message_type import MessageType
__all__ = ["MessagePriority", "MessageStatus", "MessageType"] __all__ = ["MessagePriority", "MessageStatus", "MessageType"]
@@ -1,3 +1,14 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : message_priority.py
@Time : 2025-09-27 16:56:58
@Author : chakcy
@Email : 947105045@qq.com
@description : 消息优先级枚举类
"""
from enum import IntEnum from enum import IntEnum
+11 -1
View File
@@ -1,3 +1,14 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : message_status.py
@Time : 2025-09-27 16:57:25
@Author : chakcy
@Email : 947105045@qq.com
@description : 消息状态枚举类
"""
from enum import IntEnum from enum import IntEnum
@@ -7,4 +18,3 @@ class MessageStatus(IntEnum):
COMPLETED = 2 COMPLETED = 2
FAILED = 3 FAILED = 3
RETRYING = 4 RETRYING = 4
+12 -2
View File
@@ -1,6 +1,16 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : message_type.py
@Time : 2025-09-27 16:57:58
@Author : chakcy
@Email : 947105045@qq.com
@description : 消息类型枚举类
"""
from enum import Enum from enum import Enum
class MessageType(Enum): class MessageType(Enum):
TASK = "task" # 任务 TASK = "task" # 任务
LISTEN = "listen" # 监听 LISTEN = "listen" # 监听
-51
View File
@@ -1,51 +0,0 @@
from ..mounter import TaskMounter
from typing import Callable, List
from ..constant import MessageStatus
class core:
class QueueOperation:
def __init__(self, db_path: str):
"""
Initialize the queue operation.
Args:
db_path (str): The path to the database file.
"""
...
def init_db(self):
"""
Initialize the database.
"""
...
def enqueue(self, message_dict: dict) -> str:
"""
Enqueue a message.
Args:
message_dict (dict): The message to be enqueued.
Returns:
str: The message id.
Raises:
ValueError: If the message is not valid.
"""
...
def dequeue(self, size: int = 1) -> List[dict]: ...
def get_queue_length(self) -> int: ...
def get_completed_messages(self) -> List[dict]: ...
def get_result(self, message_id: str): ...
def update_status(self, message_id: str, status: MessageStatus): ...
def update_result(self, message_id: str, result: str): ...
def delete_message(self, message_id: str): ...
def clean_old_messages(self, days: int): ...
def clean_expired_messages(self): ...
class TaskMounter:
def __init__(self, task_class: type[TaskMounter]): ...
def get_task_list(self) -> List[str]: ...
def get_task_function(self, name: str) -> Callable: ...
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 16:58:34
@Author : chakcy
@Email : 947105045@qq.com
@description : 任务周期模块
"""
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : async_task_cycle.py
@Time : 2025-09-27 16:59:17
@Author : chakcy
@Email : 947105045@qq.com
@description : 异步任务周期类
"""
from ..model import MessageItem
from ..constant import MessageStatus
from ..mounter.task_mounter import TaskMeta
import json
import asyncio
import functools
def retry_async(max_retries=3):
"""
异步重试装饰器
Args:
max_retries: 最大重试次数
delay: 重试间隔(秒)
"""
def decorator(func):
@functools.wraps(func)
async def wrapper(self, *args, **kwargs):
last_exception = None
# 使用message_item中的retry_count作为最大重试次数
task_meta: TaskMeta = self.callback.meta
retries = task_meta.max_retries
# retries = getattr(self.message_item, "retry_count", max_retries)
delay_time = task_meta.delay
# 至少尝试一次(retries+1),最多尝试max_retries+1次
actual_retries = min(retries, max_retries) if max_retries > 0 else retries
for attempt in range(actual_retries + 1):
try:
return await func(self, *args, **kwargs)
except Exception as e:
last_exception = e
if attempt < actual_retries:
await asyncio.sleep(delay_time)
else:
break
if last_exception:
raise last_exception
else:
raise Exception("Unknown error occurred during async task execution")
return wrapper
return decorator
class AsyncTaskCycle:
def __init__(self, message_item: MessageItem, callback):
self.message_item = message_item
self.callback = callback
self.task_result = None
self.task_status = None
self.task_error = None
@retry_async(max_retries=3)
async def run(self):
try:
task_result = await self.callback(self.message_item) # type: ignore
except Exception as e:
self.task_result = None
self.task_status = MessageStatus.FAILED
self.task_error = str(e)
else:
self.task_result = task_result
self.task_status = MessageStatus.COMPLETED
self.task_error = None
def get_task_result(self):
"""获取任务结果 - 优化版本"""
if self.task_result is None:
return json.dumps({"result": None})
# 如果已经是字符串,尝试解析
if isinstance(self.task_result, str):
try:
# 如果是 JSON 字符串,直接返回
json.loads(self.task_result)
return self.task_result
except:
# 如果不是 JSON,包装成 JSON
return json.dumps({"result": self.task_result})
# 如果是 MessageItem 对象,提取其中的 result 字段
if isinstance(self.task_result, MessageItem):
result_data = self.task_result.result
if isinstance(result_data, (dict, list)):
return json.dumps(result_data)
else:
return json.dumps({"result": result_data})
# 其他情况正常序列化
try:
return json.dumps(self.task_result)
except:
return json.dumps({"result": str(self.task_result)})
def get_task_status(self):
return self.task_status
def get_task_error(self):
return self.task_error
def get_task_message_item(self):
return self.message_item
def get_task_callback(self):
return self.callback
+86 -19
View File
@@ -1,8 +1,67 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : task_cycle.py
@Time : 2025-09-27 16:59:31
@Author : chakcy
@Email : 947105045@qq.com
@description : 任务周期模块
"""
from ..model import MessageItem from ..model import MessageItem
from typing import Callable, Optional from typing import Callable, Optional
from ..constant import MessageStatus from ..constant import MessageStatus
from ..mounter.task_mounter import TaskMeta
import json import json
import functools
import time
def retry_sync(max_retries=3):
"""
同步重试装饰器
Args:
max_retries: 最大重试次数
delay: 重试间隔(秒)
"""
def decorator(func):
@functools.wraps(func)
def wrapper(self, *args, **kwargs):
last_exception = None
# 使用message_item中的retry_count作为最大重试次数
task_meta: TaskMeta = self.callback.meta
retries = task_meta.max_retries
# retries = getattr(self.message_item, "retry_count", max_retries)
delay_time = task_meta.delay
# 至少尝试一次(retries+1),最多尝试max_retries+1次
actual_retries = min(retries, max_retries) if max_retries > 0 else retries
for attempt in range(actual_retries + 1):
try:
return func(self, *args, **kwargs)
except Exception as e:
last_exception = e
self.message_item.retry_count = attempt
if attempt < actual_retries:
time.sleep(delay_time)
else:
break
# 如果没有捕获到异常,则创建一个新的异常
if last_exception is not None:
raise last_exception
else:
raise Exception("Unknown error occurred during sync task execution")
return wrapper
return decorator
class TaskCycle: class TaskCycle:
def __init__(self, message_item: MessageItem, callback: Optional[Callable]): def __init__(self, message_item: MessageItem, callback: Optional[Callable]):
self.message_item = message_item self.message_item = message_item
@@ -11,48 +70,56 @@ class TaskCycle:
self.task_status = None self.task_status = None
self.task_error = None self.task_error = None
@retry_sync(max_retries=3)
def run(self): def run(self):
try: try:
task_result = self.callback(self.message_item) # type: ignore task_result = self.callback(self.message_item) # type: ignore
except Exception as e: except Exception as e:
self.task_result = None self.task_result = None
self.task_status = MessageStatus.FAILED self.task_status = MessageStatus.FAILED
self.task_error = str(e) self.task_error = str(e)
else: else:
self.task_result = task_result # type: ignore self.task_result = task_result # type: ignore
self.task_status = MessageStatus.COMPLETED self.task_status = MessageStatus.COMPLETED
self.task_error = None self.task_error = None
def get_task_result(self): def get_task_result(self):
if isinstance(self.task_result, (dict, list)): """获取任务结果 - 优化版本"""
try: if self.task_result is None:
return json.dumps(self.task_result) return json.dumps({"result": None})
except:
return json.dumps({'result': str(self.task_result)})
elif isinstance(self.task_result, str): # 如果已经是字符串,尝试解析
if isinstance(self.task_result, str):
try: try:
# 如果是 JSON 字符串,直接返回
json.loads(self.task_result) json.loads(self.task_result)
return self.task_result return self.task_result
except: except:
return json.dumps({'result': self.task_result}) # 如果不是 JSON,包装成 JSON
elif isinstance(self.task_result, (int, float, bool)): return json.dumps({"result": self.task_result})
return json.dumps({'result': self.task_result})
elif self.task_result is None: # 如果是 MessageItem 对象,提取其中的 result 字段
return 'null' if isinstance(self.task_result, MessageItem):
else: result_data = self.task_result.result
return json.dumps({'result': str(self.task_result)}) if isinstance(result_data, (dict, list)):
return json.dumps(result_data)
else:
return json.dumps({"result": result_data})
# 其他情况正常序列化
try:
return json.dumps(self.task_result)
except:
return json.dumps({"result": str(self.task_result)})
def get_task_status(self): def get_task_status(self):
return self.task_status return self.task_status
def get_task_error(self): def get_task_error(self):
return self.task_error return self.task_error
def get_task_message_item(self): def get_task_message_item(self):
return self.message_item return self.message_item
def get_task_callback(self): def get_task_callback(self):
return self.callback return self.callback
+13 -1
View File
@@ -1,4 +1,16 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 16:59:58
@Author : chakcy
@Email : 947105045@qq.com
@description : 模型模块
"""
from .message_item import MessageItem from .message_item import MessageItem
from .scheduler_config import SchedulerConfig
__all__ = ["MessageItem"] __all__ = ["MessageItem", "SchedulerConfig"]
+62 -14
View File
@@ -1,3 +1,13 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : message_item.py
@Time : 2025-09-27 17:00:27
@Author : chakcy
@Email : 947105045@qq.com
@description : 消息数据模型
"""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from datetime import datetime from datetime import datetime
@@ -5,12 +15,13 @@ from ..constant import MessageStatus, MessagePriority, MessageType
import json import json
from enum import Enum from enum import Enum
import uuid import uuid
import logging
@dataclass @dataclass
class MessageItem: class MessageItem:
# 必需字段 # 必需字段
content: dict = field( content: dict = field( # type ignore
metadata={"description": "消息内容,包含具体的任务数据或信息"} metadata={"description": "消息内容,包含具体的任务数据或信息"}
) )
@@ -70,7 +81,7 @@ class MessageItem:
) )
@classmethod @classmethod
def from_dict(cls, data: Dict[str, Any]) -> "MessageItem": def from_dict(cls, data: dict) -> "MessageItem":
"""从字典创建消息对象""" """从字典创建消息对象"""
# 处理日期时间字段 # 处理日期时间字段
datetime_fields = ["createtime", "updatetime", "expire_time"] datetime_fields = ["createtime", "updatetime", "expire_time"]
@@ -115,7 +126,13 @@ class MessageItem:
# 处理枚举字段 # 处理枚举字段
if "type" in data: if "type" in data:
if isinstance(data["type"], str): if isinstance(data["type"], str):
data["type"] = MessageType(data["type"]) # 使用枚举名称 # 根据字符串值查找对应的枚举成员
for member in MessageType:
if member.value == data["type"]:
data["type"] = member
break
else:
raise ValueError(f"Invalid message type: {data['type']}")
elif isinstance(data["type"], MessageType): elif isinstance(data["type"], MessageType):
pass # 已经是枚举类型 pass # 已经是枚举类型
else: else:
@@ -123,7 +140,13 @@ class MessageItem:
if "status" in data: if "status" in data:
if isinstance(data["status"], str): if isinstance(data["status"], str):
data["status"] = MessageStatus(data["status"]) # 使用枚举名称 # 尝试将字符串转换为整数,如果失败则使用字符串作为枚举名称
try:
data["status"] = MessageStatus(
int(data["status"])
) # 尝试作为枚举值
except ValueError:
data["status"] = MessageStatus(data["status"]) # 使用枚举名称
elif isinstance(data["status"], int): elif isinstance(data["status"], int):
data["status"] = MessageStatus(data["status"]) # 使用枚举值 data["status"] = MessageStatus(data["status"]) # 使用枚举值
elif isinstance(data["status"], MessageStatus): elif isinstance(data["status"], MessageStatus):
@@ -132,13 +155,38 @@ class MessageItem:
raise ValueError(f"Invalid message status: {data['status']}") raise ValueError(f"Invalid message status: {data['status']}")
if "priority" in data: if "priority" in data:
if isinstance(data["priority"], (int, str)): if isinstance(data["priority"], str):
data["priority"] = MessagePriority(data["priority"]) # type: ignore # 尝试将字符串转换为整数,如果失败则使用字符串作为枚举名称
try:
data["priority"] = MessagePriority(
int(data["priority"])
) # 尝试作为枚举值
except ValueError:
# 如果转换失败,尝试通过枚举名称查找
for member in MessagePriority:
if member.name == data["priority"]:
data["priority"] = member
break
else:
raise ValueError(
f"Invalid message priority: {data['priority']}"
)
elif isinstance(data["priority"], int):
data["priority"] = MessagePriority(data["priority"]) # 使用枚举值
elif isinstance(data["priority"], MessagePriority): elif isinstance(data["priority"], MessagePriority):
pass pass # 已经是枚举类型
else: else:
raise ValueError("请使用 'int' 或 'str' 类型设置 priority 字段") raise ValueError("请使用 'int' 或 'str' 类型设置 priority 字段")
if "retry_count" in data:
if isinstance(data["retry_count"], str):
if data["retry_count"].isdigit():
data["retry_count"] = int(data["retry_count"])
else:
raise ValueError("请使用 'int' 类型设置 retry_count 字段")
elif isinstance(data["retry_count"], int):
pass # 已经是整数类型
return cls(**data) return cls(**data)
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
@@ -235,13 +283,13 @@ class MessageItem:
"""打印所有字段的信息,包括类型、默认值和描述""" """打印所有字段的信息,包括类型、默认值和描述"""
from dataclasses import fields from dataclasses import fields
print(f"=== {cls.__name__} 字段信息 ===") logging.info(f"=== {cls.__name__} 字段信息 ===")
for field_info in fields(cls): for field_info in fields(cls):
print(f"字段名: {field_info.name}") logging.info(f"字段名: {field_info.name}")
print(f"类型: {field_info.type}") logging.info(f"类型: {field_info.type}")
print(f"默认值: {field_info.default}") logging.info(f"默认值: {field_info.default}")
if field_info.default_factory is not None: if field_info.default_factory is not None:
print(f"默认工厂: {field_info.default_factory}") logging.info(f"默认工厂: {field_info.default_factory}")
if "description" in field_info.metadata: if "description" in field_info.metadata:
print(f"描述: {field_info.metadata['description']}") logging.info(f"描述: {field_info.metadata['description']}")
print("-" * 50) logging.info("-" * 50)
@@ -0,0 +1,68 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : scheduler_config.py
@Time : 2025-10-28 10:21:21
@Author : chakcy
@Email : 947105045@qq.com
@description : 队列调度器配置
"""
from dataclasses import dataclass, field
from typing import Dict, Any
import logging
@dataclass
class SchedulerConfig:
receive_thread_num: int = field(
default=1,
metadata={"help": "接收消息的线程数"},
)
task_thread_num: int = field(
default=4,
metadata={"help": "任务执行线程数"},
)
shard_num: int = field(
default=4,
metadata={"help": "分片数"},
)
queue_name: str = field(
default="default",
metadata={"help": "队列名称"},
)
meta: Dict[str, Any] = field(
default_factory=dict,
metadata={"help": "队列元数据"},
)
@classmethod
def get_field_descriptions(cls) -> Dict[str, str]:
"""获取所有字段的描述信息
Returns:
Dict[str, str]: 字段名到描述信息的映射
"""
from dataclasses import fields
descriptions = {}
for field_info in fields(cls):
if "help" in field_info.metadata:
descriptions[field_info.name] = field_info.metadata["help"]
else:
descriptions[field_info.name] = "无描述信息"
return descriptions
@classmethod
def print_field_info(cls):
"""打印所有字段的信息,包括类型、默认值和描述"""
from dataclasses import fields
logging.info(f"=== {cls.__name__} 字段信息 ===")
for field_info in fields(cls):
logging.info(
f"{field_info.name}: {field_info.type} (默认值: {field_info.default})"
)
logging.info(f" 描述: {field_info.metadata.get('help', '无描述信息')}")
logging.info("-" * 50)
+14 -3
View File
@@ -1,4 +1,15 @@
from .task_mounter import TaskMounter #!/usr/bin/env python
from .listen_mounter import ListenMounter # -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 17:00:47
@Author : chakcy
@Email : 947105045@qq.com
@description : 挂载器模块
"""
__all__ = ["TaskMounter", "ListenMounter"]
from .task_mounter import task
from .listen_mounter import listener
__all__ = ["task", "listener"]
+30 -8
View File
@@ -1,3 +1,14 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : listen_mounter.py
@Time : 2025-09-27 17:01:23
@Author : chakcy
@Email : 947105045@qq.com
@description : 监听装饰器
"""
from typing import Callable, List from typing import Callable, List
@@ -5,16 +16,18 @@ class ListenMounter:
@classmethod @classmethod
def mount_Listener(cls, function: Callable): def mount_Listener(cls, function: Callable):
setattr(cls, function.__name__, function) setattr(cls, function.__name__, function)
@staticmethod @staticmethod
def listener(): def listener():
"""带参数的装饰器""" """带参数的装饰器"""
def decorator(function: Callable): def decorator(function: Callable):
# 使用自定义名称或函数原名 # 使用自定义名称或函数原名
setattr(ListenMounter, function.__name__, function) setattr(ListenMounter, function.__name__, function)
return function return function
return decorator return decorator
@classmethod @classmethod
def get_Listener_function(cls, name: str): def get_Listener_function(cls, name: str):
return getattr(cls, name, None) return getattr(cls, name, None)
@@ -23,7 +36,7 @@ class ListenMounter:
def get_Listener_list(cls) -> List[str]: def get_Listener_list(cls) -> List[str]:
"""获取所有挂载的监听函数名称列表""" """获取所有挂载的监听函数名称列表"""
listener_list = [] listener_list = []
# 遍历类属性字典 # 遍历类属性字典
for attr_name, attr_value in vars(cls).items(): for attr_name, attr_value in vars(cls).items():
# 过滤条件: # 过滤条件:
@@ -31,11 +44,20 @@ class ListenMounter:
# 2. 不是类自带的特殊方法(非双下划线开头) # 2. 不是类自带的特殊方法(非双下划线开头)
# 3. 不是类方法本身(如 mount_Listener, listener 等) # 3. 不是类方法本身(如 mount_Listener, listener 等)
if ( if (
callable(attr_value) callable(attr_value)
and not attr_name.startswith("__") and not attr_name.startswith("__")
and attr_name not in ["mount_Listener", "listener", "get_Listener_function", "get_Listener_list"] and attr_name
not in [
"mount_Listener",
"listener",
"get_Listener_function",
"get_Listener_list",
]
): ):
listener_list.append(attr_name) listener_list.append(attr_name)
return listener_list return listener_list
def listener():
return ListenMounter.listener()
+58 -9
View File
@@ -1,30 +1,74 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : task_mounter.py
@Time : 2025-09-27 17:01:38
@Author : chakcy
@Email : 947105045@qq.com
@description : 任务挂载器
"""
from typing import Callable, List from typing import Callable, List
from dataclasses import dataclass, field
@dataclass
class TaskMeta:
name: str = field(default="", metadata={"help": "任务名称"})
description: str = field(default="", metadata={"help": "任务描述"})
max_retries: int = field(default=3, metadata={"help": "任务最大重试次数"})
delay: float = field(default=1, metadata={"help": "任务延迟执行时间"})
other: dict = field(default_factory=dict, metadata={"help": "其他参数"})
class TaskMounter: class TaskMounter:
@classmethod @classmethod
def mount_task(cls, function: Callable): def mount_task(cls, function: Callable):
setattr(cls, function.__name__, function) setattr(cls, function.__name__, function)
@staticmethod @staticmethod
def task(meta: dict = {}): def task(meta: dict = {}):
"""带参数的装饰器""" """带参数的装饰器"""
def decorator(function: Callable): def decorator(function: Callable):
# 使用自定义名称或函数原名 # 使用自定义名称或函数原名
function.meta = meta # type: ignore name = meta.get("name", function.__name__)
description = meta.get("description", "")
max_retries = meta.get("max_retries", 3)
delay = meta.get("delay", 1)
meta_other = {}
for key, value in meta.items():
if key not in TaskMeta.__annotations__.keys():
meta_other[key] = value
task_meta = TaskMeta(
name=name,
description=description,
max_retries=max_retries,
delay=delay,
other=meta_other,
)
function.meta = task_meta # type: ignore
setattr(TaskMounter, function.__name__, function) setattr(TaskMounter, function.__name__, function)
return function return function
return decorator return decorator
@classmethod @classmethod
def get_task_function(cls, name: str): def get_task_function(cls, name: str):
return getattr(cls, name, None) return getattr(cls, name, None)
@classmethod
def get_task_meta(cls, task_name: str):
task_function = cls.get_task_function(task_name)
if task_function:
return getattr(task_function, "meta", {})
@classmethod @classmethod
def get_task_list(cls) -> List[str]: def get_task_list(cls) -> List[str]:
"""获取所有挂载的任务函数名称列表""" """获取所有挂载的任务函数名称列表"""
task_list = [] task_list = []
# 遍历类属性字典 # 遍历类属性字典
for attr_name, attr_value in vars(cls).items(): for attr_name, attr_value in vars(cls).items():
# 过滤条件: # 过滤条件:
@@ -32,11 +76,16 @@ class TaskMounter:
# 2. 不是类自带的特殊方法(非双下划线开头) # 2. 不是类自带的特殊方法(非双下划线开头)
# 3. 不是类方法本身(如 mount_task, get_task_list 等) # 3. 不是类方法本身(如 mount_task, get_task_list 等)
if ( if (
callable(attr_value) callable(attr_value)
and not attr_name.startswith("__") and not attr_name.startswith("__")
and attr_name not in ["mount_task", "task", "get_task_function", "get_task_list"] and attr_name
not in ["mount_task", "task", "get_task_function", "get_task_list"]
): ):
task_list.append(attr_name) task_list.append(attr_name)
return task_list return task_list
# task 装饰器
def task(meta: dict = {}):
return TaskMounter.task(meta)
+9 -3
View File
@@ -1,3 +1,9 @@
from .queue_operation import QueueOperation #!/usr/bin/env python
# -*- encoding: utf-8 -*-
__all__ = ["QueueOperation"] """
@File : __init__.py
@Time : 2025-09-27 17:01:59
@Author : chakcy
@Email : 947105045@qq.com
@description : 队列操作模块
"""
@@ -1,6 +1,18 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : listen_operation.py
@Time : 2025-09-27 17:02:18
@Author : chakcy
@Email : 947105045@qq.com
@description : 监听操作模块 - 改进版,优化数据库连接
"""
from ..mounter.listen_mounter import ListenMounter from ..mounter.listen_mounter import ListenMounter
import sqlite3 import sqlite3
from typing import List, Tuple, Union from typing import List, Tuple, Union
import logging
class ListenOperation: class ListenOperation:
@@ -10,106 +22,167 @@ class ListenOperation:
self.create_table() self.create_table()
def _get_connection(self): def _get_connection(self):
"""获取数据库连接""" """获取数据库连接 - 改进版"""
return sqlite3.connect( conn = sqlite3.connect(
self.db_dir, self.db_dir, check_same_thread=False, timeout=30.0 # 增加超时时间
check_same_thread=False,
) )
# 设置SQLite优化参数
conn.execute("PRAGMA journal_mode=WAL;") # 使用WAL模式提高并发
conn.execute("PRAGMA synchronous=NORMAL;")
conn.execute("PRAGMA cache_size=-20000;")
conn.execute("PRAGMA busy_timeout=5000;") # 设置忙时超时
conn.execute("PRAGMA mmap_size=268435456;") # 256MB内存映射
return conn
def create_table(self): def create_table(self):
if len(self.listen_fields) == 0: if len(self.listen_fields) == 0:
return return
sql = f"""
CREATE TABLE IF NOT EXISTS listen_table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key Text,
value JSON
);
CREATE TABLE IF NOT EXISTS change_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
row_id INTEGER,
column_name TEXT,
old_value TEXT,
new_value TEXT,
is_delete integer DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER IF NOT EXISTS track_value_change
AFTER UPDATE OF value ON listen_table -- 监听特定列
FOR EACH ROW
WHEN OLD.value <> NEW.value -- 仅当值实际变化时触发
BEGIN
INSERT INTO change_log (table_name, row_id, column_name, old_value, new_value)
VALUES ('listen_table', NEW.id, 'key', OLD.key, NEW.key);
END;
"""
conn = self._get_connection() conn = self._get_connection()
conn.executescript(sql)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA synchronous=NORMAL;")
conn.execute("PRAGMA cache_size=-20000;")
conn.execute("PRAGMA mmap_size=1073741824;")
conn.execute("PRAGMA temp_store=MEMORY;")
conn.execute("PRAGMA busy_timeout=5000;")
conn.commit()
# 删除原有数据 try:
# sql = f""" # 分别执行每个SQL语句而不是使用executescript
# DELETE FROM listen_table conn.execute(
# """ """
# conn.execute(sql) CREATE TABLE IF NOT EXISTS listen_table (
# conn.commit() id INTEGER PRIMARY KEY AUTOINCREMENT,
key Text,
for listen_field in self.listen_fields: value JSON
sql = f""" )
INSERT INTO
listen_table (key, value)
VALUES
(?, ?)
""" """
conn.execute(sql, (listen_field, "null")) )
conn.execute(
"""
CREATE TABLE IF NOT EXISTS change_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
row_id INTEGER,
column_name TEXT,
old_value TEXT,
new_value TEXT,
is_delete integer DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)
# 检查触发器是否已存在,如果不存在则创建
try:
conn.execute(
"""
CREATE TRIGGER IF NOT EXISTS track_value_change
AFTER UPDATE OF value ON listen_table
FOR EACH ROW
WHEN OLD.value <> NEW.value
BEGIN
INSERT INTO change_log (table_name, row_id, column_name, old_value, new_value)
VALUES ('listen_table', NEW.id, OLD.key, OLD.value, NEW.value);
END
"""
)
except sqlite3.Error:
# 如果触发器创建失败,我们继续执行其他操作
pass
conn.commit() conn.commit()
for listen_field in self.listen_fields:
# 检查是否已存在该key
cursor = conn.execute(
"SELECT id FROM listen_table WHERE key = ?", (listen_field,)
)
if cursor.fetchone() is None:
sql = """
INSERT INTO
listen_table (key, value)
VALUES
(?, ?)
"""
conn.execute(sql, (listen_field, "null"))
conn.commit()
except Exception as e:
logging.error(f"创建监听表失败: {str(e)}")
finally:
conn.close()
def listen_data(self) -> Tuple[bool, Union[List[Tuple], str]]: def listen_data(self) -> Tuple[bool, Union[List[Tuple], str]]:
sql = f""" """获取监听数据 - 改进版"""
SELECT * FROM change_log where is_delete = 0 ORDER BY id DESC LIMIT 100
"""
conn = self._get_connection() conn = self._get_connection()
result = conn.execute(sql).fetchall() try:
if len(result) == 0: sql = """
return False, "No data found" SELECT * FROM change_log where is_delete = 0 ORDER BY id DESC LIMIT 100
return True, result """
result = conn.execute(sql).fetchall()
if len(result) == 0:
return False, "No data found"
return True, result
except Exception as e:
logging.error(f"获取监听数据失败: {str(e)}")
return False, str(e)
finally:
conn.close()
def delete_change_log(self, delete_id): def delete_change_log(self, delete_id):
sql = f""" """删除变更日志 - 改进版"""
DELETE FROM change_log WHERE id = {delete_id}
"""
conn = self._get_connection() conn = self._get_connection()
conn.execute(sql) try:
sql = """
DELETE FROM change_log WHERE id = ?
"""
conn.execute(sql, (delete_id,))
conn.commit()
except Exception as e:
logging.error(f"删除变更日志失败 {delete_id}: {str(e)}")
raise e # 重新抛出异常以便上层处理
finally:
conn.close()
def update_listen_data(self, key, value): def update_listen_data(self, key, value):
sql = f""" """更新监听数据 - 改进版"""
UPDATE listen_table SET value = '{value}' WHERE key = '{key}'
"""
conn = self._get_connection() conn = self._get_connection()
conn.execute(sql) try:
conn.commit() sql = """
UPDATE listen_table SET value = ? WHERE key = ?
"""
conn.execute(sql, (value, key))
conn.commit()
except Exception as e:
logging.error(f"更新监听数据失败 {key}: {str(e)}")
raise e
finally:
conn.close()
def get_value(self, key): def get_value(self, key):
sql = f""" """获取值 - 改进版"""
SELECT value FROM listen_table WHERE key = '{key}'
"""
conn = self._get_connection() conn = self._get_connection()
result = conn.execute(sql).fetchone() try:
if result is None: sql = """
SELECT value FROM listen_table WHERE key = ?
"""
result = conn.execute(sql, (key,)).fetchone()
if result is None:
return None
return result[0]
except Exception as e:
logging.error(f"获取值失败 {key}: {str(e)}")
return None return None
return result[0] finally:
conn.close()
def get_values(self): def get_values(self):
sql = f""" """获取所有值 - 改进版"""
SELECT key, value FROM listen_table
"""
conn = self._get_connection() conn = self._get_connection()
result = conn.execute(sql).fetchall() try:
return result sql = """
SELECT key, value FROM listen_table
"""
result = conn.execute(sql).fetchall()
return result
except Exception as e:
logging.error(f"获取所有值失败: {str(e)}")
return []
finally:
conn.close()
@@ -1,122 +0,0 @@
import os
import hashlib
from typing import Tuple, Union, List
import threading
import random
from ..core import core
from ..constant import MessageStatus
class QueueOperation:
def __init__(self, shard_num: int = 4, queue_name: str = "default"):
self.shard_num = shard_num
self.db_dir = os.path.join("cache", queue_name)
self.shard_connections = threading.local()
if not os.path.exists(self.db_dir):
os.makedirs(self.db_dir)
self.init_shards()
def _get_shard_index(self, message_id: str) -> int:
"""计算消息的分片索引"""
hash_obj = hashlib.sha256(message_id.encode())
return int(hash_obj.hexdigest(), 16) % self.shard_num
def _get_shard_path(self, shard_index: int) -> str:
"""获取分片数据库路径"""
return os.path.join(self.db_dir, f"queue_shard_{shard_index}.db")
def _get_shard_conn(self, shard_index: int) -> core.QueueOperation:
"""获取分片数据库连接(线程安全)"""
if not hasattr(self.shard_connections, "shards"):
self.shard_connections.shards = {}
if shard_index not in self.shard_connections.shards:
db_path = self._get_shard_path(shard_index)
conn = core.QueueOperation(db_path)
self.shard_connections.shards[shard_index] = conn
return self.shard_connections.shards[shard_index]
def init_shards(self):
"""初始化分片数据库"""
for i in range(self.shard_num):
conn = self._get_shard_conn(i)
conn.init_db()
# 入队
def enqueue(self, message: dict) -> str:
shard_index = self._get_shard_index(message["id"])
conn = self._get_shard_conn(shard_index)
conn.enqueue(message)
return message["id"]
def dequeue(self, size: int = 1) -> List[dict]:
messages = []
collected = 0
# 随机轮询分片顺序
shard_order = list(range(self.shard_num))
random.shuffle(shard_order)
for shard_index in shard_order:
if collected >= size:
break
conn = self._get_shard_conn(shard_index)
shard_messages = conn.dequeue(size - collected)
messages.extend(shard_messages)
collected += len(shard_messages)
return messages
# 获取队列长度
def get_queue_length(self) -> int:
"""获取队列中待处理消息的数量
Returns:
int: 待处理消息数量
"""
total = 0
for i in range(self.shard_num):
conn = self._get_shard_conn(i)
total += conn.get_queue_length()
return total
# 获取完成/失败的消息
def get_completed_messages(self) -> List[dict]:
messages = []
for i in range(self.shard_num):
conn = self._get_shard_conn(i)
messages.extend(conn.get_completed_messages())
return messages
# 获取消息结果
def get_result(self, message_id: str) -> Tuple[bool, Union[str, dict]]:
conn = self._get_shard_conn(self._get_shard_index(message_id))
return conn.get_result(message_id)
# 更新消息状态
def update_status(self, message_id: str, status: MessageStatus):
conn = self._get_shard_conn(self._get_shard_index(message_id))
conn.update_status(message_id, status)
# 更新消息结果
def update_result(self, message_id: str, result: str):
conn = self._get_shard_conn(self._get_shard_index(message_id))
conn.update_result(message_id, result)
# 删除消息
def delete_message(self, message_id: str):
conn = self._get_shard_conn(self._get_shard_index(message_id))
conn.delete_message(message_id)
# 清理7天前的已完成/失败消息
def clean_old_messages(self, shard_index: int, days: int = 7):
conn = self._get_shard_conn(shard_index)
conn.clean_old_messages(days)
# 清理过期但未处理的消息
def clean_expired_messages(self, shard_index: int):
conn = self._get_shard_conn(shard_index)
conn.clean_expired_messages()
+42 -14
View File
@@ -1,25 +1,53 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 17:02:39
@Author : chakcy
@Email : 947105045@qq.com
@description : 队列调度器
"""
from .standard import StandardQueueScheduler from .standard import StandardQueueScheduler
from ._async import AsyncQueueScheduler
from .base import BaseScheduler from .base import BaseScheduler
from ..model import MessageItem from ..model import MessageItem, SchedulerConfig
from typing import Callable from typing import Callable, Literal, Dict
import logging
SCHEDULER_TYPES: Dict[str, type[BaseScheduler]] = {
"standard": StandardQueueScheduler,
"async": AsyncQueueScheduler,
}
try:
from .qt import QtQueueScheduler
print(SCHEDULER_TYPES)
if QtQueueScheduler.is_available():
SCHEDULER_TYPES["qt"] = QtQueueScheduler
except ImportError:
logging.warning(
"Qt is not available, if you want to use it, please install PyQt5/PyQt6/PySide6"
)
SchedulerType = Literal["standard", "async", "qt"]
class QueueScheduler(BaseScheduler): class QueueScheduler(BaseScheduler):
def __init__( def __init__(
self, self,
receive_thread_num=1, scheduler_type: SchedulerType = "standard",
task_thread_num=1, config: SchedulerConfig = SchedulerConfig(),
shard_num=4,
scheduler_type="standard",
): ):
self.scheduler = None super().__init__(config)
if scheduler_type == "standard": scheduler_class = SCHEDULER_TYPES.get(scheduler_type, None)
self.scheduler = StandardQueueScheduler( if scheduler_class is None:
receive_thread_num, task_thread_num, shard_num raise ValueError(f"Invalid scheduler type: {scheduler_type}")
) self.scheduler = scheduler_class(config)
else:
pass if hasattr(self.scheduler, "queue_operation"):
if self.scheduler:
self.queue_operation = self.scheduler.queue_operation self.queue_operation = self.scheduler.queue_operation
def send_message(self, message: MessageItem, callback: Callable): def send_message(self, message: MessageItem, callback: Callable):
@@ -0,0 +1,69 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 17:04:34
@Author : chakcy
@Email : 947105045@qq.com
@description : 异步调度器
"""
from .async_receive_scheduler import AsyncReceiveScheduler
from .async_task_scheduler import AsyncTaskScheduler
from .async_listen_data_scheduler import AsyncListenDataScheduler
from ...model import MessageItem
from ...queue_operation.listen_operation import ListenOperation
from typing import Callable
from queue_sqlite_core import ShardedQueueOperation
import os
from ..base import BaseScheduler
from ..cleanup_scheduler import CleanupScheduler
from ...model import SchedulerConfig
class AsyncQueueScheduler(BaseScheduler):
def __init__(
self,
config: SchedulerConfig = SchedulerConfig(),
):
self.queue_operation = ShardedQueueOperation(
config.shard_num, queue_name=config.queue_name
)
self.receive_scheduler = AsyncReceiveScheduler(
self.queue_operation, config.receive_thread_num
)
self.listen_operation = ListenOperation(
os.path.join(self.queue_operation.db_dir, "listen.db")
)
self.listen_operation.create_table()
self.listen_scheduler = AsyncListenDataScheduler(self.listen_operation)
self.task_scheduler = AsyncTaskScheduler(
self.queue_operation, config.task_thread_num
)
self.cleanup_scheduler = CleanupScheduler(self.queue_operation)
def send_message(self, message: MessageItem, callback: Callable):
self.receive_scheduler.send_message(message, callback)
def update_listen_data(self, key, value):
self.listen_operation.update_listen_data(key, value)
def get_listen_datas(self):
return self.listen_operation.get_values()
def get_listen_data(self, key):
return self.listen_operation.get_value(key)
def start(self):
self.receive_scheduler.start_receive_thread()
self.task_scheduler.start_task_thread()
self.listen_scheduler.start_listen_data()
def stop(self):
self.receive_scheduler.stop_receive_thread()
self.task_scheduler.stop_task_thread()
__all__ = ["AsyncQueueScheduler"]
@@ -0,0 +1,103 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : async_listen_data_scheduler.py
@Time : 2025-09-27 17:04:54
@Author : chakcy
@Email : 947105045@qq.com
@description : 异步监听数据调度器
"""
from ...queue_operation.listen_operation import ListenOperation
import asyncio
from ...mounter.listen_mounter import ListenMounter
import threading
import multiprocessing
import concurrent.futures
import logging
class AsyncListenDataScheduler:
def __init__(self, listen_operation: ListenOperation):
self.listen_operation = listen_operation
self.is_running = False
self.thread_num = multiprocessing.cpu_count()
self.listen_thread = None
self.process_lock = threading.Lock()
self.last_processed_id = 0
async def _process_listen_data(self, key, value, delete_id):
listen_function = ListenMounter.get_Listener_function(key)
if listen_function:
if asyncio.iscoroutinefunction(listen_function):
await listen_function(value)
else:
try:
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor(
max_workers=self.thread_num
) as executor:
await loop.run_in_executor(executor, listen_function, value)
# listen_function(value)
except Exception as e:
ValueError(f"Error in {key} listener function: {e}")
finally:
self.listen_operation.delete_change_log(delete_id=delete_id)
async def listen(self):
async with asyncio.Semaphore(self.thread_num):
while self.is_running:
status, change_data_items = self.listen_operation.listen_data()
tasks = []
if status and isinstance(change_data_items, list):
new_change_data_items = [
data
for data in change_data_items
if data[0] > self.last_processed_id
]
# 按ID升序排序,确保按顺序处理
new_change_data_items.sort(key=lambda x: x[0])
for data in new_change_data_items:
key = data[3]
new_value = data[5]
delete_id = data[0]
with self.process_lock:
if delete_id > self.last_processed_id:
self.last_processed_id = delete_id
tasks.append(
asyncio.create_task(
self._process_listen_data(key, new_value, delete_id)
)
)
if tasks and self.is_running:
await asyncio.gather(*tasks, return_exceptions=True)
else:
await asyncio.sleep(0.05)
def _run_listen_loop(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(self.listen())
except Exception as e:
logging.error(f"监听调度出错: {str(e)}")
finally:
loop.close()
def start_listen_data(self):
if self.is_running:
return
self.is_running = True
self.last_processed_id = 0
self.listen_thread = threading.Thread(target=self._run_listen_loop, daemon=True)
self.listen_thread.start()
def stop_listen_data(self):
if not self.is_running:
return
self.is_running = False
if self.listen_thread and self.listen_thread.is_alive():
self.listen_thread.join(timeout=2)
@@ -0,0 +1,134 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : async_receive_scheduler.py
@Time : 2025-09-27 17:05:08
@Author : chakcy
@Email : 947105045@qq.com
@description : 异步接收调度器
"""
from queue_sqlite_core import ShardedQueueOperation
from ...model import MessageItem
from typing import Callable
import asyncio
import threading
import concurrent.futures
import logging
from collections import namedtuple
SendMessageCallback = namedtuple("send_message_callback", ["message", "callback"])
class AsyncReceiveScheduler:
def __init__(
self, queue_operation: ShardedQueueOperation, receive_thread_num: int = 1
):
self.callbacks = dict()
self.receive_thread_num = receive_thread_num
self.is_running = False
self.lock = threading.Lock()
self.queue_operation = queue_operation
self.receive_thread = None # 单一轮询线程
def send_message(self, message: MessageItem, callback: Callable = None): # type: ignore
if callback is None:
callback = lambda message: logging.info(message)
# self.queue_operation.enqueue(message.to_dict())
self.queue_operation.enqueue(message.to_dict_by_core())
with self.lock:
self.callbacks[message.id] = callback
def send_message_batch(self, message_callback_list: list[SendMessageCallback]):
message_list = [
message_callback.message.to_dict_by_core()
for message_callback in message_callback_list
]
if message_list:
self.queue_operation.enqueue_batch(message_list)
with self.lock:
for message_callback in message_callback_list:
message = message_callback.message
callback = message_callback.callback
if callback is None:
callback = lambda m: logging.info(f"receive message: {m.id}")
self.callbacks[message.id] = callback
async def receive_message(self):
"""单一轮询线程,并行执行回调"""
def callback_default(message):
return message
async with asyncio.Semaphore(self.receive_thread_num):
while self.is_running:
message_list = self.queue_operation.get_completed_messages()
tasks = []
if message_list:
for message in message_list:
message = MessageItem.from_dict(message)
with self.lock:
callback = self.callbacks.pop(message.id, None)
if callback is None:
callback = callback_default
tasks.append(
asyncio.create_task(self._safe_callback(callback, message))
)
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
else:
await asyncio.sleep(0.05)
async def _safe_callback(self, callback, message):
"""安全执行回调函数"""
try:
# 检查回调函数是否是异步函数
if asyncio.iscoroutinefunction(callback):
await callback(message)
else:
# 如果是同步函数,则在执行器中运行以避免阻塞事件循环
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor(
max_workers=self.receive_thread_num
) as executor:
await loop.run_in_executor(executor, callback, message)
except Exception as e:
logging.error(f"回调执行错误: {str(e)}")
finally:
self.queue_operation.delete_message(message.id)
def _run_receive_loop(self):
"""在新事件循环中运行接收消息循环"""
# 创建新的事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 运行异步接收函数直到停止
loop.run_until_complete(self.receive_message())
except Exception as e:
logging.error(f"任务调度出错: {str(e)}")
finally:
loop.close()
def start_receive_thread(self):
if self.is_running:
return
self.is_running = True
# 创建单一轮询线程
self.receive_thread = threading.Thread(
target=self._run_receive_loop, daemon=True
)
self.receive_thread.start()
def stop_receive_thread(self):
if not self.is_running:
return
self.is_running = False
# 等待轮询线程结束
if self.receive_thread and self.receive_thread.is_alive():
self.receive_thread.join(timeout=5.0)
@@ -0,0 +1,150 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : async_task_scheduler.py
@Time : 2025-09-27 17:05:22
@Author : chakcy
@Email : 947105045@qq.com
@description : 异步任务调度器
"""
from queue_sqlite_core import ShardedQueueOperation
from ...model import MessageItem
from ...constant import MessageStatus
from ...cycle.async_task_cycle import AsyncTaskCycle
from ...mounter.task_mounter import TaskMounter
import asyncio
from datetime import datetime
import threading
import multiprocessing
import logging
import json
class AsyncTaskScheduler:
def __init__(
self,
queue_operation: ShardedQueueOperation,
task_thread_num: int = multiprocessing.cpu_count() * 2,
):
self.task_thread_num = task_thread_num
self.is_running = False
self.queue_operation = queue_operation
self.task_thread = None # 单一轮询线程
async def _process_message(self, message: MessageItem):
"""处理单个消息"""
try:
if message.destination == "client":
message.status = MessageStatus.COMPLETED
message.result = {"result": "success"}
message.updatetime = datetime.now()
return message
task_function = TaskMounter.get_task_function(message.destination)
if task_function is None:
raise ValueError(
f"Task function not found for destination: {message.destination}"
)
task_cycle = AsyncTaskCycle(message, task_function)
await task_cycle.run()
message.status = task_cycle.get_task_status() # type: ignore
if message.status == MessageStatus.FAILED:
message.result = {"error": task_cycle.get_task_error()}
else:
message.result = task_cycle.get_task_result() # type: ignore
message.updatetime = datetime.now()
except Exception as e:
message.status = MessageStatus.FAILED
message.result = {"error": str(e)}
message.updatetime = datetime.now()
return message
def _process_messages(self, message):
"""更新任务结果到数据库"""
try:
self.queue_operation.update_result(message.id, json.dumps(message.result))
self.queue_operation.update_status(message.id, message.status)
except Exception as e:
logging.error(f"任务结果更新失败: {str(e)}")
async def task_callback(self):
"""单一轮询线程,并行执行任务"""
# 使用信号量控制并发任务数
semaphore = asyncio.Semaphore(self.task_thread_num)
async def limited_process_message(message):
async with semaphore:
return await self._process_message(MessageItem.from_dict(message))
while self.is_running:
try:
message_list = self.queue_operation.dequeue(
size=self.task_thread_num * 2
)
if not message_list:
# 使用指数退避策略减少轮询开销
await asyncio.sleep(0.05) # 增加延迟减少CPU使用
continue
# 并发处理消息
tasks = [
asyncio.create_task(limited_process_message(message))
for message in message_list
]
# 等待所有任务完成
completed, _ = await asyncio.wait(
tasks,
return_when=asyncio.ALL_COMPLETED,
timeout=30.0, # 添加超时防止无限等待
)
# 批量处理结果
for task in completed:
try:
message = task.result()
self._process_messages(message)
except Exception as e:
logging.error(f"处理任务结果时出错: {str(e)}")
except Exception as e:
logging.error(f"任务调度出错: {str(e)}")
await asyncio.sleep(0.1) # 出错时短暂延迟
def _run_task_loop(self):
"""在新事件循环中运行任务处理循环"""
# 创建新的事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 运行异步任务处理函数直到停止
loop.run_until_complete(self.task_callback())
except Exception as e:
logging.error(f"任务调度出错: {str(e)}")
finally:
loop.close()
def start_task_thread(self):
if self.is_running:
return
self.is_running = True
self.task_thread = threading.Thread(target=self._run_task_loop)
self.task_thread.daemon = True
self.task_thread.start()
def stop_task_thread(self):
if not self.is_running:
return
self.is_running = False
if self.task_thread:
self.task_thread.join(timeout=5.0)
self.task_thread = None
@@ -1,11 +1,31 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Callable from typing import Callable
from ...model import MessageItem from ...model import MessageItem, SchedulerConfig
from queue_sqlite_core import ShardedQueueOperation
import logging
class BaseScheduler(ABC): class BaseScheduler(ABC):
"""调度器抽象类""" """调度器抽象类"""
def __init__(self, config: SchedulerConfig = SchedulerConfig()):
self._queue_operation = None
@property
def queue_operation(self) -> ShardedQueueOperation:
if self._queue_operation is None:
raise RuntimeError("请先设置队列操作对象")
return self._queue_operation
@queue_operation.setter
def queue_operation(self, value: ShardedQueueOperation | None):
"""设置队列操作对象
Args:
value (ShardedQueueOperation): 队列操作对象
"""
self._queue_operation = value
@abstractmethod @abstractmethod
def send_message(self, message: MessageItem, callback: Callable): def send_message(self, message: MessageItem, callback: Callable):
"""发送消息到队列 """发送消息到队列
@@ -40,3 +60,14 @@ class BaseScheduler(ABC):
def get_listen_data(self, key): def get_listen_data(self, key):
"""获取单个监听数据""" """获取单个监听数据"""
pass pass
def get_queue_info(self) -> dict:
try:
return {
"queue_length": self.queue_operation.get_queue_length(),
"shard_num": self.queue_operation.shard_num,
"db_dir": self.queue_operation.db_dir,
}
except Exception as e:
logging.error(f"获取队列消息失败: {str(e)}")
return {}
@@ -0,0 +1,102 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : cleanup_scheduler.py
@Time : 2025-09-27 17:02:55
@Author : chakcy
@Email : 947105045@qq.com
@description : 清理调度器
"""
# 新建清理调度器
import threading
import time
from queue_sqlite_core import ShardedQueueOperation
import logging
import os
class CleanupScheduler:
def __init__(
self,
queue_operation: ShardedQueueOperation,
interval_minutes=1,
remove_days=30,
):
self.queue_operation = queue_operation
self.interval = interval_minutes * 60 # 转换为秒
self.is_running = False
self.cleanup_thread = None
self.remove_days = remove_days
self.max_db_size_mb = 500
# 清理过期但未处理的消息
self.queue_operation.clean_expired_messages()
# 彻底删除30天前的消息
self.queue_operation.remove_expired_messages(self.remove_days)
def cleanup_expired_messages(self):
"""清理过期消息"""
while self.is_running:
try:
self.queue_operation.clean_expired_messages()
self.remove_days = self._check_and_optimize_db(self.remove_days)
except Exception as e:
logging.error(f"清理消息错误: {str(e)}")
# 休眠等待下次清理
for _ in range(self.interval):
if not self.is_running:
break
time.sleep(1)
def _check_and_optimize_db(self, remove_days):
"""检查数据库大小并优化"""
try:
# 获取数据库文件大小
db_size_mb = self._get_database_size()
if db_size_mb > self.max_db_size_mb:
logging.info(f"数据库大小 {db_size_mb}MB 超过限制,执行优化...")
print(f"数据库大小 {db_size_mb}MB 超过限制,执行优化...")
if remove_days > 1:
remove_days = remove_days - 1
self.queue_operation.remove_expired_messages(remove_days)
except Exception as e:
logging.error(f"数据库优化错误: {str(e)}")
return remove_days
def _get_database_size(self) -> int:
"""获取数据库文件大小"""
# 实现获取数据库文件大小的逻辑
total_size = 0
db_path = self.queue_operation.db_dir
for root, _, files in os.walk(db_path):
for file in files:
file_path = os.path.join(root, file)
total_size += os.path.getsize(file_path)
return total_size // (1024 * 1024)
def start_cleanup(self):
if self.is_running:
return
self.is_running = True
self.cleanup_thread = threading.Thread(
target=self.cleanup_expired_messages, daemon=True
)
self.cleanup_thread.start()
def stop_cleanup(self):
if not self.is_running:
return
self.is_running = False
if self.cleanup_thread and self.cleanup_thread.is_alive():
self.cleanup_thread.join(timeout=2.0)
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-10-26 10:26:16
@Author : chakcy
@Email : 947105045@qq.com
@description : Qt调度器 - 独立实现
"""
import logging
import os
from ..base import BaseScheduler
from ...model import MessageItem, SchedulerConfig
from queue_sqlite_core import ShardedQueueOperation
from ...queue_operation.listen_operation import ListenOperation
from .receive_scheduler import QtReceiveScheduler
from .listen_scheduler import QtListenScheduler
from .task_scheduler import QtTaskScheduler
from ..cleanup_scheduler import CleanupScheduler as QtCleanupScheduler
from qtpy.QtCore import QThreadPool
from qtpy.QtWidgets import QApplication
from typing import Callable, Dict, Any, List
class QtQueueScheduler(BaseScheduler):
"""完全独立的 Qt 队列调度器 - 修复版"""
qt_type = None
def __init__(
self,
config: SchedulerConfig = SchedulerConfig(),
):
# 初始化队列操作
self.queue_operation = ShardedQueueOperation(
config.shard_num, config.queue_name
)
# 初始化监听操作
db_dir = f"cache/{config.queue_name}"
os.makedirs(db_dir, exist_ok=True)
self.listen_operation = ListenOperation(f"{db_dir}/listen.db")
# 确保监听表被创建
self._ensure_listen_operation_tables()
# 初始化各个独立的调度器组件
self.receive_scheduler = QtReceiveScheduler(
self.queue_operation, config.receive_thread_num
)
self.task_scheduler = QtTaskScheduler(
self.queue_operation, config.task_thread_num
)
self.listen_scheduler = QtListenScheduler(self.listen_operation)
self.cleanup_scheduler = QtCleanupScheduler(self.queue_operation)
@classmethod
def is_available(cls) -> bool:
# 检查 PySide6/PyQt6/PyQt5/PySide2 模块中的一个
import importlib
qt_modules = ["PySide6", "PyQt6", "PyQt5", "PySide2"]
for module in qt_modules:
try:
importlib.import_module(module)
cls.qt_type = module
logging.info(f"已找到 Qt 模块: {module}")
print(f"已找到 Qt 模块: {module}")
return True
except ImportError: # 模块未找到
continue
return False
def _ensure_qapplication(self):
"""确保 QApplication 实例存在"""
try:
if not QApplication.instance():
# 对于非GUI应用,创建无窗口的QApplication
import sys
self.app = QApplication(sys.argv if hasattr(sys, "argv") else [])
logging.info("已创建 QApplication 实例")
except Exception as e:
logging.warning(f"创建 QApplication 失败: {str(e)}")
def _ensure_listen_operation_tables(self):
"""确保监听操作的表被正确创建"""
try:
# 强制重新创建表
self.listen_operation.create_table()
# 额外检查 change_log 表
conn = self.listen_operation._get_connection()
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='change_log'"
)
if cursor.fetchone() is None:
logging.warning("change_log 表不存在,尝试手动创建")
conn.execute(
"""
CREATE TABLE change_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
row_id INTEGER,
column_name TEXT,
old_value TEXT,
new_value TEXT,
is_delete integer DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.commit()
except Exception as e:
logging.error(f"确保监听表存在失败: {str(e)}")
def send_message(self, message: MessageItem, callback: Callable):
"""发送消息到队列"""
self.receive_scheduler.send_message(message, callback)
def update_listen_data(self, key: str, value: str):
"""更新监听数据"""
self.listen_operation.update_listen_data(key, value)
def get_listen_datas(self) -> List:
"""获取所有监听数据"""
return self.listen_operation.get_values()
def get_listen_data(self, key: str):
"""获取单个监听数据"""
return self.listen_operation.get_value(key)
def start(self):
"""启动所有调度器组件"""
# 确保有 QApplication 实例(对于 GUI 应用)
self._ensure_qapplication()
self.receive_scheduler.start()
self.task_scheduler.start()
self.cleanup_scheduler.start_cleanup()
self.listen_scheduler.start()
logging.info("Qt 队列调度器已完全启动")
def stop(self):
"""停止所有调度器组件"""
self.listen_scheduler.stop()
self.cleanup_scheduler.stop_cleanup()
self.task_scheduler.stop()
self.receive_scheduler.stop()
logging.info("Qt 队列调度器已完全停止")
def get_queue_info(self) -> Dict[str, Any]:
"""获取队列信息"""
try:
return {
"queue_length": self.queue_operation.get_queue_length(),
"shard_num": self.queue_operation.shard_num,
"db_dir": self.queue_operation.db_dir,
"active_threads": QThreadPool.globalInstance().activeThreadCount(), # type: ignore
"max_threads": QThreadPool.globalInstance().maxThreadCount(), # type: ignore
}
except Exception as e:
logging.error(f"获取队列信息失败: {str(e)}")
return {}
@@ -0,0 +1,56 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : callback_task.py
@Time : 2025-10-28 11:40:02
@Author : chakcy
@Email : 947105045@qq.com
@description : 回调任务
"""
import asyncio
import logging
from queue_sqlite_core import ShardedQueueOperation
from ...model import MessageItem
from typing import Callable
from qtpy.QtCore import QRunnable
class QtCallbackTask(QRunnable):
"""Qt 回调任务"""
def __init__(
self,
callback: Callable,
message: MessageItem,
queue_operation: ShardedQueueOperation,
):
super().__init__()
self.callback = callback
self.message = message
self.queue_operation = queue_operation
self.setAutoDelete(True)
def run(self):
"""执行回调函数"""
try:
# 检查是否是协程函数
if asyncio.iscoroutinefunction(self.callback):
# 对于异步回调,在当前线程中运行事件循环
asyncio.run(self._run_async_callback())
else:
self.callback(self.message)
except Exception as e:
logging.error(f"回调执行错误: {str(e)}")
finally:
# 删除消息
try:
self.queue_operation.delete_message(self.message.id)
except Exception as e:
logging.error(f"删除消息失败 {self.message.id}: {str(e)}")
async def _run_async_callback(self):
"""运行异步回调"""
try:
await self.callback(self.message)
except Exception as e:
logging.error(f"异步回调执行错误: {str(e)}")
@@ -0,0 +1,217 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : listen_scheduler.py
@Time : 2025-10-28 14:22:38
@Author : chakcy
@Email : 947105045@qq.com
@description : 监听调度器 - 改进版,解决数据库锁定问题
"""
import logging
import time
import sqlite3
from .listen_task import QtListenTask
from ...queue_operation.listen_operation import ListenOperation
from qtpy.QtCore import QThreadPool, QMutex, QMutexLocker
import threading
class QtListenScheduler:
"""Qt 监听调度器 - 改进版,解决数据库锁定问题"""
def __init__(self, listen_operation: ListenOperation):
self.listen_operation = listen_operation
self.is_running = False
self.listen_thread = None
self.thread_pool = QThreadPool.globalInstance()
self.db_mutex = QMutex() # 数据库操作互斥锁
# 监听配置
self.poll_interval = 0.1 # 轮询间隔(秒)
self.max_retries = 5 # 最大重试次数
self.retry_delay = 0.2 # 重试延迟(秒)
# 确保监听表存在
self._ensure_listen_tables()
def _ensure_listen_tables(self):
"""确保监听相关的表存在"""
try:
# 使用互斥锁保护数据库操作
with QMutexLocker(self.db_mutex):
# 检查表是否存在,如果不存在则创建
conn = self.listen_operation._get_connection()
# 检查 change_log 表是否存在
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='change_log'"
)
if cursor.fetchone() is None:
logging.info("创建缺失的 change_log 表")
conn.execute(
"""
CREATE TABLE change_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT,
row_id INTEGER,
column_name TEXT,
old_value TEXT,
new_value TEXT,
is_delete integer DEFAULT 0,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
is_processed integer DEFAULT 0
)
"""
)
conn.commit()
else:
# 如果表存在,检查是否已有 is_processed 列
cursor = conn.execute("PRAGMA table_info(change_log)")
columns = [row[1] for row in cursor.fetchall()]
if "is_processed" not in columns:
conn.execute(
"ALTER TABLE change_log ADD COLUMN is_processed INTEGER DEFAULT 0"
)
conn.commit()
except Exception as e:
logging.error(f"确保监听表存在失败: {str(e)}")
def _safe_listen_data(self):
"""安全地获取监听数据,带重试机制"""
for attempt in range(self.max_retries):
try:
# 使用互斥锁保护数据库操作
with QMutexLocker(self.db_mutex):
conn = self.listen_operation._get_connection()
cursor = conn.execute(
"SELECT id, table_name, row_id, column_name, old_value, new_value, is_delete, timestamp "
"FROM change_log WHERE is_processed = 0 ORDER BY id ASC"
)
change_data_items = cursor.fetchall()
return True, change_data_items
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < self.max_retries - 1:
logging.warning(
f"数据库锁定,第 {attempt + 1} 次重试获取监听数据..."
)
time.sleep(self.retry_delay)
else:
logging.error(f"获取监听数据失败: {str(e)}")
return False, []
except Exception as e:
logging.error(f"获取监听数据时发生未知错误: {str(e)}")
return False, []
return False, []
def _mark_processed(self, log_id):
"""标记指定ID的日志为已处理"""
try:
with QMutexLocker(self.db_mutex):
conn = self.listen_operation._get_connection()
# 添加 is_processed 字段来标记是否已经处理
# 首先检查表结构是否包含该字段,如果没有则添加
cursor = conn.execute("PRAGMA table_info(change_log)")
columns = [row[1] for row in cursor.fetchall()]
if "is_processed" not in columns:
conn.execute(
"ALTER TABLE change_log ADD COLUMN is_processed INTEGER DEFAULT 0"
)
# 更新指定ID的记录为已处理
conn.execute(
"UPDATE change_log SET is_processed = 1 WHERE id = ?", (log_id,)
)
conn.commit()
except Exception as e:
logging.error(f"标记已处理记录失败: {str(e)}")
def _process_listen_data(self, change_data_items):
"""处理监听数据"""
processed_count = 0
for data in change_data_items:
try:
# 修正字段索引
if len(data) >= 8:
delete_id = data[0] # id
table_name = data[1] # table_name
row_id = data[2] # row_id
column_name = data[3] # column_name
old_value = data[4] # old_value
new_value = data[5] # new_value
# 使用线程池执行监听任务
task = QtListenTask(
column_name,
new_value,
int(delete_id),
self.listen_operation,
self.db_mutex,
self.max_retries,
self.retry_delay,
)
self.thread_pool.start(task) # type: ignore
self._mark_processed(int(delete_id))
processed_count += 1
except Exception as e:
logging.error(f"处理监听数据失败: {str(e)}")
return processed_count
def _listen_loop(self):
"""监听数据变化循环 - 改进版"""
consecutive_errors = 0
max_consecutive_errors = 10
while self.is_running:
try:
status, change_data_items = self._safe_listen_data()
if status and change_data_items:
processed_count = self._process_listen_data(change_data_items)
consecutive_errors = 0 # 重置连续错误计数
if processed_count > 0:
logging.debug(f"成功处理 {processed_count} 个监听数据项")
elif not status:
# 没有数据或获取失败,短暂休眠
time.sleep(self.poll_interval)
consecutive_errors += 1
# 如果连续错误太多,延长休眠时间
if consecutive_errors >= max_consecutive_errors:
# logging.warning("连续多次获取监听数据失败,延长休眠时间")
time.sleep(self.poll_interval * 5)
consecutive_errors = 0 # 重置计数
else:
# 有状态但没有数据,正常休眠
time.sleep(self.poll_interval)
consecutive_errors = 0
except Exception as e:
logging.error(f"监听循环错误: {str(e)}")
consecutive_errors += 1
time.sleep(self.poll_interval * 2) # 出错时延长休眠
def start(self):
"""启动监听调度器"""
if self.is_running:
return
self.is_running = True
self.listen_thread = threading.Thread(target=self._listen_loop, daemon=True)
self.listen_thread.start()
logging.info("Qt 监听调度器已启动")
def stop(self):
"""停止监听调度器"""
if not self.is_running:
return
self.is_running = False
if self.listen_thread and self.listen_thread.is_alive():
self.listen_thread.join(timeout=3.0)
logging.info("Qt 监听调度器已停止")
@@ -0,0 +1,87 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : listen_task.py
@Time : 2025-10-28 14:30:41
@Author : chakcy
@Email : 947105045@qq.com
@description : 监听任务 - 改进版,解决数据库锁定问题
"""
from queue_sqlite.queue_operation.listen_operation import ListenOperation
from ...mounter.listen_mounter import ListenMounter
import logging
from qtpy.QtCore import QRunnable, QMutexLocker
import asyncio
from typing import Callable
import time
import sqlite3
class QtListenTask(QRunnable):
"""Qt 监听任务 - 改进版,解决数据库锁定问题"""
def __init__(
self,
key: str,
value: str,
delete_id: int,
listen_operation: ListenOperation,
db_mutex,
max_retries=5,
retry_delay=0.2,
):
super().__init__()
self.key = key
self.value = value
self.delete_id = delete_id
self.listen_operation = listen_operation
self.db_mutex = db_mutex
self.max_retries = max_retries
self.retry_delay = retry_delay
self.setAutoDelete(True)
def run(self):
"""执行监听回调"""
try:
listen_function = ListenMounter.get_Listener_function(self.key)
if listen_function:
if asyncio.iscoroutinefunction(listen_function):
# 异步监听函数
asyncio.run(self._run_async_listener(listen_function))
else:
# 同步监听函数
listen_function(self.value)
except Exception as e:
logging.error(f"监听函数执行错误 {self.key}: {str(e)}")
finally:
# 安全删除变更日志
self._safe_delete_change_log()
def _safe_delete_change_log(self):
"""安全删除变更日志,带重试机制"""
for attempt in range(self.max_retries):
try:
# 使用互斥锁保护数据库操作
with QMutexLocker(self.db_mutex):
self.listen_operation.delete_change_log(self.delete_id)
break # 成功则退出循环
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < self.max_retries - 1:
logging.debug(
f"删除变更日志时数据库锁定,第 {attempt + 1} 次重试..."
)
time.sleep(self.retry_delay)
else:
logging.error(f"删除变更日志失败 {self.delete_id}: {str(e)}")
break
except Exception as e:
logging.error(f"删除变更日志失败 {self.delete_id}: {str(e)}")
break
async def _run_async_listener(self, listen_function: Callable):
"""执行异步监听函数"""
try:
await listen_function(self.value)
except Exception as e:
logging.error(f"异步监听函数执行错误 {self.key}: {str(e)}")
@@ -0,0 +1,117 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : receive_scheduler.py
@Time : 2025-10-28 14:31:06
@Author : chakcy
@Email : 947105045@qq.com
@description : 接收调度器
"""
from typing import Callable, Optional
from queue_sqlite_core import ShardedQueueOperation
from qtpy.QtCore import QThreadPool
import threading
from ...model import MessageItem
import time
import logging
from .callback_task import QtCallbackTask
from collections import namedtuple
SendMessageCallback = namedtuple("send_message_callback", ["message", "callback"])
class QtReceiveScheduler:
"""Qt 接收调度器 - 独立实现"""
def __init__(
self, queue_operation: ShardedQueueOperation, receive_thread_num: int = 1
):
self.callbacks = {}
self.is_running = False
self.lock = threading.Lock()
self.queue_operation = queue_operation
self.receive_thread = None
self.thread_pool = QThreadPool.globalInstance()
def send_message(self, message: MessageItem, callback: Optional[Callable] = None):
"""发送消息到队列"""
if callback is None:
callback = lambda message: logging.info(f"收到消息: {message.id}")
# 入队消息
self.queue_operation.enqueue(message.to_dict_by_core())
with self.lock:
self.callbacks[message.id] = callback
def send_message_batch(self, message_callback_list: list[SendMessageCallback]):
message_list = [
message_callback.message.to_dict_by_core()
for message_callback in message_callback_list
]
if message_list:
self.queue_operation.enqueue_batch(message_list)
with self.lock:
for message_callback in message_callback_list:
message = message_callback.message
callback = message_callback.callback
if callback is None:
callback = lambda m: logging.info(f"receive message: {m.id}")
self.callbacks[message.id] = callback
def _receive_loop(self):
"""接收消息循环"""
while self.is_running:
try:
# 获取已完成的消息
message_list = self.queue_operation.get_completed_messages()
if message_list:
for message_data in message_list:
try:
message = MessageItem.from_dict(message_data)
with self.lock:
callback = self.callbacks.pop(message.id, None)
if callback is None:
callback = lambda msg: None
# 使用线程池执行回调
task = QtCallbackTask(
callback, message, self.queue_operation
)
self.thread_pool.start(task) # type: ignore
except Exception as e:
logging.error(f"处理完成消息失败: {str(e)}")
# 即使处理失败也尝试删除消息
try:
self.queue_operation.delete_message(message_data["id"])
except:
pass
else:
time.sleep(0.05) # 短暂休眠避免CPU空转
except Exception as e:
logging.error(f"接收消息循环错误: {str(e)}")
time.sleep(0.1) # 出错时稍长休眠
def start(self):
"""启动接收调度器"""
if self.is_running:
return
self.is_running = True
self.receive_thread = threading.Thread(target=self._receive_loop, daemon=True)
self.receive_thread.start()
logging.info("Qt 接收调度器已启动")
def stop(self):
"""停止接收调度器"""
if not self.is_running:
return
self.is_running = False
if self.receive_thread and self.receive_thread.is_alive():
self.receive_thread.join(timeout=3.0)
logging.info("Qt 接收调度器已停止")
@@ -0,0 +1,148 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : task_executor.py
@Time : 2025-10-28 14:31:25
@Author : chakcy
@Email : 947105045@qq.com
@description : 任务执行器
"""
import asyncio
import logging
from typing import Any, Callable, Dict
from datetime import datetime
from ...constant import MessageStatus
from ...mounter.task_mounter import TaskMounter
from queue_sqlite_core import ShardedQueueOperation
from ...model.message_item import MessageItem
from qtpy.QtCore import QRunnable
class QtTaskExecutor(QRunnable):
"""Qt 任务执行器 - 修复版"""
def __init__(
self, message_data: Dict[str, Any], queue_operation: ShardedQueueOperation
):
super().__init__()
self.message_data = message_data
self.queue_operation = queue_operation
self.setAutoDelete(True)
def run(self):
"""执行任务"""
try:
message = MessageItem.from_dict(self.message_data)
if message.destination == "client":
self._process_client_message(message)
return
task_function = TaskMounter.get_task_function(message.destination)
if task_function is None:
raise ValueError(f"任务函数未找到: {message.destination}")
# 执行任务
if asyncio.iscoroutinefunction(task_function):
# 异步任务 - 在当前线程运行事件循环
asyncio.run(self._execute_async_task(message, task_function))
else:
# 同步任务
self._execute_sync_task(message, task_function)
except Exception as e:
logging.error(f"任务执行错误: {str(e)}")
# 更新任务状态为失败
try:
self.queue_operation.update_status(
self.message_data["id"], MessageStatus.FAILED.value
)
self.queue_operation.update_result(
self.message_data["id"], f'{{"error": "{str(e)}"}}'
)
except Exception as update_error:
logging.error(f"更新任务状态失败: {str(update_error)}")
def _process_client_message(self, message: MessageItem):
"""处理客户端消息"""
message.status = MessageStatus.COMPLETED
message.result = {"result": "success"}
message.updatetime = datetime.now()
self._update_task_result(message)
def _execute_sync_task(self, message: MessageItem, task_function: Callable):
"""执行同步任务"""
from ...cycle.task_cycle import TaskCycle
try:
task_cycle = TaskCycle(message, task_function)
task_cycle.run()
status = task_cycle.get_task_status()
if not status:
raise ValueError("任务未完成")
message.status = status
if message.status == MessageStatus.FAILED:
message.result = {"error": task_cycle.get_task_error()}
else:
# 获取序列化后的结果
result_str = task_cycle.get_task_result()
try:
import json
message.result = json.loads(result_str)
except:
message.result = {"result": result_str}
self._update_task_result(message)
except Exception as e:
logging.error(f"同步任务执行失败 {message.id}: {str(e)}")
message.status = MessageStatus.FAILED
message.result = {"error": str(e)}
self._update_task_result(message)
async def _execute_async_task(self, message: MessageItem, task_function: Callable):
"""执行异步任务"""
from ...cycle.async_task_cycle import AsyncTaskCycle
try:
task_cycle = AsyncTaskCycle(message, task_function)
await task_cycle.run()
status = task_cycle.get_task_status()
if not status:
raise ValueError("任务未完成")
message.status = status
if message.status == MessageStatus.FAILED:
message.result = {"error": task_cycle.get_task_error()}
else:
# 获取序列化后的结果
result_str = task_cycle.get_task_result()
try:
import json
message.result = json.loads(result_str)
except:
message.result = {"result": result_str}
self._update_task_result(message)
except Exception as e:
logging.error(f"异步任务执行失败 {message.id}: {str(e)}")
message.status = MessageStatus.FAILED
message.result = {"error": str(e)}
self._update_task_result(message)
def _update_task_result(self, message: MessageItem):
"""更新任务结果到数据库"""
import json
try:
# 序列化结果
result_str = json.dumps(message.result) if message.result else "{}"
self.queue_operation.update_result(message.id, result_str)
self.queue_operation.update_status(message.id, message.status.value)
except Exception as e:
logging.error(f"更新任务结果失败 {message.id}: {str(e)}")
@@ -0,0 +1,69 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : task_scheduler.py
@Time : 2025-10-28 14:19:39
@Author : chakcy
@Email : 947105045@qq.com
@description : Qt 任务调度器
"""
import logging
from qtpy.QtCore import QThreadPool
from .task_executor import QtTaskExecutor
from queue_sqlite_core import ShardedQueueOperation
import time
import threading
class QtTaskScheduler:
"""Qt 任务调度器 - 独立实现"""
def __init__(
self, queue_operation: ShardedQueueOperation, task_thread_num: int = 4
):
self.is_running = False
self.queue_operation = queue_operation
self.task_thread = None
self.thread_pool = QThreadPool.globalInstance()
# 设置最大线程数
self.thread_pool.setMaxThreadCount(max(1, task_thread_num)) # type: ignore
def _task_loop(self):
"""任务处理循环"""
while self.is_running:
try:
# 出队消息进行处理
message_list = self.queue_operation.dequeue(
size=self.thread_pool.maxThreadCount() * 2 # type: ignore
)
if message_list:
for message_data in message_list:
# 使用线程池执行任务
task = QtTaskExecutor(message_data, self.queue_operation)
self.thread_pool.start(task) # type: ignore
else:
time.sleep(0.05) # 短暂休眠
except Exception as e:
logging.error(f"任务调度循环错误: {str(e)}")
time.sleep(0.1)
def start(self):
"""启动任务调度器"""
if self.is_running:
return
self.is_running = True
self.task_thread = threading.Thread(target=self._task_loop, daemon=True)
self.task_thread.start()
logging.info("Qt 任务调度器已启动")
def stop(self):
"""停止任务调度器"""
if not self.is_running:
return
self.is_running = False
if self.task_thread and self.task_thread.is_alive():
self.task_thread.join(timeout=3.0)
logging.info("Qt 任务调度器已停止")
@@ -1,18 +1,35 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : __init__.py
@Time : 2025-09-27 17:03:16
@Author : chakcy
@Email : 947105045@qq.com
@description : 标准调度器
"""
from .receive_scheduler import ReceiveScheduler from .receive_scheduler import ReceiveScheduler
from .task_scheduler import TaskScheduler from .task_scheduler import TaskScheduler
from .listen_data_scheduler import ListenDataScheduler from .listen_data_scheduler import ListenDataScheduler
from ...model import MessageItem from ...model import MessageItem
from typing import Callable from typing import Callable
from ...queue_operation import QueueOperation from queue_sqlite_core import ShardedQueueOperation
from ...queue_operation.listen_operation import ListenOperation from ...queue_operation.listen_operation import ListenOperation
from .cleanup_scheduler import CleanupScheduler from ..cleanup_scheduler import CleanupScheduler
import os import os
from ..base import BaseScheduler from ..base import BaseScheduler
from ...model import SchedulerConfig
class StandardQueueScheduler(BaseScheduler): class StandardQueueScheduler(BaseScheduler):
def __init__(self, receive_thread_num=1, task_thread_num=1, shard_num=4): def __init__(
self.queue_operation = QueueOperation(shard_num) self,
config: SchedulerConfig = SchedulerConfig(),
):
self.queue_operation = ShardedQueueOperation(
config.shard_num, config.queue_name
)
self.listen_operation = ListenOperation( self.listen_operation = ListenOperation(
os.path.join(self.queue_operation.db_dir, "listen.db") os.path.join(self.queue_operation.db_dir, "listen.db")
) )
@@ -20,9 +37,11 @@ class StandardQueueScheduler(BaseScheduler):
self.listen_scheduler = ListenDataScheduler(self.listen_operation) self.listen_scheduler = ListenDataScheduler(self.listen_operation)
self.receive_scheduler = ReceiveScheduler( self.receive_scheduler = ReceiveScheduler(
self.queue_operation, receive_thread_num self.queue_operation, config.receive_thread_num
)
self.task_scheduler = TaskScheduler(
self.queue_operation, config.task_thread_num
) )
self.task_scheduler = TaskScheduler(self.queue_operation, task_thread_num)
self.cleanup_scheduler = CleanupScheduler(self.queue_operation) self.cleanup_scheduler = CleanupScheduler(self.queue_operation)
def send_message(self, message: MessageItem, callback: Callable): def send_message(self, message: MessageItem, callback: Callable):
@@ -48,7 +67,6 @@ class StandardQueueScheduler(BaseScheduler):
self.listen_scheduler.stop_listen_data() self.listen_scheduler.stop_listen_data()
self.task_scheduler.stop_task_thread() self.task_scheduler.stop_task_thread()
self.cleanup_scheduler.stop_cleanup() self.cleanup_scheduler.stop_cleanup()
# self.queue_operation.close_all_connections()
__all__ = ["StandardQueueScheduler"] __all__ = ["StandardQueueScheduler"]
@@ -1,51 +0,0 @@
# 新建清理调度器
import threading
import time
from ...queue_operation import QueueOperation
class CleanupScheduler:
def __init__(self, queue_operation: QueueOperation, interval_minutes=60):
self.queue_operation = queue_operation
self.interval = interval_minutes * 60 # 转换为秒
self.is_running = False
self.cleanup_thread = None
def cleanup_expired_messages(self):
"""清理过期消息"""
while self.is_running:
try:
# 清理7天前的已完成/失败消息
for i in range(self.queue_operation.shard_num):
self.queue_operation.clean_old_messages(i, 7)
# 清理过期但未处理的消息
for i in range(self.queue_operation.shard_num):
self.queue_operation.clean_expired_messages(i)
except Exception as e:
print(f"清理消息错误: {str(e)}")
# 休眠等待下次清理
for _ in range(self.interval):
if not self.is_running:
break
time.sleep(1)
def start_cleanup(self):
if self.is_running:
return
self.is_running = True
self.cleanup_thread = threading.Thread(
target=self.cleanup_expired_messages, daemon=True
)
self.cleanup_thread.start()
def stop_cleanup(self):
if not self.is_running:
return
self.is_running = False
if self.cleanup_thread and self.cleanup_thread.is_alive():
self.cleanup_thread.join(timeout=2.0)
@@ -1,15 +1,33 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : listen_data_scheduler.py
@Time : 2025-09-27 17:03:39
@Author : chakcy
@Email : 947105045@qq.com
@description : 监听数据调度器
"""
from ...queue_operation.listen_operation import ListenOperation from ...queue_operation.listen_operation import ListenOperation
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from ...mounter.listen_mounter import ListenMounter from ...mounter.listen_mounter import ListenMounter
import threading import threading
import multiprocessing
import time
import logging
class ListenDataScheduler: class ListenDataScheduler:
def __init__(self, listen_operation: ListenOperation): def __init__(self, listen_operation: ListenOperation):
self.listen_operation = listen_operation self.listen_operation = listen_operation
self.is_running = False self.is_running = False
self.executor = ThreadPoolExecutor(max_workers=1) self.executor = ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
self.listen_thread = None self.listen_thread = None
# 使用一个线程锁来确保监听逻辑的原子性
self.process_lock = threading.Lock()
# 记录最后处理的ID,避免重复处理
self.last_processed_id = 0
def _process_listen_data(self, key, value, delete_id): def _process_listen_data(self, key, value, delete_id):
listen_function = ListenMounter.get_Listener_function(key) listen_function = ListenMounter.get_Listener_function(key)
@@ -19,24 +37,51 @@ class ListenDataScheduler:
except Exception as e: except Exception as e:
ValueError(f"Error in {key} listener function: {e}") ValueError(f"Error in {key} listener function: {e}")
finally: finally:
self.listen_operation.delete_change_log(delete_id=delete_id) # 确保变更日志被删除
try:
self.listen_operation.delete_change_log(delete_id=delete_id)
except Exception as e:
# 记录错误但不抛出异常
logging.error(f"Error in deleting change log: {e}")
def listen(self): def listen(self):
while self.is_running: while self.is_running:
# 获取比上次处理ID更大的变更记录,避免重复处理
status, change_data_items = self.listen_operation.listen_data() status, change_data_items = self.listen_operation.listen_data()
if status: if status and isinstance(change_data_items, list):
for data in change_data_items: # 过滤出ID大于上次处理ID的记录
key = data[6] new_change_data_items = [
new_value = data[7] data
for data in change_data_items
if data[0] > self.last_processed_id
]
# 按ID升序排序,确保按顺序处理
new_change_data_items.sort(key=lambda x: x[0])
for data in new_change_data_items:
key = data[3]
new_value = data[5]
delete_id = data[0] delete_id = data[0]
self.executor.submit(
self._process_listen_data, key, new_value, delete_id if self.is_running:
) # 提交任务处理
self.executor.submit(
self._process_listen_data, key, new_value, delete_id
)
# 更新最后处理ID
with self.process_lock:
if delete_id > self.last_processed_id:
self.last_processed_id = delete_id
# 添加小延迟,避免过度占用CPU
time.sleep(0.001)
def start_listen_data(self): def start_listen_data(self):
if self.is_running: if self.is_running:
return return
self.is_running = True self.is_running = True
self.last_processed_id = 0 # 重置最后处理ID
self.listen_thread = threading.Thread(target=self.listen) self.listen_thread = threading.Thread(target=self.listen)
self.listen_thread.start() self.listen_thread.start()
@@ -1,13 +1,30 @@
from ...queue_operation import QueueOperation #!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : receive_scheduler.py
@Time : 2025-09-27 17:03:55
@Author : chakcy
@Email : 947105045@qq.com
@description : 接收调度器
"""
from queue_sqlite_core import ShardedQueueOperation
from ...model import MessageItem from ...model import MessageItem
from typing import Callable from typing import Callable
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import threading import threading
import time import time
import logging
from collections import namedtuple
SendMessageCallback = namedtuple("send_message_callback", ["message", "callback"])
class ReceiveScheduler: class ReceiveScheduler:
def __init__(self, queue_operation: QueueOperation, receive_thread_num: int = 1): def __init__(
self, queue_operation: ShardedQueueOperation, receive_thread_num: int = 1
):
self.callbacks = dict() self.callbacks = dict()
self.receive_thread_num = receive_thread_num self.receive_thread_num = receive_thread_num
self.is_running = False self.is_running = False
@@ -20,12 +37,26 @@ class ReceiveScheduler:
def send_message(self, message: MessageItem, callback: Callable = None): # type: ignore def send_message(self, message: MessageItem, callback: Callable = None): # type: ignore
if callback is None: if callback is None:
callback = lambda message: print(message) callback = lambda message: logging.info(f"receive message: {message.id}")
# self.queue_operation.enqueue(message.to_dict())
self.queue_operation.enqueue(message.to_dict_by_core()) self.queue_operation.enqueue(message.to_dict_by_core())
with self.lock: with self.lock:
self.callbacks[message.id] = callback self.callbacks[message.id] = callback
def send_message_batch(self, message_callback_list: list[SendMessageCallback]):
message_list = [
message_callback.message.to_dict_by_core()
for message_callback in message_callback_list
]
if message_list:
self.queue_operation.enqueue_batch(message_list)
with self.lock:
for message_callback in message_callback_list:
message = message_callback.message
callback = message_callback.callback
if callback is None:
callback = lambda m: logging.info(f"receive message: {m.id}")
self.callbacks[message.id] = callback
def receive_message(self): def receive_message(self):
"""单一轮询线程,并行执行回调""" """单一轮询线程,并行执行回调"""
@@ -49,13 +80,8 @@ class ReceiveScheduler:
"""安全执行回调函数""" """安全执行回调函数"""
try: try:
callback(message) callback(message)
# with ThreadPoolExecutor(max_workers=1) as executor:
# future = executor.submit(callback, message)
# future.result(timeout=5.0)
# except TimeoutError:
# print(f"回调执行超时: {message.id}")
except Exception as e: except Exception as e:
print(f"回调执行错误: {str(e)}") logging.error(f"回调执行错误: {str(e)}")
finally: finally:
self.queue_operation.delete_message(message.id) self.queue_operation.delete_message(message.id)
@@ -1,4 +1,16 @@
from ...queue_operation import QueueOperation #!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : task_scheduler.py
@Time : 2025-09-27 17:04:09
@Author : chakcy
@Email : 947105045@qq.com
@description : 任务调度器
"""
import logging
from queue_sqlite_core import ShardedQueueOperation
from ...model import MessageItem from ...model import MessageItem
from ...constant import MessageStatus from ...constant import MessageStatus
from ...cycle.task_cycle import TaskCycle from ...cycle.task_cycle import TaskCycle
@@ -7,10 +19,16 @@ from concurrent.futures import ThreadPoolExecutor
from datetime import datetime from datetime import datetime
import time import time
import threading import threading
import multiprocessing
import json
class TaskScheduler: class TaskScheduler:
def __init__(self, queue_operation: QueueOperation, task_thread_num: int = 2): def __init__(
self,
queue_operation: ShardedQueueOperation,
task_thread_num: int = multiprocessing.cpu_count() * 2,
):
self.task_thread_num = task_thread_num self.task_thread_num = task_thread_num
self.is_running = False self.is_running = False
self.executor = ThreadPoolExecutor(max_workers=task_thread_num) # 并行执行任务 self.executor = ThreadPoolExecutor(max_workers=task_thread_num) # 并行执行任务
@@ -46,10 +64,10 @@ class TaskScheduler:
def _update_result(self, message): def _update_result(self, message):
"""更新任务结果到数据库""" """更新任务结果到数据库"""
try: try:
self.queue_operation.update_result(message.id, message.result) self.queue_operation.update_result(message.id, json.dumps(message.result))
self.queue_operation.update_status(message.id, message.status) self.queue_operation.update_status(message.id, message.status)
except Exception as e: except Exception as e:
print(f"任务结果更新失败: {str(e)}") logging.error(f"任务结果更新失败: {str(e)}")
def task_callback(self): def task_callback(self):
"""单一轮询线程,并行执行任务""" """单一轮询线程,并行执行任务"""
@@ -68,7 +86,7 @@ class TaskScheduler:
else: else:
time.sleep(0.1) # 适当休眠 time.sleep(0.1) # 适当休眠
except Exception as e: except Exception as e:
print(f"任务调度错误: {str(e)}") logging.error(f"任务调度错误: {str(e)}")
time.sleep(1) time.sleep(1)
def start_task_thread(self): def start_task_thread(self):
+290 -14
View File
@@ -2,6 +2,21 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.12" version = "0.8.12"
@@ -35,12 +50,36 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.19.0" version = "3.19.0"
@@ -76,23 +115,41 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "core"
version = "0.1.0"
dependencies = [
"chrono",
"pyo3",
"r2d2",
"r2d2_sqlite",
"rusqlite",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "fallible-iterator" name = "fallible-iterator"
version = "0.3.0" version = "0.3.0"
@@ -105,6 +162,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.3" version = "0.3.3"
@@ -114,9 +181,15 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi",
"wasi", "wasi 0.14.2+wasi-0.2.4",
] ]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.5" version = "0.14.5"
@@ -171,6 +244,23 @@ version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "io-uring"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@@ -214,6 +304,12 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.9.1" version = "0.9.1"
@@ -223,6 +319,26 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -232,6 +348,15 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -261,6 +386,12 @@ dependencies = [
"windows-targets", "windows-targets",
] ]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@@ -353,6 +484,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "queue_sqlite_core"
version = "0.2.1"
dependencies = [
"chrono",
"pyo3",
"r2d2",
"r2d2_sqlite",
"rand",
"rusqlite",
"serde",
"serde_json",
"sha2",
"thiserror",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.40" version = "1.0.40"
@@ -442,12 +591,24 @@ dependencies = [
"smallvec", "smallvec",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]] [[package]]
name = "scheduled-thread-pool" name = "scheduled-thread-pool"
version = "0.2.7" version = "0.2.7"
@@ -463,12 +624,72 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.15.1" version = "1.15.1"
@@ -492,6 +713,46 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
"io-uring",
"libc",
"mio",
"pin-project-lite",
"slab",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@@ -506,9 +767,9 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.17.0" version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"js-sys", "js-sys",
@@ -528,6 +789,12 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.14.2+wasi-0.2.4" version = "0.14.2+wasi-0.2.4"
@@ -654,6 +921,15 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -1,11 +1,11 @@
[package] [package]
name = "core" name = "queue_sqlite_core"
version = "0.1.0" version = "0.2.1"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib] [lib]
name = "core" name = "queue_sqlite_core"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
@@ -13,4 +13,14 @@ chrono = "0.4.41"
pyo3 = "0.25.0" pyo3 = "0.25.0"
r2d2 = "0.8" r2d2 = "0.8"
r2d2_sqlite = "0.25" r2d2_sqlite = "0.25"
rand = "0.9.2"
rusqlite = { version = "0.32", features = ["bundled"] } rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.145"
sha2 = "0.10.9"
thiserror = "2.0.16"
tokio = "1.47.1"
uuid = "1.18.1"
[package.metadata.maturin]
generate-importlib = true
@@ -1,9 +1,5 @@
[build-system]
requires = ["maturin>=1.9,<2.0"]
build-backend = "maturin"
[project] [project]
name = "core" name = "queue_sqlite_core"
requires-python = ">=3.7" requires-python = ">=3.7"
classifiers = [ classifiers = [
"Programming Language :: Rust", "Programming Language :: Rust",
@@ -11,5 +7,22 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
] ]
dynamic = ["version"] dynamic = ["version"]
description = "Python bindings for queue_sqlite_core Rust library"
dependencies = []
[build-system]
requires = ["maturin>=1.9,<2.0"]
build-backend = "maturin"
[tool.maturin] [tool.maturin]
features = ["pyo3/extension-module"] features = ["pyo3/extension-module"]
[[tool.uv.index]]
default = true
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
[dependency-groups]
dev = [
"twine>=4.0.2",
"maturin>=1.9.4",
]
@@ -0,0 +1,42 @@
from typing import Callable
class QueueOperation:
def __init__(self, queue_path: str): ...
def init_db(self) -> None: ...
def enqueue(self, message: dict) -> str: ...
def enqueue_batch(self, messages: list[dict]) -> list[str]: ...
def dequeue(self, size: int = 1) -> list[dict]: ...
def get_queue_length(self) -> int: ...
def get_completed_messages(self) -> list[dict]: ...
def get_result(self, id: str) -> dict: ...
def update_status(self, id: str, status: str) -> None: ...
def update_result(self, id: str, result: str) -> None: ...
def delete_message(self, id: str) -> None: ...
def clean_expired_messages(self) -> None: ...
def clean_old_messages(self, days: int) -> None: ...
def remove_expired_messages(self, days: int) -> None: ...
class ShardedQueueOperation:
@property
def shard_num(self) -> int: ...
@property
def db_dir(self) -> str: ...
def __init__(self, shard_num: int, queue_name: str): ...
def _get_shard_index(self, message_id: str) -> int: ...
def enqueue(self, message: dict) -> str: ...
def enqueue_batch(self, messages: list[dict]) -> list[str]: ...
def dequeue(self, size: int = 1) -> list[dict]: ...
def get_queue_length(self) -> int: ...
def get_completed_messages(self) -> list[dict]: ...
def get_result(self, id: str) -> dict: ...
def update_status(self, id: str, status: str) -> None: ...
def update_result(self, id: str, result: str) -> None: ...
def delete_message(self, id: str) -> None: ...
def clean_expired_messages(self) -> None: ...
def clean_old_messages(self, days: int) -> None: ...
def remove_expired_messages(self, days: int) -> None: ...
class TaskMounter:
def __init__(self, task_mounter_class: type): ...
def get_task_list(self) -> list[str]: ...
def get_task_function(self, task_name: str) -> Callable: ...
@@ -2,12 +2,14 @@ mod queue_operation;
mod task_mounter; mod task_mounter;
use pyo3::prelude::*; use pyo3::prelude::*;
use queue_operation::QueueOperation; use queue_operation::{QueueOperation, ShardedQueueOperation};
use task_mounter::TaskMounter; // 导入结构体 // 导入结构体 use task_mounter::TaskMounter; // 导入结构体 // 导入结构体
#[pymodule] #[pymodule]
fn core(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // #[pymodule(gil_used = false)]
fn queue_sqlite_core(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<TaskMounter>()?; m.add_class::<TaskMounter>()?;
m.add_class::<QueueOperation>()?; m.add_class::<QueueOperation>()?;
m.add_class::<ShardedQueueOperation>()?;
Ok(()) Ok(())
} }
@@ -0,0 +1,936 @@
// src/lib.rs
use chrono::{DateTime, Duration, Utc};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rand::seq::SliceRandom;
use rusqlite::{params, Connection, Row};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex};
/// 单个分片的队列操作
#[pyclass]
pub struct QueueOperation {
pool: Pool<SqliteConnectionManager>,
}
#[derive(Debug, Serialize, Deserialize)]
struct Message {
id: String,
message_type: String,
status: i32,
content: String,
createtime: String,
updatetime: String,
result: Option<String>,
priority: i32,
source: String,
destination: String,
retry_count: i32,
expire_time: Option<String>,
tags: Option<String>,
metadata: Option<String>,
is_deleted: i32,
}
impl TryFrom<&Row<'_>> for Message {
type Error = rusqlite::Error;
fn try_from(row: &Row) -> Result<Self, Self::Error> {
Ok(Message {
id: row.get("id")?,
message_type: row.get("type")?,
status: row.get("status")?,
content: row.get("content")?,
createtime: row.get("createtime")?,
updatetime: row.get("updatetime")?,
result: row.get("result")?,
priority: row.get("priority")?,
source: row.get("source")?,
destination: row.get("destination")?,
retry_count: row.get("retry_count")?,
expire_time: row.get("expire_time")?,
tags: row.get("tags")?,
metadata: row.get("metadata")?,
is_deleted: row.get("is_deleted")?,
})
}
}
#[pymethods]
impl QueueOperation {
#[new]
pub fn new(queue_path: String) -> PyResult<Self> {
let manager = SqliteConnectionManager::file(queue_path);
let pool = Pool::new(manager)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(QueueOperation { pool })
}
/// 初始化数据库和表
/// Args:
/// - self: QueueOperation 实例
/// Returns:
/// - None
pub fn init_db(&self) -> PyResult<()> {
let conn = self.get_connection()?;
// 创建消息表
conn.execute(
"CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
status INTEGER NOT NULL,
content TEXT NOT NULL,
createtime DATETIME NOT NULL,
updatetime DATETIME NOT NULL,
result TEXT,
priority INTEGER NOT NULL,
source TEXT NOT NULL,
destination TEXT NOT NULL,
retry_count INTEGER NOT NULL,
expire_time DATETIME,
tags TEXT,
metadata TEXT,
is_deleted INTEGER NOT NULL DEFAULT 0
)",
[],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
// 创建索引
let indexes = [
"CREATE INDEX IF NOT EXISTS idx_status ON messages(status)",
"CREATE INDEX IF NOT EXISTS idx_priority ON messages(priority)",
"CREATE INDEX IF NOT EXISTS idx_dequeue ON messages(status, priority DESC, createtime ASC) WHERE is_deleted = 0 AND status = 0",
"CREATE INDEX IF NOT EXISTS idx_expire_time ON messages(expire_time) WHERE expire_time IS NOT NULL",
];
for index_sql in indexes.iter() {
conn.execute(index_sql, [])
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
}
// 设置 SQLite 优化参数
let pragmas = [
"PRAGMA journal_mode=WAL",
"PRAGMA synchronous=NORMAL",
"PRAGMA cache_size=-20000",
"PRAGMA mmap_size=134217728",
"PRAGMA temp_store=MEMORY",
"PRAGMA busy_timeout=3000",
];
for pragma in pragmas.iter() {
conn.execute_batch(pragma)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
}
Ok(())
}
/// 入队
/// Args:
/// - self: QueueOperation 实例
/// - message: PyDict 实例,包含消息内容
/// Returns:
/// - String: 消息ID
pub fn enqueue(&self, message: &Bound<'_, PyDict>) -> PyResult<String> {
let mut conn = self.get_connection()?;
let required_fields = [
"id",
"type",
"status",
"content",
"createtime",
"updatetime",
"priority",
"source",
"destination",
"retry_count",
];
for field in required_fields.iter() {
if !message.contains(field)? {
return Err(PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!(
"Missing required field: {}",
field
)));
}
}
let tx = conn
.transaction()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
// 从PyDict获取值
let id: String = message.get_item("id")?.unwrap().extract()?;
let message_type: String = message.get_item("type")?.unwrap().extract()?;
let status: i32 = message.get_item("status")?.unwrap().extract()?;
let content: String = message.get_item("content")?.unwrap().extract()?;
let createtime: String = message.get_item("createtime")?.unwrap().extract()?;
let updatetime: String = message.get_item("updatetime")?.unwrap().extract()?;
let priority: i32 = message.get_item("priority")?.unwrap().extract()?;
let source: String = message.get_item("source")?.unwrap().extract()?;
let destination: String = message.get_item("destination")?.unwrap().extract()?;
let retry_count: i32 = message.get_item("retry_count")?.unwrap().extract()?;
// 可选字段
let result: Option<String> = match message.get_item("result")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let expire_time: Option<String> = match message.get_item("expire_time")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let tags: Option<String> = match message.get_item("tags")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let metadata: Option<String> = match message.get_item("metadata")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
tx.execute(
"INSERT INTO messages (
id, type, status, content, createtime, updatetime, result,
priority, source, destination, retry_count, expire_time, tags, metadata
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![
id,
message_type,
status,
content,
createtime,
updatetime,
result,
priority,
source,
destination,
retry_count,
expire_time,
tags,
metadata,
],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
tx.commit()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(id)
}
pub fn enqueue_batch(&self, messages: Vec<Bound<'_, PyDict>>) -> PyResult<Vec<String>> {
let mut conn = self.get_connection()?;
let tx = conn
.transaction()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut inserted_ids = Vec::new();
{
let mut stmt = tx
.prepare(
"INSERT INTO messages (
id, type, status, content, createtime, updatetime, result,
priority, source, destination, retry_count, expire_time, tags, metadata
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
for message in messages {
let id: String = message.get_item("id")?.unwrap().extract()?;
let message_type: String = message.get_item("type")?.unwrap().extract()?;
let status: i32 = message.get_item("status")?.unwrap().extract()?;
let content: String = message.get_item("content")?.unwrap().extract()?;
let createtime: String = message.get_item("createtime")?.unwrap().extract()?;
let updatetime: String = message.get_item("updatetime")?.unwrap().extract()?;
let priority: i32 = message.get_item("priority")?.unwrap().extract()?;
let source: String = message.get_item("source")?.unwrap().extract()?;
let destination: String = message.get_item("destination")?.unwrap().extract()?;
let retry_count: i32 = message.get_item("retry_count")?.unwrap().extract()?;
let result: Option<String> = match message.get_item("result")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let expire_time: Option<String> = match message.get_item("expire_time")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let tags: Option<String> = match message.get_item("tags")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
let metadata: Option<String> = match message.get_item("metadata")? {
Some(item) => {
if item.is_none() {
None
} else {
Some(item.extract()?)
}
}
None => None,
};
stmt.execute(params![
id,
message_type,
status,
content,
createtime,
updatetime,
result,
priority,
source,
destination,
retry_count,
expire_time,
tags,
metadata,
])
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
inserted_ids.push(id);
}
}
tx.commit()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(inserted_ids)
}
/// 出队
/// Args:
/// - self: QueueOperation 实例
/// - size: i32, 获取消息数量
/// Returns:
/// - Vec<HashMap<String, String>>: 获取的消息列表
pub fn dequeue(&self, size: i32) -> PyResult<Vec<HashMap<String, String>>> {
let limited_size = std::cmp::min(size, 500);
let conn = self.get_connection()?;
let now = Utc::now().to_rfc3339();
let mut stmt = conn
.prepare(
"UPDATE messages
SET status = 1, updatetime = ?1
WHERE id IN (
SELECT id FROM messages
WHERE is_deleted = 0
AND status = 0
AND (expire_time IS NULL OR expire_time > ?1)
ORDER BY priority DESC, createtime ASC
LIMIT ?2
)
RETURNING *",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let rows = stmt
.query_map(params![now, limited_size], |row| {
let message: Message = row.try_into()?;
let mut map = HashMap::new();
map.insert("id".to_string(), message.id);
map.insert("type".to_string(), message.message_type);
map.insert("status".to_string(), message.status.to_string());
map.insert("content".to_string(), message.content);
map.insert("createtime".to_string(), message.createtime);
map.insert("updatetime".to_string(), message.updatetime);
map.insert("result".to_string(), message.result.unwrap_or_default());
map.insert("priority".to_string(), message.priority.to_string());
map.insert("source".to_string(), message.source);
map.insert("destination".to_string(), message.destination);
map.insert("retry_count".to_string(), message.retry_count.to_string());
map.insert(
"expire_time".to_string(),
message.expire_time.unwrap_or_default(),
);
map.insert("tags".to_string(), message.tags.unwrap_or_default());
map.insert("metadata".to_string(), message.metadata.unwrap_or_default());
Ok(map)
})
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut result = Vec::new();
for row in rows {
result.push(
row.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?,
);
if result.len() > 1000 {
break;
}
}
Ok(result)
}
/// 获取队列长度
/// Args:
/// - self: QueueOperation 实例
/// Returns:
/// - i32: 队列长度
pub fn get_queue_length(&self) -> PyResult<i32> {
let conn = self.get_connection()?;
let now = Utc::now().to_rfc3339();
let count: i32 = conn
.query_row(
"SELECT COUNT(*) FROM messages
WHERE is_deleted = 0
AND status = 0
AND (expire_time IS NULL OR expire_time > ?1)",
params![now],
|row| row.get(0),
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(count)
}
/// 获取已完成的消息
/// Args:
/// - self: QueueOperation 实例
/// Returns:
/// - Vec<HashMap<String, String>>: 已完成的消息列表
pub fn get_completed_messages(&self) -> PyResult<Vec<HashMap<String, String>>> {
let conn = self.get_connection()?;
let mut stmt = conn
.prepare(
"SELECT * FROM messages
WHERE is_deleted = 0
AND (status = 2 OR status = 3)
LIMIT 10000",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let rows = stmt
.query_map([], |row| {
let message: Message = row.try_into()?;
let mut map = HashMap::new();
map.insert("id".to_string(), message.id);
map.insert("type".to_string(), message.message_type);
map.insert("status".to_string(), message.status.to_string());
map.insert("content".to_string(), message.content);
map.insert("createtime".to_string(), message.createtime);
map.insert("updatetime".to_string(), message.updatetime);
map.insert("result".to_string(), message.result.unwrap_or_default());
map.insert("priority".to_string(), message.priority.to_string());
map.insert("source".to_string(), message.source);
map.insert("destination".to_string(), message.destination);
map.insert("retry_count".to_string(), message.retry_count.to_string());
map.insert(
"expire_time".to_string(),
message.expire_time.unwrap_or_default(),
);
map.insert("tags".to_string(), message.tags.unwrap_or_default());
map.insert("metadata".to_string(), message.metadata.unwrap_or_default());
Ok(map)
})
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut result = Vec::new();
for row in rows {
result.push(
row.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?,
);
if result.len() > 1000 {
break;
}
}
Ok(result)
}
/// 获取消息详情
/// Args:
/// - self: QueueOperation 实例
/// - id: String, 消息ID
/// Returns:
/// - HashMap<String, String>: 消息详情
pub fn get_result(&self, id: String) -> PyResult<HashMap<String, String>> {
let conn = self.get_connection()?;
let mut stmt = conn
.prepare(
"SELECT * FROM messages
WHERE id = ?1
AND (status = 2 OR status = 3)",
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let message: Message = stmt
.query_row(params![id], |row| row.try_into())
.map_err(|e| {
// 更好地处理查询不到结果的情况
if e == rusqlite::Error::QueryReturnedNoRows {
PyErr::new::<pyo3::exceptions::PyValueError, _>(
"Message not found or not completed",
)
} else {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string())
}
})?;
let mut result = HashMap::new();
result.insert("id".to_string(), message.id);
result.insert("type".to_string(), message.message_type);
result.insert("status".to_string(), message.status.to_string());
result.insert("content".to_string(), message.content);
result.insert("createtime".to_string(), message.createtime);
result.insert("updatetime".to_string(), message.updatetime);
result.insert("result".to_string(), message.result.unwrap_or_default());
result.insert("priority".to_string(), message.priority.to_string());
result.insert("source".to_string(), message.source);
result.insert("destination".to_string(), message.destination);
result.insert("retry_count".to_string(), message.retry_count.to_string());
result.insert(
"expire_time".to_string(),
message.expire_time.unwrap_or_default(),
);
result.insert("tags".to_string(), message.tags.unwrap_or_default());
result.insert("metadata".to_string(), message.metadata.unwrap_or_default());
Ok(result)
}
/// 更新消息状态
/// Args:
/// - self: QueueOperation 实例
/// - id: String, 消息ID
/// - status: i32, 新状态
/// Returns:
/// - () : 无返回值
pub fn update_status(&self, id: String, status: i32) -> PyResult<()> {
let conn = self.get_connection()?;
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE messages SET status = ?1, updatetime = ?2 WHERE id = ?3",
params![status, now, id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
/// 更新消息结果
/// Args:
/// - self: QueueOperation 实例
/// - id: String, 消息ID
/// - result: String, 新结果
/// Returns:
/// - () : 无返回值
pub fn update_result(&self, id: String, result: String) -> PyResult<()> {
let conn = self.get_connection()?;
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE messages SET result = ?1, updatetime = ?2 WHERE id = ?3",
params![result, now, id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
/// 删除消息
/// Args:
/// - self: QueueOperation 实例
/// - id: String, 删除的消息ID
/// Returns:
/// - () : 无返回值
pub fn delete_message(&self, id: String) -> PyResult<()> {
let conn = self.get_connection()?;
conn.execute(
"UPDATE messages SET is_deleted = 1 WHERE id = ?1",
params![id],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
/// 清理过期消息
/// Args:
/// - self: QueueOperation 实例
/// Returns:
/// - () : 无返回值
pub fn clean_expired_messages(&self) -> PyResult<()> {
let conn = self.get_connection()?;
let now = Utc::now().to_rfc3339();
conn.execute(
"UPDATE messages SET is_deleted = 1
WHERE is_deleted = 0
AND expire_time IS NOT NULL
AND expire_time < ?1",
params![now],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
/// 清理旧消息
/// Args:
/// - self: QueueOperation 实例
/// - days: i64, 清理旧消息的天数
/// Returns:
/// - () : 无返回值
pub fn clean_old_messages(&self, days: i64) -> PyResult<()> {
let conn = self.get_connection()?;
let cutoff_time = (Utc::now() - Duration::days(days)).to_rfc3339();
conn.execute(
"UPDATE messages SET is_deleted = 1
WHERE is_deleted = 0
AND status IN (2, 3)
AND updatetime < ?1",
params![cutoff_time],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
/// 删除过期消息
/// Args:
/// - self: QueueOperation 实例
/// - days: i64, 删除过期消息的天数
/// Returns:
/// - () : 无返回值
pub fn remove_expired_messages(&self, days: i64) -> PyResult<()> {
let now: DateTime<Utc> = Utc::now();
let expire_time = now - Duration::days(days);
let iso_format = expire_time.to_rfc3339();
let conn = self
.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute(
"
DELETE FROM messages
WHERE is_deleted = 0
AND expire_time IS NOT NULL
AND expire_time < ?1
",
params![iso_format],
)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
conn.execute_batch("VACUUM")
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
}
}
/// 获取数据库连接
impl QueueOperation {
fn get_connection(&self) -> PyResult<r2d2::PooledConnection<SqliteConnectionManager>> {
self.pool
.get()
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))
}
}
/// 分片队列操作
#[pyclass]
pub struct ShardedQueueOperation {
shards: Vec<QueueOperation>,
#[pyo3(get)]
shard_num: usize,
#[pyo3(get)]
db_dir: String,
}
#[pymethods]
impl ShardedQueueOperation {
#[new]
pub fn new(shard_num: usize, queue_name: String) -> PyResult<Self> {
// 创建缓存目录
let db_dir = format!("cache/{}", queue_name);
std::fs::create_dir_all(&db_dir)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
let mut shards = Vec::with_capacity(shard_num);
for i in 0..shard_num {
let db_path = format!("{}/queue_shard_{}.db", db_dir, i);
let queue_op = QueueOperation::new(db_path)?;
queue_op.init_db()?;
shards.push(queue_op);
}
Ok(ShardedQueueOperation {
shards,
shard_num,
db_dir,
})
}
/// 获取分片索引
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message_id: &str, 消息ID
/// Returns:
/// - usize: 分片索引
fn _get_shard_index(&self, message_id: &str) -> usize {
let mut hasher = Sha256::new();
hasher.update(message_id.as_bytes());
let result = hasher.finalize();
// 将哈希值转换为 usize
let hash_bytes: [u8; 8] = result[..8].try_into().unwrap();
let hash_value = u64::from_be_bytes(hash_bytes) as usize;
hash_value % self.shard_num
}
/// 添加消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message: &Bound<'_, PyDict>, 待添加的消息
/// Returns:
/// - String: 添加的消息ID
pub fn enqueue(&self, message: &Bound<'_, PyDict>) -> PyResult<String> {
let message_id_bound = message
.get_item("id")?
.ok_or_else(|| PyErr::new::<pyo3::exceptions::PyKeyError, _>("Missing id field"))?;
let message_id: String = message_id_bound.extract()?;
let shard_index = self._get_shard_index(&message_id);
self.shards[shard_index].enqueue(message)
}
pub fn enqueue_batch(&self, messages: Vec<Bound<'_, PyDict>>) -> PyResult<Vec<String>> {
let mut shard_groups: HashMap<usize, Vec<Bound<'_, PyDict>>> = HashMap::new();
for message in messages {
let message_id_bound = message
.get_item("id")?
.ok_or_else(|| PyErr::new::<pyo3::exceptions::PyKeyError, _>("Missing is field"))?;
let message_id: String = message_id_bound.extract()?;
let shard_index = self._get_shard_index(&message_id);
shard_groups
.entry(shard_index)
.or_insert_with(Vec::new)
.push(message);
}
let mut all_ids = Vec::new();
for (shard_index, shard_messages) in shard_groups {
let shard_ids = self.shards[shard_index].enqueue_batch(shard_messages)?;
all_ids.extend(shard_ids);
}
Ok(all_ids)
}
/// 获取消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// - size: i32, 获取的消息数量
/// Returns:
/// - Vec<HashMap<String, String>>: 获取的消息列表
pub fn dequeue(&self, size: i32) -> PyResult<Vec<HashMap<String, String>>> {
let mut all_messages = Vec::new();
let mut collected = 0;
// 限制单次获取的最大消息数量
let max_size = std::cmp::min(size, 1000);
// 随机轮询分片顺序
let mut shard_order: Vec<usize> = (0..self.shard_num).collect();
let mut rng = rand::thread_rng();
shard_order.shuffle(&mut rng);
for shard_index in shard_order {
if collected >= max_size {
break;
}
let remaining = size - collected;
let shard_messages = self.shards[shard_index].dequeue(remaining)?;
collected += shard_messages.len() as i32;
all_messages.extend(shard_messages);
if all_messages.len() > 1000 {
break;
}
}
Ok(all_messages)
}
/// 获取队列长度
/// Args:
/// - self: ShardedQueueOperation 实例
/// Returns:
/// - i32: 队列长度
pub fn get_queue_length(&self) -> PyResult<i32> {
let mut total = 0;
for shard in &self.shards {
total += shard.get_queue_length()?;
}
Ok(total)
}
/// 获取已完成的消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// Returns:
/// - Vec<HashMap<String, String>>: 已完成的消息列表
pub fn get_completed_messages(&self) -> PyResult<Vec<HashMap<String, String>>> {
let mut all_messages = Vec::new();
for shard in &self.shards {
let shard_messages = shard.get_completed_messages()?;
all_messages.extend(shard_messages);
if all_messages.len() > 1000 {
break;
}
}
Ok(all_messages)
}
/// 获取消息详情
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message_id: String, 消息ID
/// Returns:
/// - HashMap<String, String>: 消息详情
pub fn get_result(&self, message_id: String) -> PyResult<HashMap<String, String>> {
let shard_index = self._get_shard_index(&message_id);
self.shards[shard_index].get_result(message_id)
}
/// 更新消息状态
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message_id: String, 消息ID
/// - status: i32, 新状态
/// Returns:
/// - () : 无返回值
pub fn update_status(&self, message_id: String, status: i32) -> PyResult<()> {
let shard_index = self._get_shard_index(&message_id);
self.shards[shard_index].update_status(message_id, status)
}
/// 更新消息结果
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message_id: String, 消息ID
/// - result: String, 新结果
/// Returns:
/// - () : 无返回值
pub fn update_result(&self, message_id: String, result: String) -> PyResult<()> {
let shard_index = self._get_shard_index(&message_id);
self.shards[shard_index].update_result(message_id, result)
}
/// 删除消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// - message_id: String, 待删除的消息ID
/// Returns:
/// - () : 无返回值
pub fn delete_message(&self, message_id: String) -> PyResult<()> {
let shard_index = self._get_shard_index(&message_id);
self.shards[shard_index].delete_message(message_id)
}
/// 清理过期消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// Returns:
/// - () : 无返回值
pub fn clean_expired_messages(&self) -> PyResult<()> {
for shard in &self.shards {
shard.clean_expired_messages()?;
}
Ok(())
}
/// 清理旧消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// - days: i64, 清理旧消息的天数
/// Returns:
/// - () : 无返回值
pub fn remove_expired_messages(&self, days: i64) -> PyResult<()> {
for shard in &self.shards {
shard.remove_expired_messages(days)?;
}
Ok(())
}
/// 清理旧消息
/// Args:
/// - self: ShardedQueueOperation 实例
/// - days: i64, 清理旧消息的天数
/// Returns:
/// - () : 无返回值
pub fn clean_old_messages(&self, days: i64) -> PyResult<()> {
for shard in &self.shards {
shard.clean_old_messages(days)?;
}
Ok(())
}
}
+1432
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
from .example import async_example
__all__ = ["async_example"]
+19
View File
@@ -0,0 +1,19 @@
from queue_sqlite.mounter import task
from queue_sqlite.model import MessageItem
@task(meta={"task_name": "test"})
async def async_example(message_item: MessageItem):
def fibonacci_generator():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 示例:获取前10项
fib = fibonacci_generator()
message_item.result = {"fibonacci": [next(fib) for _ in range(500)]}
return message_item.to_json()
# 输出:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
+4 -3
View File
@@ -1,5 +1,6 @@
from queue_sqlite.mounter.listen_mounter import ListenMounter from queue_sqlite.mounter.listen_mounter import listener
@ListenMounter.listener()
@listener()
def key_1(data): def key_1(data):
print(data) print(data)
+3 -3
View File
@@ -1,8 +1,8 @@
from queue_sqlite.mounter.task_mounter import TaskMounter from queue_sqlite.mounter import task
from queue_sqlite.model import MessageItem from queue_sqlite.model import MessageItem
@TaskMounter.task(meta={"task_name": "test"}) @task(meta={"task_name": "test"})
def example(message_item: MessageItem): def example(message_item: MessageItem):
def fibonacci_generator(): def fibonacci_generator():
a, b = 0, 1 a, b = 0, 1
@@ -16,4 +16,4 @@ def example(message_item: MessageItem):
return message_item.to_json() return message_item.to_json()
# 输出:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34] # 输出:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
+74
View File
@@ -0,0 +1,74 @@
from queue_sqlite.model import MessageItem
from queue_sqlite.scheduler import QueueScheduler
import time
from async_taks import *
import threading
import psutil
class TestAsyncScheduler:
callback_counter = 0
lock = threading.Lock()
is_monitoring = False
@classmethod
def monitor_resources(cls):
"""监控资源使用情况"""
while cls.is_monitoring:
cpu = psutil.cpu_percent()
memory = psutil.virtual_memory().percent
print(f"CPU使用率: {cpu}%, 内存使用率: {memory}%")
time.sleep(0.5)
@classmethod
async def _callback(cls, message_item: MessageItem):
with cls.lock:
cls.callback_counter += 1
# if cls.callback_counter % 100 == 0: # 每100个任务打印一次
# print(f"callback: {cls.callback_counter}")
# print(f"callback: {message_item}")
# print(f"callback: {message_item.id}")
# print(f"callback: {message_item.expire_time}")
# print(f"callback: {message_item.tags}")
@classmethod
def test_async_scheduler(cls):
"""测试异步调度器"""
TASK_NUM = 10000
messages = []
for i in range(TASK_NUM):
message_item = MessageItem(content={"num": i}, destination="async_example")
messages.append(message_item)
cls.is_monitoring = True
monitor_thread = threading.Thread(target=cls.monitor_resources, daemon=True)
monitor_thread.start()
queue_scheduler = QueueScheduler(
scheduler_type="async",
)
start_time = time.perf_counter()
queue_scheduler.start()
for message_item in messages:
message_item: MessageItem
queue_scheduler.send_message(message_item, cls._callback)
while (
queue_scheduler.queue_operation.get_queue_length() > 0
or cls.callback_counter < TASK_NUM
): # 30秒超时
time.sleep(0.5)
print(f"当前队列长度:{queue_scheduler.queue_operation.get_queue_length()}")
print(f"当前回调数量:{cls.callback_counter}")
queue_scheduler.stop()
print(f"当前队列长度:{queue_scheduler.queue_operation.get_queue_length()}")
print(f"测试函数:斐波那契数列")
print(f"次数:{TASK_NUM}")
print(f"总耗时: {time.perf_counter() - start_time:.2f}秒")
cls.is_monitoring = False
monitor_thread.join(timeout=1)
+4 -6
View File
@@ -1,5 +1,5 @@
from queue_sqlite.core import core import queue_sqlite_core as core
from queue_sqlite.mounter import TaskMounter from queue_sqlite.mounter.task_mounter import TaskMounter
from queue_sqlite.model import MessageItem from queue_sqlite.model import MessageItem
@@ -11,14 +11,12 @@ class TestCore:
task_mounter = core.TaskMounter(TaskMounter) task_mounter = core.TaskMounter(TaskMounter)
task_mounter.get_task_list() task_mounter.get_task_list()
print( print(
task_mounter.get_task_function("task")({"num": 1})(lambda x: x + 1).meta[ f"task: {task_mounter.get_task_function("task")({"num": 1})(lambda x: x + 1).meta}"
"num"
]
) )
from tasks import example from tasks import example
task_mounter.get_task_list() task_mounter.get_task_list()
print(task_mounter.get_task_function("<lambda>")) print(task_mounter.get_task_function("<lambda>")(1))
@classmethod @classmethod
def test_queue_operation_init_db(cls): def test_queue_operation_init_db(cls):
+6 -3
View File
@@ -5,7 +5,7 @@ import time
class TestListen: class TestListen:
def test_listen_data(self): def test_listen_data(self):
scheduler = QueueScheduler() scheduler = QueueScheduler(scheduler_type="qt")
scheduler.start() scheduler.start()
scheduler.update_listen_data("key_1", "value_1") scheduler.update_listen_data("key_1", "value_1")
time.sleep(0.001) time.sleep(0.001)
@@ -16,5 +16,8 @@ class TestListen:
scheduler.update_listen_data("key_1", "value_4") scheduler.update_listen_data("key_1", "value_4")
time.sleep(0.001) time.sleep(0.001)
scheduler.stop() scheduler.stop()
print(scheduler.get_listen_datas()) # print(scheduler.get_listen_datas())
print(scheduler.get_listen_data("key_1")) # print(scheduler.get_listen_data("key_1"))
# TestListen().test_listen_data()
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""
@File : test_pyside6_scheduler.py
@Time : 2025-10-26 10:40:00
@Author : chakcy
@description : 独立 PySide6 调度器测试
"""
import time
from queue_sqlite.scheduler import QueueScheduler
from queue_sqlite.model import MessageItem
from queue_sqlite.mounter import task
@task(meta={"task_name": "pyside6_example"})
def pyside6_example_task(message_item: MessageItem):
"""PySide6 调度器测试任务"""
print(f"处理任务: {message_item.id}")
# 模拟一些工作
result = sum(i * i for i in range(1000))
message_item.result = {
"status": "completed",
"result": result,
"task_id": message_item.id,
}
return message_item
@task(meta={"task_name": "async_pyside6_task"})
async def async_pyside6_task(message_item: MessageItem):
"""异步任务测试"""
import asyncio
print(f"处理异步任务: {message_item.id}")
await asyncio.sleep(0.1) # 模拟异步操作
message_item.result = {"status": "async_completed", "task_id": message_item.id}
return message_item
def task_callback(message_item: MessageItem):
"""任务完成回调"""
print(f"任务完成回调: {message_item.id}, 结果: {message_item.result}")
def test_pyside6_scheduler():
"""测试独立的 PySide6 调度器"""
# 创建独立的 PySide6 调度器
scheduler = QueueScheduler(
scheduler_type="qt", # 指定使用 PySide6 调度器
)
print("启动 qt 调度器...")
scheduler.start()
try:
# 发送同步任务
for i in range(5):
message = MessageItem(
content={"task_index": i, "type": "sync"},
destination="pyside6_example_task",
)
scheduler.send_message(message, task_callback)
print(f"发送同步消息: {message.id}")
# 发送异步任务
for i in range(5):
message = MessageItem(
content={"task_index": i, "type": "async"},
destination="async_pyside6_task",
)
scheduler.send_message(message, task_callback)
print(f"发送异步消息: {message.id}")
# 等待任务处理
print("等待任务处理...")
for i in range(10):
queue_info = scheduler.scheduler.get_queue_info()
print(f"队列状态: {queue_info}")
time.sleep(1)
if queue_info.get("queue_length", 0) == 0:
break
finally:
print("停止 PySide6 调度器...")
scheduler.stop()
print("测试完成")
if __name__ == "__main__":
test_pyside6_scheduler()
+4 -3
View File
@@ -25,6 +25,9 @@ class TestStress:
def _callback(cls, message_item: MessageItem): def _callback(cls, message_item: MessageItem):
with cls.lock: with cls.lock:
cls.callback_counter += 1 cls.callback_counter += 1
# if cls.callback_counter % 100 == 0: # 每100个任务打印一次
# print(f"callback: {cls.callback_counter}")
# print(message_item.id)
# print(f"callback: {message_item}") # print(f"callback: {message_item}")
# print(f"callback: {message_item.id}") # print(f"callback: {message_item.id}")
# print(f"callback: {message_item.expire_time}") # print(f"callback: {message_item.expire_time}")
@@ -53,9 +56,7 @@ class TestStress:
cls.is_monitoring = True cls.is_monitoring = True
monitor_thread = threading.Thread(target=cls.monitor_resources, daemon=True) monitor_thread = threading.Thread(target=cls.monitor_resources, daemon=True)
monitor_thread.start() monitor_thread.start()
queue_scheduler = QueueScheduler( queue_scheduler = QueueScheduler()
receive_thread_num=4, task_thread_num=8, shard_num=12
)
# threads = [] # threads = []
# for message_item in messages: # for message_item in messages:
Generated
+841
View File
@@ -0,0 +1,841 @@
version = 1
revision = 3
requires-python = ">=3.11"
[manifest]
members = [
"queue-sqlite",
"queue-sqlite-core",
]
[[package]]
name = "backports-tarfile"
version = "1.2.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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 = "46.0.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
]
[[package]]
name = "docutils"
version = "0.22.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/c0/89fe6215b443b919cb98a5002e107cb5026854ed1ccb6b5833e0768419d1/docutils-0.22.2.tar.gz", hash = "sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d", size = 2289092, upload-time = "2025-09-20T17:55:47.994Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" },
]
[[package]]
name = "id"
version = "1.5.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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 = "jaraco-classes"
version = "3.4.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
]
[[package]]
name = "jaraco-context"
version = "6.0.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" },
]
[[package]]
name = "jaraco-functools"
version = "4.3.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "more-itertools" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" },
]
[[package]]
name = "jeepney"
version = "0.9.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
[[package]]
name = "keyring"
version = "25.6.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
{ name = "jaraco-classes" },
{ name = "jaraco-context" },
{ name = "jaraco-functools" },
{ name = "jeepney", marker = "sys_platform == 'linux'" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "maturin"
version = "1.9.6"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/35/c3370188492f4c139c7a318f438d01b8185c216303c49c4bc885c98b6afb/maturin-1.9.6.tar.gz", hash = "sha256:2c2ae37144811d365509889ed7220b0598487f1278c2441829c3abf56cc6324a", size = 214846, upload-time = "2025-10-07T12:45:08.408Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/5c/b435418ba4ba2647a1f7a95d53314991b1e556e656ae276dea993c3bce1d/maturin-1.9.6-py3-none-linux_armv6l.whl", hash = "sha256:26e3ab1a42a7145824210e9d763f6958f2c46afb1245ddd0bab7d78b1f59bb3f", size = 8134483, upload-time = "2025-10-07T12:44:44.274Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/1c/8e58eda6601f328b412cdeeaa88a9b6a10e591e2a73f313e8c0154d68385/maturin-1.9.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5263dda3f71feef2e4122baf5c4620e4b3710dbb7f2121f85a337182de214369", size = 15776470, upload-time = "2025-10-07T12:44:47.476Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/33/8c967cce6848cdd87a2e442c86120ac644b80c5ed4c32e3291bde6a17df8/maturin-1.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe78262c2800c92f67d1ce3c0f6463f958a692cc67bfb572e5dbf5b4b696a8ba", size = 8226557, upload-time = "2025-10-07T12:44:49.844Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/bd/3e2675cdc8b7270700ba30c663c852a35694441732a107ac30ebd6878bd8/maturin-1.9.6-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7ab827c6e8c022eb2e1e7fb6deede54549c8460b20ccc2e9268cc6e8cde957a8", size = 8166544, upload-time = "2025-10-07T12:44:51.396Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/1f/a2047ddf2230e700d5f8a13dd4b9af5ce806ad380c32e58105888205926e/maturin-1.9.6-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:0246202377c49449315305209f45c8ecef6e2d6bd27a04b5b6f1ab3e4ea47238", size = 8641010, upload-time = "2025-10-07T12:44:53.658Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/1f/265d63c7aa6faf363d4a3f23396f51bc6b4d5c7680a4190ae68dba25dea2/maturin-1.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f5bac167700fbb6f8c8ed1a97b494522554b4432d7578e11403b894b6a91d99f", size = 7965945, upload-time = "2025-10-07T12:44:55.248Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/ca/a8e61979ccfe080948bcc1bddd79356157aee687134df7fb013050cec783/maturin-1.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7f53d3b1d8396d3fea3e1ee5fd37558bca5719090f3d194ba1c02b0b56327ae3", size = 7978820, upload-time = "2025-10-07T12:44:56.919Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/4a/81b412f8ad02a99801ef19ec059fba0822d1d28fb44cb6a92e722f05f278/maturin-1.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:7f506eb358386d94d6ec3208c003130cf4b69cab26034fc0cbbf8bf83afa4c2e", size = 10452064, upload-time = "2025-10-07T12:44:58.232Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/12/cc96c7a8cb51d8dcc9badd886c361caa1526fba7fa69d1e7892e613b71d4/maturin-1.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2d6984ab690af509f525dbd2b130714207c06ebb14a5814edbe1e42b17ae0de", size = 8852401, upload-time = "2025-10-07T12:44:59.8Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/8e/653ac3c9f2c25cdd81aefb0a2d17ff140ca5a14504f5e3c7f94dcfe4dbb7/maturin-1.9.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5c2252b0956bb331460ac750c805ddf0d9b44442449fc1f16e3b66941689d0bc", size = 8425057, upload-time = "2025-10-07T12:45:01.711Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/29/f13490328764ae9bfc1da55afc5b707cebe4fa75ad7a1573bfa82cfae0c6/maturin-1.9.6-py3-none-win32.whl", hash = "sha256:f2c58d29ebdd4346fd004e6be213d071fdd94a77a16aa91474a21a4f9dbf6309", size = 7165956, upload-time = "2025-10-07T12:45:03.766Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/9f/dd51e5ac1fce47581b8efa03d77a03f928c0ef85b6e48a61dfa37b6b85a2/maturin-1.9.6-py3-none-win_amd64.whl", hash = "sha256:1b39a5d82572c240d20d9e8be024d722dfb311d330c5e28ddeb615211755941a", size = 8145722, upload-time = "2025-10-07T12:45:05.487Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/f2/e97aaba6d0d78c5871771bf9dd71d4eb8dac15df9109cf452748d2207412/maturin-1.9.6-py3-none-win_arm64.whl", hash = "sha256:ac02a30083553d2a781c10cd6f5480119bf6692fd177e743267406cad2ad198c", size = 6857006, upload-time = "2025-10-07T12:45:06.813Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "more-itertools"
version = "10.8.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
]
[[package]]
name = "nh3"
version = "0.3.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/a6/c6e942fc8dcadab08645f57a6d01d63e97114a30ded5f269dc58e05d4741/nh3-0.3.1.tar.gz", hash = "sha256:6a854480058683d60bdc7f0456105092dae17bef1f300642856d74bd4201da93", size = 18590, upload-time = "2025-10-07T03:27:58.217Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/24/4becaa61e066ff694c37627f5ef7528901115ffa17f7a6693c40da52accd/nh3-0.3.1-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:80dc7563a2a3b980e44b221f69848e3645bbf163ab53e3d1add4f47b26120355", size = 1420887, upload-time = "2025-10-07T03:27:25.654Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/49/16a6ec9098bb9bdf0fb9f09d6464865a3a48858d8d96e779a998ec3bdce0/nh3-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f600ad86114df21efc4a3592faa6b1d099c0eebc7e018efebb1c133376097da", size = 791700, upload-time = "2025-10-07T03:27:27.041Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/cc/1c024d7c23ad031dfe82ad59581736abcc403b006abb0d2785bffa768b54/nh3-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:669a908706cd28203d9cfce2f567575686e364a1bc6074d413d88d456066f743", size = 830225, upload-time = "2025-10-07T03:27:28.315Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/08/4a87f9212373bd77bba01c1fd515220e0d263316f448d9c8e4b09732a645/nh3-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a5721f59afa0ab3dcaa0d47e58af33a5fcd254882e1900ee4a8968692a40f79d", size = 999112, upload-time = "2025-10-07T03:27:29.782Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/cf/94783911eb966881a440ba9641944c27152662a253c917a794a368b92a3c/nh3-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2cb6d9e192fbe0d451c7cb1350dadedbeae286207dbf101a28210193d019752e", size = 1070424, upload-time = "2025-10-07T03:27:31.2Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/44/efb57b44e86a3de528561b49ed53803e5d42cd0441dcfd29b89422160266/nh3-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:474b176124c1b495ccfa1c20f61b7eb83ead5ecccb79ab29f602c148e8378489", size = 996129, upload-time = "2025-10-07T03:27:32.595Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/d3/87c39ea076510e57ee99a27fa4c2335e9e5738172b3963ee7c744a32726c/nh3-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a2434668f4eef4eab17c128e565ce6bea42113ce10c40b928e42c578d401800", size = 980310, upload-time = "2025-10-07T03:27:34.282Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/30/00cfbd2a4d268e8d3bda9d1542ba4f7a20fbed37ad1e8e51beeee3f6fdae/nh3-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:0f454ba4c6aabafcaae964ae6f0a96cecef970216a57335fabd229a265fbe007", size = 584439, upload-time = "2025-10-07T03:27:36.103Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/fa/39d27a62a2f39eb88c2bd50d9fee365a3645e456f3ec483c945a49c74f47/nh3-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:22b9e9c9eda497b02b7273b79f7d29e1f1170d2b741624c1b8c566aef28b1f48", size = 592388, upload-time = "2025-10-07T03:27:37.075Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/39/7df1c4ee13ef65ee06255df8101141793e97b4326e8509afbce5deada2b5/nh3-0.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:42e426f36e167ed29669b77ae3c4b9e185e4a1b130a86d7c3249194738a1d7b2", size = 579337, upload-time = "2025-10-07T03:27:38.055Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/28/a387fed70438d2810c8ac866e7b24bf1a5b6f30ae65316dfe4de191afa52/nh3-0.3.1-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:1de5c1a35bed19a1b1286bab3c3abfe42e990a8a6c4ce9bb9ab4bde49107ea3b", size = 1433666, upload-time = "2025-10-07T03:27:39.118Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/f9/500310c1f19cc80770a81aac3c94a0c6b4acdd46489e34019173b2b15a50/nh3-0.3.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaba26591867f697cffdbc539faddeb1d75a36273f5bfe957eb421d3f87d7da1", size = 819897, upload-time = "2025-10-07T03:27:40.488Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/d4/ebb0965d767cba943793fa8f7b59d7f141bd322c86387a5e9485ad49754a/nh3-0.3.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:489ca5ecd58555c2865701e65f614b17555179e71ecc76d483b6f3886b813a9b", size = 803562, upload-time = "2025-10-07T03:27:41.86Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/9c/df037a13f0513283ecee1cf99f723b18e5f87f20e480582466b1f8e3a7db/nh3-0.3.1-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a25662b392b06f251da6004a1f8a828dca7f429cd94ac07d8a98ba94d644438", size = 1050854, upload-time = "2025-10-07T03:27:43.29Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/9d/488fce56029de430e30380ec21f29cfaddaf0774f63b6aa2bf094c8b4c27/nh3-0.3.1-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38b4872499ab15b17c5c6e9f091143d070d75ddad4a4d1ce388d043ca556629c", size = 1002152, upload-time = "2025-10-07T03:27:44.358Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/4a/24b0118de34d34093bf03acdeca3a9556f8631d4028814a72b9cc5216382/nh3-0.3.1-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48425995d37880281b467f7cf2b3218c1f4750c55bcb1ff4f47f2320a2bb159c", size = 912333, upload-time = "2025-10-07T03:27:45.757Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/0e/16b3886858b3953ef836dea25b951f3ab0c5b5a431da03f675c0e999afb8/nh3-0.3.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94292dd1bd2a2e142fa5bb94c0ee1d84433a5d9034640710132da7e0376fca3a", size = 796945, upload-time = "2025-10-07T03:27:47.169Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/bb/aac139cf6796f2e0fec026b07843cea36099864ec104f865e2d802a25a30/nh3-0.3.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dd6d1be301123a9af3263739726eeeb208197e5e78fc4f522408c50de77a5354", size = 837257, upload-time = "2025-10-07T03:27:48.243Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/d7/1d770876a288a3f5369fd6c816363a5f9d3a071dba24889458fdeb4f7a49/nh3-0.3.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b74bbd047b361c0f21d827250c865ff0895684d9fcf85ea86131a78cfa0b835b", size = 1004142, upload-time = "2025-10-07T03:27:49.278Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/2a/c4259e8b94c2f4ba10a7560e0889a6b7d2f70dce7f3e93f6153716aaae47/nh3-0.3.1-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b222c05ae5139320da6caa1c5aed36dd0ee36e39831541d9b56e048a63b4d701", size = 1075896, upload-time = "2025-10-07T03:27:50.527Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/06/b15ba9fea4773741acb3382dcf982f81e55f6053e8a6e72a97ac91928b1d/nh3-0.3.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:b0d6c834d3c07366ecbdcecc1f4804c5ce0a77fa52ee4653a2a26d2d909980ea", size = 1003235, upload-time = "2025-10-07T03:27:51.673Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/13/74707f99221bbe0392d18611b51125d45f8bd5c6be077ef85575eb7a38b1/nh3-0.3.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:670f18b09f75c86c3865f79543bf5acd4bbe2a5a4475672eef2399dd8cdb69d2", size = 987308, upload-time = "2025-10-07T03:27:53.003Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/81/24bf41a5ce7648d7e954de40391bb1bcc4b7731214238c7138c2420f962c/nh3-0.3.1-cp38-abi3-win32.whl", hash = "sha256:d7431b2a39431017f19cd03144005b6c014201b3e73927c05eab6ca37bb1d98c", size = 591695, upload-time = "2025-10-07T03:27:54.43Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/ca/263eb96b6d32c61a92c1e5480b7f599b60db7d7fbbc0d944be7532d0ac42/nh3-0.3.1-cp38-abi3-win_amd64.whl", hash = "sha256:c0acef923a1c3a2df3ee5825ea79c149b6748c6449781c53ab6923dc75e87d26", size = 600564, upload-time = "2025-10-07T03:27:55.966Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/67/d5e07efd38194f52b59b8af25a029b46c0643e9af68204ee263022924c27/nh3-0.3.1-cp38-abi3-win_arm64.whl", hash = "sha256:a3e810a92fb192373204456cac2834694440af73d749565b4348e30235da7f0b", size = 586369, upload-time = "2025-10-07T03:27:57.234Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/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://pypi.tuna.tsinghua.edu.cn/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 = "psutil"
version = "7.1.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242, upload-time = "2025-09-17T20:14:56.126Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682, upload-time = "2025-09-17T20:14:58.25Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994, upload-time = "2025-09-17T20:14:59.901Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163, upload-time = "2025-09-17T20:15:01.481Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625, upload-time = "2025-09-17T20:15:04.492Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" },
]
[[package]]
name = "pycparser"
version = "2.23"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pyqt5"
version = "5.15.11"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pyqt5-qt5" },
{ name = "pyqt5-sip" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/64/42ec1b0bd72d87f87bde6ceb6869f444d91a2d601f2e67cd05febc0346a1/PyQt5-5.15.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", size = 6579776, upload-time = "2024-07-19T08:39:19.775Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/f5/3fb696f4683ea45d68b7e77302eff173493ac81e43d63adb60fa760b9f91/PyQt5-5.15.11-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", size = 7016415, upload-time = "2024-07-19T08:39:32.977Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/8c/4065950f9d013c4b2e588fe33cf04e564c2322842d84dbcbce5ba1dc28b0/PyQt5-5.15.11-cp38-abi3-manylinux_2_17_x86_64.whl", hash = "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", size = 8188103, upload-time = "2024-07-19T08:39:40.561Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/f0/ae5a5b4f9b826b29ea4be841b2f2d951bcf5ae1d802f3732b145b57c5355/PyQt5-5.15.11-cp38-abi3-win32.whl", hash = "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", size = 5433308, upload-time = "2024-07-19T08:39:46.932Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/d5/68eb9f3d19ce65df01b6c7b7a577ad3bbc9ab3a5dd3491a4756e71838ec9/PyQt5-5.15.11-cp38-abi3-win_amd64.whl", hash = "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", size = 6865864, upload-time = "2024-07-19T08:39:53.572Z" },
]
[[package]]
name = "pyqt5-qt5"
version = "5.15.17"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f9/accb06e76e23fb23053d48cc24fd78dec6ed14cb4d5cbadb0fd4a0c1b02e/PyQt5_Qt5-5.15.17-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8b8094108e748b4bbd315737cfed81291d2d228de43278f0b8bd7d2b808d2b9", size = 39972275, upload-time = "2025-05-24T11:15:42.259Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/1a/e1601ad6934cc489b8f1e967494f23958465cf1943712f054c5a306e9029/PyQt5_Qt5-5.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b68628f9b8261156f91d2f72ebc8dfb28697c4b83549245d9a68195bd2d74f0c", size = 37135109, upload-time = "2025-05-24T11:15:59.786Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/e1/13d25a9ff2ac236a264b4603abaa39fa8bb9a7aa430519bb5f545c5b008d/PyQt5_Qt5-5.15.17-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b018f75d1cc61146396fa5af14da1db77c5d6318030e5e366f09ffdf7bd358d8", size = 61112954, upload-time = "2025-05-24T11:16:26.036Z" },
]
[[package]]
name = "pyqt5-sip"
version = "12.17.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/08/88a20c862f40b5c178c517cdc7e93767967dec5ac1b994e226d517991c9b/pyqt5_sip-12.17.1.tar.gz", hash = "sha256:0eab72bcb628f1926bf5b9ac51259d4fa18e8b2a81d199071135458f7d087ea8", size = 104136, upload-time = "2025-10-08T09:04:19.893Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/e4/451e465c75584a7cbd10e10404317b7443af83f56a64e02080b1f3cda5b5/pyqt5_sip-12.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5134d637efadd108a70306bab55b3d7feaa951bf6b8162161a67ae847bea9130", size = 122581, upload-time = "2025-10-08T09:04:13.607Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/b2/330f97434b21fbc99ab16f6ce71358ff5ea1bf1f09ed14dfe6b28b5ed8f5/pyqt5_sip-12.17.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:155cf755266c8bf64428916e2ff720d5efa1aec003d4ccc40c003b147dbdac03", size = 276844, upload-time = "2025-10-08T09:15:33.713Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/fd/53925099d0fc8aaf7adee613b6cebfb3fdfcd1238add64ff9edf6711e5f8/pyqt5_sip-12.17.1-cp311-cp311-win32.whl", hash = "sha256:9dfa7fe4ac93b60004430699c4bf56fef842a356d64dfea7cbc6d580d0427d6d", size = 49099, upload-time = "2025-10-08T09:11:23.928Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/f8/f47a849c17676557c4220fbce9fcc24e15736af247c4dddcaf9ff0124b57/pyqt5_sip-12.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ddd214cf40119b86942a5da2da5a7345334955ab00026d8dcc56326b30e6d3c", size = 58988, upload-time = "2025-10-08T09:08:34.903Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/15/291f83f336558300626bebb0c403084ec171bbc8a70683e3376234422eb6/pyqt5_sip-12.17.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c362606de782d2d46374a38523632786f145c517ee62de246a6069e5f2c5f336", size = 124521, upload-time = "2025-10-08T09:04:15.825Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/85/ea1ae099260fd1859d71b31f51760b4226abfa778d5796b76d92c8fe6dcd/pyqt5_sip-12.17.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:140cc582151456103ebb149fefc678f3cae803e7720733db51212af5219cd45c", size = 282182, upload-time = "2025-10-08T09:15:35.752Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/b3/d5b50c721651a0f2ccbef6f8db3dabf3db296b9ec239ba007f5615f57dd7/pyqt5_sip-12.17.1-cp312-cp312-win32.whl", hash = "sha256:9dc1f1525d4d42c080f6cfdfc70d78239f8f67b0a48ea0745497251d8d848b1d", size = 49447, upload-time = "2025-10-08T09:11:24.843Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/b6/474d8b17763683ab45fb364f3a44f25fdc25d97b47b29ad8819b95a15ac8/pyqt5_sip-12.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:d5e2e9e175559017cd161d661e0ee0b551684f824bb90800c5a8c8a3bea9355e", size = 57946, upload-time = "2025-10-08T09:08:35.775Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/58/9ecb688050e79ffe7bbd9fc917aa13f63856a5081ac46bbce87bb11ab971/pyqt5_sip-12.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9ebbd7769ccdaaa6295e9c872553b6cde17f38e171056f17300d8af9a14d1fc8", size = 124485, upload-time = "2025-10-08T09:04:17.473Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/9f/ae691360a9f18e3e06fd297e854d7ad175367e35ea184fd2fcf6c79b8c25/pyqt5_sip-12.17.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b023da906a70af2cf5e6fc1932f441ede07530f3e164dd52c6c2bb5ab7c6f424", size = 281923, upload-time = "2025-10-08T09:15:37.004Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/31/491c45423174a359a4b8a8d84a7b541c453f48497ae928cbe4006bcd3e01/pyqt5_sip-12.17.1-cp313-cp313-win32.whl", hash = "sha256:36dbef482bd638786b909f3bda65b7b3d5cbd6cbf16797496de38bae542da307", size = 49400, upload-time = "2025-10-08T09:11:25.769Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/61/e28681dd5200094f7b2e6671e85c02a4d6693da36d23ad7d39ffbc70b15c/pyqt5_sip-12.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:d04e5551bbc3bcec98acc63b3b0618ddcbf31ff107349225b516fe7e7c0a7c8b", size = 57979, upload-time = "2025-10-08T09:08:37.036Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/f9/06c09dc94474ffe3f518f80e47fc69d34abf8e4a971ae7e7c667d6ff30a7/pyqt5_sip-12.17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c49918287e1ad77956d1589f1d3d432a0be7630c646ea02cf652413a48e14458", size = 124400, upload-time = "2025-10-08T08:38:23.927Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/ae/be6e338ea427deac5cd81a93f51ae3fb6505d99d6d5e5d5341bcc099327e/pyqt5_sip-12.17.1-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:944a4bf1e1ee18ad03a54964c1c6433fb6de582313a1f0b17673e7203e22fc83", size = 282291, upload-time = "2025-10-08T08:38:25.735Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/a3/8b758518bd0dd5d1581f7a6d522c9b4d9b58d05087b1d0b4dfaad5376434/pyqt5_sip-12.17.1-cp314-cp314-win32.whl", hash = "sha256:99a2935fd662a67748625b1e6ffa0a2d1f2da068b9df6db04fa59a4a5d4ee613", size = 50578, upload-time = "2025-10-08T08:38:28.72Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/8c/e96f9877548810b1e537f46fc21ba74552dd4e8c498658114a8353bdf659/pyqt5_sip-12.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:aaa33232cc80793d14fdb3b149b27eec0855612ed66aad480add5ac49b9cee63", size = 59763, upload-time = "2025-10-08T08:38:27.443Z" },
]
[[package]]
name = "pyqt6"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pyqt6-qt6" },
{ name = "pyqt6-sip" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/57/48985490c01584e00b70040ec0eb02dfe950471097201acb1b65deb633e5/pyqt6-6.10.0.tar.gz", hash = "sha256:710ecfd720d9a03b2c684881ae37f528e11d17e8f1bf96431d00a6a73f308e36", size = 1079921, upload-time = "2025-10-22T12:04:05.717Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/99/ca0386e84e39e4f2ef30322f1d39d515f2a3efbc542cccb33e65a1cf7c46/pyqt6-6.10.0-1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:54b6b022369e4e6ade8cf79c0f988558839df7b2c285f814b4567d15a0fcb756", size = 37745881, upload-time = "2025-10-24T18:33:56.106Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/48/be73fb730c6f617f456ab73150db384b17a7a08394d7e2ded55f42de8a7b/pyqt6-6.10.0-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:0eb82f152a83a8ae39f7d3ba580829ff7c0e8179d19d70f396853c10c8ddc5ac", size = 59987475, upload-time = "2025-10-22T12:03:47.661Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/31/e55f7b718df92423b5af3f0fe11e770e56d7576c03adc6b6866b0e8beb46/pyqt6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:43e94a0ad4713055b47b4676d23432349845729912e4f3d20ac95935931c5e6f", size = 39028922, upload-time = "2025-10-22T12:03:51.916Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/3d/48465697f530addaeaf41f4d5b3e54108ebabd18ab6da19823de5099e807/pyqt6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:357da0f1465557dde249a31bc1f152320b7628a644e1d55d2db09b635394f39f", size = 40619932, upload-time = "2025-10-22T12:03:56.115Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/e4/0c4745cd0b31c17bd85d5da697404679f4faa6424aa7b57112277c1e8027/pyqt6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:8b5e4ea573733017a76bd12ea1b53351fd7f6dc57f8abf4329c4a41fea6dde04", size = 25816996, upload-time = "2025-10-22T12:04:00.085Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/40/0891706e69c60267b6933465fa299138bafa72324d6855c6962ca05a5f9f/pyqt6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:c2b5fc1a028e95b096f3a5966611cc8194e8e9e69984c41477417e18b5ce1362", size = 25663908, upload-time = "2025-10-22T12:04:03.593Z" },
]
[[package]]
name = "pyqt6-qt6"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/b2/309ba75490d614530f10a5b922555374098706364ec1cc411f98068beb4d/pyqt6_qt6-6.10.0-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:9b48d9eba795a78d21393b1909e7a0406512038ea4667463e4882e284bf95817", size = 68426539, upload-time = "2025-10-22T11:37:22.689Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/20/1de54c91f6f34be6042bdb6913c71e253a6bddf002d8aa78cb1acc67f5a8/pyqt6_qt6-6.10.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:768ced73dbb2c85e6d72ea63fd7409ba934d4b24580ef073a3eb16fd88d3fe70", size = 62509741, upload-time = "2025-10-22T11:37:27.652Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/32/1d84ab75795b0984935e32d2a51200b94328a45954c28360a97d02f1c376/pyqt6_qt6-6.10.0-py3-none-manylinux_2_34_x86_64.whl", hash = "sha256:cd38b281bb7229b8114f13126aacd77f831a97105fdb1d900260a48ead8c9fcd", size = 83787113, upload-time = "2025-10-22T11:37:32.921Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/56/e8daedfb0e4a79e19500b6271958b915389360b39a3e2847961828d2e0b3/pyqt6_qt6-6.10.0-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:497df665091451ab7fde3355e262b243d2a2d97924b899a5cbb193a0cc573b7e", size = 82848172, upload-time = "2025-10-22T11:37:38.367Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/82/bbbee33205574f57b17c25059c6b8726a129961b55a1fe34f9216b4b0a70/pyqt6_qt6-6.10.0-py3-none-win_amd64.whl", hash = "sha256:d11bc03908cacbd2e3426289ad51dcf6d2896af6ffec595626ce9cf9b93e770d", size = 76322424, upload-time = "2025-10-22T11:37:43.865Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b7/fb3172c8e85fdcdc942600dbec3d7ebd8d5224159cd4f32ced6cc43949cc/pyqt6_qt6-6.10.0-py3-none-win_arm64.whl", hash = "sha256:7f3ae89337e905dba436c226027ebd8777d4cdeef386131ad9a1b0a53ecc1a81", size = 56552467, upload-time = "2025-10-22T11:37:48.444Z" },
]
[[package]]
name = "pyqt6-sip"
version = "13.10.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/4a/96daf6c2e4f689faae9bd8cebb52754e76522c58a6af9b5ec86a2e8ec8b4/pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe", size = 92548, upload-time = "2025-05-23T12:26:49.901Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/9c/ea9ba7786f471ce025dff71653eec4a6c067d24d36d28cced457dd31314c/pyqt6_sip-13.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e", size = 110707, upload-time = "2025-05-23T12:26:26.666Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/00/984a94f14ba378c802a8e304803bb6dc6961cd9f24befa1bf3987731f0c3/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd", size = 317301, upload-time = "2025-05-23T12:26:28.182Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/b1/c3b433ebcee2503571d71be025de5dab4489d7153007fd5ae79c543eeedb/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc", size = 294277, upload-time = "2025-05-23T12:26:29.406Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/96/4e909f0a4f7a9ad0076a0e200c10f96a5a09492efb683f3d66c885f9aba4/pyqt6_sip-13.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03", size = 53418, upload-time = "2025-05-23T12:26:30.536Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/96/153c418d8c167fc56f2e62372b8862d577f3ece41b24c5205a05b0c2b0cd/pyqt6_sip-13.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de", size = 44969, upload-time = "2025-05-23T12:26:31.498Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/5b/1240017e0d59575289ba52b58fd7f95e7ddf0ed2ede95f3f7e2dc845d337/pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c", size = 112199, upload-time = "2025-05-23T12:26:32.503Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/11/1fc3bae02a12a3ac8354aa579b56206286e8b5ca9586677b1058c81c2f74/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6", size = 322757, upload-time = "2025-05-23T12:26:33.752Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/40/de9491213f480a27199690616959a17a0f234962b86aa1dd4ca2584e922d/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7", size = 304251, upload-time = "2025-05-23T12:26:35.66Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/21/cc80e03f1052408c62c341e9fe9b81454c94184f4bd8a95d29d2ec86df92/pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e", size = 53519, upload-time = "2025-05-23T12:26:36.797Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/cf/53bd0863252b260a502659cb3124d9c9fe38047df9360e529b437b4ac890/pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92", size = 45349, upload-time = "2025-05-23T12:26:37.729Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/1e/979ea64c98ca26979d8ce11e9a36579e17d22a71f51d7366d6eec3c82c13/pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1", size = 112227, upload-time = "2025-05-23T12:26:38.758Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/21/84c230048e3bfef4a9209d16e56dcd2ae10590d03a31556ae8b5f1dcc724/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca", size = 322920, upload-time = "2025-05-23T12:26:39.856Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/57/74b4eb7a51b9133958daa8409b55de95e44feb694d4e2e3eba81a070ca20/pyqt6_sip-13.10.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8a76a06a8e5c5b1f17a3f6f3c834ca324877e07b960b18b8b9bbfd9c536ec658", size = 112354, upload-time = "2025-10-08T08:44:00.22Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/cb/fdef02e0d6ee8443a9683a43650d61c6474b634b6ae6e1c6f097da6310bf/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9128d770a611200529468397d710bc972f1dcfe12bfcbb09a3ccddcd4d54fa5b", size = 323488, upload-time = "2025-10-08T08:44:01.965Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/5b/8ede8d6234c3ea884cbd097d7d47ff9910fb114efe041af62b4453acd23b/pyqt6_sip-13.10.2-cp314-cp314-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d820a0fae7315932c08f27dc0a7e33e0f50fe351001601a8eb9cf6f22b04562e", size = 303881, upload-time = "2025-10-08T08:44:04.086Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/44/b5e78b072d1594643b0f1ff348f2bf54d4adb5a3f9b9f0989c54e33238d6/pyqt6_sip-13.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:3213bb6e102d3842a3bb7e59d5f6e55f176c80880ff0b39d0dac0cfe58313fb3", size = 55098, upload-time = "2025-10-08T08:44:08.943Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/91/357e9fcef5d830c3d50503d35e0357818aca3540f78748cc214dfa015d00/pyqt6_sip-13.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:ce33ff1f94960ad4b08035e39fa0c3c9a67070bec39ffe3e435c792721504726", size = 46088, upload-time = "2025-10-08T08:44:10.014Z" },
]
[[package]]
name = "pyside6"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pyside6-addons" },
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/98/84b16f78b5d92dd234fb1eb9890a350a5b0c83d985bb8c44a92f813a2d02/pyside6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:c2cbc5dc2a164e3c7c51b3435e24203e90e5edd518c865466afccbd2e5872bb0", size = 558115, upload-time = "2025-10-08T09:47:09.246Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/76/0961c8c5653ecb60a6881b649dcb6b71a6be5bd1c8d441ecc48ac7f50b1a/pyside6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ae8c3c8339cd7c3c9faa7cc5c52670dcc8662ccf4b63a6fed61c6345b90c4c01", size = 557762, upload-time = "2025-10-08T09:47:11.819Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/73/6187502fff8b6599443d15c46dd900b2ded24be5aacb2becce33f6faf566/pyside6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:9f402f883e640048fab246d36e298a5e16df9b18ba2e8c519877e472d3602820", size = 558299, upload-time = "2025-10-08T09:47:14.255Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/67/94794ebaf198bbdb35cb77f19f38370f9b323b036ab149874bc33c38faab/pyside6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:70a8bcc73ea8d6baab70bba311eac77b9a1d31f658d0b418e15eb6ea36c97e6f", size = 564367, upload-time = "2025-10-08T09:47:16.287Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/cc/552331d413c1b933d54ed45e33cc7ff29d0b239677975fe2528e7ac8bfbc/pyside6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:4b709bdeeb89d386059343a5a706fc185cee37b517bda44c7d6b64d5fdaf3339", size = 548826, upload-time = "2025-10-08T09:47:18.399Z" },
]
[[package]]
name = "pyside6-addons"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/23/9fbdec2ce16244ac3fe28e6d44c39c70465c93a03325939a792fd00fde7f/pyside6_addons-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:88e61e21ee4643cdd9efb39ec52f4dc1ac74c0b45c5b7fa453d03c094f0a8a5c", size = 322248256, upload-time = "2025-10-08T09:47:37.844Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/b8/d129210f2c7366b4e1bf5bb6230be42052b29e8ba1b1d7db6ef333cf5a39/pyside6_addons-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:08d4ed46c4c9a353a9eb84134678f8fdd4ce17fb8cce2b3686172a7575025464", size = 170238987, upload-time = "2025-10-08T09:47:51.446Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/ae/ede1edd009395092219f3437d2ee59f9ba93739c28c040542ed47c6cc831/pyside6_addons-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:15d32229d681be0bba1b936c4a300da43d01e1917ada5b57f9e03a387c245ab0", size = 165939425, upload-time = "2025-10-08T09:48:02.073Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/5d/a3c32f85ac7f905c95679967c0ddda0ba043c273b75623cc90d8185064e4/pyside6_addons-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:99d93a32c17c5f6d797c3b90dd58f2a8bae13abde81e85802c34ceafaee11859", size = 164814172, upload-time = "2025-10-08T09:48:12.891Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/2a/4ff71b09571202c8e1320c45276fc1d0cd81ee53107dfc17bb22d4243f88/pyside6_addons-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:92536427413f3b6557cf53f1a515cd766725ee46a170aff57ad2ff1dfce0ffb1", size = 34104251, upload-time = "2025-10-08T09:48:18.287Z" },
]
[[package]]
name = "pyside6-essentials"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "shiboken6" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/55/bad02ab890c8b8101abef0db4a2e5304be78a69e23a438e4d8555b664467/pyside6_essentials-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:003e871effe1f3e5b876bde715c15a780d876682005a6e989d89f48b8b93e93a", size = 105034090, upload-time = "2025-10-08T09:48:24.944Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/75/e17efc7eb900993e0e3925885635c6cf373c817196f09bcbcc102b00ac94/pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1d5e013a8698e37ab8ef360e6960794eb5ef20832a8d562e649b8c5a0574b2d8", size = 76362150, upload-time = "2025-10-08T09:48:31.849Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/62/fbd1e81caafcda97b147c03f5b06cfaadd8da5fa8298f527d2ec648fa5b7/pyside6_essentials-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b1dd0864f0577a448fb44426b91cafff7ee7cccd1782ba66491e1c668033f998", size = 75454169, upload-time = "2025-10-08T09:48:38.21Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/3a/d8211d17e6ca70f641c6ebd309f08ef18930acda60e74082c75875a274da/pyside6_essentials-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:fc167eb211dd1580e20ba90d299e74898e7a5a1306d832421e879641fc03b6fe", size = 74361794, upload-time = "2025-10-08T09:48:44.335Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/e9/0e22e3c10325c4ff09447fadb43f7962afb82cef0b65358f5704251c6b32/pyside6_essentials-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:6dd0936394cb14da2fd8e869899f5e0925a738b1c8d74c2f22503720ea363fb1", size = 55099467, upload-time = "2025-10-08T09:48:50.902Z" },
]
[[package]]
name = "pytest"
version = "8.4.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "qtpy"
version = "2.4.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" },
]
[[package]]
name = "queue-sqlite"
version = "0.2.2"
source = { editable = "." }
dependencies = [
{ name = "queue-sqlite-core" },
]
[package.optional-dependencies]
pyqt5 = [
{ name = "pyqt5" },
{ name = "qtpy" },
]
pyqt6 = [
{ name = "pyqt6" },
{ name = "qtpy" },
]
pyside6 = [
{ name = "pyside6" },
{ name = "qtpy" },
]
[package.dev-dependencies]
dev = [
{ name = "maturin" },
{ name = "psutil" },
{ name = "pytest" },
{ name = "twine" },
]
[package.metadata]
requires-dist = [
{ name = "pyqt5", marker = "extra == 'pyqt5'" },
{ name = "pyqt6", marker = "extra == 'pyqt6'" },
{ name = "pyside6", marker = "extra == 'pyside6'" },
{ name = "qtpy", marker = "extra == 'pyqt5'" },
{ name = "qtpy", marker = "extra == 'pyqt6'" },
{ name = "qtpy", marker = "extra == 'pyside6'" },
{ name = "queue-sqlite-core", editable = "src/queue_sqlite_core" },
]
provides-extras = ["pyqt5", "pyqt6", "pyside6"]
[package.metadata.requires-dev]
dev = [
{ name = "maturin", specifier = ">=1.9.4" },
{ name = "psutil", specifier = ">=7.0.0" },
{ name = "pytest", specifier = ">=7.4.4" },
{ name = "twine", specifier = ">=4.0.2" },
]
[[package]]
name = "queue-sqlite-core"
source = { editable = "src/queue_sqlite_core" }
[package.dev-dependencies]
dev = [
{ name = "maturin" },
{ name = "twine" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
{ name = "maturin", specifier = ">=1.9.4" },
{ name = "twine", specifier = ">=4.0.2" },
]
[[package]]
name = "readme-renderer"
version = "44.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "docutils" },
{ name = "nh3" },
{ name = "pygments" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
[[package]]
name = "rfc3986"
version = "2.0.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" },
]
[[package]]
name = "rich"
version = "14.2.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
]
[[package]]
name = "secretstorage"
version = "3.4.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "jeepney" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" },
]
[[package]]
name = "shiboken6"
version = "6.10.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/78/3e730aea82089dd82b1e092bc265778bda329459e6ad9b7134eec5fff3f2/shiboken6-6.10.0-cp39-abi3-macosx_13_0_universal2.whl", hash = "sha256:7a5f5f400ebfb3a13616030815708289c2154e701a60b9db7833b843e0bee543", size = 476535, upload-time = "2025-10-08T09:49:08Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/09/4ffa3284a17b6b765d45b41c9a7f1b2cde6c617c853ac6f170fb62bbbece/shiboken6-6.10.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e612734da515d683696980107cdc0396a3ae0f07b059f0f422ec8a2333810234", size = 271098, upload-time = "2025-10-08T09:49:09.47Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/29/00e26f33a0fb259c2edce9c761a7a438d7531ca514bdb1a4c072673bd437/shiboken6-6.10.0-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:b01377e68d14132360efb0f4b7233006d26aa8ae9bb50edf00960c2a5f52d148", size = 267698, upload-time = "2025-10-08T09:49:10.694Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/30/e4624a7e3f0dc9796b701079b77defcce0d32d1afc86bb1d0df04bc3d9e2/shiboken6-6.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:0bc5631c1bf150cbef768a17f5f289aae1cb4db6c6b0c19b2421394e27783717", size = 1234227, upload-time = "2025-10-08T09:49:12.774Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/e5/0ab862005ea87dc8647ba958a3099b3b0115fd6491c65da5c5a0f6364db1/shiboken6-6.10.0-cp39-abi3-win_arm64.whl", hash = "sha256:dfc4beab5fec7dbbebbb418f3bf99af865d6953aa0795435563d4cbb82093b61", size = 1794775, upload-time = "2025-10-08T09:49:14.641Z" },
]
[[package]]
name = "twine"
version = "6.2.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "id" },
{ name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" },
{ name = "packaging" },
{ name = "readme-renderer" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "rfc3986" },
{ name = "rich" },
{ name = "urllib3" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]