Compare commits
19
Commits
2a516ec5a3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793c842bbf | ||
|
|
14b3ea707f | ||
|
|
45bae10ceb | ||
|
|
38c0b6cea3 | ||
|
|
bfa97e760e | ||
|
|
45817e5542 | ||
|
|
05ed2745b0 | ||
|
|
8745db8ab1 | ||
|
|
fa5db8e573 | ||
|
|
ac67b60d14 | ||
|
|
58f4ad84ee | ||
|
|
6bd1e28815 | ||
|
|
c6bcb64c61 | ||
|
|
0c8c6ffbc2 | ||
|
|
e6291fe3a4 | ||
|
|
cfc4028a51 | ||
|
|
bcb3304338 | ||
|
|
80b1c89014 | ||
|
|
7189d6e2a4 |
+9
-1
@@ -5,12 +5,12 @@ build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
.python-version
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
output.md
|
||||
.arts
|
||||
uv.lock
|
||||
cache/
|
||||
|
||||
# IDE files
|
||||
@@ -19,3 +19,11 @@ cache/
|
||||
|
||||
# test files
|
||||
.pytest_cache/
|
||||
|
||||
# dynamic library
|
||||
*.so
|
||||
*.dll
|
||||
*.pyd
|
||||
|
||||
# log
|
||||
log/
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.13
|
||||
3.12
|
||||
|
||||
@@ -1,225 +1,346 @@
|
||||
# SQLite 任务队列系统
|
||||
# Queue SQLite - 高性能 SQLite 任务队列系统
|
||||
|
||||
这个项目是一个基于 SQLite 的高性能任务队列系统,使用 Rust 和 Python 混合编程实现。系统提供了任务调度、消息队列管理和任务生命周期管理的完整解决方案。
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 主要特性
|
||||
- 🚀 高性能:使用 Rust 实现核心操作,确保高性能
|
||||
|
||||
- 📚 多分片支持:支持数据库分片,提高并发处理能力
|
||||
一个基于 SQLite 的高性能任务队列系统,采用 Rust 核心操作,支持任务挂载、消息监听、优先级处理、重试机制和自动清理过期消息。适合构建可靠、可扩展的后台任务处理系统。
|
||||
|
||||
- ⏱️ 智能调度:提供接收调度器、任务调度器和清理调度器
|
||||
## 🌟 特性
|
||||
|
||||
- 🔒 任务生命周期管理:支持任务状态跟踪、重试机制和过期处理
|
||||
### 核心优势
|
||||
|
||||
- 📊 监控支持:内置资源监控功能
|
||||
- 🚀 高性能:Rust 核心提供毫秒级任务处理
|
||||
- 💾 持久化存储:基于 SQLite 的可靠消息存储
|
||||
- 🔄 多调度器支持:标准、异步、Qt 三种调度模式
|
||||
- 🎯 智能分片:自动哈希分片,支持横向扩展
|
||||
- 📊 全面监控:内置资源使用监控和队列状态查看
|
||||
|
||||
- 🧩 任务挂载系统:通过装饰器轻松添加新任务
|
||||
### 功能亮点
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
src/
|
||||
├── core/ # Rust 核心实现
|
||||
│ ├── lib.rs # 主模块
|
||||
│ ├── queue_operation.rs # 队列操作实现
|
||||
│ └── task_mounter.rs # 任务挂载实现
|
||||
│
|
||||
├── queue_sqlite/ # Python 实现
|
||||
│ ├── constant/ # 常量定义(消息优先级、状态、类型)
|
||||
│ ├── core/ # 核心接口
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── queue_operation/ # 队列操作封装
|
||||
│ ├── scheduler/ # 调度器实现
|
||||
│ └── task_cycle/ # 任务生命周期管理
|
||||
│
|
||||
tests/ # 测试代码
|
||||
- ✅ 任务装饰器:使用 @task 装饰器轻松注册任务
|
||||
- ✅ 监听装饰器:使用 @listener 装饰器实现数据变更监听
|
||||
- ✅ 优先级队列:支持 LOW/NORMAL/HIGH/URGENT 四级优先级
|
||||
- ✅ 重试机制:可配置的最大重试次数和延迟重试
|
||||
- ✅ 过期清理:自动清理过期和完成的消息
|
||||
- ✅ 批量操作:支持消息批量入队和处理
|
||||
- ✅ 异步支持:原生支持 async/await 异步任务
|
||||
- ✅ Qt 集成:可选 Qt 调度器用于 GUI 应用
|
||||
|
||||
## 📦 安装
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Python 3.11+
|
||||
- Rust 1.65+ (用于编译核心扩展)
|
||||
- SQLite 3.35+
|
||||
|
||||
### 安装方式
|
||||
|
||||
#### 方式一:从源码安装(推荐)
|
||||
|
||||
```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
|
||||
from queue_sqlite.scheduler import QueueScheduler
|
||||
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
|
||||
)
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start_queue_scheduler()
|
||||
# 1. 注册任务
|
||||
@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"
|
||||
)
|
||||
|
||||
# 定义回调函数
|
||||
def callback(message_item):
|
||||
print(f"任务完成: {message_item.id}")
|
||||
# 2. 创建调度器
|
||||
scheduler = QueueScheduler(scheduler_type="standard")
|
||||
|
||||
# 发送消息
|
||||
scheduler.send_message(message, callback)
|
||||
# 3. 启动调度器
|
||||
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",
|
||||
)
|
||||
|
||||
# 停止调度器
|
||||
scheduler.stop_queue_scheduler()
|
||||
def callback(result_message):
|
||||
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
|
||||
# tests/test_stress.py
|
||||
class TestStress:
|
||||
@classmethod
|
||||
def test_stress(cls):
|
||||
TASK_NUM = 10000
|
||||
scheduler = QueueScheduler(receive_thread_num=4, task_thread_num=8, shard_num=12)
|
||||
scheduler.start_queue_scheduler()
|
||||
import asyncio
|
||||
from queue_sqlite.scheduler import QueueScheduler
|
||||
from queue_sqlite.model import MessageItem
|
||||
from queue_sqlite.mounter import task
|
||||
|
||||
# 发送大量任务
|
||||
for i in range(TASK_NUM):
|
||||
message = MessageItem(content={"num": i}, destination="example")
|
||||
scheduler.send_message(message, cls._callback)
|
||||
|
||||
# 等待所有任务完成
|
||||
while scheduler.queue_operation.get_queue_length() > 0:
|
||||
time.sleep(0.5)
|
||||
@task(meta={"name": "async_processor", "max_retries": 2})
|
||||
async def async_data_fetcher(message_item):
|
||||
"""异步数据获取任务"""
|
||||
url = message_item.content["url"]
|
||||
# 模拟异步 HTTP 请求
|
||||
await asyncio.sleep(0.5)
|
||||
return {"url": url, "data": "fetched", "status": 200}
|
||||
|
||||
scheduler.stop_queue_scheduler()
|
||||
|
||||
async def main():
|
||||
scheduler = QueueScheduler(scheduler_type="async")
|
||||
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 3.7+
|
||||
```python
|
||||
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
|
||||
cd sqlite-task-queue
|
||||
```
|
||||
# 更新监听数据(会自动触发监听函数)
|
||||
scheduler.update_listen_data("user_activity_log", "用户登录")
|
||||
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
|
||||
cd src/core
|
||||
maturin develop --release
|
||||
```
|
||||
将会在 `src/core/target/release` 目录下生成 `core.dll` 或 `core.so` 文件。
|
||||
将该文件复制到 `queue_sqlite/core` 目录下(dll文件需改名为pyd后缀)。
|
||||
config = SchedulerConfig(
|
||||
receive_thread_num=2, # 接收线程数
|
||||
task_thread_num=8, # 任务执行线程数
|
||||
shard_num=4, # 数据库分片数
|
||||
queue_name="production", # 队列名称
|
||||
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",
|
||||
|
||||
可处理 10,000+ 任务(斐波那契数列前500项计算)/分钟
|
||||
# 可选字段
|
||||
id="custom-uuid", # 默认自动生成
|
||||
type=MessageType.TASK,
|
||||
priority=MessagePriority.HIGH,
|
||||
source="web_api",
|
||||
tags="urgent,processing",
|
||||
|
||||
平均任务延迟 < 50ms
|
||||
# 时间控制
|
||||
expire_time=datetime.now() + timedelta(hours=1), # 1小时后过期
|
||||
retry_count=0,
|
||||
|
||||
CPU 使用率 < 60%
|
||||
# 自定义元数据
|
||||
metadata={"user_id": 123, "request_id": "abc123"}
|
||||
)
|
||||
```
|
||||
|
||||
内存占用 < 500MB
|
||||
## 📊 系统架构
|
||||
|
||||
### 贡献指南
|
||||
### 架构图
|
||||
|
||||
欢迎贡献代码!请遵循以下步骤:
|
||||
```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 │ │
|
||||
│ └───────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
1. Fork 仓库
|
||||
### 组件说明
|
||||
|
||||
创建新分支 (git checkout -b feature/your-feature)
|
||||
1. **MessageIte**: 核心数据模型,包含消息的所有属性和方法
|
||||
2. **TaskMounter**: 任务过载器,通过装饰器注册任务函数
|
||||
3. **ListenMounter**:监听挂载器,通过装饰器注册监听函数
|
||||
4. **TaskCycle**:任务生命周期管理器,处理重试和状态更新
|
||||
5. **QueueScheduler**:统一调度器接口,支持三种实现:
|
||||
- **StandardQueueScheduler**:统一调度器接口,支持三种实现:
|
||||
- **AsyncQueueScheduler**:异步/等待实现
|
||||
- **QtQueueScheduler**:Qt 线程池实现(GUI应用)
|
||||
6. **CleanupScheduler**:自动清理过期消息
|
||||
7. **ShardedQueueOperation**:Rust 实现的高性能分片队列操作
|
||||
|
||||
提交更改 (git commit -am 'Add some feature')
|
||||
## 🧪 测试
|
||||
|
||||
推送到分支 (git push origin feature/your-feature)
|
||||
### 运行测试套件
|
||||
|
||||
2. 创建 Pull Request
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python -m -v -s pytest tests/
|
||||
|
||||
### 许可证
|
||||
# 运行特定测试
|
||||
python -m pytest tests/test_stress.py -v
|
||||
python -m pytest tests/test_async_scheduler.py -v
|
||||
```
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
### 性能测试示例
|
||||
|
||||
```python
|
||||
from tests.test_stress import TestStress
|
||||
|
||||
# 压力测试:处理 10000 个任务
|
||||
TestStress.test_stress()
|
||||
|
||||
# 异步调度器测试
|
||||
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** - 为您的应用提供可靠、高效的任务队列解决方案。
|
||||
|
||||
+15
-10
@@ -6,22 +6,25 @@ classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: System :: Distributed Computing",
|
||||
"Typing :: Typed",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"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."
|
||||
name = "queue-sqlite"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
version = "0.2.2"
|
||||
|
||||
[project.optional-dependencies]
|
||||
pyside6 = ["PySide6"]
|
||||
pyqt6 = ["PyQt6"]
|
||||
pyqt5 = ["qtpy", "PyQt5"]
|
||||
pyqt6 = ["qtpy", "PyQt6"]
|
||||
pyside6 = ["qtpy", "PySide6"]
|
||||
|
||||
[[tool.uv.index]]
|
||||
default = true
|
||||
@@ -41,9 +44,11 @@ dev = [
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/queue_sqlite"]
|
||||
platforms = ["windows_x86_64", "linux_x86_64"]
|
||||
|
||||
[tool.hatch.build]
|
||||
artifacts = [
|
||||
"src/queue_sqlite/core/*.pyd",
|
||||
[tool.uv.workspace]
|
||||
members = [
|
||||
"src/queue_sqlite_core",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
queue-sqlite-core = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# 需要遍历的目录
|
||||
root_dir = "./"
|
||||
# 遍历目录
|
||||
|
||||
+14
-12
@@ -1,27 +1,29 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
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):
|
||||
# 排除 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 != 'scripts']
|
||||
dirs[:] = [d for d in dirs if d != 'build']
|
||||
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 != "scripts"]
|
||||
dirs[:] = [d for d in dirs if d != "build"]
|
||||
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)
|
||||
md_file.write(f'`{file_path}`\n')
|
||||
md_file.write('```python\n')
|
||||
with open(file_path, 'r', encoding='utf-8') as py_file:
|
||||
md_file.write(f"`{file_path}`\n")
|
||||
md_file.write("```python\n")
|
||||
with open(file_path, "r", encoding="utf-8") as py_file:
|
||||
md_file.write(py_file.read())
|
||||
md_file.write('\n```\n\n')
|
||||
md_file.write("\n```\n\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 指定目录和输出文件名
|
||||
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)
|
||||
print(f'Markdown 文件已生成:{output_markdown_file}')
|
||||
print(f"Markdown 文件已生成:{output_markdown_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(())
|
||||
}
|
||||
}
|
||||
@@ -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__,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
#!/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_status import MessageStatus
|
||||
from .message_type import 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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -7,4 +18,3 @@ class MessageStatus(IntEnum):
|
||||
COMPLETED = 2
|
||||
FAILED = 3
|
||||
RETRYING = 4
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
#!/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
|
||||
|
||||
|
||||
|
||||
@@ -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.
@@ -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 : 任务周期模块
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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 typing import Callable, Optional
|
||||
from ..constant import MessageStatus
|
||||
from ..mounter.task_mounter import TaskMeta
|
||||
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:
|
||||
def __init__(self, message_item: MessageItem, callback: Optional[Callable]):
|
||||
self.message_item = message_item
|
||||
@@ -11,6 +70,7 @@ class TaskCycle:
|
||||
self.task_status = None
|
||||
self.task_error = None
|
||||
|
||||
@retry_sync(max_retries=3)
|
||||
def run(self):
|
||||
try:
|
||||
task_result = self.callback(self.message_item) # type: ignore
|
||||
@@ -24,24 +84,33 @@ class TaskCycle:
|
||||
self.task_error = None
|
||||
|
||||
def get_task_result(self):
|
||||
if isinstance(self.task_result, (dict, list)):
|
||||
try:
|
||||
return json.dumps(self.task_result)
|
||||
except:
|
||||
return json.dumps({'result': str(self.task_result)})
|
||||
"""获取任务结果 - 优化版本"""
|
||||
if self.task_result is None:
|
||||
return json.dumps({"result": None})
|
||||
|
||||
elif isinstance(self.task_result, str):
|
||||
# 如果已经是字符串,尝试解析
|
||||
if isinstance(self.task_result, str):
|
||||
try:
|
||||
# 如果是 JSON 字符串,直接返回
|
||||
json.loads(self.task_result)
|
||||
return self.task_result
|
||||
except:
|
||||
return json.dumps({'result': self.task_result})
|
||||
elif isinstance(self.task_result, (int, float, bool)):
|
||||
return json.dumps({'result': self.task_result})
|
||||
elif self.task_result is None:
|
||||
return 'null'
|
||||
# 如果不是 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': str(self.task_result)})
|
||||
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
|
||||
@@ -54,5 +123,3 @@ class TaskCycle:
|
||||
|
||||
def get_task_callback(self):
|
||||
return self.callback
|
||||
|
||||
|
||||
|
||||
@@ -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 .scheduler_config import SchedulerConfig
|
||||
|
||||
|
||||
__all__ = ["MessageItem"]
|
||||
__all__ = ["MessageItem", "SchedulerConfig"]
|
||||
|
||||
@@ -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 typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
@@ -5,12 +15,13 @@ from ..constant import MessageStatus, MessagePriority, MessageType
|
||||
import json
|
||||
from enum import Enum
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageItem:
|
||||
# 必需字段
|
||||
content: dict = field(
|
||||
content: dict = field( # type ignore
|
||||
metadata={"description": "消息内容,包含具体的任务数据或信息"}
|
||||
)
|
||||
|
||||
@@ -70,7 +81,7 @@ class MessageItem:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "MessageItem":
|
||||
def from_dict(cls, data: dict) -> "MessageItem":
|
||||
"""从字典创建消息对象"""
|
||||
# 处理日期时间字段
|
||||
datetime_fields = ["createtime", "updatetime", "expire_time"]
|
||||
@@ -115,7 +126,13 @@ class MessageItem:
|
||||
# 处理枚举字段
|
||||
if "type" in data:
|
||||
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):
|
||||
pass # 已经是枚举类型
|
||||
else:
|
||||
@@ -123,6 +140,12 @@ class MessageItem:
|
||||
|
||||
if "status" in data:
|
||||
if isinstance(data["status"], str):
|
||||
# 尝试将字符串转换为整数,如果失败则使用字符串作为枚举名称
|
||||
try:
|
||||
data["status"] = MessageStatus(
|
||||
int(data["status"])
|
||||
) # 尝试作为枚举值
|
||||
except ValueError:
|
||||
data["status"] = MessageStatus(data["status"]) # 使用枚举名称
|
||||
elif isinstance(data["status"], int):
|
||||
data["status"] = MessageStatus(data["status"]) # 使用枚举值
|
||||
@@ -132,13 +155,38 @@ class MessageItem:
|
||||
raise ValueError(f"Invalid message status: {data['status']}")
|
||||
|
||||
if "priority" in data:
|
||||
if isinstance(data["priority"], (int, str)):
|
||||
data["priority"] = MessagePriority(data["priority"]) # type: ignore
|
||||
if isinstance(data["priority"], str):
|
||||
# 尝试将字符串转换为整数,如果失败则使用字符串作为枚举名称
|
||||
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):
|
||||
pass
|
||||
pass # 已经是枚举类型
|
||||
else:
|
||||
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)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
@@ -235,13 +283,13 @@ class MessageItem:
|
||||
"""打印所有字段的信息,包括类型、默认值和描述"""
|
||||
from dataclasses import fields
|
||||
|
||||
print(f"=== {cls.__name__} 字段信息 ===")
|
||||
logging.info(f"=== {cls.__name__} 字段信息 ===")
|
||||
for field_info in fields(cls):
|
||||
print(f"字段名: {field_info.name}")
|
||||
print(f"类型: {field_info.type}")
|
||||
print(f"默认值: {field_info.default}")
|
||||
logging.info(f"字段名: {field_info.name}")
|
||||
logging.info(f"类型: {field_info.type}")
|
||||
logging.info(f"默认值: {field_info.default}")
|
||||
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:
|
||||
print(f"描述: {field_info.metadata['description']}")
|
||||
print("-" * 50)
|
||||
logging.info(f"描述: {field_info.metadata['description']}")
|
||||
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)
|
||||
@@ -1,4 +1,15 @@
|
||||
from .task_mounter import TaskMounter
|
||||
from .listen_mounter import ListenMounter
|
||||
#!/usr/bin/env python
|
||||
# -*- 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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,10 +20,12 @@ class ListenMounter:
|
||||
@staticmethod
|
||||
def listener():
|
||||
"""带参数的装饰器"""
|
||||
|
||||
def decorator(function: Callable):
|
||||
# 使用自定义名称或函数原名
|
||||
setattr(ListenMounter, function.__name__, function)
|
||||
return function
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
@@ -33,9 +46,18 @@ class ListenMounter:
|
||||
if (
|
||||
callable(attr_value)
|
||||
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)
|
||||
|
||||
return listener_list
|
||||
|
||||
|
||||
def listener():
|
||||
return ListenMounter.listener()
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
#!/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 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:
|
||||
@@ -9,17 +30,40 @@ class TaskMounter:
|
||||
@staticmethod
|
||||
def task(meta: dict = {}):
|
||||
"""带参数的装饰器"""
|
||||
|
||||
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)
|
||||
return function
|
||||
|
||||
return decorator
|
||||
|
||||
@classmethod
|
||||
def get_task_function(cls, name: str):
|
||||
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
|
||||
def get_task_list(cls) -> List[str]:
|
||||
"""获取所有挂载的任务函数名称列表"""
|
||||
@@ -34,9 +78,14 @@ class TaskMounter:
|
||||
if (
|
||||
callable(attr_value)
|
||||
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)
|
||||
|
||||
return task_list
|
||||
|
||||
|
||||
# task 装饰器
|
||||
def task(meta: dict = {}):
|
||||
return TaskMounter.task(meta)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
from .queue_operation import QueueOperation
|
||||
|
||||
__all__ = ["QueueOperation"]
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@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
|
||||
import sqlite3
|
||||
from typing import List, Tuple, Union
|
||||
import logging
|
||||
|
||||
|
||||
class ListenOperation:
|
||||
@@ -10,21 +22,39 @@ class ListenOperation:
|
||||
self.create_table()
|
||||
|
||||
def _get_connection(self):
|
||||
"""获取数据库连接"""
|
||||
return sqlite3.connect(
|
||||
self.db_dir,
|
||||
check_same_thread=False,
|
||||
"""获取数据库连接 - 改进版"""
|
||||
conn = sqlite3.connect(
|
||||
self.db_dir, check_same_thread=False, timeout=30.0 # 增加超时时间
|
||||
)
|
||||
|
||||
# 设置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):
|
||||
if len(self.listen_fields) == 0:
|
||||
return
|
||||
sql = f"""
|
||||
conn = self._get_connection()
|
||||
|
||||
try:
|
||||
# 分别执行每个SQL语句而不是使用executescript
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS listen_table (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key Text,
|
||||
value JSON
|
||||
);
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS change_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
table_name TEXT,
|
||||
@@ -34,35 +64,37 @@ class ListenOperation:
|
||||
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 -- 监听特定列
|
||||
AFTER UPDATE OF value ON listen_table
|
||||
FOR EACH ROW
|
||||
WHEN OLD.value <> NEW.value -- 仅当值实际变化时触发
|
||||
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;
|
||||
VALUES ('listen_table', NEW.id, OLD.key, OLD.value, NEW.value);
|
||||
END
|
||||
"""
|
||||
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;")
|
||||
)
|
||||
except sqlite3.Error:
|
||||
# 如果触发器创建失败,我们继续执行其他操作
|
||||
pass
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 删除原有数据
|
||||
# sql = f"""
|
||||
# DELETE FROM listen_table
|
||||
# """
|
||||
# conn.execute(sql)
|
||||
# conn.commit()
|
||||
|
||||
for listen_field in self.listen_fields:
|
||||
sql = f"""
|
||||
# 检查是否已存在该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
|
||||
@@ -71,45 +103,86 @@ class ListenOperation:
|
||||
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]]:
|
||||
sql = f"""
|
||||
"""获取监听数据 - 改进版"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
sql = """
|
||||
SELECT * FROM change_log where is_delete = 0 ORDER BY id DESC LIMIT 100
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
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):
|
||||
sql = f"""
|
||||
DELETE FROM change_log WHERE id = {delete_id}
|
||||
"""
|
||||
"""删除变更日志 - 改进版"""
|
||||
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):
|
||||
sql = f"""
|
||||
UPDATE listen_table SET value = '{value}' WHERE key = '{key}'
|
||||
"""
|
||||
"""更新监听数据 - 改进版"""
|
||||
conn = self._get_connection()
|
||||
conn.execute(sql)
|
||||
try:
|
||||
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):
|
||||
sql = f"""
|
||||
SELECT value FROM listen_table WHERE key = '{key}'
|
||||
"""
|
||||
"""获取值 - 改进版"""
|
||||
conn = self._get_connection()
|
||||
result = conn.execute(sql).fetchone()
|
||||
try:
|
||||
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
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_values(self):
|
||||
sql = f"""
|
||||
"""获取所有值 - 改进版"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
sql = """
|
||||
SELECT key, value FROM listen_table
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
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()
|
||||
@@ -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 ._async import AsyncQueueScheduler
|
||||
from .base import BaseScheduler
|
||||
from ..model import MessageItem
|
||||
from typing import Callable
|
||||
from ..model import MessageItem, SchedulerConfig
|
||||
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):
|
||||
def __init__(
|
||||
self,
|
||||
receive_thread_num=1,
|
||||
task_thread_num=1,
|
||||
shard_num=4,
|
||||
scheduler_type="standard",
|
||||
scheduler_type: SchedulerType = "standard",
|
||||
config: SchedulerConfig = SchedulerConfig(),
|
||||
):
|
||||
self.scheduler = None
|
||||
if scheduler_type == "standard":
|
||||
self.scheduler = StandardQueueScheduler(
|
||||
receive_thread_num, task_thread_num, shard_num
|
||||
)
|
||||
else:
|
||||
pass
|
||||
if self.scheduler:
|
||||
super().__init__(config)
|
||||
scheduler_class = SCHEDULER_TYPES.get(scheduler_type, None)
|
||||
if scheduler_class is None:
|
||||
raise ValueError(f"Invalid scheduler type: {scheduler_type}")
|
||||
self.scheduler = scheduler_class(config)
|
||||
|
||||
if hasattr(self.scheduler, "queue_operation"):
|
||||
self.queue_operation = self.scheduler.queue_operation
|
||||
|
||||
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 typing import Callable
|
||||
from ...model import MessageItem
|
||||
from ...model import MessageItem, SchedulerConfig
|
||||
from queue_sqlite_core import ShardedQueueOperation
|
||||
import logging
|
||||
|
||||
|
||||
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
|
||||
def send_message(self, message: MessageItem, callback: Callable):
|
||||
"""发送消息到队列
|
||||
@@ -40,3 +60,14 @@ class BaseScheduler(ABC):
|
||||
def get_listen_data(self, key):
|
||||
"""获取单个监听数据"""
|
||||
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)
|
||||
@@ -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 .task_scheduler import TaskScheduler
|
||||
from .listen_data_scheduler import ListenDataScheduler
|
||||
from ...model import MessageItem
|
||||
from typing import Callable
|
||||
from ...queue_operation import QueueOperation
|
||||
from queue_sqlite_core import ShardedQueueOperation
|
||||
from ...queue_operation.listen_operation import ListenOperation
|
||||
from .cleanup_scheduler import CleanupScheduler
|
||||
from ..cleanup_scheduler import CleanupScheduler
|
||||
import os
|
||||
from ..base import BaseScheduler
|
||||
from ...model import SchedulerConfig
|
||||
|
||||
|
||||
class StandardQueueScheduler(BaseScheduler):
|
||||
def __init__(self, receive_thread_num=1, task_thread_num=1, shard_num=4):
|
||||
self.queue_operation = QueueOperation(shard_num)
|
||||
def __init__(
|
||||
self,
|
||||
config: SchedulerConfig = SchedulerConfig(),
|
||||
):
|
||||
self.queue_operation = ShardedQueueOperation(
|
||||
config.shard_num, config.queue_name
|
||||
)
|
||||
self.listen_operation = ListenOperation(
|
||||
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.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)
|
||||
|
||||
def send_message(self, message: MessageItem, callback: Callable):
|
||||
@@ -48,7 +67,6 @@ class StandardQueueScheduler(BaseScheduler):
|
||||
self.listen_scheduler.stop_listen_data()
|
||||
self.task_scheduler.stop_task_thread()
|
||||
self.cleanup_scheduler.stop_cleanup()
|
||||
# self.queue_operation.close_all_connections()
|
||||
|
||||
|
||||
__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 concurrent.futures import ThreadPoolExecutor
|
||||
from ...mounter.listen_mounter import ListenMounter
|
||||
import threading
|
||||
import multiprocessing
|
||||
import time
|
||||
import logging
|
||||
|
||||
|
||||
class ListenDataScheduler:
|
||||
def __init__(self, listen_operation: ListenOperation):
|
||||
self.listen_operation = listen_operation
|
||||
self.is_running = False
|
||||
self.executor = ThreadPoolExecutor(max_workers=1)
|
||||
self.executor = ThreadPoolExecutor(max_workers=multiprocessing.cpu_count())
|
||||
self.listen_thread = None
|
||||
# 使用一个线程锁来确保监听逻辑的原子性
|
||||
self.process_lock = threading.Lock()
|
||||
# 记录最后处理的ID,避免重复处理
|
||||
self.last_processed_id = 0
|
||||
|
||||
def _process_listen_data(self, key, value, delete_id):
|
||||
listen_function = ListenMounter.get_Listener_function(key)
|
||||
@@ -19,24 +37,51 @@ class ListenDataScheduler:
|
||||
except Exception as e:
|
||||
ValueError(f"Error in {key} listener function: {e}")
|
||||
finally:
|
||||
# 确保变更日志被删除
|
||||
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):
|
||||
while self.is_running:
|
||||
# 获取比上次处理ID更大的变更记录,避免重复处理
|
||||
status, change_data_items = self.listen_operation.listen_data()
|
||||
if status:
|
||||
for data in change_data_items:
|
||||
key = data[6]
|
||||
new_value = data[7]
|
||||
if status and isinstance(change_data_items, list):
|
||||
# 过滤出ID大于上次处理ID的记录
|
||||
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]
|
||||
|
||||
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):
|
||||
if self.is_running:
|
||||
return
|
||||
self.is_running = True
|
||||
self.last_processed_id = 0 # 重置最后处理ID
|
||||
self.listen_thread = threading.Thread(target=self.listen)
|
||||
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 typing import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
|
||||
SendMessageCallback = namedtuple("send_message_callback", ["message", "callback"])
|
||||
|
||||
|
||||
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.receive_thread_num = receive_thread_num
|
||||
self.is_running = False
|
||||
@@ -20,12 +37,26 @@ class ReceiveScheduler:
|
||||
|
||||
def send_message(self, message: MessageItem, callback: Callable = None): # type: ignore
|
||||
if callback is None:
|
||||
callback = lambda message: print(message)
|
||||
# self.queue_operation.enqueue(message.to_dict())
|
||||
callback = lambda message: logging.info(f"receive message: {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_message(self):
|
||||
"""单一轮询线程,并行执行回调"""
|
||||
|
||||
@@ -49,13 +80,8 @@ class ReceiveScheduler:
|
||||
"""安全执行回调函数"""
|
||||
try:
|
||||
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:
|
||||
print(f"回调执行错误: {str(e)}")
|
||||
logging.error(f"回调执行错误: {str(e)}")
|
||||
finally:
|
||||
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 ...constant import MessageStatus
|
||||
from ...cycle.task_cycle import TaskCycle
|
||||
@@ -7,10 +19,16 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
import time
|
||||
import threading
|
||||
import multiprocessing
|
||||
import json
|
||||
|
||||
|
||||
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.is_running = False
|
||||
self.executor = ThreadPoolExecutor(max_workers=task_thread_num) # 并行执行任务
|
||||
@@ -46,10 +64,10 @@ class TaskScheduler:
|
||||
def _update_result(self, message):
|
||||
"""更新任务结果到数据库"""
|
||||
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)
|
||||
except Exception as e:
|
||||
print(f"任务结果更新失败: {str(e)}")
|
||||
logging.error(f"任务结果更新失败: {str(e)}")
|
||||
|
||||
def task_callback(self):
|
||||
"""单一轮询线程,并行执行任务"""
|
||||
@@ -68,7 +86,7 @@ class TaskScheduler:
|
||||
else:
|
||||
time.sleep(0.1) # 适当休眠
|
||||
except Exception as e:
|
||||
print(f"任务调度错误: {str(e)}")
|
||||
logging.error(f"任务调度错误: {str(e)}")
|
||||
time.sleep(1)
|
||||
|
||||
def start_task_thread(self):
|
||||
|
||||
+290
-14
@@ -2,6 +2,21 @@
|
||||
# It is not intended for manual editing.
|
||||
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]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -35,12 +50,36 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.0"
|
||||
@@ -76,23 +115,41 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"pyo3",
|
||||
"r2d2",
|
||||
"r2d2_sqlite",
|
||||
"rusqlite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
@@ -105,6 +162,16 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
@@ -114,9 +181,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
@@ -171,6 +244,23 @@ version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -214,6 +304,12 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
@@ -223,6 +319,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -232,6 +348,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.36.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -261,6 +386,12 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -353,6 +484,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
@@ -442,12 +591,24 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scheduled-thread-pool"
|
||||
version = "0.2.7"
|
||||
@@ -463,12 +624,72 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -492,6 +713,46 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
@@ -506,9 +767,9 @@ checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -528,6 +789,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
@@ -654,6 +921,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "core"
|
||||
version = "0.1.0"
|
||||
name = "queue_sqlite_core"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[lib]
|
||||
name = "core"
|
||||
name = "queue_sqlite_core"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
@@ -13,4 +13,14 @@ chrono = "0.4.41"
|
||||
pyo3 = "0.25.0"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.25"
|
||||
rand = "0.9.2"
|
||||
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]
|
||||
name = "core"
|
||||
name = "queue_sqlite_core"
|
||||
requires-python = ">=3.7"
|
||||
classifiers = [
|
||||
"Programming Language :: Rust",
|
||||
@@ -11,5 +7,22 @@ classifiers = [
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
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]
|
||||
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;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use queue_operation::QueueOperation;
|
||||
use queue_operation::{QueueOperation, ShardedQueueOperation};
|
||||
use task_mounter::TaskMounter; // 导入结构体 // 导入结构体
|
||||
|
||||
#[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::<QueueOperation>()?;
|
||||
m.add_class::<ShardedQueueOperation>()?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
Generated
+1432
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
from .example import async_example
|
||||
|
||||
__all__ = ["async_example"]
|
||||
@@ -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]
|
||||
@@ -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):
|
||||
print(data)
|
||||
@@ -1,8 +1,8 @@
|
||||
from queue_sqlite.mounter.task_mounter import TaskMounter
|
||||
from queue_sqlite.mounter import task
|
||||
from queue_sqlite.model import MessageItem
|
||||
|
||||
|
||||
@TaskMounter.task(meta={"task_name": "test"})
|
||||
@task(meta={"task_name": "test"})
|
||||
def example(message_item: MessageItem):
|
||||
def fibonacci_generator():
|
||||
a, b = 0, 1
|
||||
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
from queue_sqlite.core import core
|
||||
from queue_sqlite.mounter import TaskMounter
|
||||
import queue_sqlite_core as core
|
||||
from queue_sqlite.mounter.task_mounter import TaskMounter
|
||||
from queue_sqlite.model import MessageItem
|
||||
|
||||
|
||||
@@ -11,14 +11,12 @@ class TestCore:
|
||||
task_mounter = core.TaskMounter(TaskMounter)
|
||||
task_mounter.get_task_list()
|
||||
print(
|
||||
task_mounter.get_task_function("task")({"num": 1})(lambda x: x + 1).meta[
|
||||
"num"
|
||||
]
|
||||
f"task: {task_mounter.get_task_function("task")({"num": 1})(lambda x: x + 1).meta}"
|
||||
)
|
||||
from tasks import example
|
||||
|
||||
task_mounter.get_task_list()
|
||||
print(task_mounter.get_task_function("<lambda>"))
|
||||
print(task_mounter.get_task_function("<lambda>")(1))
|
||||
|
||||
@classmethod
|
||||
def test_queue_operation_init_db(cls):
|
||||
|
||||
@@ -5,7 +5,7 @@ import time
|
||||
|
||||
class TestListen:
|
||||
def test_listen_data(self):
|
||||
scheduler = QueueScheduler()
|
||||
scheduler = QueueScheduler(scheduler_type="qt")
|
||||
scheduler.start()
|
||||
scheduler.update_listen_data("key_1", "value_1")
|
||||
time.sleep(0.001)
|
||||
@@ -16,5 +16,8 @@ class TestListen:
|
||||
scheduler.update_listen_data("key_1", "value_4")
|
||||
time.sleep(0.001)
|
||||
scheduler.stop()
|
||||
print(scheduler.get_listen_datas())
|
||||
print(scheduler.get_listen_data("key_1"))
|
||||
# print(scheduler.get_listen_datas())
|
||||
# print(scheduler.get_listen_data("key_1"))
|
||||
|
||||
|
||||
# TestListen().test_listen_data()
|
||||
|
||||
@@ -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()
|
||||
@@ -25,6 +25,9 @@ class TestStress:
|
||||
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(message_item.id)
|
||||
# print(f"callback: {message_item}")
|
||||
# print(f"callback: {message_item.id}")
|
||||
# print(f"callback: {message_item.expire_time}")
|
||||
@@ -53,9 +56,7 @@ class TestStress:
|
||||
cls.is_monitoring = True
|
||||
monitor_thread = threading.Thread(target=cls.monitor_resources, daemon=True)
|
||||
monitor_thread.start()
|
||||
queue_scheduler = QueueScheduler(
|
||||
receive_thread_num=4, task_thread_num=8, shard_num=12
|
||||
)
|
||||
queue_scheduler = QueueScheduler()
|
||||
|
||||
# threads = []
|
||||
# for message_item in messages:
|
||||
|
||||
@@ -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" },
|
||||
]
|
||||
Reference in New Issue
Block a user