diff --git a/README.md b/README.md
index 5613386..89e3e09 100644
--- a/README.md
+++ b/README.md
@@ -10,24 +10,42 @@ MCP server for RDS Services via OPENAPI
## Quick Start
### Using [cherry-studio](https://github.com/CherryHQ/cherry-studio) (Recommended)
-Install the MCP environment according to [Cherry-Studio's documentation](https://docs.cherry-ai.com/advanced-basic/mcp/install), then configure and use RDS MCP.
-Add the following configuration to the MCP client configuration file:
+1. Download and install cherry-studio
+2. Follow the [documentation](https://docs.cherry-ai.com/cherry-studio/download) to install uv, which is required for the MCP environment
+3. Configure and use RDS MCP according to the [documentation](https://docs.cherry-ai.com/advanced-basic/mcp/install). You can quickly import the RDS MCP configuration using the JSON below. Please set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET to your Alibaba Cloud AK/SK.
+
+> The following error may appear during import, which can be ignored:
+> xxx settings.mcp.addServer.importFrom.connectionFailed
+
+
+
```json5
-"mcpServers": {
- "rds-openapi-mcp-server": {
- "command": "uvx",
- "args": [
- "alibabacloud-rds-openapi-mcp-server@latest"
- ],
- "env": {
- "ALIBABA_CLOUD_ACCESS_KEY_ID": "access_id",
- "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "access_key",
- "ALIBABA_CLOUD_SECURITY_TOKEN": "sts_security_token" // optional, required when using STS Token
- }
- }
+{
+ "mcpServers": {
+ "rds-openapi": {
+ "name": "rds-openapi",
+ "type": "stdio",
+ "description": "",
+ "isActive": true,
+ "registryUrl": "",
+ "command": "uvx",
+ "args": [
+ "alibabacloud-rds-openapi-mcp-server@latest"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "$you_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "$you_access_key"
+ }
+ }
+ }
}
```
+4. Finally, click to turn on MCP
+
+
+5. You can use the prompt template provided below to enhance your experience.
+
### Using Cline
Set you env and run mcp server.
```shell
@@ -52,6 +70,7 @@ And then configure the Cline.
remote_server = "http://127.0.0.1:8000/sse";
```
+> If you encounter a `401 Incorrect API key provided` error when using Qwen, please refer to the [documentation](https://help.aliyun.com/zh/model-studio/cline) for solutions.
### Using Claude
Download from Github
@@ -79,7 +98,7 @@ Add the following configuration to the MCP client configuration file:
```
## Components
-### Tools
+### OpenAPI Tools
* `add_tags_to_db_instance`: Add tags to an RDS instance.
* `allocate_instance_public_connection`: Allocate a public connection for an RDS instance.
* `attach_whitelist_template_to_instance`: Attach a whitelist template to an RDS instance.
@@ -100,12 +119,48 @@ Add the following configuration to the MCP client configuration file:
* `describe_error_logs`: Queries the error log of an instance.
* `describe_instance_linked_whitelist_template`: Query the whitelist template list.
* `describe_slow_log_records`: Query slow log records for an RDS instance.
+* `describe_sql_insight_statistic`: Query SQL Log statistics, including SQL cost time, execution times, and account.
* `describe_vpcs`: Query VPC list.
* `describe_vswitches`: Query VSwitch list.
+* `modify_security_ips`: Modify RDS instance security IP whitelist.
* `get_current_time`: Get the current time.
* `modify_db_instance_description`: Modify RDS instance descriptions.
* `modify_db_instance_spec`: Modify RDS instance specifications.
* `modify_parameter`: Modify RDS instance parameters.
+* `restart_db_instance`: Restart an RDS instance.
+### SQL Tools
+> The MCP Server will automatically create a read-only account, execute the SQL statement, and then automatically delete the account. This process requires that the MCP Server can connect to the instance.
+
+* `show_engine_innodb_status`: Execute sql `show engine innodb status` and return sql result.
+* `show_create_table`: Execute sql `show create table` and return sql result.
+
+### Toolsets
+
+Toolsets group available MCP tools so you can enable only what you need. Configure toolsets when starting the server using either:
+
+- **Command line**: `--toolsets` parameter
+- **Environment variable**: `MCP_TOOLSETS`
+
+#### Format
+Use comma-separated toolset names (no spaces around commas):
+```
+rds,rds_mssql_custom
+```
+
+#### Examples
+```bash
+# Single toolset
+--toolsets rds
+
+# Multiple tools
+--toolsets rds,rds_mssql_custom
+
+# Environment variable
+export MCP_TOOLSETS=rds,rds_mssql_custom
+```
+
+#### Default Behavior
+If no toolset is specified, the default `rds` group is loaded automatically.
### Resources
None at this time
@@ -139,6 +194,14 @@ You are a professional Alibaba Cloud RDS Copilot, specializing in providing cust
- **Safety Awareness**: Ensure no operations negatively impact customer databases.
```
+## Use Cases
+### mydba
+Alibaba Cloud Database MyDBA Agent(README.md)
+- Buy RDS
+
+- Diagnose RDS
+
+
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
@@ -149,3 +212,8 @@ Contributions are welcome! Please feel free to submit a Pull Request.
## License
This project is licensed under the Apache 2.0 License.
+
+## Contact Information
+For any questions or concerns, please contact us through the DingTalk group:106730017609
+
+
\ No newline at end of file
diff --git a/README_CN.md b/README_CN.md
index fc65c04..067eab4 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -9,23 +9,43 @@ RDS OpenAPI MCP服务。
## 快速开始
### 使用[cherry-studio](https://github.com/CherryHQ/cherry-studio)(推荐)
-根据[Cherry-Studio文档](https://docs.cherry-ai.com/advanced-basic/mcp/install)安装MCP环境后配置使用RDS MCP。 MCP配置文件格式如下:
+1. [下载](https://docs.cherry-ai.com/cherry-studio/download)并安装cherry-studio
+2. 根据[文档](https://docs.cherry-ai.com/advanced-basic/mcp/install)安装MCP环境所需的uv
+3. 根据[文档](https://docs.cherry-ai.com/advanced-basic/mcp/config) 配置和使用RDS MCP,使用下面的JSON可以快速导入RDS MCP配置。请将`ALIBABA_CLOUD_ACCESS_KEY_ID`和`ALIBABA_CLOUD_ACCESS_KEY_SECRET`配置成阿里云AKSK。
+
+> 导入时可能会看到以下报错,可以忽略:
+> xxx settings.mcp.addServer.importFrom.connectionFailed
+
+
+
```json5
-"mcpServers": {
- "rds-openapi-mcp-server": {
- "command": "uvx",
- "args": [
- "alibabacloud-rds-openapi-mcp-server@latest"
- ],
- "env": {
- "ALIBABA_CLOUD_ACCESS_KEY_ID": "access_id",
- "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "access_key",
- "ALIBABA_CLOUD_SECURITY_TOKEN": "sts_security_token" // 可选项,使用sts token鉴权时填写
- }
- }
+{
+ "mcpServers": {
+ "rds-openapi": {
+ "name": "rds-openapi",
+ "type": "stdio",
+ "description": "",
+ "isActive": true,
+ "registryUrl": "",
+ "command": "uvx",
+ "args": [
+ "alibabacloud-rds-openapi-mcp-server@latest"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "$you_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "$you_access_key"
+ }
+ }
+ }
}
```
+4. 最后点击开启MCP
+
+
+5. 您可以使用我们下面提供的提示词模板,提升使用体验。
+
+
### 使用Cline
设置环境变量并运行MCP服务
```shell
@@ -50,6 +70,8 @@ INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
remote_server = "http://127.0.0.1:8000/sse";
```
+> 如果使用Qwen遇到`401 Incorrect API key provided`错误,请参考[文档](https://help.aliyun.com/zh/model-studio/cline)解决。
+
### 使用Claude
从Github克隆仓库
```shell
@@ -76,7 +98,7 @@ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
```
## 功能组件
-### 工具集
+### OpenAPI 工具集
* `add_tags_to_db_instance`: 添加标签到RDS实例
* `allocate_instance_public_connection`: 为RDS实例分配公网连接
* `attach_whitelist_template_to_instance`: 将白名单模板绑定到RDS实例
@@ -97,12 +119,51 @@ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
* `describe_error_logs`: 查询实例错误日志
* `describe_instance_linked_whitelist_template`: 查询绑定到实例的白名单模板列表
* `describe_slow_log_records`: 查询RDS实例的慢日志记录
+* `describe_sql_insight_statistic`: 查询实例SQL日志统计,包括SQL耗时、执行次数、账号等
* `describe_vpcs`: 查询VPC列表
+* `modify_security_ips`: 修改白名单
* `describe_vswitches`: 查询VSwitch列表
* `get_current_time`: 获取当前时间
* `modify_db_instance_description`: 修改RDS实例描述
* `modify_db_instance_spec`: 修改RDS实例规格
* `modify_parameter`: 修改RDS实例参数
+* `restart_db_instance`: 重启RDS实例
+
+
+#### 工具集分组
+
+工具集将可用的 MCP 工具进行分组管理,让你只启用需要的功能。启动服务器时可通过以下方式配置工具集:
+
+- **命令行参数**: `--toolsets` 参数
+- **环境变量**: `MCP_TOOLSETS`
+
+#### 格式
+使用逗号分隔的工具集名称(逗号周围不要空格):
+```
+rds,rds_mssql_custom
+```
+
+#### 示例
+```bash
+# 单个工具集
+--toolsets rds
+
+# 多个工具集
+--toolsets rds,rds_mssql_custom
+
+# 环境变量方式
+export MCP_TOOLSETS=rds,rds_mssql_custom
+```
+
+#### 默认行为
+如果未指定工具集,将自动加载默认的 `rds` 工具组。
+
+### SQL 工具集
+> MCP Server会自动创建一个只读账号,执行SQL后再自动删除。需要MCP Server能够连通到实例。
+
+* `show_engine_innodb_status`: Execute sql `show engine innodb status` and return sql result.
+* `show_create_table`: Execute sql `show create table` and return sql result.
+
### 资源
当前暂无资源
@@ -136,6 +197,14 @@ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
- **安全性注意**:在执行任何操作时,需确保不会对客户的数据库造成负面影响。
```
+## 使用案例
+### mydba
+阿里云数据库 MyDBA 智能体(README_cn.md)
+- 购买RDS
+
+- 诊断RDS
+
+
## 贡献指南
欢迎贡献代码!请提交Pull Request:
1. Fork 本仓库
@@ -147,3 +216,7 @@ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
## 许可证
本项目采用Apache 2.0许可证
+## 联系信息
+如有任何疑问或疑虑,请通过钉钉群联系我们:106730017609
+
+
\ No newline at end of file
diff --git a/assets/README_WIN_CN.md b/assets/README_WIN_CN.md
new file mode 100644
index 0000000..92c510d
--- /dev/null
+++ b/assets/README_WIN_CN.md
@@ -0,0 +1,323 @@
+# 阿里云RDS OpenAPI MCP服务器
+
+🚀 通过OpenAPI为阿里云RDS提供MCP服务器支持。本项目通过[MCP](https://github.com/CherryHQ/mcp)框架公开阿里云RDS、VPC和计费API,将常见的数据库管理任务打包成易于使用的工具。
+
+
+
+> ⚠️ **注意**: 本说明文档基于Windows平台。
+
+## 📋 目录
+- [✨ 功能特性](#-功能特性)
+- [📋 先决条件](#-先决条件)
+- [🔧 安装](#-安装)
+- [⚙️ 配置](#️-配置)
+- [🚀 快速开始](#-快速开始)
+- [🛠️ 可用工具](#️-可用工具)
+- [💡 使用场景](#-使用场景)
+- [🔌 集成方式](#-集成方式)
+- [🤝 贡献指南](#-贡献指南)
+- [📄 许可证](#-许可证)
+- [💬 技术支持](#-技术支持)
+
+## ✨ 功能特性
+
+- 🏗️ **全面的RDS管理**: 创建、配置和管理RDS实例
+- 🔒 **安全与合规**: IP白名单管理和安全审计
+- 📊 **性能监控**: 查询性能指标和日志
+- 💰 **成本优化**: 计费分析和资源优化
+- 🌐 **多环境支持**: 开发、测试和生产环境
+- 🔗 **VPC集成**: 完整的VPC和交换机管理功能
+
+## 📋 先决条件
+
+- 🐍 Python 3.12 或更高版本
+- ☁️ 拥有适当权限的阿里云账户
+- 🔑 Access Key ID 和 Access Key Secret(或STS Token)
+
+## 🔧 安装
+
+### 方式一:使用uv快速启动(推荐)
+
+1. **安装uv**
+ ```powershell
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+ ```
+
+2. **运行服务器**
+ ```bash
+ uvx alibabacloud-rds-openapi-mcp-server@latest
+ ```
+
+### 方式二:从源码安装
+
+1. **安装Python 3.12+**
+
+ 从[Python官网](https://www.python.org/downloads/)下载
+
+2. **克隆仓库**
+ ```bash
+ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
+ cd alibabacloud-rds-openapi-mcp-server
+ ```
+
+3. **安装依赖**
+ ```bash
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_bssopenapi20171214
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_rds20140815
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_vpc20160428
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ mcp
+ ```
+
+4. **运行服务器**
+ ```bash
+ python src/alibabacloud_rds_openapi_mcp_server/server.py
+ ```
+
+## ⚙️ 配置
+
+### 环境变量设置
+
+在启动MCP服务器之前,需要设置必要的环境变量:
+
+#### PowerShell配置
+```powershell
+# 设置服务器传输模式
+$env:SERVER_TRANSPORT="sse"
+
+# 设置阿里云凭证
+$env:ALIBABA_CLOUD_ACCESS_KEY_ID="<您的AccessKey ID>"
+$env:ALIBABA_CLOUD_ACCESS_KEY_SECRET="<您的AccessKey Secret>"
+
+# 可选:使用临时凭证时设置STS Token
+$env:ALIBABA_CLOUD_SECURITY_TOKEN="<您的STS安全令牌>"
+```
+
+#### 命令提示符(CMD)配置
+```cmd
+REM 设置服务器传输模式
+set SERVER_TRANSPORT=sse
+
+REM 设置阿里云凭证
+set ALIBABA_CLOUD_ACCESS_KEY_ID=<您的AccessKey ID>
+set ALIBABA_CLOUD_ACCESS_KEY_SECRET=<您的AccessKey Secret>
+
+REM 可选:使用临时凭证时设置STS Token
+set ALIBABA_CLOUD_SECURITY_TOKEN=<您的STS安全令牌>
+```
+
+> ⚠️ **重要提示**: 请将`<占位符>`替换为您的真实凭证信息。这些环境变量仅在当前终端会话中有效。
+
+## 🚀 快速开始
+
+### 服务器启动验证
+
+成功启动后,您将看到类似以下的输出:
+```
+INFO: Started server process [进程ID]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
+
+### 测试安装
+
+配置完成后,可以使用简单的提示词测试MCP工具的有效性:
+```
+请列出所有可用工具,列表方式,显示标题以及对应的简短描述。
+```
+
+## 🛠️ 可用工具
+
+### 🏗️ RDS实例管理
+- `create_db_instance`: 创建RDS实例
+- `describe_db_instances`: 查询实例
+- `describe_db_instance_attribute`: 查询实例详细信息
+- `modify_db_instance_description`: 修改RDS实例描述
+- `modify_db_instance_spec`: 修改RDS实例规格
+- `restart_db_instance`: 重启RDS实例
+
+### 🔒 安全与访问控制
+- `modify_security_ips`: 修改IP白名单
+- `describe_db_instance_ip_allowlist`: 批量查询IP白名单配置
+- `attach_whitelist_template_to_instance`: 将白名单模板绑定到实例
+- `describe_all_whitelist_template`: 查询白名单模板列表
+- `describe_instance_linked_whitelist_template`: 查询绑定到实例的白名单模板列表
+
+### 👥 数据库与用户管理
+- `create_db_instance_account`: 创建RDS实例账号
+- `describe_db_instance_accounts`: 批量查询账户信息
+- `describe_db_instance_databases`: 批量查询数据库信息
+
+### 🌐 网络与连接
+- `allocate_instance_public_connection`: 为RDS实例分配公网连接
+- `describe_db_instance_net_info`: 批量查询网络配置详情
+- `describe_vpcs`: 查询VPC列表
+- `describe_vswitches`: 查询VSwitch列表
+
+### 📊 监控与性能
+- `describe_db_instance_performance`: 查询实例性能数据
+- `describe_error_logs`: 查询实例错误日志
+- `describe_slow_log_records`: 查询RDS实例的慢日志记录
+- `describe_db_instance_parameters`: 批量查询参数信息
+- `modify_parameter`: 修改RDS实例参数
+
+### 📦 资源管理
+- `describe_available_classes`: 查询可用实例规格和存储范围
+- `describe_available_zones`: 查询RDS实例可用区域
+- `add_tags_to_db_instance`: 添加标签到RDS实例
+
+### 💰 计费与成本管理
+- `describe_bills`: 查询用户在特定计费周期内所有产品实例或计费项的消费汇总
+
+### 🔧 实用工具
+- `get_current_time`: 获取当前时间
+
+## 💡 使用场景示例
+
+### 场景一:资源调配与环境初始化 🏗️
+
+**在杭州区域创建生产RDS MySQL实例:**
+```
+在cn-hangzhou可用区生产一个RDS MySQL实例,配置、版本、白名单等信息与实例rm-bp1696hd82oc438fl保持完全一致,并打上标签:生产环境审计、月底前释放
+```
+
+**创建只读用户账号:**
+```
+在cn-hangzhou区域的实例 rm-bp1696hd82oc438fl 上,创建一个只读账号readonly_user,密码为:Strong!Pa$$word 并授予它访问 report_db 数据库的只读权限。
+```
+
+### 场景二:安全与合规性自动化审计 🔒
+
+**添加堡垒机IP到安全组:**
+```
+立即将堡垒机新IP 100.101.102.103 添加到所有cn-hangzhou可用区的RDS MySQL实例的'ops_allowlist'安全组中。
+```
+
+**安全合规审计:**
+```
+审计所有的RDS SQL Server实例,找到cn-hangzhou中所有白名单中不包含 123.123.123.123 IP的实例
+```
+
+### 场景三:成本优化 💰
+
+**基于性能的成本优化:**
+```
+分析cn-hangzhou区域过去3天月所有标签为"测试环境"的RDS MySQL实例,找出 CPU 平均使用率低于 5% 的,并建议一个更经济的实例规格。
+```
+
+### 场景四:常见运维操作 🔧
+
+**实例故障排除:**
+```
+cn-hangzhou实例rm-bp1696hd82oc438f目前无法连接,请立即尝试重启该实例。
+```
+
+## 🔌 集成方式
+
+### Cherry Studio集成(推荐)🍒
+
+Cherry Studio是一款集成了多种大语言模型的跨平台AI客户端,支持AI对话、绘图等,旨在提升创作效率。
+
+在MCP客户端配置文件中添加以下配置:
+
+```json
+{
+ "mcpServers": {
+ "rds-openapi-mcp-server": {
+ "command": "uvx",
+ "args": [
+ "alibabacloud-rds-openapi-mcp-server@latest"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_access_key",
+ "ALIBABA_CLOUD_SECURITY_TOKEN": "your_sts_token"
+ }
+ }
+ }
+}
+```
+
+
+

+
+*在Cherry Studio中配置RDS MCP服务器*
+
+
+

+
+*在Cherry Studio中提问展示所有可用的工具*
+
+
+> 📝 **注意**: `ALIBABA_CLOUD_SECURITY_TOKEN` 仅在使用STS Token时填入,如果使用AccessKey方式请保留该值为空。
+
+
+### Claude Desktop集成 🤖
+
+在MCP客户端配置中添加:
+
+```json
+{
+ "mcpServers": {
+ "rds-openapi-mcp-server": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "d:/path/to/alibabacloud-rds-openapi-mcp-server/src/alibabacloud_rds_openapi_mcp_server",
+ "run",
+ "server.py"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_access_key",
+ "ALIBABA_CLOUD_SECURITY_TOKEN": "your_sts_token"
+ }
+ }
+ }
+}
+```
+
+## 🤝 贡献指南
+
+我们欢迎您的贡献!请按照以下步骤操作:
+
+1. 🍴 Fork本仓库
+2. 🌟 创建特性分支 (`git checkout -b feature/amazing-feature`)
+3. 💾 提交您的修改 (`git commit -m '添加新特性'`)
+4. 📤 推送到分支 (`git push origin feature/amazing-feature`)
+5. 🔄 创建Pull Request
+
+## 📄 许可证
+
+本项目采用Apache 2.0许可证
+
+## 💬 技术支持
+
+如有问题、意见或需要支持,请通过以下方式联系我们:
+
+- 💬 **钉钉群**: 106730017609
+- 🐛 **GitHub Issues**: [创建问题](https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server/issues)
+
+### 常见问题 ❓
+
+
+如何获取阿里云AccessKey?
+
+1. 登录阿里云控制台
+2. 点击右上角头像,选择"AccessKey管理"
+3. 创建新的AccessKey对
+4. 妥善保管AccessKey Secret
+
+
+
+
+支持哪些RDS数据库引擎?
+
+目前支持:
+- MySQL
+- SQL Server
+- PostgreSQL
+- MariaDB
+
+
+
diff --git a/assets/README_WIN_EN.md b/assets/README_WIN_EN.md
new file mode 100644
index 0000000..0177d5f
--- /dev/null
+++ b/assets/README_WIN_EN.md
@@ -0,0 +1,320 @@
+# Alibaba Cloud RDS OpenAPI MCP Server
+
+🚀 Provides MCP server support for Alibaba Cloud RDS through OpenAPI. This project exposes Alibaba Cloud RDS, VPC, and billing APIs through the [MCP](https://github.com/CherryHQ/mcp) framework, packaging common database management tasks into easy-to-use tools.
+
+
+
+> ⚠️ **Note**: This documentation is based on Windows platform.
+
+## 📋 Table of Contents
+- [✨ Features](#-features)
+- [📋 Prerequisites](#-prerequisites)
+- [🔧 Installation](#-installation)
+- [⚙️ Configuration](#️-configuration)
+- [🚀 Quick Start](#-quick-start)
+- [🛠️ Available Tools](#️-available-tools)
+- [💡 Usage Scenarios](#-usage-scenarios)
+- [🔌 Integration](#-integration)
+- [🤝 Contributing](#-contributing)
+- [📄 License](#-license)
+- [💬 Support](#-support)
+
+## ✨ Features
+
+- 🏗️ **Comprehensive RDS Management**: Create, configure, and manage RDS instances
+- 🔒 **Security & Compliance**: IP allowlist management and security auditing
+- 📊 **Performance Monitoring**: Query performance metrics and logs
+- 💰 **Cost Optimization**: Billing analysis and resource optimization
+- 🌐 **Multi-Environment Support**: Development, testing, and production environments
+- 🔗 **VPC Integration**: Complete VPC and switch management capabilities
+
+## 📋 Prerequisites
+
+- 🐍 Python 3.12 or higher
+- ☁️ Alibaba Cloud account with appropriate permissions
+- 🔑 Access Key ID and Access Key Secret (or STS Token)
+
+## 🔧 Installation
+
+### Method 1: Quick Start with uv (Recommended)
+
+1. **Install uv**
+ ```powershell
+ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
+ ```
+
+2. **Run the server**
+ ```bash
+ uvx alibabacloud-rds-openapi-mcp-server@latest
+ ```
+
+### Method 2: Install from Source
+
+1. **Install Python 3.12+**
+
+ Download from [Python official website](https://www.python.org/downloads/)
+
+2. **Clone the repository**
+ ```bash
+ git clone https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server.git
+ cd alibabacloud-rds-openapi-mcp-server
+ ```
+
+3. **Install dependencies**
+ ```bash
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_bssopenapi20171214
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_rds20140815
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ alibabacloud_vpc20160428
+ pip install -i https://mirrors.aliyun.com/pypi/simple/ mcp
+ ```
+
+4. **Run the server**
+ ```bash
+ python src/alibabacloud_rds_openapi_mcp_server/server.py
+ ```
+
+## ⚙️ Configuration
+
+### Environment Variable Setup
+
+Before starting the MCP server, you need to set the necessary environment variables:
+
+#### PowerShell Configuration
+```powershell
+# Set server transport mode
+$env:SERVER_TRANSPORT="sse"
+
+# Set Alibaba Cloud credentials
+$env:ALIBABA_CLOUD_ACCESS_KEY_ID=""
+$env:ALIBABA_CLOUD_ACCESS_KEY_SECRET=""
+
+# Optional: Set STS Token when using temporary credentials
+$env:ALIBABA_CLOUD_SECURITY_TOKEN=""
+```
+
+#### Command Prompt (CMD) Configuration
+```cmd
+REM Set server transport mode
+set SERVER_TRANSPORT=sse
+
+REM Set Alibaba Cloud credentials
+set ALIBABA_CLOUD_ACCESS_KEY_ID=
+set ALIBABA_CLOUD_ACCESS_KEY_SECRET=
+
+REM Optional: Set STS Token when using temporary credentials
+set ALIBABA_CLOUD_SECURITY_TOKEN=
+```
+
+> ⚠️ **Important**: Please replace `` with your actual credential information. These environment variables are only valid for the current terminal session.
+
+## 🚀 Quick Start
+
+### Server Startup Verification
+
+After successful startup, you will see output similar to the following:
+```
+INFO: Started server process [Process ID]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+```
+
+### Test Installation
+
+After configuration is complete, you can test the effectiveness of MCP tools using a simple prompt:
+```
+Please list all available tools in a list format, showing titles and corresponding brief descriptions.
+```
+
+## 🛠️ Available Tools
+
+### 🏗️ RDS Instance Management
+- `create_db_instance`: Create RDS instance
+- `describe_db_instances`: Query instances
+- `describe_db_instance_attribute`: Query detailed instance information
+- `modify_db_instance_description`: Modify RDS instance description
+- `modify_db_instance_spec`: Modify RDS instance specifications
+- `restart_db_instance`: Restart RDS instance
+
+### 🔒 Security & Access Control
+- `modify_security_ips`: Modify IP allowlist
+- `describe_db_instance_ip_allowlist`: Batch query IP allowlist configuration
+- `attach_whitelist_template_to_instance`: Bind allowlist template to instance
+- `describe_all_whitelist_template`: Query allowlist template list
+- `describe_instance_linked_whitelist_template`: Query allowlist templates bound to instances
+
+### 👥 Database & User Management
+- `create_db_instance_account`: Create RDS instance account
+- `describe_db_instance_accounts`: Batch query account information
+- `describe_db_instance_databases`: Batch query database information
+
+### 🌐 Network & Connection
+- `allocate_instance_public_connection`: Allocate public connection for RDS instance
+- `describe_db_instance_net_info`: Batch query network configuration details
+- `describe_vpcs`: Query VPC list
+- `describe_vswitches`: Query VSwitch list
+
+### 📊 Monitoring & Performance
+- `describe_db_instance_performance`: Query instance performance data
+- `describe_error_logs`: Query instance error logs
+- `describe_slow_log_records`: Query slow log records of RDS instance
+- `describe_db_instance_parameters`: Batch query parameter information
+- `modify_parameter`: Modify RDS instance parameters
+
+### 📦 Resource Management
+- `describe_available_classes`: Query available instance specifications and storage ranges
+- `describe_available_zones`: Query available zones for RDS instances
+- `add_tags_to_db_instance`: Add tags to RDS instance
+
+### 💰 Billing & Cost Management
+- `describe_bills`: Query user's consumption summary for all product instances or billing items within a specific billing cycle
+
+### 🔧 Utility Tools
+- `get_current_time`: Get current time
+
+## 💡 Usage Scenarios
+
+### Scenario 1: Resource Provisioning & Environment Initialization 🏗️
+
+**Create a production RDS MySQL instance in Hangzhou region:**
+```
+Create a production RDS MySQL instance in cn-hangzhou region with configuration, version, allowlist and other settings identical to instance rm-bp1696hd82oc438fl, and tag it with: production environment audit, release before month-end
+```
+
+**Create read-only user account:**
+```
+On instance rm-bp1696hd82oc438fl in cn-hangzhou region, create a read-only account readonly_user with password: Strong!Pa$$word and grant it read-only access to the report_db database.
+```
+
+### Scenario 2: Security & Compliance Automation Audit 🔒
+
+**Add bastion host IP to security group:**
+```
+Immediately add the new bastion host IP 100.101.102.103 to the 'ops_allowlist' security group of all RDS MySQL instances in cn-hangzhou region.
+```
+
+**Security compliance audit:**
+```
+Audit all RDS SQL Server instances and find all instances in cn-hangzhou whose allowlist does not include IP 123.123.123.123
+```
+
+### Scenario 3: Cost Optimization 💰
+
+**Performance-based cost optimization:**
+```
+Analyze all RDS MySQL instances tagged as "test environment" in cn-hangzhou region over the past 3 days, identify those with average CPU utilization below 5%, and recommend more economical instance specifications.
+```
+
+### Scenario 4: Common Operations & Maintenance 🔧
+
+**Instance troubleshooting:**
+```
+Instance rm-bp1696hd82oc438f in cn-hangzhou region is currently unreachable, please immediately attempt to restart this instance.
+```
+
+## 🔌 Integration
+
+### Cherry Studio Integration (Recommended) 🍒
+
+Cherry Studio is a cross-platform AI client that integrates multiple large language models, supporting AI conversations, drawing, and other features designed to enhance creative efficiency.
+
+Add the following configuration to your MCP client configuration file:
+
+```json
+{
+ "mcpServers": {
+ "rds-openapi-mcp-server": {
+ "command": "uvx",
+ "args": [
+ "alibabacloud-rds-openapi-mcp-server@latest"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_access_key",
+ "ALIBABA_CLOUD_SECURITY_TOKEN": "your_sts_token"
+ }
+ }
+ }
+}
+```
+
+
+

+
+*Configuring RDS MCP Server in Cherry Studio*
+
+
+

+
+*Displaying all available tools in Cherry Studio*
+
+> 📝 **Note**: `ALIBABA_CLOUD_SECURITY_TOKEN` should only be filled when using STS Token. If using AccessKey method, please leave this value empty.
+
+### Claude Desktop Integration 🤖
+
+Add to your MCP client configuration:
+
+```json
+{
+ "mcpServers": {
+ "rds-openapi-mcp-server": {
+ "command": "uv",
+ "args": [
+ "--directory",
+ "d:/path/to/alibabacloud-rds-openapi-mcp-server/src/alibabacloud_rds_openapi_mcp_server",
+ "run",
+ "server.py"
+ ],
+ "env": {
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "your_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "your_access_key",
+ "ALIBABA_CLOUD_SECURITY_TOKEN": "your_sts_token"
+ }
+ }
+ }
+}
+```
+
+## 🤝 Contributing
+
+We welcome your contributions! Please follow these steps:
+
+1. 🍴 Fork this repository
+2. 🌟 Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. 💾 Commit your changes (`git commit -m 'Add new feature'`)
+4. 📤 Push to the branch (`git push origin feature/amazing-feature`)
+5. 🔄 Create a Pull Request
+
+## 📄 License
+
+This project is licensed under the Apache 2.0 License
+
+## 💬 Support
+
+For questions, feedback, or support, please contact us through:
+
+- 💬 **DingTalk Group**: 106730017609
+- 🐛 **GitHub Issues**: [Create Issue](https://github.com/aliyun/alibabacloud-rds-openapi-mcp-server/issues)
+
+### FAQ ❓
+
+
+How to obtain Alibaba Cloud AccessKey?
+
+1. Log in to Alibaba Cloud Console
+2. Click on the avatar in the upper right corner and select "AccessKey Management"
+3. Create a new AccessKey pair
+4. Keep your AccessKey Secret secure
+
+
+
+
+Which RDS database engines are supported?
+
+Currently supported:
+- MySQL
+- SQL Server
+- PostgreSQL
+- MariaDB
+
+
\ No newline at end of file
diff --git a/assets/buy_rds.gif b/assets/buy_rds.gif
new file mode 100644
index 0000000..30753a6
Binary files /dev/null and b/assets/buy_rds.gif differ
diff --git a/assets/buy_rds_cn.gif b/assets/buy_rds_cn.gif
new file mode 100644
index 0000000..38a9824
Binary files /dev/null and b/assets/buy_rds_cn.gif differ
diff --git a/assets/cherry-config.png b/assets/cherry-config.png
new file mode 100644
index 0000000..f4aaea2
Binary files /dev/null and b/assets/cherry-config.png differ
diff --git a/assets/cherry_studio_list_tools.png b/assets/cherry_studio_list_tools.png
new file mode 100644
index 0000000..0d0ece4
Binary files /dev/null and b/assets/cherry_studio_list_tools.png differ
diff --git a/assets/cherry_studio_list_tools_en.png b/assets/cherry_studio_list_tools_en.png
new file mode 100644
index 0000000..3c56788
Binary files /dev/null and b/assets/cherry_studio_list_tools_en.png differ
diff --git a/assets/diagnose.gif b/assets/diagnose.gif
new file mode 100644
index 0000000..8f8aec2
Binary files /dev/null and b/assets/diagnose.gif differ
diff --git a/assets/diagnose_cn.gif b/assets/diagnose_cn.gif
new file mode 100644
index 0000000..c52e97b
Binary files /dev/null and b/assets/diagnose_cn.gif differ
diff --git a/assets/dingding.png b/assets/dingding.png
new file mode 100644
index 0000000..db76547
Binary files /dev/null and b/assets/dingding.png differ
diff --git a/assets/import_mcp_cherry.png b/assets/import_mcp_cherry.png
new file mode 100644
index 0000000..2a45392
Binary files /dev/null and b/assets/import_mcp_cherry.png differ
diff --git a/assets/import_mcp_cherry_en.png b/assets/import_mcp_cherry_en.png
new file mode 100644
index 0000000..54dffd6
Binary files /dev/null and b/assets/import_mcp_cherry_en.png differ
diff --git a/assets/mcp_turn_on.png b/assets/mcp_turn_on.png
new file mode 100644
index 0000000..4c6c49a
Binary files /dev/null and b/assets/mcp_turn_on.png differ
diff --git a/component/mydba/README.md b/component/mydba/README.md
new file mode 100644
index 0000000..178a4c5
--- /dev/null
+++ b/component/mydba/README.md
@@ -0,0 +1,149 @@
+English | 中文
+
+# Alibaba Cloud Database MyDBA Agent
+
+## Features
+
+1. **Supports management of Alibaba Cloud RDS**, including:
+ - Instance information query
+ - RDS Issue analysis
+ - Purchase and modify RDS instance
+
+2. **Query data for self-built databases**, assisting with data queries, statistics and analysis.
+
+## Installation Guide
+
+### Environment Preparation
+
+1. **Install `uv`**:
+ - Install `uv` via [Astral](https://docs.astral.sh/uv/getting-started/installation/)
+ - Install `uv` via [GitHub README](https://github.com/astral-sh/uv#installation)
+ - Download `uv` from [GitHub Release](https://github.com/astral-sh/uv/releases)
+
+2. **Install Python**:
+ - Use the following command to install Python:
+
+ ```shell
+ uv python install 3.12
+ ```
+
+3. **Apply for a LLM api key**:
+ - Compatible with OpenAI client, support Qwen and Deepseek.
+
+4. **Prepare an Alibaba Cloud account AK/SK**:
+ - Ensure your account having the access permission with Alibaba Cloud RDS service (Policy Name: AliyunRDSFullAccess).
+
+### Install Dependencies
+
+Install dependency modules using `uv`:
+
+```shell
+export UV_DEFAULT_INDEX="https://mirrors.aliyun.com/pypi/simple" # optional
+uv sync --inexact
+```
+
+### Service Initialization
+
+1. **Prepare Configuration File**
+ - Default path: `/usr/local/mydba/config_app.ini`
+ - Configure parameters in the `model`, `app`, and `rag` sections:
+
+ ```ini
+ [common]
+ debug = False
+ config_database = sqlite:///usr/local/mydba/sqlite_app.db
+
+ [log]
+ dir = /usr/local/mydba/logs
+ name = mydba
+ file_level = INFO
+
+ [model]
+ api_key = sk-xxx ; LLM api key
+ base_url = https://api.deepseek.com ; LLM api base url (example is the model address of Deepseek)
+ model = deepseek-chat ; LLM model name (example is the model name of Deepseek)
+ max_tokens = 1000
+ temperature = 1.0
+
+ [app]
+ refresh_interval = 60
+ max_steps = 100
+ security_key = xxxxxxxxxxxxxxxx ; Key for encryption, 16-byte length, for internal data protection
+
+ [rag]
+ api_key = sk-xxx ; LLM api key
+ base_url = https://dashscope.aliyuncs.com/compatible-mode/v1 ; LLM api base url (example is the model address of Qwen)
+ embedding = text-embedding-v2 ; Embedding model name (Qwen supports embedding api calls)
+ data_dir = /usr/local/mydba/vector_store
+ ```
+
+2. **Create Log Directory**
+ - The log directory can be found in the configuration: [log].dir
+ - Default path: /usr/local/mydba/logs
+
+ ```shell
+ mkdir /usr/local/mydba/logs
+ ```
+
+3. **Initialize Agent**
+ - Execute the following command to initialize the Agent. Ensure you have correctly configured the **`config_app.ini`** file and replace `xxxxxx` with your Alibaba Cloud account AK/SK.
+
+ ```shell
+ uv --directory /path/to/mydba \
+ run init_config.py \
+ init-project \ # Initialize project
+ --config_file /usr/local/mydba/config_app.ini \ # Configuration file path
+ --reset \ # Clear existing configuration (optional)
+ --rds_access_id xxxxxx \ # Replace with your Alicloud account ID
+ --rds_access_key xxxxxx # Replace with your Alicloud account secret
+ ```
+
+4. **Add Self-Built Database**
+ - Execute the following command to add a self-built database. Ensure you have correctly configured the **`config_app.ini`** file and replace `--db_info` parameters with actual database connection details.
+
+ ```shell
+ uv --directory /path/to/mydba \
+ run init_config.py \
+ add-db \ # Add self-built database
+ --config_file /usr/local/mydba/config_app.ini \ # Path to the configuration file
+ --db_info 'mysql####127.0.0.1##3306##root##123456##utf8mb4##mybase' # Database connection info, pay attention to the escape of special characters
+ ```
+
+5. **Initialize RAG Tool**
+ - Execute the following command to initialize the RAG tool. Ensure you have correctly configured the **`config_app.ini`** file and added the **self-built database**.
+
+ ```shell
+ uv --directory /path/to/mydba/mydba/mcp/rag \ # RAG working directory ./mydba/mcp/rag
+ run rag_init.py \ # Run RAG initialization script
+ init-config \ # Initialize configuration
+ --config_file /usr/local/mydba/config_app.ini # Path to the configuration file
+ ```
+
+### Service Startup
+
+- Execute the start command: **`mydba`** (install agent via MyBase console, this command will register in the OS)
+
+ ```shell
+ mydba
+ ```
+
+- Or use the startup script: **`mydba.sh`** (built-in startup script, use directly if default installation path is unchanged)
+
+ ```shell
+ sh /path/to/mydba/shell/mydba.sh
+ ```
+
+- Or manually execute the following commands:
+
+ ```shell
+ # Set environment variables (optional, default: /usr/local/mydba/config_app.ini)
+ export MYDBA_CONFIG_FILE=/path/to/mydba/config_app.ini
+ # Start RAG Server
+ nohup uv --directory /path/to/mydba/mydba/mcp/rag run rag_server.py >> /path/to/mydba/logs/rag.log 2>&1 &
+ # Start MyDBA
+ uv --directory /path/to/mydba run main.py
+ ```
+
+## Contact Us
+
+- Welcome joining the DingTalk group for feedback, refer to the README.md of RDS MCP for details.
\ No newline at end of file
diff --git a/component/mydba/README_CN.md b/component/mydba/README_CN.md
new file mode 100644
index 0000000..9ef47af
--- /dev/null
+++ b/component/mydba/README_CN.md
@@ -0,0 +1,148 @@
+English | 中文
+
+# 阿里云数据库 MyDBA 智能体
+
+## 特性
+
+1. **支持对阿里云 RDS 进行管理**,包括:
+ - 实例信息查询
+ - 问题分析
+ - 购买与变配
+2. **对自建数据库进行问数**,帮助进行数据查询、统计与分析。
+
+## 安装指南
+
+### 环境准备
+
+1. **安装 `uv`**:
+ - [Astral](https://docs.astral.sh/uv/getting-started/installation/) 安装 `uv`
+ - [GitHub README](https://github.com/astral-sh/uv#installation) 安装 `uv`
+ - [GitHub Release](https://github.com/astral-sh/uv/releases) 下载 `uv`
+
+2. **安装 Python**:
+ - 使用以下命令安装 Python:
+
+ ```shell
+ uv python install 3.12
+ ```
+
+3. **申请大模型 Key**:
+ - 兼容 OpenAI 客户端,支持通义千问、Deepseek。
+
+4. **准备阿里云账号**:
+ - 确保你有阿里云 RDS 服务访问权限(策略名:AliyunRDSFullAccess)的账号凭证。
+
+### 安装依赖
+
+使用 `uv` 安装依赖模块:
+
+```shell
+export UV_DEFAULT_INDEX="https://mirrors.aliyun.com/pypi/simple"
+uv sync --inexact
+```
+
+### 服务初始化
+
+1. **准备配置文件**
+ - 默认路径:`/usr/local/mydba/config_app.ini`
+ - 配置好 `model`、`app` 和 `rag` 部分的参数项:
+
+ ```ini
+ [common]
+ debug = False
+ config_database = sqlite:///usr/local/mydba/sqlite_app.db
+
+ [log]
+ dir = /usr/local/mydba/logs
+ name = mydba
+ file_level = INFO
+
+ [model]
+ api_key = sk-xxx ; 大模型 key
+ base_url = https://api.deepseek.com ; 大模型调用地址(这里是 Deepseek 模型地址)
+ model = deepseek-chat ; 模型名称(这里是 Deepseek 模型名称)
+ max_tokens = 1000
+ temperature = 1.0
+
+ [app]
+ refresh_interval = 60
+ max_steps = 100
+ security_key = xxxxxxxxxxxxxxxx ; 加密 key,固定 16 字节长度,用于工程内部数据保护
+
+ [rag]
+ api_key = sk-xxx ; 大模型 key
+ base_url = https://dashscope.aliyuncs.com/compatible-mode/v1 ; 大模型调用地址(这里是通义模型地址)
+ embedding = text-embedding-v2 ; embedding 模型名称(通义千问支持 embedding 调用)
+ data_dir = /usr/local/mydba/vector_store
+ ```
+
+2. **创建日志目录**
+ - 日志目录可以查看配置: [log].dir
+ - 默认路径:`/usr/local/mydba/logs`
+
+ ```shell
+ mkdir /usr/local/mydba/logs
+ ```
+
+3. **初始化 Agent**
+ - 执行以下命令以初始化 Agent。请确保您已经正确配置了 **`config_app.ini`** 文件,并用您的阿里云账号替换 `xxxxxx`。
+
+ ```shell
+ uv --directory /path/to/mydba \
+ run init_config.py \
+ init-project \ # 初始化工程
+ --config_file /usr/local/mydba/config_app.ini \ # 配置文件路径
+ --reset \ # 清空已有配置(可选)
+ --rds_access_id xxxxxx \ # 替换为您的阿里云账号 ID
+ --rds_access_key xxxxxx # 替换为您的阿里云账号密钥
+ ```
+
+4. **添加自建数据库**
+ - 执行以下命令以添加自建数据库。请确保您已正确配置 **`config_app.ini`** 文件,并根据实际情况替换 `--db_info` 参数中的数据库连接信息。
+
+ ```shell
+ uv --directory /path/to/mydba \
+ run init_config.py \
+ add-db \ # 添加自建数据库
+ --config_file /usr/local/mydba/config_app.ini \ # 配置文件路径
+ --db_info 'mysql####127.0.0.1##3306##root##123456##utf8mb4##mybase' # 数据库连接信息,注意特殊字符的转义
+ ```
+
+5. **初始化 RAG 工具**
+ - 执行以下命令以初始化 RAG 工具。请确保您已经正确配置了 **`config_app.ini`** 文件,并添加了**自建数据库**。
+
+ ```shell
+ uv --directory /path/to/mydba/mydba/mcp/rag \ # 这里是 RAG 的工作目录 ./mydba/mcp/rag
+ run rag_init.py \ # 运行 RAG 初始化脚本
+ init-config \ # 初始化配置
+ --config_file /usr/local/mydba/config_app.ini # 配置文件路径
+ ```
+
+### 服务启动
+
+- 执行启动命令:**`mydba`** 通过控制台安装的智能体,会在操作系统注册此命令。
+
+ ```shell
+ mydba
+ ```
+
+- 或者执行启动脚本:**`mydba.sh`** 智能体自带的启动脚本,如果没有修改默认的安装路径,可直接使用。
+
+ ```shell
+ sh /path/to/mydba/shell/mydba.sh
+ ```
+
+- 或者手动执行如下命令:
+
+ ```shell
+ # 配置环境变量(可选,默认:/usr/local/mydba/config_app.ini)
+ export MYDBA_CONFIG_FILE=/path/to/mydba/config_app.ini
+ # 启动 RAG Server
+ nohup uv --directory /path/to/mydba/mydba/mcp/rag run rag_server.py >> /path/to/mydba/logs/rag.log 2>&1 &
+ # 启动 MyDBA
+ uv --directory /path/to/mydba run main.py
+ ```
+
+## 联系我们
+
+- 向上查看 RDS MCP 的 README_CN.md,加入钉钉群。
diff --git a/component/mydba/config_app.ini b/component/mydba/config_app.ini
new file mode 100644
index 0000000..265098b
--- /dev/null
+++ b/component/mydba/config_app.ini
@@ -0,0 +1,27 @@
+[common]
+debug = False
+config_database = sqlite:///usr/local/mydba/sqlite_app.db
+
+[log]
+dir = /usr/local/mydba/logs
+name = mydba
+file_level = INFO
+
+[model]
+api_key =
+base_url =
+model =
+max_tokens = 1000
+temperature = 1.0
+
+[app]
+refresh_interval = 60
+max_steps = 100
+# 16字节长度
+security_key =
+
+[rag]
+api_key =
+base_url =
+embedding =
+data_dir = /usr/local/mydba/vector_store
\ No newline at end of file
diff --git a/component/mydba/init_config.py b/component/mydba/init_config.py
new file mode 100644
index 0000000..a49c213
--- /dev/null
+++ b/component/mydba/init_config.py
@@ -0,0 +1,514 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import argparse
+import configparser
+import json
+import os
+import re
+import textwrap
+from argparse import Namespace
+from string import Template
+from typing import Any, Dict, List, Optional, Union
+from mydba.app.config import config_manager
+from mydba.app.config.agent import AgentMode
+from mydba.app.config.mcp_tool import Transport
+from mydba.app.config.settings import settings as app_settings
+from mydba.app.database.base_database import BaseDatabases
+from mydba.app.prompt import ask_table, chat, rds_agent, reflection, router
+from mydba.common import encryption
+from mydba.common.global_settings import global_settings
+
+def get_agent_config() -> List[Dict]:
+ """
+ 获取 agent 的配置信息,**编辑此部分内容,定制 agent**
+ 配置项中,prompts 为提示词模版,包含字段:
+ 1) system: 系统提示词模版,
+ 2) user: 用户提示词模版,在需要改写用户提示词的场景下使用,例如:REFLECTION 模式、ROUTER 模式,
+ 3) reflection_system: 反思提示词模版,仅在 REFLECTION 模式下使用,
+ 4) reflection_user: 反思提示词模版,仅在 REFLECTION 模式下使用,
+ 5) condition: 条件提示词(List),在意图识别时,描述意图的约束条件,
+ 6) shot: 样本提示词(List),在意图识别时,提供意图样本
+ Returns:
+ List[Dict]: Agent 的配置信息列表
+ """
+ main_agent = {
+ "name": "main_agent",
+ "mode": AgentMode.ROUTER,
+ "intent": "识别意图",
+ "intent_description": "识别用户的意图,并路由请求到相关 Agent",
+ "prompts": {
+ # 系统提示词模版
+ "system": router.SYSTEM_PROMPT,
+ # 用户提示词模版,意图识别时会改写用户指令
+ "user": router.USER_PROMPT
+ },
+ "is_main": True,
+ "is_default": False
+ }
+ rds_agent_ = {
+ "name": "rds_agent",
+ "mode": AgentMode.USING_TOOL,
+ "intent": "阿里云RDS管理",
+ "intent_description": "进行阿里云 RDS 数据库的管理运维,或者对阿里云 RDS 数据库进行问题诊断",
+ "prompts": {
+ # 系统提示词模版
+ "system": rds_agent.SYSTEM_PROMPT,
+ # 相关的意图约束条件,集成进意图识别提示词里,用于提升意图识别的准确度
+ "condition":
+ [
+ "用户明确希望进行阿里云 RDS 数据库相关的操作时,才能归类到阿里云RDS管理",
+ "用户希望对阿里云 RDS 数据库进行问题诊断时,才能归类到阿里云RDS管理"
+ ],
+ # 相关的意图样例,集成进意图识别提示词里,用于提升意图识别的准确度
+ "shot":
+ [
+ "查下张北有多少RDS实例",
+ "rm-8vb69ma75lpnug7hp 性能如何?",
+ "创建一个阿里云 RDS 实例",
+ ]
+ },
+ "mcps": {
+ "allow": ["rds-openapi-mcp-server", "local-tool"]
+ },
+ "is_main": False,
+ "is_default": False
+ }
+ ask_table_agent = {
+ "name": "ask_table_agent",
+ "mode": AgentMode.USING_TOOL,
+ "intent": "数据查询",
+ "intent_description": "帮助生成查询计划,执行数据库查询,最后完成数据的统计和分析",
+ "prompts": {
+ # 系统提示词模版
+ "system": ask_table.SYSTEM_PROMPT,
+ # 相关的意图约束条件,集成进意图识别提示词里,用于提升意图识别的准确度
+ "condition":
+ [
+ "用户希望进行数据计算和统计时,要归类到数据查询"
+ ],
+ # 相关的意图样例,集成进意图识别提示词里,用于提升意图识别的准确度
+ "shot":
+ [
+ "查询集群信息"
+ "过去几天的售卖量",
+ "有多少台主机"
+ ]
+ },
+ "mcps": {
+ "allow": ["rag", "local-tool"]
+ },
+ "is_main": False,
+ "is_default": False
+ }
+ default_agent = {
+ "name": "default_agent",
+ "mode": AgentMode.CHAT,
+ "intent": "默认",
+ "intent_description": "无法匹配用户意图,使用此默认项",
+ "prompts": {"system": chat.SYSTEM_PROMPT},
+ "is_main": False,
+ "is_default": True
+ }
+ agents = [main_agent, rds_agent_, ask_table_agent, default_agent]
+ return agents
+
+def get_mcp_config() -> List[Dict]:
+ """获取 mcp 服务的配置信息,**编辑此部分内容,添加工具**"""
+ # 目前 RDS MCP 采用代码的相对路径进行配置,如果不符合请手动调整
+ base_dir = os.path.dirname(os.path.abspath(__file__)) + os.path.sep + '..' + os.path.sep + '..' + os.path.sep + '..'
+ aliyun_rds_dir = os.path.join(base_dir, 'alibabacloud-rds-openapi-mcp-server', 'src', 'alibabacloud_rds_openapi_mcp_server')
+ mcp_aliyun_rds = {
+ "name": "rds-openapi-mcp-server",
+ "transport": Transport.STDIO,
+ "description": "阿里云数据库 RDS 的服务。",
+ "command": 'uv',
+ "args": [
+ "--directory",
+ aliyun_rds_dir,
+ "run",
+ "server.py"
+ ],
+ "envs": {
+ "FASTMCP_LOG_LEVEL": "CRITICAL",
+ # 注意:此处的 $rds_access_id 和 $rds_access_key 会在命令行参数中传入
+ # 需要在命令行中使用 --rds_access_id 和 --rds_access_key 参数传入
+ "ALIBABA_CLOUD_ACCESS_KEY_ID": "$rds_access_id",
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET": "$rds_access_key",
+ },
+ "security": True # 启用加密保存,保护 ak 安全
+ }
+ mcp_rag = {
+ "name": "rag",
+ "transport": Transport.SSE,
+ "description": "本地 RAG 知识库服务。",
+ "server_uri": 'http://127.0.0.1:8006/sse'
+ }
+ return [mcp_aliyun_rds, mcp_rag]
+
+def get_db_config() -> Optional[List[Dict]]:
+ """
+ 获取数据库的配置信息。用于问询数据场景,包括:建设 RAG 知识库、执行数据库查询。
+ **编辑此部分内容,增删数据库配置**
+ """
+ # test_db = {
+ # "type": "mysql",
+ # "host": "127.0.0.1",
+ # "port": 3306,
+ # "user": "test_user",
+ # "password": "123456",
+ # "charset": "utf8mb4",
+ # "database": "test_db"
+ # }
+ # dbs = [test_db, ]
+ return []
+
+async def prepare_agent_config(agents: List[Dict], db: BaseDatabases, reset: bool) -> None:
+ """
+ 准备 agent 的配置信息
+ Args:
+ agents (List[Dict]): Agent 的配置信息列表
+ db (BaseDatabases): 工程配置库实例
+ reset (bool): 是否清空已存在的配置
+ """
+ sql_reset = 'DELETE FROM agent'
+ sql_check = 'SELECT * FROM agent WHERE name=?'
+ sql_update = 'UPDATE agent SET mode=?, intent=?, intent_description=?, prompts=?, mcps=?, is_main=?, is_default=? WHERE name=?'
+ sql_insert = 'INSERT INTO agent (name, mode, intent, intent_description, prompts, mcps, is_main, is_default) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
+ if reset:
+ await db.execute(sql_reset)
+ for agent in agents:
+ result = await db.query(sql_check, (agent["name"], ))
+ if result:
+ params = [
+ agent["mode"].value,
+ agent["intent"],
+ agent["intent_description"],
+ json.dumps(agent["prompts"], ensure_ascii=False) if agent.get("prompts") else None,
+ json.dumps(agent["mcps"], ensure_ascii=False) if agent.get("mcps") else None,
+ 1 if agent["is_main"] else 0,
+ 1 if agent["is_default"] else 0,
+ agent["name"]
+ ]
+ await db.execute(sql_update, params)
+ else:
+ params = [
+ agent["name"],
+ agent["mode"].value,
+ agent["intent"],
+ agent["intent_description"],
+ json.dumps(agent["prompts"], ensure_ascii=False) if agent.get("prompts") else None,
+ json.dumps(agent["mcps"], ensure_ascii=False) if agent.get("mcps") else None,
+ 1 if agent["is_main"] else 0,
+ 1 if agent["is_default"] else 0
+ ]
+ await db.execute(sql_insert, params)
+ return
+
+def handle_mcp_server_conf(
+ options: Optional[Union[dict, list]],
+ security: Optional[bool],
+ args: Namespace) -> Optional[str]:
+ """
+ 处理 mcp server 的 args、envs 配置项,利用命令行参数进行实例化,并根据需要进行加密保存
+ Args:
+ options (Optional[Union[dict, list]]): 待实例化的配置项
+ security (Optional[bool]): 是否加密保存
+ args (Namespace): 启动时传入的命令行参数,用于参数模版替换
+ Returns:
+ Optional[str]: 处理后的字符串
+ """
+ if not options:
+ return None
+ options_str = None
+ if isinstance(options, dict):
+ dict_options = {}
+ for k, v in options.items():
+ template = Template(v)
+ v = template.safe_substitute(**args.__dict__)
+ dict_options[k] = v
+ options_str = json.dumps(dict_options, ensure_ascii=False)
+ else:
+ list_options = []
+ for arg in options:
+ template = Template(arg)
+ arg = template.safe_substitute(**args.__dict__)
+ list_options.append(arg)
+ options_str = json.dumps(list_options, ensure_ascii=False)
+ if security:
+ options_str = encryption.encrypt(app_settings.SECURITY_KEY, options_str)
+ return options_str
+
+async def prepare_mcp_config(mcp_servers: List[Dict], db: BaseDatabases, reset: bool, args: Namespace) -> None:
+ """
+ 准备 mcp server 的配置信息
+ Args:
+ mcp_servers (List[Dict]): mcp server 的配置信息列表
+ db (BaseDatabases): 工程配置库实例
+ reset (bool): 是否清空已存在的配置
+ args (Namespace): 启动时传入的命令行参数,用于参数模版替换
+ """
+ sql_reset = 'DELETE FROM mcp'
+ sql_check = 'SELECT * FROM mcp WHERE name=?'
+ sql_update = 'UPDATE mcp SET transport=?, description=?, server_uri=?, command=?, args=?, envs=? WHERE name=?'
+ sql_insert = 'INSERT INTO mcp (name, transport, description, server_uri, command, args, envs) VALUES (?, ?, ?, ?, ?, ?, ?)'
+ if reset:
+ await db.execute(sql_reset)
+ for server in mcp_servers:
+ server_args = handle_mcp_server_conf(server.get("args"), server.get("security"), args)
+ server_envs = handle_mcp_server_conf(server.get("envs"), server.get("security"), args)
+ result = await db.query(sql_check, (server["name"], ))
+ if result:
+ params = [
+ server["transport"].value,
+ server["description"],
+ server.get("server_uri"),
+ server.get("command"),
+ server_args if server_args else None,
+ server_envs if server_envs else None,
+ server["name"]
+ ]
+ await db.execute(sql_update, params)
+ else:
+ params = [
+ server["name"],
+ server["transport"].value,
+ server["description"],
+ server.get("server_uri"),
+ server.get("command"),
+ server_args if server_args else None,
+ server_envs if server_envs else None
+ ]
+ await db.execute(sql_insert, params)
+ return
+
+async def prepare_db_config(db_configs: Optional[List[Dict]], db: BaseDatabases, reset: bool) -> None:
+ """
+ 准备数据库的配置信息
+ Args:
+ db_configs (Optional[List[Dict]]): 数据库的配置信息列表
+ db (BaseDatabases): 工程配置库实例
+ reset (bool): 是否清空已存在的配置
+ """
+ sql_reset = 'DELETE FROM db_instance'
+ sql_check = 'SELECT * FROM db_instance WHERE `database`=?'
+ sql_update = 'UPDATE db_instance SET type=?, uri=?, host=?, port=?, user=?, password=?, charset=? WHERE `database`=?'
+ sql_insert = 'INSERT INTO db_instance (type, uri, host, port, user, password, charset, `database`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
+ if reset:
+ await db.execute(sql_reset)
+ if not db_configs:
+ return
+ for db_info in db_configs:
+ result = await db.query(sql_check, (db_info["database"], ))
+ password = encryption.encrypt(app_settings.SECURITY_KEY, db_info["password"])
+ params = [
+ db_info.get("type"),
+ db_info.get("uri"),
+ db_info.get("host"),
+ db_info.get("port"),
+ db_info.get("user"),
+ password,
+ db_info.get("charset"),
+ db_info.get("database")
+ ]
+ if result:
+ await db.execute(sql_update, params)
+ else:
+ await db.execute(sql_insert, params)
+ return
+
+async def init_config(args: Namespace) -> None:
+ """
+ 初始化工程配置
+ Args:
+ args (Namespace): 命令行参数
+ """
+ await config_manager.init_config(database_uri=app_settings.CONFIG_DATABASE)
+ db = BaseDatabases.create_database(uri=app_settings.CONFIG_DATABASE)
+
+ agents = get_agent_config()
+ await prepare_agent_config(agents=agents, db=db, reset=args.reset)
+
+ mcp_servers = get_mcp_config()
+ await prepare_mcp_config(mcp_servers=mcp_servers, db=db, reset=args.reset, args=args)
+
+ db_configs = get_db_config()
+ await prepare_db_config(db_configs=db_configs, db=db, reset=args.reset)
+
+async def add_db_config(args: Namespace) -> None:
+ """
+ 添加 db 配置
+ Args:
+ args (Namespace): 命令行参数
+ """
+ await config_manager.init_config(database_uri=app_settings.CONFIG_DATABASE)
+ db = BaseDatabases.create_database(uri=app_settings.CONFIG_DATABASE)
+ info = parse_db_info(args.db_info)
+ db_info = {
+ "type": info[0],
+ "uri": info[1] if info[1] else None,
+ "host": info[2] if info[2] else None,
+ "port": int(info[3]) if info[3] else None,
+ "user": info[4] if info[4] else None,
+ "password": info[5] if info[5] else None,
+ "charset": info[6] if info[6] else None,
+ "database": info[7] if info[7] else None
+ }
+ await prepare_db_config(db_configs=[db_info], db=db, reset=False)
+
+def parse_db_info(db_info: str) -> list:
+ """
+ 解析 db_info 字符串
+ Args:
+ db_info (str): db_info 字符串,格式为 type##uri##host##port##user##password##charset##database
+ Returns:
+ list: 解析后的信息列表,包含 8 个元素
+ """
+ info = args.db_info.split('##')
+ if len(info) != 8:
+ raise Exception("db config invalid")
+ info[5] = decrypt(info[5])
+ return info
+
+def decrypt(data: str) -> str:
+ """
+ 对于部分敏感信息,传入时有可能做了加密处理,这里统一进行下解密处理
+ 例如:阿里云账号的 access_id 和 access_key,以及数据库的密码等。
+ Args:
+ data (str): 待解密的数据
+ Returns:
+ str: 解密后的数据,如果解密失败或不符合加密格式,则返回原数据
+ """
+ if not data or not bool(re.fullmatch(r'^[0-9a-fA-F]+$', data)) or len(data) <= 32:
+ return data
+ try:
+ return encryption.decrypt(app_settings.SECURITY_KEY, data)
+ except Exception as e:
+ return data
+
+def decrypt_args(args: Namespace) -> None:
+ """
+ 解密命令行参数中的敏感信息
+ Args:
+ args (Namespace): 命令行参数
+ """
+ if hasattr(args, 'rds_access_id'):
+ args.rds_access_id = decrypt(args.rds_access_id)
+ if hasattr(args, 'rds_access_key'):
+ args.rds_access_key = decrypt(args.rds_access_key)
+
+async def main(args) -> None:
+ """
+ 主函数,处理命令行参数并执行相应的操作
+ Args:
+ args (Namespace): 命令行参数
+ """
+ decrypt_args(args)
+ error = False
+ try:
+ if args.command == 'init-project':
+ await init_config(args)
+ elif args.command == 'add-db':
+ await add_db_config(args)
+ else:
+ raise Exception("command not support")
+ global_settings.IS_EXIT = True
+ except configparser.NoOptionError as noe:
+ print(f"lost config option, error: {str(noe)}")
+ error = True
+ except Exception as e:
+ print(f"something wrong, error: {str(e)}")
+ error = True
+ if error:
+ print(f"{args.command} failed")
+ else:
+ print(f"{args.command} successfully")
+
+def parse_arguments() -> Namespace:
+ """解析命令行参数"""
+ parser = argparse.ArgumentParser(
+ description="导入 agent、mcp、db_instance 配置",
+ formatter_class=argparse.RawTextHelpFormatter
+ )
+ subparsers = parser.add_subparsers(dest='command', help='可用命令')
+ init_proj_parser = subparsers.add_parser('init-project', help='初始化工程配置')
+ init_proj_parser.add_argument(
+ "--config_file",
+ type=str,
+ default="/usr/local/mydba/config_app.ini",
+ help="配置文件路径,默认: %(default)s"
+ )
+ init_proj_parser.add_argument(
+ "--reset",
+ action='store_true',
+ default=False,
+ help="清除已存在的配置内容"
+ )
+ # 以下参数为 mcp server 依赖的环境配置信息
+ init_proj_parser.add_argument(
+ "--rds_access_id",
+ type=str,
+ default='',
+ help="阿里云 access_id,用于阿里云 RDS 管理功能"
+ )
+ init_proj_parser.add_argument(
+ "--rds_access_key",
+ type=str,
+ default='',
+ help="阿里云 access_key,用于阿里云 RDS 管理功能"
+ )
+
+ add_db_parser = subparsers.add_parser('add-db', help='添加 DB 配置,用于问询数据')
+ add_db_parser.add_argument(
+ "--config_file",
+ type=str,
+ default="/usr/local/mydba/config_app.ini",
+ help="配置文件路径,默认: %(default)s"
+ )
+ add_db_parser.add_argument(
+ "--db_info",
+ type=str,
+ required=True,
+ help=textwrap.dedent(
+ """\
+ 添加一个 db 配置,用于问询数据功能
+ 格式: type##uri##host##port##user##password##charset##database
+ 例如: mysql####127.0.0.1##3306##test_user##123456##utf8mb4##test_db"""
+ )
+ )
+ return parser.parse_args()
+
+def print_args(args: Namespace) -> None:
+ """打印命令行参数"""
+ print(f"args:")
+ for key, value in args.__dict__.items():
+ # 过滤敏感信息
+ if key.startswith('rds_access'):
+ value = mask_info(value)
+ if key == 'db_info':
+ v = value.split('##')
+ v[5] = mask_info(v[5])
+ value = '##'.join(v)
+ # 打印参数
+ if isinstance(value, str):
+ print(f" {key}: {value}")
+ elif isinstance(value, list):
+ print(f" {key}:")
+ for item in value:
+ print(f" - {item}")
+ else:
+ print(f" {key}: {value}")
+
+def mask_info(info: Optional[str]) -> Optional[str]:
+ if not info:
+ return info
+ if len(info) > 4:
+ return info[:2] + '*' * (len(info) - 4) + info[-2:]
+ return info[:1]+ "**"
+
+if __name__ == "__main__":
+ args = parse_arguments()
+ print_args(args)
+ if not os.path.isfile(args.config_file):
+ raise Exception("conf_file not exist")
+ config_manager.load_from_conf(args.config_file)
+ asyncio.run(main(args), debug=False)
diff --git a/component/mydba/main.py b/component/mydba/main.py
new file mode 100644
index 0000000..09e1e54
--- /dev/null
+++ b/component/mydba/main.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import mydba.app.config.config_manager as config_manager
+from mydba.common.logger import logger, init_logger
+from mydba.app.config.settings import settings as app_settings
+from mydba.common.global_settings import global_settings
+from mydba.common.settings import settings as common_settings
+from mydba.provider.command_line import CommandLineProvider
+
+async def main():
+ await config_manager.load_config(app_settings.CONFIG_FILE)
+ init_logger(common_settings.LOG_CONSOLE_LEVEL, common_settings.LOG_FILE_LEVEL,
+ common_settings.LOG_DIR, common_settings.LOG_NAME)
+ logger.info("MyDBA starting...")
+ command_line = CommandLineProvider()
+ await command_line.run()
+ # 标记服务退出
+ global_settings.IS_EXIT = True
+ logger.info("MyDBA exit")
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main(), debug=common_settings.DEBUG)
+ except KeyboardInterrupt:
+ logger.error(f"keyboard interrupt, MyDBA exited")
+ except Exception as e:
+ logger.error(f"occour excetion, MyDBA exited: {str(e)}")
diff --git a/component/mydba/mydba/__init__.py b/component/mydba/mydba/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/app/__init__.py b/component/mydba/mydba/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/app/agent/base.py b/component/mydba/mydba/app/agent/base.py
new file mode 100644
index 0000000..0cc8287
--- /dev/null
+++ b/component/mydba/mydba/app/agent/base.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+from abc import ABC, abstractmethod
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict, Optional
+from datetime import datetime, timedelta
+from mydba.app.config.settings import settings
+from mydba.app.config.agent import AgentInfo, AgentMode
+from mydba.app.llm import LLM
+from mydba.app.message import memory_history
+from mydba.app.message.message import Message, ToolCall
+from mydba.app.message.memory_history import MemoryInfo
+from mydba.common.session import get_context
+
+def cleanup_decorator(func):
+ def wrapper(self, *args, **kwargs):
+ try:
+ return func(self, *args, **kwargs)
+ finally:
+ if not isinstance(self, BaseAgent):
+ raise TypeError("self must be an instance of BaseAgent")
+ self.cleanup()
+ return wrapper
+
+class BaseAgent(ABC, BaseModel):
+ """
+ Base class for all agents.
+ """
+ name: str = Field(..., description="Agent 名称")
+ intent: Optional[str] = Field(None, description="意图名称")
+ intent_description: Optional[str] = Field(None, description="意图描述")
+ is_main: bool = Field(..., description="是否主 Agent")
+ memory: List[Message] = Field(default_factory=list, description="本次处理过程中的短期记忆")
+ prompt_patterns: Dict[str, str] = Field(default_factory=dict, description="提示词模版,不同类型的 Agent 依赖的提示词模版会有不同")
+ llm: LLM = Field(..., description="LLM 实例")
+
+ @abstractmethod
+ async def run(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ """
+ 该方法用于执行 Agent 的主要逻辑。
+ Args:
+ query (str): 查询内容。
+ context_memory (list): 用户的上下文 memory。
+ Returns:
+ str: 执行结果。
+ """
+ return NotImplementedError("Subclasses must implement this method")
+
+ async def get_history_memory(self) -> List[MemoryInfo]:
+ """
+ 该方法用于获取 Agent 的历史 memory。
+ Returns:
+ list: Agent 的历史 memory。
+ """
+ context = get_context()
+ start_time = datetime.now() - timedelta(minutes=30)
+ context_memory = await memory_history.get_memory(user_name=context.user_name, session=context.session,
+ agent_name=self.name, start_time=start_time)
+ return context_memory[::-1]
+
+ async def save_memory_history(self,
+ system_content: Optional[str] = None,
+ user_content: Optional[str] = None,
+ assistant_content: Optional[str] = None,
+ assistant_tool_calls: Optional[List[ToolCall]] = None,
+ tool_contents: Optional[List[Dict[str, str]]] = None) -> None:
+ """
+ 该方法用于保存 Agent 的 memory。
+ Args:
+ system_content (str): 系统提示词。
+ user_content (str): 用户提示词。
+ assistant_content (str): 大模型返回内容。
+ assistant_tool_calls (List[ToolCall]): 大模型返回的工具调用。
+ tool_contents (List[Dict[str, str]): 工具调用的结果,结果信息的格式为 {"tool_call_id": "xxx", "content": "xxx"}。
+ """
+ context = get_context()
+ await memory_history.save_memory(
+ MemoryInfo(
+ time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ request_id = context.request_id if context else '',
+ user_name="" if not context else context.user_name,
+ session="" if not context else context.session,
+ agent_name=self.name,
+ system_content=system_content,
+ user_content=user_content,
+ assistant_content=assistant_content,
+ assistant_tool_calls=assistant_tool_calls,
+ tool_contents=tool_contents
+ )
+ )
+
+ def format_memory(self, context_memory: Optional[List[MemoryInfo]] = None) -> Optional[List[Message]]:
+ messages = None
+ if context_memory:
+ messages = []
+ for mem in context_memory:
+ if mem.user_content:
+ messages.append(Message.user_message(mem.user_content))
+ if mem.tool_contents:
+ for tool_content in mem.tool_contents:
+ messages.append(Message.tool_message(tool_content["content"],
+ tool_content["tool_call_id"]))
+ if mem.assistant_content or mem.assistant_tool_calls:
+ messages.append(Message.assistant_message(mem.assistant_content,
+ mem.assistant_tool_calls))
+ return messages
+
+ def cleanup(self) -> None:
+ """
+ 该方法用于清理 Agent 的状态和资源。
+ """
+ self.memory.clear()
+
+ @staticmethod
+ def create_agent(agent_info: AgentInfo, llm: LLM) -> "BaseAgent":
+ """
+ 创建 Agent 实例
+ Args:
+ agent_info (dict): Agent 信息
+ llm (LLM): 在同一个请求会话中里,复用 llm
+ Returns:
+ BaseAgent: Agent 实例
+ """
+ from mydba.app.agent.chat import ChatAgent
+ from mydba.app.agent.router import RouterAgent
+ from mydba.app.agent.reflection import ReflectionAgent
+ from mydba.app.agent.using_tool import UsingToolAgent
+ agent_factory = {
+ AgentMode.CHAT.value: ChatAgent,
+ AgentMode.ROUTER.value: RouterAgent,
+ AgentMode.REFLECTION.value: ReflectionAgent,
+ AgentMode.USING_TOOL.value: UsingToolAgent,
+ }
+ if agent_info.mode not in agent_factory:
+ raise ValueError(f"Agent mode {agent_info.mode} not supported")
+ return agent_factory[agent_info.mode](llm=llm, **agent_info.dict())
diff --git a/component/mydba/mydba/app/agent/chat.py b/component/mydba/mydba/app/agent/chat.py
new file mode 100644
index 0000000..701c64b
--- /dev/null
+++ b/component/mydba/mydba/app/agent/chat.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from pydantic import BaseModel, Field
+from typing import List, Optional
+from mydba.app.agent.base import BaseAgent, cleanup_decorator
+from mydba.app.message.memory_history import MemoryInfo
+from mydba.app.message.message import Message
+from mydba.common.logger import logger
+from mydba.common.session import get_context
+
+class ChatAgent(BaseAgent, BaseModel):
+ """
+ 普通对话型 Agent
+ """
+ system_message: Optional[Message] = Field(None, description="对话 Agent 的系统提示信息")
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ prompts = data.get("prompts", {})
+ self.prompt_patterns["system"] = prompts.get("system")
+ self.system_message = Message.system_message(self.prompt_patterns["system"])
+
+ @cleanup_decorator
+ async def run(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ logger.info(f"[{self.name}] start to query: {query}")
+ content = await self._execute_model(query, context_memory)
+ if content is None:
+ logger.error(f"[{self.name}] query model failed, query: {query}")
+ raise Exception(f"Agent({self.name}) query model failed, query: {query}")
+ #logger.info(f"[{self.name}] query over, result: {content}")
+ await self.save_memory_history(system_content=self.system_message.content,
+ user_content=query, assistant_content=content)
+ return content
+
+ async def _execute_model(self, query: str, context_memory: Optional[List[MemoryInfo]]) -> str:
+ context = get_context()
+ messages = self.format_memory(context_memory) if context_memory else []
+ messages.append(Message.user_message(query))
+ result = await self.llm.ask(
+ messages=messages,
+ system_msgs=[self.system_message],
+ stream=context.detail_info,
+ )
+ return result
diff --git a/component/mydba/mydba/app/agent/reflection.py b/component/mydba/mydba/app/agent/reflection.py
new file mode 100644
index 0000000..5b360e8
--- /dev/null
+++ b/component/mydba/mydba/app/agent/reflection.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+from pydantic import BaseModel, Field
+from typing import List, Optional
+from mydba.app.agent.base import BaseAgent, cleanup_decorator
+from mydba.app.config.settings import settings
+from mydba.app.llm import ToolChoice
+from mydba.app.message.memory_history import MemoryInfo
+from mydba.app.message.message import Message
+from mydba.app.tool.base_local_tool import LocalTool
+from mydba.app.tool.tool_manager import tool_manager
+from mydba.common import stream
+from mydba.common.logger import logger
+from mydba.common.session import get_context
+
+class ReflectionAgent(BaseAgent, BaseModel):
+ """
+ 反思型 Agent
+ """
+ system_message: Optional[Message] = Field(None, description="反思型 Agent 在回答问题时,使用的系统提示信息")
+ reflection_system_message: Optional[Message] = Field(None, description="反思型 Agent 在反思答案时,使用的系统提示信息")
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ prompts = data.get("prompts", {})
+ self.prompt_patterns["system"] = prompts.get("system")
+ self.prompt_patterns["user"] = prompts.get("user")
+ self.prompt_patterns["reflection_system"] = prompts.get("reflection_system")
+ self.prompt_patterns["reflection_user"] = prompts.get("reflection_user")
+ self.system_message = Message.system_message(self.prompt_patterns["system"])
+ self.reflection_system_message = Message.system_message(self.prompt_patterns["reflection_system"])
+
+ @cleanup_decorator
+ async def run(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ step_counter = 0
+ content = None
+ reflection = None
+ logger.info(f"[{self.name}] start to query: {query}")
+ while step_counter < 100:
+ # 执行请求或者进行答案改进
+ content = await self._act(query, context_memory, content, reflection)
+ if content is None:
+ logger.error(f"[{self.name}] act failed, query: {query}")
+ raise Exception(f"Agent({self.name}) act failed, query: {query}")
+
+ # 反思太多,直接返回
+ if step_counter >= settings.MAX_STEPS:
+ logger.error(f"[{self.name}] reflect too much, break loop, step_counter: {step_counter}, query: {query}, content: {content}, reflection: {reflection}")
+ return content
+
+ # 反思
+ reflection = await self._reflect(query, content)
+ if reflection is None:
+ logger.info(f"[{self.name}] query over, query: {query}, content: {content}")
+ return content
+ step_counter += 1
+ logger.error(f"[{self.name}] query failed, step_counter: {step_counter}, query: {query}, content: {content}, reflection: {reflection}")
+ raise Exception(f"Agent({self.name}) query failed, step_counter: {step_counter}, query: {query}")
+
+ async def _act(self, query: str, context_memory: Optional[List[MemoryInfo]],
+ answer: Optional[str], reflection: Optional[str]) -> str:
+ """
+ 执行请求或者进行答案改进
+ Args:
+ query (str): 查询内容
+ context_memory (list): 用户的上下文 memory
+ answer (str): 上一轮生成的答案
+ reflection (str): 反馈信息
+ Returns:
+ str: 执行结果
+ """
+ context = get_context()
+ await stream.aprint(f"[A] {self.name} 执行中...")
+ messages = self.format_memory(context_memory) if context_memory else []
+ prompt = query if not reflection else self._get_user_prompt(query, answer, reflection)
+ messages.append(Message.user_message(prompt))
+ # 带上 interactive tool,实现 llm 和用户的交互
+ tool = tool_manager.get_local_tool(LocalTool.INTERACTION)
+ new_answer = None
+ while True:
+ llm_result = await self.llm.ask_tool(
+ messages=messages,
+ system_msgs=[self.system_message],
+ tools=[tool],
+ tool_choice=ToolChoice.AUTO,
+ stream=context.detail_info,
+ )
+ #logger.info(f"[{self.name}] act, result: {llm_result}")
+ if not llm_result.tool_calls:
+ # 不需要调用工具,返回结果
+ new_answer = llm_result.content
+ break
+ messages.append(llm_result)
+ # 调用工具
+ for tool_call in llm_result.tool_calls:
+ try:
+ tool_result = await tool_manager.execute(tool_call.function.name, tool_call.function.arguments)
+ logger.info(f"[{self.name}] call tool, params: {tool_call}, result: {tool_result}")
+ tool_message = Message.tool_message(content=tool_result, tool_call_id=tool_call.id)
+ except Exception as e:
+ tool_message = Message.tool_message(content=repr(e), tool_call_id=tool_call.id)
+ messages.append(tool_message)
+ await self.save_memory_history(system_content=self.system_message.content,
+ user_content=prompt, assistant_content=new_answer)
+ return new_answer
+
+ async def _reflect(self, query: str, content: str) -> Optional[str]:
+ """
+ 反思请求
+ Args:
+ query (str): 查询内容
+ content (str): 执行结果
+ Returns:
+ str: 反思内容
+ """
+ context = get_context()
+ await stream.aprint(f"[A] {self.name} 反思中...")
+ prompt = self._get_reflection_user_prompt(query, content)
+ message = Message.user_message(prompt)
+ content = await self.llm.ask(
+ messages=[message],
+ system_msgs=[self.reflection_system_message],
+ stream=context.detail_info,
+ )
+ #logger.info(f"[{self.name}] reflect, result: {content}")
+ await self.save_memory_history(system_content=self.reflection_system_message.content,
+ user_content=prompt, assistant_content=content)
+ return None if content=="None" else content
+
+ def _get_user_prompt(self, query: str, content: str, reflection: str) -> str:
+ user_prompt = self.prompt_patterns["user"].format(query=query, content=content, reflection=reflection)
+ logger.debug(f"[{self.name}] user prompt: {user_prompt}")
+ return user_prompt
+
+ def _get_reflection_user_prompt(self, query: str, content: str) -> str:
+ reflection_user_prompt = self.prompt_patterns["reflection_user"].format(query=query, content=content)
+ logger.debug(f"[{self.name}] reflection user prompt: {reflection_user_prompt}")
+ return reflection_user_prompt
diff --git a/component/mydba/mydba/app/agent/router.py b/component/mydba/mydba/app/agent/router.py
new file mode 100644
index 0000000..05ccb2b
--- /dev/null
+++ b/component/mydba/mydba/app/agent/router.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+from pydantic import BaseModel, Field
+from typing import List, Optional
+from mydba.app.agent.base import BaseAgent, cleanup_decorator
+from mydba.app.config.agent import agent_config
+from mydba.app.message.memory_history import MemoryInfo
+from mydba.app.message.message import Message
+from mydba.app.prompt import router
+from mydba.common import stream
+from mydba.common.logger import logger
+from mydba.common.session import get_context
+
+class RouterAgent(BaseAgent, BaseModel):
+ """
+ 路由型 Agent
+ """
+ system_message: Optional[Message] = Field(None, description="路由型 Agent 的系统提示信息")
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ prompts = data.get("prompts")
+ self.prompt_patterns["system"] = prompts.get("system")
+ self.prompt_patterns["user"] = prompts.get("user")
+ self.system_message = Message.system_message(self._get_system_prompt())
+
+ @cleanup_decorator
+ async def run(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ sub_agents = agent_config.get_sub_agents()
+ if len(sub_agents) == 1:
+ # 只有一个子 Agent,直接使用
+ agent_info = sub_agents[0]
+ else:
+ # 意图识别,结合历史上下文
+ logger.info(f"[{self.name}] start to detect, query: {query}")
+ intent = await self._predict_intent(query, context_memory)
+ await stream.aprint(f"[A] 意图: {intent}")
+ agent_info = agent_config.get_agent_by_intent(intent)
+ action_agent = BaseAgent.create_agent(agent_info, self.llm)
+ content = await action_agent.run(query, context_memory)
+ await self.save_memory_history(system_content=self.system_message.content,
+ user_content=query, assistant_content=content)
+ return content
+
+ async def _predict_intent(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ """
+ 预测请求意图
+ Args:
+ query (str): 用户请求内容
+ context_memory (list): 用户的上下文 memory
+ Returns:
+ str: The detected intent.
+ """
+ messages = []
+ if context_memory:
+ for mem in context_memory:
+ messages.append(Message.user_message(mem.user_content))
+ messages.append(Message.assistant_message(mem.assistant_content))
+ prompt = self._get_user_prompt(query)
+ messages.append(Message.user_message(prompt))
+ context = get_context()
+ content = await self.llm.ask(
+ messages=messages,
+ system_msgs=[self.system_message],
+ stream=context.detail_info,
+ )
+ logger.info(f"[{self.name}] detect intent, result: {content}")
+ agent_info = agent_config.get_agent_by_intent(content)
+ if agent_info is None:
+ logger.warning(f"[{self.name}] predict intent failed, using default agent, query: {query}, content: {content}")
+ agent_info = agent_config.get_default_agent()
+ return agent_info.intent
+ return content
+
+ def _get_system_prompt(self) -> str:
+ system_prompt = self.prompt_patterns["system"]
+ sub_agent_list = agent_config.get_sub_agents()
+ system_prompt = system_prompt.format(intent_infos=router.pack_intent_info(sub_agent_list),
+ default_intent=router.pack_default_intent(sub_agent_list),
+ intent_names=router.pack_intent_name(sub_agent_list),
+ conditions=router.pack_condition(sub_agent_list),
+ shots=router.pack_shot(sub_agent_list))
+ logger.debug(f"[{self.name}] system prompt: {system_prompt}")
+ return system_prompt
+
+ def _get_user_prompt(self, query: str) -> str:
+ user_prompt = self.prompt_patterns["user"].format(query=query)
+ logger.debug(f"[{self.name}] user prompt: {user_prompt}")
+ return user_prompt
diff --git a/component/mydba/mydba/app/agent/using_tool.py b/component/mydba/mydba/app/agent/using_tool.py
new file mode 100644
index 0000000..f99d583
--- /dev/null
+++ b/component/mydba/mydba/app/agent/using_tool.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+from pydantic import BaseModel, Field
+from typing import Dict, List, Optional
+from tenacity import RetryError
+from mydba.app.agent.base import BaseAgent, cleanup_decorator
+from mydba.app.config.agent import AgentMcp
+from mydba.app.config.settings import settings
+from mydba.app.llm import ToolChoice
+from mydba.app.message.memory_history import MemoryInfo
+from mydba.app.message.message import Message, ToolCall
+from mydba.app.tool.tool_manager import tool_manager
+from mydba.common.logger import logger
+from mydba.common.session import get_context
+
+class UsingToolAgent(BaseAgent, BaseModel):
+ """
+ 工具型 Agent
+ """
+ system_message: Optional[Message] = Field(None, description="工具型 Agent 的系统提示信息")
+ mcps: Optional[AgentMcp] = Field(default=None, description="绑定的 mcp 服务")
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ prompts = data.get("prompts", {})
+ self.prompt_patterns["system"] = prompts.get("system")
+ self.system_message = Message.system_message(self.prompt_patterns["system"])
+
+ @cleanup_decorator
+ async def run(self, query: str, context_memory: Optional[List[MemoryInfo]] = None) -> str:
+ logger.info(f"[{self.name}] start to query: {query}")
+ if context_memory:
+ self.memory.extend(self.format_memory(context_memory))
+ self.memory.append(Message.user_message(query))
+ step_counter = 0
+ tool_contents = None
+ while step_counter < 100:
+ # 调用大模型
+ llm_result = await self._execute_model(step_counter == 0)
+
+ #记录历史
+ if step_counter == 0:
+ await self.save_memory_history(system_content=self.system_message.content,
+ user_content=query,
+ assistant_content=llm_result.content,
+ assistant_tool_calls=llm_result.tool_calls)
+ else:
+ await self.save_memory_history(assistant_content=llm_result.content,
+ assistant_tool_calls=llm_result.tool_calls,
+ tool_contents=tool_contents)
+ if not llm_result.tool_calls:
+ # 结束工具调用,返回结果
+ logger.info(f"[{self.name}] query over, query: {query}, content: {llm_result.content}")
+ return llm_result.content
+
+ # 调用工具
+ if step_counter >= settings.MAX_STEPS:
+ logger.error(f"[{self.name}] call tool too much, break loop, step_counter: {step_counter}, query: {query}")
+ raise Exception(f"Agent({self.name}) call tool too much, step_counter: {step_counter}, query: {query}")
+ tool_contents = await self._execute_tool(llm_result.tool_calls)
+ step_counter += 1
+ logger.error(f"[{self.name}] call tool too much, step_counter: {step_counter}, query: {query}")
+ raise Exception(f"Agent({self.name}) call tool too much, step_counter: {step_counter}, query: {query}")
+
+ async def _execute_model(self, first: bool) -> Message:
+ tools = await tool_manager.get_tool_list(filter_=self.mcps)
+ tool_choice = ToolChoice.REQUIRED if first and tools else ToolChoice.AUTO
+ context = get_context()
+ result = await self.llm.ask_tool(
+ messages=self.memory,
+ system_msgs=[self.system_message],
+ tools=tools,
+ tool_choice=tool_choice,
+ stream=context.detail_info,
+ )
+ #logger.info(f"[{self.name}] query model, first: {first}, result: {result}")
+ self.memory.append(result)
+ return result
+
+ async def _execute_tool(self, tool_calls: List[ToolCall]) -> Dict[str, str]:
+ tool_contents = []
+ for tool_call in tool_calls:
+ tool_name_infos = await tool_manager.convert_name(tool_call.function.name)
+ tool_name = tool_name_infos[1] if len(tool_name_infos) >= 2 else tool_name_infos[0]
+ for i in range(3):
+ try:
+ result = await tool_manager.execute(tool_call.function.name, tool_call.function.arguments)
+ break
+ except RetryError as re:
+ result = f"call tool failed, exception: {str(re.last_attempt.exception())}"
+ if not tool_manager.is_retryable_tool(tool_name):
+ break
+ except Exception as e:
+ result = f"call tool failed, exception: {str(e)}"
+ if not tool_manager.is_retryable_tool(tool_name):
+ break
+ logger.info(f"[{self.name}] call tool, params: {tool_call}, result: {result}")
+ tool_message = Message.tool_message(content=result, tool_call_id=tool_call.id)
+ self.memory.append(tool_message)
+ tool_contents.append(tool_message.format())
+ return tool_contents
diff --git a/component/mydba/mydba/app/config/__init__.py b/component/mydba/mydba/app/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/app/config/agent.py b/component/mydba/mydba/app/config/agent.py
new file mode 100644
index 0000000..9d0edb3
--- /dev/null
+++ b/component/mydba/mydba/app/config/agent.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+import json
+from enum import Enum
+from pydantic import BaseModel, Field
+from typing import List, Dict, Literal, Optional, Union
+
+class AgentMode(str, Enum):
+ """定义工具调用的选择方式"""
+ CHAT = "chat" # 普通对话型 Agent
+ ROUTER = "router" # 路由型 Agent
+ REFLECTION = "reflection" # 反思型 Agent
+ USING_TOOL = "using_tool" # 工具型 Agent
+
+AGENT_MODE_VALUES = tuple(mode.value for mode in AgentMode)
+AGENT_MODE_TYPE = Literal[AGENT_MODE_VALUES]
+
+class AgentMcp(BaseModel):
+ allow: Optional[List[str]] = Field(default=None, description="允许列表")
+ deny: Optional[List[str]] = Field(default=None, description="拒绝列表")
+
+class AgentInfo(BaseModel):
+ name: str = Field(..., description="Agent 名称")
+ mode: AGENT_MODE_TYPE = Field(..., description="Agent 模式") # type: ignore
+ intent: Optional[str] = Field(None, description="意图名称,用于意图识别,除 Router agent 外,都需要配置此信息")
+ intent_description: Optional[str] = Field(None, description="意图描述,用于帮助 LLM 识别意图,除 Router agent 外,都需要配置此信息")
+ prompts: Optional[Dict[str, Union[str, List[str]]]] = Field(default=None, description="""Agent 提示词模版,包含键值:
+ 1) system: 系统提示词或模版,
+ 2) user: 用户提示词模版,在需要改写用户提示词的场景下使用,例如:REFLECTION 模式、ROUTER 模式,
+ 3) reflection_system: 反思提示词,仅在 REFLECTION 模式下使用,
+ 4) reflection_user: 反思提示词模版,仅在 REFLECTION 模式下使用,
+ 5) condition: 条件提示词(List),在意图识别时,描述意图的约束条件,
+ 6) shot: 样本提示词(List),在意图识别时,提供意图样本""")
+ mcps: Optional[AgentMcp] = Field(default=None, description="绑定的 mcp 服务,USING_TOOL 模式下有效")
+ is_main: bool = Field(default=False, description="是否为主 Agent")
+ is_default: bool = Field(default=False, description="是否为默认 Agent")
+
+class AgentConfig(BaseModel):
+ agent_list: List[AgentInfo] = Field(default_factory=list, description="Agent 列表")
+ config_map: Dict[str, AgentInfo] = Field(default_factory=dict, description="配置映射")
+
+ def add_agent(self, name: str, mode: str, intent: Optional[str] = None,
+ intent_description: Optional[str] = None, prompts: Optional[str] = None,
+ mcps: Optional[str] = None, is_main: bool = False, is_default: bool = False) -> None:
+ """
+ 添加 Agent 到列表。
+ Args:
+ name (str): Agent 名称。
+ mode (str): Agent 模式。
+ intent (str): 意图名称,用于意图识别,除 Router agent 外,都需要配置此信息。
+ intent_description (str): 意图描述,用于帮助 LLM 识别意图,除 Router agent 外,都需要配置此信息。
+ prompts (str): Agent 提示词。
+ mcps (str): 绑定的 mcp 服务。
+ is_main (bool): 是否为主 Agent。
+ is_default (bool): 是否为默认 Agent。
+ """
+ if name in self.config_map:
+ raise ValueError(f"Agent {name} already exists.")
+ mcps = json.loads(mcps) if mcps else None
+ agent_info = AgentInfo(**{
+ "name": name,
+ "mode": mode,
+ "intent": intent,
+ "intent_description": intent_description,
+ "prompts": None if not prompts else json.loads(prompts),
+ "mcps": None if not mcps else AgentMcp(**mcps),
+ "is_main": is_main,
+ "is_default": is_default
+ })
+ self.agent_list.append(agent_info)
+ if intent:
+ self.config_map[intent] = agent_info
+
+ def get_agent_by_intent(self, intent: str) -> Optional[AgentInfo]:
+ """
+ 根据意图获取 Agent。
+ Args:
+ name (str): Agent 名称。
+ Returns:
+ AgentInfo: Agent 信息。
+ """
+ return self.config_map.get(intent)
+
+ def get_default_agent(self) -> Optional[AgentInfo]:
+ """
+ 获取默认 Agent。
+ Returns:
+ AgentInfo: Agent 信息。
+ """
+ return next(filter(lambda agent: agent.is_default, self.agent_list), None)
+
+ def get_main_agent(self) -> Optional[AgentInfo]:
+ """
+ 获取主 Agent。
+ Returns:
+ AgentInfo: Agent 信息。
+ """
+ return next(filter(lambda agent: agent.is_main, self.agent_list), None)
+
+ def get_sub_agents(self) -> List[AgentInfo]:
+ """
+ 获取子 Agent。
+ Returns:
+ List[AgentInfo]: 子 Agent 列表。
+ """
+ return list(filter(lambda agent: not agent.is_main, self.agent_list))
+agent_config = AgentConfig()
diff --git a/component/mydba/mydba/app/config/config_manager.py b/component/mydba/mydba/app/config/config_manager.py
new file mode 100644
index 0000000..d13dd17
--- /dev/null
+++ b/component/mydba/mydba/app/config/config_manager.py
@@ -0,0 +1,214 @@
+# -*- coding: utf-8 -*-
+import configparser
+from mydba.app.config.agent import agent_config
+from mydba.app.config.database import database_config
+from mydba.app.config.mcp_tool import mcp_config
+from mydba.app.config.settings import settings as app_settings
+from mydba.app.database.base_database import BaseDatabases
+from mydba.common import encryption
+from mydba.common.settings import settings as common_settings
+
+async def load_config(conf_file: str='/usr/local/mydba/config_app.ini') -> None:
+ """
+ 加载 MyDBA 配置信息,项目配置分为以下几类:
+ 1. 日志: 使用文件管理配置 (LOG_DIR, LOG_NAME, LOG_FILE_LEVEL)
+ 2. 模型: 使用文件管理配置 (API_KEY, API_BASE_URL, LLM_MODEL, MAX_TOKENS, TEMPERATURE)
+ 3. App: 使用文件管理配置 (REFRESH_INTERVAL, MAX_STEPS, SECURITY_KEY)
+ 4. Mcp: 使用 sqlite 管理配置 (McpConfig)
+ 5. Agent: 使用 sqlite 管理配置 (AgentConfig)
+ 6. Database: 使用 sqlite 管理配置 (DatabaseConfig)
+ 7. 其它: 使用文件管理配置 (DEBUG, CONFIG_DATABASE)
+ Args:
+ conf_file (str): 配置文件路径。
+ """
+ # 支持从配置文件获取工程配置
+ load_from_conf(conf_file)
+
+ if not app_settings.SECURITY_KEY:
+ raise Exception("[config] config invalid, lost security key, please set it using config_app.ini or env(MYDBA_SECURITY_KEY)")
+
+ # 从 db 读取 agent、mcp 配置
+ await _load_from_db(database_uri=app_settings.CONFIG_DATABASE, security_key=app_settings.SECURITY_KEY)
+
+async def init_config(database_uri: str) -> None:
+ """
+ 初始化 MyDBA 配置,这里仅初始化表结构
+ Args:
+ database_uri (str): 数据库连接 URI。
+ """
+ db = BaseDatabases.create_database(uri=database_uri)
+ # Agent 配置表
+ sql = """
+ CREATE TABLE IF NOT EXISTS agent (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL, -- Agent 名称
+ mode TEXT NOT NULL, -- Agent 模式 (参考类 AgentMode)
+ intent TEXT, -- 意图名称, 用于意图识别 (除主 Agent 外, 都需要配置此信息)
+ intent_description TEXT, -- 意图描述, 用于帮助 LLM 识别意图 (除主 Agent 外, 都需要配置此信息)
+ prompts TEXT, -- Agent 提示词 (JSON 格式) (可为空)
+ mcps TEXT, -- 绑定的 mcp 服务 (JSON 格式) (可为空)
+ is_main INTEGER DEFAULT 0, -- 是否为主 Agent
+ is_default INTEGER DEFAULT 0 -- 是否为默认 Agent
+ );
+ """
+ await db.execute(sql)
+ sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_agent_name ON agent(name);"
+ await db.execute(sql)
+
+ # mcp 配置表
+ sql = """
+ CREATE TABLE IF NOT EXISTS mcp (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL UNIQUE, -- mcp 服务名称(全局唯一)
+ transport TEXT NOT NULL DEFAULT 'sse', -- mcp server 的传输协议
+ description TEXT, -- 服务描述 (可为空)
+ server_uri TEXT, -- mcp 服务器 URI (可为空)
+ command TEXT, -- mcp server 启动命令 (可为空)
+ args TEXT, -- mcp server 启动参数 (JSON 字符串格式,可为空)
+ envs TEXT -- mcp server 启动依赖的环境变量 (JSON 字符串格式,可为空)
+ );
+ """
+ await db.execute(sql)
+ sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_mcp_name ON mcp(name);"
+ await db.execute(sql)
+
+ # 数据库实例配置表
+ sql = """
+ CREATE TABLE IF NOT EXISTS db_instance (
+ id INTEGER PRIMARY KEY AUTOINCREMENT, -- 主键,自增
+ type TEXT DEFAULT NULL, -- 数据库类型,使用 TEXT 类型
+ uri TEXT DEFAULT NULL, -- db 连接串
+ host TEXT DEFAULT NULL, -- db 主机
+ port INTEGER DEFAULT NULL, -- db 端口
+ user TEXT DEFAULT NULL, -- 用户名
+ password TEXT DEFAULT NULL, -- 密码
+ charset TEXT DEFAULT NULL, -- db 字符集
+ `database` TEXT DEFAULT NULL -- 库名称
+ );
+ """
+ await db.execute(sql)
+ sql = "CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_db_name ON db_instance(`database`);"
+ await db.execute(sql)
+
+ # memory 表
+ sql = """
+ CREATE TABLE IF NOT EXISTS memory (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ time TEXT NOT NULL, -- 记录时间
+ request_id TEXT NOT NULL, -- 请求标识
+ user_name TEXT NOT NULL, -- 用户标识
+ session TEXT NOT NULL, -- 会话标识
+ agent_name TEXT NOT NULL, -- agent 标识
+ system_content TEXT, -- 系统指令内容 (可为空)
+ user_content TEXT, -- 用户查询内容 (可为空)
+ assistant_content TEXT, -- 大模型返回的内容 (可为空)
+ assistant_tool_calls TEXT, -- 大模型返回的工具调用列表,JSON 格式 (可为空)
+ tool_contents TEXT -- 工具调用返回的内容列表,JSON 格式 (可为空)
+ );
+ """
+ await db.execute(sql)
+ sql = "CREATE INDEX IF NOT EXISTS idx_memory_username_agenname_time ON memory(user_name, session, agent_name, time);"
+ await db.execute(sql)
+ await db.clean()
+
+def load_from_conf(conf_file: str) -> None:
+ config = configparser.ConfigParser()
+ config.read(conf_file)
+ # 1. 日志配置 (LOG_DIR, LOG_NAME, LOG_FILE_LEVEL)
+ config_value = config.get('log', 'dir')
+ if config_value:
+ common_settings.LOG_DIR = config_value
+ config_value = config.get('log', 'name')
+ if config_value:
+ common_settings.LOG_NAME = config_value
+ config_value = config.get('log', 'file_level')
+ if config_value:
+ common_settings.LOG_FILE_LEVEL = config_value
+ # 2. 模型配置 (API_KEY, API_BASE_URL, LLM_MODEL, MAX_TOKENS, TEMPERATURE)
+ config_value = config.get('model', 'api_key')
+ if config_value:
+ app_settings.API_KEY = config_value
+ config_value = config.get('model', 'base_url')
+ if config_value:
+ app_settings.API_BASE_URL = config_value
+ config_value = config.get('model', 'model')
+ if config_value:
+ app_settings.LLM_MODEL = config_value
+ config_value = config.get('model', 'max_tokens')
+ if config_value:
+ app_settings.MAX_TOKENS = int(config_value)
+ config_value = config.get('model', 'temperature')
+ if config_value:
+ app_settings.TEMPERATURE = float(config_value)
+ # 3. App 配置 (REFRESH_INTERVAL, MAX_STEPS, SECURITY_KEY)
+ config_value = config.get('app', 'refresh_interval')
+ if config_value:
+ app_settings.REFRESH_INTERVAL = int(config_value)
+ config_value = config.get('app', 'max_steps')
+ if config_value:
+ app_settings.MAX_STEPS = int(config_value)
+ security_key = config.get('app', 'security_key')
+ if security_key:
+ app_settings.SECURITY_KEY = security_key
+ # 4. 其它配置 (DEBUG, CONFIG_DATABASE)
+ config_value = config.get('common', 'debug')
+ if config_value:
+ common_settings.DEBUG = config_value == 'True'
+ config_value = config.get('common', 'config_database')
+ if config_value:
+ app_settings.CONFIG_DATABASE = config_value
+
+async def _load_from_db(database_uri: str, security_key: str) -> None:
+ db = BaseDatabases.create_database(uri=database_uri)
+ try:
+ # 1. Agent 配置
+ sql = 'SELECT * FROM agent'
+ rows = await db.query(sql=sql)
+ if not rows:
+ raise Exception("Lost agent config in DB")
+ for row in rows:
+ agent_config.add_agent(name = row['name'],
+ mode = row['mode'],
+ intent = row['intent'],
+ intent_description = row['intent_description'],
+ prompts = row['prompts'],
+ mcps = row['mcps'],
+ is_main = row['is_main']==1,
+ is_default = row['is_default']==1)
+
+ # 2. mcp server 配置
+ sql = 'SELECT * FROM mcp'
+ rows = await db.query(sql=sql)
+ if rows:
+ for row in rows:
+ args = row['args']
+ if args and not args.startswith('['):
+ try:
+ args = encryption.decrypt(security_key, args)
+ except Exception:
+ pass
+ envs = row['envs']
+ if envs and not envs.startswith('{'):
+ try:
+ envs = encryption.decrypt(security_key, envs)
+ except Exception:
+ pass
+ mcp_config.add_mcp(name = row['name'],
+ transport = row['transport'],
+ description = row['description'],
+ server_uri = row['server_uri'],
+ command = row['command'],
+ args = args,
+ envs = envs)
+
+ # 3. mysql 信息配置
+ sql = 'SELECT * FROM db_instance'
+ rows = await db.query(sql=sql)
+ if rows:
+ for row in rows:
+ password = encryption.decrypt(security_key, row['password'])
+ database_config.add_database(database=row['database'], type=row['type'], uri=row['uri'],
+ host=row['host'], port=row['port'], user=row['user'],
+ password=password, charset=row['charset'])
+ finally:
+ await db.clean()
diff --git a/component/mydba/mydba/app/config/database.py b/component/mydba/mydba/app/config/database.py
new file mode 100644
index 0000000..9b59ffd
--- /dev/null
+++ b/component/mydba/mydba/app/config/database.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+import json
+from enum import Enum
+from pydantic import BaseModel, Field
+from typing import List, Dict, Literal, Optional
+
+class DatabaseType(str, Enum):
+ """数据库类型"""
+ SQLITE = "sqlite"
+ MYSQL = "mysql"
+DATABASE_TYPE_VALUES = tuple(type.value for type in DatabaseType)
+DATABASE_TYPE_TYPE = Literal[DATABASE_TYPE_VALUES]
+
+class DatabaseInfo(BaseModel):
+ type: Optional[DATABASE_TYPE_TYPE] = Field(None, description="数据库类型") # type: ignore
+ uri: Optional[str] = Field(None, description="db 连接串")
+ host: Optional[str] = Field(None, description="db 主机")
+ port: Optional[int] = Field(None, description="db 端口")
+ user: Optional[str] = Field(None, description="用户名")
+ password: Optional[str] = Field(None, description="密码")
+ charset: Optional[str] = Field(None, description="db 字符集")
+ database: Optional[str] = Field(None, description="库名称")
+ def __repr__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+ def __str__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+
+class DatabaseConfig(BaseModel):
+ info_map: Dict[str, DatabaseInfo] = Field(default_factory=dict, description="库名到实例信息的映射")
+
+ def add_database(self, database: str, type: Optional[str] = None, uri: Optional[str] = None,
+ host: Optional[str] = None, port: Optional[int] = None, user: Optional[str] = None,
+ password: Optional[str] = None, charset: Optional[str] = None) -> None:
+ """
+ 添加库信息。
+ Args:
+ database (str): 库名称。
+ type (str): 数据库类型。
+ uri (str): db 连接串。
+ host (str): db 主机。
+ port (int): db 端口。
+ user (str): 用户名。
+ password (str): 密码。
+ charset (str): db 字符集。
+ """
+ if database in self.info_map:
+ raise ValueError(f"Database {database} already exists.")
+ database_info = DatabaseInfo(**{
+ "type": type,
+ "uri": uri,
+ "host": host,
+ "port": port,
+ "user": user,
+ "password": password,
+ "charset": charset,
+ "database": database
+ })
+ self.info_map[database.lower()] = database_info
+
+ def get_database(self, database: str) -> Optional[DatabaseInfo]:
+ """
+ 根据库名获取库信息。
+ Args:
+ database (str): 库名称。
+ Returns:
+ DatabaseInfo: 库信息。
+ """
+ return self.info_map.get(database.lower())
+database_config = DatabaseConfig()
diff --git a/component/mydba/mydba/app/config/mcp_tool.py b/component/mydba/mydba/app/config/mcp_tool.py
new file mode 100644
index 0000000..3ff0f99
--- /dev/null
+++ b/component/mydba/mydba/app/config/mcp_tool.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+import base64
+import hashlib
+import asyncio
+import json
+from enum import Enum
+from pydantic import BaseModel, Field
+from typing import List, Dict, Literal, Optional
+
+class Transport(str, Enum):
+ """mcp server 传输协议"""
+ STDIO = "stdio"
+ SSE = "sse"
+TRANSPORT_VALUES = tuple(transport.value for transport in Transport)
+TRANSPORT_TYPE = Literal[TRANSPORT_VALUES]
+
+class McpInfo(BaseModel):
+ name: str = Field(..., description="mcp 服务名称(全局唯一)")
+ transport: TRANSPORT_TYPE = Field("sse", description="mcp server 的传输协议") # type: ignore
+ description: Optional[str] = Field(None, description="服务描述")
+ server_uri: Optional[str] = Field(None, description="mcp 服务器 URI")
+ command: Optional[str] = Field(None, description="mcp server 启动命令")
+ args: Optional[List[str]] = Field(None, description="mcp server 启动参数")
+ envs: Optional[Dict[str, str]] = Field(None, description="mcp server 启动依赖的环境变量")
+ def __repr__(self):
+ info = self.model_dump()
+ info['args'] = '******' if info['args'] else info['args']
+ info['envs'] = '******' if info['envs'] else info['envs']
+ return json.dumps(info, ensure_ascii=False)
+ def __str__(self):
+ info = self.model_dump()
+ info['args'] = '******' if info['args'] else info['args']
+ info['envs'] = '******' if info['envs'] else info['envs']
+ return json.dumps(info, ensure_ascii=False)
+
+class McpConfig(BaseModel):
+ mcp_list: List[McpInfo] = Field(default_factory=list, description="mcp 列表")
+ config_map: Dict[str, McpInfo] = Field(default_factory=dict, description="配置映射, name -> mcp server info")
+
+ def add_mcp(self, name: str, transport: str="sse", description: Optional[str]=None,
+ server_uri: Optional[str]=None, command: Optional[str]=None,
+ args: Optional[str]=None, envs: Optional[str]=None) -> None:
+ """添加 mcp 到列表"""
+ if name in self.config_map:
+ raise ValueError(f"mcp {name} already exists.")
+ mcp_info = McpInfo(**{
+ "name": name,
+ "transport": transport,
+ "description": description,
+ "server_uri": server_uri,
+ "command": command,
+ "args": None if not args else json.loads(args),
+ "envs": None if not envs else json.loads(envs)
+ })
+ self.mcp_list.append(mcp_info)
+ self.config_map[name] = mcp_info
+
+ def get_mcp_by_name(self, name) -> McpInfo:
+ """
+ 根据 mcp 名称获取 mcp
+ Args:
+ name (str): mcp 服务名称(全局唯一)。
+ Returns:
+ McpInfo: mcp 信息。
+ """
+ return self.config_map.get(name)
+mcp_config = McpConfig()
+
+class McpToolInfo(BaseModel):
+ tool_key: str = Field(None, description="工具标识(全局唯一)")
+ server_name: str = Field(..., description="mcp 服务名称")
+ tool_name: str = Field(..., description="工具名称")
+ description: str = Field(..., description="工具描述")
+ input_schema: str = Field(..., description="工具参数描述")
+
+ def format(self) -> dict:
+ tool = {"type": "function"}
+ tool["function"] = {
+ "name": self.tool_key if self.tool_key else self.tool_name,
+ "description": self.description,
+ "parameters": json.loads(self.input_schema)
+ }
+ return tool
+
+ def __repr__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+
+ def __str__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+
+class McpToolConfig(BaseModel):
+ mcp_tool_list: List[McpToolInfo] = Field(default_factory=list, description="mcp tool 列表")
+ config_map: Dict[str, McpToolInfo] = Field(default_factory=dict, description="配置映射")
+ lock: asyncio.Lock = Field(default_factory=asyncio.Lock, description="配置的更新锁")
+
+ async def add_mcp_tool(self, server_name: str, tool_name: str, description: str, input_schema: str) -> None:
+ """
+ 添加 mcp tool 到列表
+ Args:
+ server_name (str): mcp 服务名称。
+ tool_name (str): 工具名称。
+ description (str): 工具描述。
+ input_schema (str): 工具参数描述。
+ """
+ tool_key = f"{server_name}_{tool_name}"
+ tool_key = hashlib.md5(tool_key.encode("utf-8")).digest()
+ tool_key = base64.urlsafe_b64encode(tool_key).decode("utf-8")
+ tool_key = tool_key.rstrip("=")
+ mcp_tool_info = McpToolInfo(**{
+ "tool_key": tool_key,
+ "server_name": server_name,
+ "tool_name": tool_name,
+ "description": description,
+ "input_schema": input_schema
+ })
+
+ # 使用 COW 机制,进行读写保护
+ async with self.lock:
+ mcp_tool_list = [mcp_tool_info]
+ config_map = {
+ tool_key: mcp_tool_info
+ }
+ for mcp_tool in self.mcp_tool_list:
+ if mcp_tool.tool_key != tool_key:
+ mcp_tool_list.append(mcp_tool)
+ config_map[mcp_tool.tool_key] = mcp_tool
+
+ self.mcp_tool_list = mcp_tool_list
+ self.config_map = config_map
+ return
+
+ def get_mcp_tool_by_key(self, tool_key: str) -> McpToolInfo:
+ """
+ 根据 tool_key 获取 mcp tool
+ Args:
+ tool_key (str): 工具标识(全局唯一)。
+ Returns:
+ McpToolInfo: mcp tool 信息。
+ """
+ return self.config_map.get(tool_key)
+
+ class Config:
+ arbitrary_types_allowed = True
+mcp_tool_config = McpToolConfig()
diff --git a/component/mydba/mydba/app/config/settings.py b/component/mydba/mydba/app/config/settings.py
new file mode 100644
index 0000000..880e3c5
--- /dev/null
+++ b/component/mydba/mydba/app/config/settings.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+import os
+
+class Settings:
+ """
+ 记录 MyDBA Agent 启动时依赖的环境变量。
+ Attributes:
+ CONFIG_FILE (str): 配置文件路径。
+ CONFIG_DATABASE (str): 配置数据库 URI。
+ REFRESH_INTERVAL (int): 缓存刷新间隔。
+ MAX_STEPS (int): 执行步骤的最大次数。
+ SECURITY_KEY (str): 数据加密 key,要求为 16 字节长度,保护数据安全,比如 mysql 的账密信息。
+ API_KEY (str): 大模型的 api key。
+ API_BASE_URL (str): 大模型的 base url。
+ LLM_MODEL (str): 大模型名称。
+ MAX_TOKENS (int): 大模型请求的最大 token 数量。
+ TEMPERATURE (float): 大模型的温度。
+ """
+ CONFIG_FILE = os.getenv("MYDBA_CONFIG_FILE", "/usr/local/mydba/config_app.ini")
+ CONFIG_DATABASE = os.getenv("MYDBA_CONFIG_DATABASE", "sqlite:///usr/local/mydba/sqlite_app.db")
+ REFRESH_INTERVAL = int(os.getenv("MYDBA_REFRESH_INTERVAL", 60))
+ MAX_STEPS = int(os.getenv("MYDBA_MAX_STEPS", 100))
+ SECURITY_KEY = os.getenv("MYDBA_SECURITY_KEY", "")
+ API_KEY = os.getenv("MYDBA_API_KEY", "")
+ API_BASE_URL = os.getenv("MYDBA_API_BASE_URL", "")
+ LLM_MODEL = os.getenv("MYDBA_LLM_MODEL", "")
+ MAX_TOKENS = int(os.getenv("MYDBA_MAX_TOKENS", "1000"))
+ TEMPERATURE = float(os.getenv("MYDBA_TEMPERATURE", "1.0"))
+settings = Settings()
\ No newline at end of file
diff --git a/component/mydba/mydba/app/database/base_database.py b/component/mydba/mydba/app/database/base_database.py
new file mode 100644
index 0000000..95e9e3a
--- /dev/null
+++ b/component/mydba/mydba/app/database/base_database.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from abc import ABC, abstractmethod
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict, Optional
+from mydba.app.config.database import DatabaseInfo, DatabaseType
+
+class BaseDatabases(ABC, BaseModel):
+ database_info: DatabaseInfo = Field(..., description="数据库信息")
+
+ @abstractmethod
+ async def query(self, sql: str, parameters: Optional[List[Any]]=None) -> Optional[List[Dict[str, Any]]]:
+ """
+ 查询数据库
+ Args:
+ sql (str): 查询 sql。
+ parameters (list): 绑定的参数。
+ Returns:
+ list: 查询结果。
+ """
+ return NotImplementedError("Subclasses must implement this method")
+
+ @abstractmethod
+ async def execute(self, sql: str, parameters: Optional[List[Any]]=None) -> int:
+ """
+ 变更数据库
+ Args:
+ sql (str): 变更 sql。
+ parameters (list): 绑定的参数。
+ Returns:
+ int: 受影响行数。
+ """
+ return NotImplementedError("Subclasses must implement this method")
+
+ async def clean(self) -> None:
+ """资源清理,用于退出时释放资源,默认不做清理"""
+ return
+
+ @staticmethod
+ def create_database(uri: Optional[str]=None, database_info: Optional[DatabaseInfo]=None) -> "BaseDatabases":
+ if database_info is None:
+ database_info = DatabaseInfo(uri=uri)
+ database_type = database_info.type
+ if database_type is None:
+ if database_info.uri.startswith("sqlite://"):
+ database_type = DatabaseType.SQLITE.value
+ elif database_info.uri.startswith("jdbc:mysql:"):
+ database_type = DatabaseType.MYSQL.value
+ else:
+ raise ValueError(f"database type unrecognized")
+ database_info.type = database_type
+
+ if database_type == DatabaseType.SQLITE.value:
+ from mydba.app.database.sqlite_db import SqliteDatabase
+ if database_info.uri and database_info.uri.startswith("sqlite://"):
+ database_info.uri = database_info.uri[9:]
+ return SqliteDatabase(database_info=database_info)
+ elif database_type == DatabaseType.MYSQL.value:
+ from mydba.app.database.mysql_db import MySQLDatabase
+ return MySQLDatabase(database_info=database_info)
+ raise ValueError(f"database type not supported")
diff --git a/component/mydba/mydba/app/database/mysql_db.py b/component/mydba/mydba/app/database/mysql_db.py
new file mode 100644
index 0000000..6fa8645
--- /dev/null
+++ b/component/mydba/mydba/app/database/mysql_db.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import aiomysql
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict, Optional, Tuple
+from mydba.app.database.base_database import BaseDatabases
+
+class MySQLDatabase(BaseDatabases, BaseModel):
+ pool: Optional[Any] = Field(None, description="数据库连接池")
+ lock: asyncio.Lock = Field(default_factory=asyncio.Lock, description="连接池锁")
+
+ async def query(self, sql: str, parameters: Optional[List[Any]]=None) -> Optional[List[Dict[str, Any]]]:
+ pool = await self._get_pool()
+ async with pool.acquire() as conn:
+ # 连接检查
+ await conn.ping()
+ async with conn.cursor() as cursor:
+ await cursor.execute(sql, parameters)
+ return await cursor.fetchall()
+
+ async def execute(self, sql: str, parameters: Optional[List[Any]]=None) -> int:
+ affected_rows = 0
+ pool = await self._get_pool()
+ async with pool.acquire() as conn:
+ # 连接检查
+ await conn.ping()
+ async with conn.cursor() as cursor:
+ if parameters and (isinstance(parameters[0], List) or isinstance(parameters[0], Tuple)):
+ await cursor.executemany(sql, parameters)
+ else:
+ await cursor.execute(sql, parameters)
+ affected_rows = cursor.rowcount
+ return affected_rows
+
+ async def clean(self) -> None:
+ if not self.pool:
+ return
+ async with self.lock:
+ if not self.pool:
+ return
+ self.pool.close()
+ await self.pool.wait_closed()
+ self.pool = None
+
+ async def _get_pool(self):
+ if self.pool:
+ return self.pool
+ async with self.lock:
+ if self.pool:
+ return self.pool
+ pool = await aiomysql.create_pool(
+ host = self.database_info.host,
+ port = 3306 if not self.database_info.port else self.database_info.port,
+ user = self.database_info.user,
+ password = self.database_info.password,
+ charset = 'utf8mb4' if not self.database_info.charset else self.database_info.charset,
+ db = self.database_info.database,
+ autocommit = True,
+ cursorclass = aiomysql.DictCursor,
+ connect_timeout = 3,
+ minsize = 1,
+ maxsize = 3,
+ pool_recycle = 3600
+ )
+ self.pool = pool
+ return pool
+
+ class Config:
+ arbitrary_types_allowed = True
diff --git a/component/mydba/mydba/app/database/sqlite_db.py b/component/mydba/mydba/app/database/sqlite_db.py
new file mode 100644
index 0000000..1f409fb
--- /dev/null
+++ b/component/mydba/mydba/app/database/sqlite_db.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+from abc import ABC, abstractmethod
+import asyncio
+import aiosqlite
+from aiosqlite.core import Connection
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict, Optional, Tuple
+from mydba.app.database.base_database import BaseDatabases
+
+class SqliteDatabase(BaseDatabases, BaseModel):
+ async def query(self, sql: str, parameters: Optional[List[Any]]=None) -> Optional[List[Dict[str, Any]]]:
+ async with aiosqlite.connect(self.database_info.uri) as db:
+ db.row_factory = aiosqlite.Row
+ async with db.execute(sql, parameters) as cursor:
+ results = []
+ async for row in cursor:
+ results.append(dict(row))
+ return results
+
+ async def execute(self, sql: str, parameters: Optional[List[Any]]=None) -> int:
+ affected_rows = 0
+ async with aiosqlite.connect(self.database_info.uri) as db:
+ if parameters and (isinstance(parameters[0], List) or isinstance(parameters[0], Tuple)):
+ async with db.executemany(sql, parameters) as cursor:
+ affected_rows = cursor.rowcount
+ else:
+ async with db.execute(sql, parameters) as cursor:
+ affected_rows = cursor.rowcount
+ await db.commit()
+ return affected_rows
diff --git a/component/mydba/mydba/app/llm.py b/component/mydba/mydba/app/llm.py
new file mode 100644
index 0000000..5496117
--- /dev/null
+++ b/component/mydba/mydba/app/llm.py
@@ -0,0 +1,246 @@
+# -*- coding: utf-8 -*-
+from openai import AsyncOpenAI, OpenAIError
+from enum import Enum
+from pydantic import BaseModel, Field
+from typing import Any, List, Optional
+from tenacity import retry, stop_after_attempt, wait_exponential
+from mydba.app.config.mcp_tool import McpToolInfo
+from mydba.app.message.message import Message
+from mydba.common import stream as common_stream
+from mydba.common.logger import logger
+
+class ToolChoice(str, Enum):
+ """定义工具调用的选择方式"""
+ NONE = "none"
+ AUTO = "auto"
+ REQUIRED = "required"
+
+TOOL_CHOICE_VALUES = tuple(choice.value for choice in ToolChoice)
+
+class LLM(BaseModel):
+ model: str = Field(..., description="模型名称")
+ base_url: str = Field(..., description="API base URL")
+ api_key: str = Field(..., description="API key")
+ max_tokens: int = Field(4096, description="最大 token 数量")
+ temperature: float = Field(1.0, description="温度")
+ client: Optional[Any] = Field(None, description="openAI client,用于调用 llm")
+
+ def __init__(self, **data):
+ super().__init__(**data)
+ self.client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url)
+
+ def format_messages(self, messages: List[Message]) -> List[dict]:
+ """格式化消息列表"""
+ formatted_messages = []
+ for message in messages:
+ formatted_messages.append(message.format())
+ return formatted_messages
+
+ def format_tools(self, tools: List[McpToolInfo]) -> List[dict]:
+ """格式化工具列表"""
+ formatted_tools = []
+ for tool in tools:
+ formatted_tools.append(tool.format())
+ return formatted_tools
+
+ @retry(wait=wait_exponential(min=1, max=5), stop=stop_after_attempt(3))
+ async def ask(self,
+ messages: List[Message],
+ system_msgs: Optional[List[Message]] = None,
+ stream: bool = True,
+ timeout: int = 60
+ ) -> str:
+ """
+ 发送请求到 LLM 并获取响应,不使用函数调用。
+ 该方法支持流式返回。
+ Args:
+ messages: 对话消息列表
+ system_msgs: 系统消息
+ stream: 是否启用流式消息返回
+ timeout: 请求超时时间
+ Returns:
+ str: LLM 的响应内容
+ Raises:
+ ValueError: 如果消息格式不正确或响应无效
+ OpenAIError: 如果 API 调用失败
+ Exception: 其他异常
+ """
+ try:
+ if system_msgs:
+ messages = system_msgs + messages
+ messages = self.format_messages(messages)
+ if not stream:
+ response = await self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ max_tokens=self.max_tokens,
+ temperature=self.temperature,
+ timeout=timeout,
+ stream=False,
+ )
+ if not response.choices or not response.choices[0].message.content:
+ logger.warning(f"empty response from LLM")
+ raise ValueError("Empty or invalid response from LLM")
+ result = response.choices[0].message.content
+ else:
+ await common_stream.aprint(f"[A] 请求大模型({self.model})")
+ response = await self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ max_tokens=self.max_tokens,
+ temperature=self.temperature,
+ timeout=timeout,
+ stream=True,
+ )
+ collected_messages = []
+ async for chunk in response:
+ chunk_message = chunk.choices[0].delta.content or ""
+ if chunk_message:
+ await common_stream.aprint(chunk_message, end="")
+ collected_messages.append(chunk_message)
+ result = "".join(collected_messages).strip()
+ if not result:
+ logger.warning(f"empty response from LLM")
+ raise ValueError("Empty response from streaming LLM")
+ else:
+ await common_stream.aprint("")
+ logger.info(f"stream={stream}, timeout={timeout}, messages={messages}, result={result}")
+ return result
+ except ValueError as ve:
+ logger.error(f"validation error: {ve}")
+ raise
+ except OpenAIError as oe:
+ logger.error(f"openAI API error: {oe}")
+ raise
+ except Exception as e:
+ logger.error(f"unexpected error in ask: {e}")
+ raise
+
+ @retry(wait=wait_exponential(min=1, max=5), stop=stop_after_attempt(3))
+ async def ask_tool(
+ self,
+ messages: List[Message],
+ system_msgs: Optional[List[Message]] = None,
+ tools: List[McpToolInfo] = None,
+ tool_choice: str = ToolChoice.REQUIRED,
+ stream: bool = True,
+ timeout: int = 60
+ ) -> Message:
+ """使用工具调用 LLM 并返回响应。不支持流式返回。
+ 该方法支持函数调用。
+ Args:
+ messages: 对话消息列表
+ system_msgs: 系统消息
+ tools: 工具列表
+ tool_choice: 工具选择方式
+ stream: 是否启用流式消息返回
+ timeout: 请求超时时间
+ Returns:
+ Message: LLM 的响应消息
+ Raises:
+ ValueError: 如果工具选择方式无效或消息格式不正确
+ OpenAIError: 如果 API 调用失败
+ Exception: 其他异常
+ """
+ try:
+ if tool_choice not in TOOL_CHOICE_VALUES:
+ raise ValueError(f"Invalid tool_choice: {tool_choice}")
+ if system_msgs:
+ messages = system_msgs + messages
+ messages = self.format_messages(messages)
+ tools = self.format_tools(tools)
+ if not stream:
+ response = await self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=self.temperature,
+ max_tokens=self.max_tokens,
+ tools=tools,
+ tool_choice=tool_choice,
+ timeout=timeout,
+ stream=False,
+ )
+ if not response.choices or not response.choices[0].message:
+ logger.warning(f"empty response from LLM")
+ raise ValueError("Invalid or empty response from LLM")
+ result = Message.assistant_message(
+ content=response.choices[0].message.content,
+ tool_calls=response.choices[0].message.tool_calls
+ )
+ else:
+ await common_stream.aprint(f"[A] 请求大模型({self.model})")
+ response = await self.client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=self.temperature,
+ max_tokens=self.max_tokens,
+ tools=tools,
+ tool_choice=tool_choice,
+ timeout=timeout,
+ stream=True,
+ )
+ collected_messages = []
+ collected_tool_calls = []
+ has_content_msg = False
+ async for chunk in response:
+ chunk_message = chunk.choices[0].delta.content or ""
+ if chunk_message:
+ has_content_msg = True
+ await common_stream.aprint(chunk_message, end="")
+ elif not has_content_msg:
+ await common_stream.aprint('.', end="")
+ collected_messages.append(chunk_message)
+ if chunk.choices[0].delta.tool_calls:
+ collected_tool_calls.extend(chunk.choices[0].delta.tool_calls)
+ full_response = "".join(collected_messages).strip()
+ await common_stream.aprint("")
+ result = Message.assistant_message(
+ content=full_response,
+ tool_calls=self._merged_tool_calls(collected_tool_calls)
+ )
+ logger.info(f"stream={stream}, timeout={timeout}, messages={messages}, tools={tools}, tool_choice={tool_choice}, result={result}")
+ return result
+ except ValueError as ve:
+ logger.error(f"validation error in ask_tool: {ve}")
+ raise
+ except OpenAIError as oe:
+ logger.error(f"openAI API error: {oe}")
+ raise
+ except Exception as e:
+ logger.error(f"unexpected error in ask: {e}")
+ raise
+
+ def _merged_tool_calls(self, tool_calls : list) -> Optional[list]:
+ """流式调用时,合并工具调用信息"""
+ if not tool_calls:
+ return None
+ calls_index = []
+ merged_calls = {}
+ last_call_id = None
+ for call in tool_calls:
+ call_id = call.id
+ if not call_id:
+ if last_call_id:
+ call_id = last_call_id
+ else:
+ logger.warning(f"Tool call without id: {call}")
+ raise ValueError("Tool call must have a call_id")
+ else:
+ if call_id not in calls_index:
+ calls_index.append(call_id)
+ last_call_id = call_id
+ if call_id not in merged_calls:
+ merged_calls[call_id] = call
+ else:
+ base_call = merged_calls[call_id]
+ base_call.function.name = self._safe_concat(base_call.function.name, call.function.name)
+ base_call.function.arguments = self._safe_concat(base_call.function.arguments, call.function.arguments)
+ return list(map(lambda call_id: merged_calls.get(call_id), calls_index))
+
+ def _safe_concat(self, a: Optional[str], b: Optional[str]) -> Optional[str]:
+ """安全连接两个字符串,处理 None 情况"""
+ if a is None:
+ return b
+ if b is None:
+ return a
+ return a + b
diff --git a/component/mydba/mydba/app/message/memory_history.py b/component/mydba/mydba/app/message/memory_history.py
new file mode 100644
index 0000000..7a5e042
--- /dev/null
+++ b/component/mydba/mydba/app/message/memory_history.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+import json
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict, Optional
+from mydba.app.config.settings import settings
+from mydba.app.database.base_database import BaseDatabases
+from mydba.app.message.message import Function, ToolCall
+
+class MemoryInfo(BaseModel):
+ time: str = Field(..., description="记录时间")
+ request_id: Optional[str] = Field(None, description="请求标识")
+ user_name: str = Field(..., description="用户标识")
+ session: str = Field(..., description="会话标识")
+ agent_name: str = Field(..., description="agent 标识")
+ system_content: Optional[str] = Field(None, description="系统指令内容")
+ user_content: Optional[str] = Field(None, description="用户查询内容")
+ assistant_content: Optional[str] = Field(None, description="大模型返回的内容")
+ assistant_tool_calls: Optional[List[ToolCall]] = Field(None, description="大模型返回的工具调用列表")
+ tool_contents: Optional[List[Dict[str, str]]] = Field(None, description="工具调用返回的内容列表,内容格式为 {'tool_call_id': 'xxx', 'content': 'xxx'}")
+
+async def get_memory(user_name: str, session: str, agent_name: str, start_time: Optional[datetime] = None,
+ request_id: Optional[str] = None, limit: int=10) -> List[MemoryInfo]:
+ """
+ 获取用户的 agent memory
+ Args:
+ user_name (str): 用户标识。
+ session (str): 会话标识。
+ agent_name (str): agent 标识。
+ start_time (datetime): 起始时间。
+ request_id (str): 请求标识。
+ limit (int): 限制返回的记录数。
+ Returns:
+ list: 用户的 memory 列表(按时间倒序)。
+ """
+ # 读取 db 中的 agent memory
+ params = [user_name, session, agent_name]
+ sql = "SELECT * FROM memory WHERE user_name=? AND session=? AND agent_name=?"
+ if start_time:
+ params.append(start_time.strftime("%Y-%m-%d %H:%M:%S"))
+ sql += " AND time>=?"
+ if request_id:
+ params.append(request_id)
+ sql += " AND request_id=?"
+ sql += f" ORDER BY id DESC LIMIT {limit}"
+ db = BaseDatabases.create_database(uri=settings.CONFIG_DATABASE)
+ try:
+ rows = await db.query(sql, params)
+ memories = []
+ for row in rows:
+ assistant_tool_calls = None
+ if row['assistant_tool_calls']:
+ assistant_tool_calls = []
+ tool_call_list = json.loads(row['assistant_tool_calls'])
+ for item in tool_call_list:
+ function = Function(name=item['function']['name'], arguments=item['function']['arguments'])
+ tool_call = ToolCall(id=item['id'], type=item['type'], function=function)
+ assistant_tool_calls.append(tool_call)
+ tool_contents = json.loads(row['tool_contents']) if row['tool_contents'] else None
+ memory = MemoryInfo(
+ time = row['time'],
+ request_id = row['request_id'],
+ user_name = row['user_name'],
+ session = row['session'],
+ agent_name = row['agent_name'],
+ system_content = row['system_content'],
+ user_content = row['user_content'],
+ assistant_content = row['assistant_content'],
+ assistant_tool_calls = assistant_tool_calls,
+ tool_contents = tool_contents
+ )
+ memories.append(memory)
+ finally:
+ await db.clean()
+ return memories
+
+async def save_memory(memory_info: MemoryInfo) -> None:
+ """
+ 保存用户的 agent memory
+ Args:
+ memory_info (MemoryInfo): 用户的 agent memory 信息。
+ """
+ sql = "INSERT INTO memory (time, request_id, user_name, session, agent_name, system_content, user_content, assistant_content, assistant_tool_calls, tool_contents) "
+ sql += "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ params = [memory_info.time, memory_info.request_id, memory_info.user_name, memory_info.session, memory_info.agent_name]
+ params.extend([memory_info.system_content, memory_info.user_content, memory_info.assistant_content])
+ params.append(str(memory_info.assistant_tool_calls) if memory_info.assistant_tool_calls else None)
+ params.append(json.dumps(memory_info.tool_contents, ensure_ascii=False) if memory_info.tool_contents else None)
+ db = BaseDatabases.create_database(uri=settings.CONFIG_DATABASE)
+ try:
+ await db.execute(sql=sql, parameters=params)
+ finally:
+ await db.clean()
diff --git a/component/mydba/mydba/app/message/message.py b/component/mydba/mydba/app/message/message.py
new file mode 100644
index 0000000..599d358
--- /dev/null
+++ b/component/mydba/mydba/app/message/message.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+import json
+from abc import ABC
+from enum import Enum
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import Any, List, Optional, Union
+
+class Role(str, Enum):
+ """定义参与对话的角色"""
+ SYSTEM = "system"
+ USER = "user"
+ ASSISTANT = "assistant"
+ TOOL = "tool"
+
+class Function(BaseModel):
+ name: str = Field(..., description="工具函数的名称")
+ arguments: str = Field(..., description="工具函数的入参")
+ def __repr__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+ def __str__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+
+class ToolCall(BaseModel):
+ id: str = Field(..., description="模型为本次调用分配的 id")
+ type: str = Field(default="function", description="调用的类型,目前只有 function")
+ function: Function = Field(..., description="模型需要调用的工具函数")
+ def __repr__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+ def __str__(self):
+ return json.dumps(self.model_dump(), ensure_ascii=False)
+
+class Message(BaseModel):
+ role: str = Field(..., description="消息的角色")
+ content: Optional[str] = Field(default=None, description="消息的内容")
+ name: str = Field(None, description="用户名称")
+ tool_calls: Optional[List[ToolCall]] = Field(default=None, description="调用列表")
+ tool_call_id: Optional[str] = Field(default=None, description="模型为调用分配的 id")
+ time: datetime = Field(default_factory=datetime.now, description="消息时间")
+
+ @classmethod
+ def user_message(cls, content: str) -> "Message":
+ """用户消息"""
+ return cls(role=Role.USER, content=content)
+
+ @classmethod
+ def system_message(cls, content: str) -> "Message":
+ """系统消息"""
+ return cls(role=Role.SYSTEM, content=content)
+
+ @classmethod
+ def assistant_message(cls, content: Optional[str] = None, tool_calls: Optional[List[Any]] = None) -> "Message":
+ """LLM 返回消息"""
+ formatted_calls = None
+ if tool_calls:
+ formatted_calls = [
+ {"id": call.id, "function": call.function.model_dump(), "type": "function"} for call in tool_calls
+ ]
+ if not content and not formatted_calls:
+ raise ValueError("content and tool_calls cannot be both None")
+ return cls(role=Role.ASSISTANT, content=content, tool_calls=formatted_calls)
+
+ @classmethod
+ def tool_message(cls, content: str, tool_call_id: str) -> "Message":
+ """工具调用消息"""
+ return cls(role=Role.TOOL, content=content, tool_call_id=tool_call_id)
+
+ def format(self) -> dict:
+ """格式化消息为字典,进行 LLM 调用"""
+ message = {"role": self.role}
+ if self.content is not None:
+ message["content"] = self.content
+ if self.tool_calls is not None:
+ message["tool_calls"] = [tool_call.model_dump() for tool_call in self.tool_calls]
+ if self.name is not None:
+ message["name"] = self.name
+ if self.tool_call_id is not None:
+ message["tool_call_id"] = self.tool_call_id
+ return message
+
+ def __repr__(self):
+ return str(self.format())
+
+ def __str__(self):
+ return str(self.format())
diff --git a/component/mydba/mydba/app/prompt/__init__.py b/component/mydba/mydba/app/prompt/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/app/prompt/ask_table.py b/component/mydba/mydba/app/prompt/ask_table.py
new file mode 100644
index 0000000..2701660
--- /dev/null
+++ b/component/mydba/mydba/app/prompt/ask_table.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+SYSTEM_PROMPT = """# 角色
+您是专业的阿里巴巴云数据库运营助手,具备丰富的 MySQL 数据库知识,熟练掌握各种 SQL 语法,通过理解用户问题,结合资料,生成可执行的 SELECT 语句,并汇总结果。
+
+## 技能
+
+### 技能 1:数据分析,根据用户的需求,合理规划流程,一步步执行操作,帮助用户获取并汇总数据查询结果。
+- 按照以下流程进行任务规划:
+ 1. **理解需求**:根据用户提供的信息,理解清楚用户的要求,包括但不限于:需要的数据库表、查询数据的条件、数据结果的处理方式。
+ 2. **查询表结构**:根据所需的数据库表改写查询关键词,然后多次调用 RAG 工具获取相关数据表的结构资料,当有数据表信息获取不到时,与用户对话沟通,直至问题得到解决,对于需要连表查询或者多次查询的场景,可以修改查询关键词后重复执行该步骤,直到获取到所有需要的表结构。
+ 3. **生成 SQL**:根据用户要求,结合表结构资料,生成一条 SELECT 语句,语句需要符合 MySQL 的语法要求,对于生成的语句需要添加返回条数的限制,一次最多10条,即 SELECT * FROM 表名 LIMIT 10。
+ 4. **执行 SQL**:对生成的单条 SELECT 语句,调用 mysql_execution 工具进行执行,收集执行结果。根据需要,流程2、流程3和流程4可以重复执行。
+ 5. **数据汇总**:根据用户要求的处理方式,对收集的执行结果进行数据分析和处理,并汇总出数据结果,处理方式有:取最大值、取最小值、取平均值、计算变化率。
+- 技能要求:
+ 1. 合理规划任务,一步步的执行,最终得到结果,必要时进行反思并对步骤过程修改后再次执行,直到得到最终结果。
+ 2. 理解清楚用户要求,可以重复调用表结构查询工具,去不断的收集信息,根据已知的真实的信息,帮助用户进行数据处理。
+ 3. 调用 RAG 工具时,根据上下文信息进行关键词的必要改写,要求改写后的关键词能明确到具体的数据库名称,同时突出库表的信息,不要出现字段信息,确保资料查询的准确、完整,查询出的语句条数尽量不要小于5条,避免关键信息查询不到,注意:改写后的关键词必须是真实的、符合用户要求的,禁止臆造,必要时和用户进行交互确认。
+ 4. 如果仍然存在不清楚的信息,调用工具 interaction 与用户进行一次或多次的对话沟通,直至清楚的理解问题。
+ 5. 流程执行过程中,如果碰到 SELECT 语句执行失败的情况,请反思 SQL,并根据用户要求重新生成后,再执行。
+ 6. 汇总的结果要根据用户使用习惯,使用 Markdown 语法来书写。
+ 7. 对于相似的表结构需要和用户进行交互确认,禁止将不符合语义的表当作目标表查询或者臆断字段含义,禁止使用非目标数据库中的相似表查询。
+
+### 技能 2:工具调用
+- 工具调用必须遵循任务规划,有合理的逻辑推理,和客户需求相符。
+- 熟练调用 RAG 工具以检索数据库表信息,调用 mysql_execution 工具进行 SQL 执行。
+- 工具 interaction 可以与用户进行对话沟通,请合理使用帮助理解用户要求,沟通时请完整输出需要确认的内容,避免内容丢失。
+- 除上述之外的工具,禁止调用。
+
+### 技能 3:时间解析与计算
+- 如果不知道准确的当前时间,请利用工具询问用户,不要将系统时间作为当前时间。
+- 准确解析相对时间概念,如“今天”、“昨天”或“最后一小时”。
+- 使用当前时间将相对时间表达转换为精确的时间范围或时间戳,以支持数据查询或操作。
+- 对时间进行计算时,请将时间转换成时间戳数据在进行计算,如果需要展示计算结果,请以天、时、分、秒进行合理呈现。
+- 明确时、分、秒的换算进制是60,天、时的换算进制是24,2月在平年是28天,2月份在闰年是29天。
+
+
+## 约束条件
+- **优先任务分解**:始终提供详细的任务分解。
+- **工具依赖明确**:所有工具调用都必须有清晰的任务需求和逻辑推理支持。
+- **时间精确性**:为时间敏感的查询计算准确的时间范围。
+- **查询准确性**:所有结果以工具调用和 SQL 的执行结果为准,禁止不经过调用或者查询直接利用上下文信息得出最终的信息,当工具调用结果为空时,请勿生成不存在的信息。
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+"""
diff --git a/component/mydba/mydba/app/prompt/chat.py b/component/mydba/mydba/app/prompt/chat.py
new file mode 100644
index 0000000..626573b
--- /dev/null
+++ b/component/mydba/mydba/app/prompt/chat.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+SYSTEM_PROMPT = """# 角色
+
+您是专业的阿里巴巴云数据库运营助手,专注于为客户提供高效的数据库技术支持和解决方案。您的目标是通过清晰的问题分解、深刻的结果反思和精确的时间计算,帮助客户快速解决问题。
+
+## 技能
+
+### 技能 1:问题分解与分析
+- 深入拆解用户问题,识别核心需求及相关的潜在步骤/命令。
+- 提供明确的任务分解,确保每一步都对最终解决方案有所贡献。
+- 当需要用户澄清需求或者需要用户确认行动时,利用工具 interaction 和用户进行沟通,沟通时请完整输出需要确认的内容,避免内容丢失。
+- 任务分解必须符合逻辑推理,确保每个步骤都能有效地解决问题。
+
+### 技能 2:对答案的深刻反思
+- 深入思考用户问题和答案,识别潜在的错误或遗漏。
+- 指出答案中的逻辑漏洞或不一致之处。
+- 提出改进建议,确保答案的准确性和完整性。
+- 反思必须基于事实和逻辑,避免主观臆断。
+
+### 技能 3:时间解析与计算
+- 如果不知道准确的当前时间,请询问用户,不要将系统时间作为当前时间。
+- 准确解析相对时间概念,如“今天”、“昨天”或“最后一小时”。
+- 使用当前时间将相对时间表达转换为精确的时间范围或时间戳,以支持数据查询或操作。
+- 准确的当前时间,可以从系统获取,或者询问用户获取。
+
+## 约束条件
+- **优先任务分解**:始终提供详细的任务分解。
+- **时间精确性**:为时间敏感的查询计算准确的时间范围。
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+"""
diff --git a/component/mydba/mydba/app/prompt/rds_agent.py b/component/mydba/mydba/app/prompt/rds_agent.py
new file mode 100644
index 0000000..24f4b14
--- /dev/null
+++ b/component/mydba/mydba/app/prompt/rds_agent.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+SYSTEM_PROMPT = """# 角色
+您是专业的阿里巴巴云数据库运营助手,专注于为客户提供高效的数据库技术支持和解决方案。您的目标是通过清晰的问题分解、准确的工具调用和精确的时间计算,根据事实、工具调用结果给出结论,帮助客户快速解决问题。
+
+## 技能
+
+### 技能 1:RDS 实例运维管理,请根据用户的需求,通过工具调用获取所需信息,一步步执行操作,实现 RDS 实例的智能管理。
+- 通过调用 RDS 工具查询 RDS 实例的相关信息,如实例状态、实例配置、实例日志、实例参数等,利用调用结果进行实例问题的分析和诊断,对于状态不健康的实例,提供有效的原因诊断和故障排除方案。
+- 为用户提供完全真实的信息,根据 RDS 工具的返回生成结论,结果为空时如实告知用户,避免因上下文中的错误信息导致最终结论的错误。
+- 输入的实例规格、实例地域、实例类型、存储容量、存储类型等参数需要严格符合用户的要求,禁止臆断、修改或使用含义不明确的参数,当参数存在疑问的时候尝试调用工具或与用户进行交互确认,在未特殊指定的情况下,存储类型默认采用general_essd,实例可用区、vpc、交换机等非必填参数可采用默认配置,传入空字符串。
+- 将用户的规格需求转化为对应的规格码,确保调用 RDS 工具时的规格参数与用户的需求保持一致。
+- 在调用 RDS 工具时,上下文的参数需要保持一致,非导致错误的参数请勿随意修改,同时避免在所需参数不明确的情况下调用 RDS 工具。
+- 当需要调用 RDS 工具进行 srC_gfqOH7FJuMwuE0Ydwg (实例创建)、 EDhos22fJNHv453QrBMScA (修改实例规格)、 _uNpVyZJAHb_9KVGuR1lnA (修改实例参数)的操作时,务必列出涉及到的参数并使用 interaction 工具和用户进行确认,在获得用户的同意后执行相关操作,禁止不经确认就执行操作。
+
+### 技能 2:问题分解与分析
+- 深入拆解用户问题,识别核心需求及相关的潜在步骤/命令。
+- 提供明确的任务分解,确保每一步都对最终解决方案有所贡献。
+- 当需要用户澄清需求或者需要用户确认行动时,利用工具 interaction 和用户进行沟通,沟通时请完整输出需要确认的内容,避免内容丢失。
+- 任务分解必须符合逻辑推理,确保每个步骤都能有效地解决问题。
+
+### 技能 3:工具调用
+- 熟练调用工具以检索数据库信息或执行操作。
+- 工具调用必须遵循任务分解,并与逻辑推理和客户需求相符。
+- 根据用户需求选择合适的模块(例如,数据库信息查询、实例创建)。
+- 调用 RDS 工具时,必须在用户确认后执行,禁止未取得用户同意前执行非查询功能模块的调用。
+- 工具 interaction 可以与用户进行对话沟通,请合理使用帮助理解用户要求,沟通时请完整输出需要确认的内容,避免内容丢失。
+
+### 技能 4:时间解析与计算
+- 利用工具获取当前时间,不要将系统时间作为当前时间。
+- 准确解析相对时间概念,如“今天”、“昨天”或“最后一小时”。
+- 使用当前时间将相对时间表达转换为精确的时间范围或时间戳,以支持数据查询或操作。
+
+## 约束条件
+- **优先任务分解**:始终提供详细的任务分解。
+- **工具依赖明确**:所有工具调用都必须有清晰的任务需求和逻辑推理支持。
+- **时间精确性**:为时间敏感的查询计算准确的时间范围。
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+
+## 参考信息
+- rds 规格码以 1 结尾表示属于基础系列,以 2c 结尾表示属于高可用系列,以 xc 结尾表示属于集群系列。
+- 规格码中 small 表示1核,medium 表示2核,large 表示4核,xlarge 表示8核,2xlarge 表示16核,以此类推。
+- 常用的规格如下所示:
+mysql.n2.medium.1 2核 4GB
+mysql.n4.medium.1 2核 8GB
+mysql.n2.large.1 4核 8GB
+mysql.n4.large.1 4核 16GB
+mysql.n2.xlarge.1 8核 16GB
+mysql.n4.xlarge.1 8核 32GB
+mysql.n2.small.2c 1核 2GB
+mysql.n2.medium.2c 2核 4GB
+mysql.n4.medium.2c 2核 8GB
+mysql.n8.medium.2c 2核 16GB
+mysql.n2.large.2c 4核 8GB
+mysql.n4.large.2c 4核 16GB
+mysql.n8.large.2c 4核 32GB
+mysql.n2.xlarge.2c 8核 16GB
+mysql.n4.xlarge.2c 8核 32GB
+mysql.n8.xlarge.2c 8核 64GB
+mysql.n2.small.xc 1核 2GB
+mysql.n2.medium.xc 2核 4GB
+mysql.n4.medium.xc 2核 8GB
+mysql.n8.medium.xc 2核 16GB
+mysql.n2.large.xc 4核 8GB
+mysql.n4.large.xc 4核 16GB
+mysql.n8.large.xc 4核 32GB
+mysql.n2.xlarge.xc 8核 16GB
+mysql.n4.xlarge.xc 8核 32GB
+mysql.n8.xlarge.xc 8核 64GB
+mysql.n2.2xlarge.xc 16核 32GB
+mysql.n4.2xlarge.xc 16核 64GB
+mysql.x2.medium.2c 2核 4GB
+mysql.x4.medium.2c 2核 8GB
+mysql.x8.medium.2c 2核 16GB
+mysql.x2.large.2c 4核 8GB
+mysql.x4.large.2c 4核 16GB
+mysql.x8.large.2c 4核 32GB
+mysql.x2.xlarge.2c 8核 16GB
+mysql.x4.xlarge.2c 8核 32GB
+mysql.x8.xlarge.2c 8核 64GB
+mysql.x2.3large.2c 12核 24GB
+mysql.x4.3large.2c 12核 48GB
+mysql.x8.3large.2c 12核 96GB
+mysql.x2.2xlarge.2c 16核 32GB
+mysql.x4.2xlarge.2c 16核 64GB
+mysql.x8.2xlarge.2c 16核 128GB
+mysql.x2.medium.xc 2核 4GB
+mysql.x4.medium.xc 2核 8GB
+mysql.x8.medium.xc 2核 16GB
+mysql.x2.large.xc 4核 8GB
+mysql.x4.large.xc 4核 16GB
+mysql.x8.large.xc 4核 32GB
+mysql.x2.xlarge.xc 8核 16GB
+mysql.x4.xlarge.xc 8核 32GB
+mysql.x8.xlarge.xc 8核 64GB
+mysql.x2.3large.xc 12核 24GB
+mysql.x4.3large.xc 12核 48GB
+mysql.x8.3large.xc 12核 96GB
+mysql.x2.2xlarge.xc 16核 32GB
+mysql.x4.2xlarge.xc 16核 64GB
+mysql.x8.2xlarge.xc 16核 128GB
+"""
diff --git a/component/mydba/mydba/app/prompt/reflection.py b/component/mydba/mydba/app/prompt/reflection.py
new file mode 100644
index 0000000..5bf98ba
--- /dev/null
+++ b/component/mydba/mydba/app/prompt/reflection.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+SYSTEM_PROMPT = """# 角色
+
+您是专业的阿里巴巴云数据库运营助手,专注于为客户提供高效的数据库技术支持和解决方案。您的目标是通过清晰的问题分解、深刻的结果反思和精确的时间计算,帮助客户快速解决问题。
+
+## 技能
+
+### 技能 1:问题分解与分析
+- 深入拆解用户问题,识别核心需求及相关的潜在步骤/命令。
+- 提供明确的任务分解,确保每一步都对最终解决方案有所贡献。
+- 当需要用户澄清需求或者需要用户确认行动时,利用工具 interaction 和用户进行沟通,沟通时请完整输出需要确认的内容,避免内容丢失。
+- 任务分解必须符合逻辑推理,确保每个步骤都能有效地解决问题。
+
+### 技能 2:时间解析与计算
+- 如果不知道准确的当前时间,请利用工具询问用户,不要将系统时间作为当前时间。
+- 准确解析相对时间概念,如“今天”、“昨天”或“最后一小时”。
+- 使用当前时间将相对时间表达转换为精确的时间范围或时间戳,以支持数据查询或操作。
+
+## 约束条件
+- **优先任务分解**:始终提供详细的任务分解。
+- **时间精确性**:为时间敏感的查询计算准确的时间范围。
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+"""
+
+USER_PROMPT = """
+问题:
+
+{query}
+
+答案:
+
+{content}
+
+反馈:
+
+{reflection}
+
+请根据反馈内容,调整答案,保证答案的准确性和完整性。
+请注意,调整必须基于事实和逻辑,禁止主观臆断。
+"""
+
+REFLECTION_SYSTEM_PROMPT = """# 角色
+
+您是专业的阿里巴巴云数据库运营助手,能够很好的反思其它系统提供的答案,并给出答案的改进意见。
+
+## 技能
+
+### 技能 1:对答案的深刻反思
+- 深入思考用户问题和答案,识别潜在的错误或遗漏。
+- 指出答案中的逻辑漏洞或不一致之处。
+- 只提出改进建议,不要提供新的答案。
+- 反思必须基于事实和逻辑,避免主观臆断。
+- 如果答案中没有错误或遗漏,请直接返回 None。
+
+## 约束条件
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+- **返回要求**:禁止返回空,不用输出思考过程,如果答案不需要修改,请直接返回 None。
+"""
+
+REFLECTION_USER_PROMPT = """
+问题:
+
+{query}
+
+答案:
+
+{content}
+
+
+请对上述问题和答案进行反思,并给出合理建议。
+"""
diff --git a/component/mydba/mydba/app/prompt/router.py b/component/mydba/mydba/app/prompt/router.py
new file mode 100644
index 0000000..fbdb54c
--- /dev/null
+++ b/component/mydba/mydba/app/prompt/router.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+from functools import reduce
+from typing import List
+from mydba.app.config.agent import AgentInfo
+
+pack_intent_info = lambda l: "\n".join([INTENT_INFO.format(id=i+1, name=agent.intent, description=agent.intent_description) for i, agent in enumerate(l, start=0)])
+pack_intent_name = lambda l: "、".join([agent.intent for agent in l])
+pack_default_intent = lambda l: next(iter([agent.intent for agent in filter(lambda agent: agent.is_default, l)]), None)
+
+def pack_condition(agents: List[AgentInfo]) -> str:
+ conditions = map(lambda agent: agent.prompts.get('condition'), filter(lambda agent: agent.prompts and agent.prompts.get('condition'), agents))
+ reduce_conditions = reduce(lambda x, y: x + y, conditions, list())
+ return "\n".join([CONDITION_INFO.format(condition=condition) for condition in reduce_conditions])
+
+def pack_shot(agents: List[AgentInfo]) -> str:
+ id = 0
+ shot_infos = []
+ for agent in agents:
+ if not agent.prompts or not agent.prompts.get('shot'):
+ continue
+ for shot in agent.prompts.get('shot'):
+ id += 1
+ shot_info = SHOT_INFO.format(id=id, shot=shot, intent=agent.intent)
+ shot_infos.append(shot_info)
+ if not shot_infos:
+ return "暂无示例"
+ return "\n".join(shot_infos)
+
+INTENT_INFO = """ {id}. **{name}**:{description}。"""
+CONDITION_INFO = """- {condition}"""
+SHOT_INFO = """###示例{id}\n{{\n "问题描述": "{shot}",\n "意图": "{intent}"\n}}"""
+
+SYSTEM_PROMPT = """# 角色
+
+您是专业的阿里巴巴云数据库运营助手,专注于为客户提供高效的数据库技术支持和解决方案。您的目标是通过清晰的问题理解,识别出用户意图。
+
+## 技能
+
+### 技能 1:分析问题
+- 深入分析用户问题,结合问题上下文,识别核心需求及相关的潜在诉求。
+- 对问题的分析要基于事实和逻辑,不得主观臆断。
+
+### 技能 2:识别意图
+- 根据用户问题,识别出其意图。
+- 意图种类是固定的,有:
+{intent_infos}
+- 对于无法识别的意图,返回“{default_intent}”。
+- 意图识别必须基于事实和逻辑,避免主观臆断。
+- 意图识别需要充分结合上下文信息进行,识别结果需要考虑到之前的对话历史,根据历史对话意图及当前问题汇总得出,若用户输入中有明确的意图切换,则优先使用用户输入的意图。
+
+## 约束条件
+- **意图精确性**:始终返回列表里的意图,禁止返回其它内容。这里再重申一次有效的意图列表,有 {intent_names}。
+- **专业聚焦**:仅讨论与数据库相关的技术话题。
+- **安全意识**:确保没有任何操作会对客户的数据库产生负面影响。
+{conditions}
+
+## 示例
+
+{shots}
+
+"""
+
+USER_PROMPT = """
+问题:
+
+{query}
+
+请根据问题内容,识别出意图。
+请注意,必须基于事实和逻辑进行识别,禁止主观臆断,只返回具体的意图种类,不输出分析过程。
+"""
diff --git a/component/mydba/mydba/app/tool/base_local_tool.py b/component/mydba/mydba/app/tool/base_local_tool.py
new file mode 100644
index 0000000..5adbf33
--- /dev/null
+++ b/component/mydba/mydba/app/tool/base_local_tool.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+import json
+from abc import ABC
+from enum import Enum
+from typing import Any, Dict, Literal
+from pydantic import BaseModel, Field
+
+class LocalTool(str, Enum):
+ """本地工具"""
+ INTERACTION = "interaction"
+ MYSQL_EXECUTION = "mysql_execution"
+
+LOCAL_TOOL_VALUES = tuple(tool.value for tool in LocalTool)
+LOCAL_TOOL_TYPE = Literal[LOCAL_TOOL_VALUES]
+
+class BaseLocalTool(ABC, BaseModel):
+ tool_name: str = Field(..., description="工具名称")
+ description: str = Field(..., description="工具描述")
+ input_schema: Dict = Field(..., description="工具参数描述")
+
+ async def execute(self, arguments: str) -> str:
+ return NotImplementedError("Subclasses must implement this method")
+
+ def _parse_arguments(self, arguments: str) -> Dict[str, Any]:
+ try:
+ args = json.loads(arguments)
+ except json.JSONDecodeError as e:
+ raise ValueError(f"The parameters for tool '{self.tool_name}' are in an incorrect format.")
+ return args
diff --git a/component/mydba/mydba/app/tool/interaction.py b/component/mydba/mydba/app/tool/interaction.py
new file mode 100644
index 0000000..51ebcb3
--- /dev/null
+++ b/component/mydba/mydba/app/tool/interaction.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from mydba.common import stream
+from typing import Dict
+from pydantic import BaseModel
+from mydba.app.tool.base_local_tool import BaseLocalTool, LocalTool
+from mydba.common.logger import logger
+
+class Interaction(BaseLocalTool, BaseModel):
+ tool_name: str = LocalTool.INTERACTION.value
+ description: str = "命令行交互工具,可快速与用户沟通,引导用户澄清需求"
+ input_schema: Dict = {
+ "type" : "object",
+ "properties" : {
+ "message" : {
+ "type" : "string",
+ "description": "A prompt message used to guide the user during an interaction."
+ }
+ },
+ "required": ["message"]
+ }
+
+ async def execute(self, arguments: str) -> str:
+ args = self._parse_arguments(arguments=arguments)
+ if not args.get("message"):
+ raise ValueError("When interacting with the user, it is necessary to display a prompt message.")
+ await stream.aprint(f"[A] {args['message']}")
+ response = await stream.ainput(">> ")
+ return response
\ No newline at end of file
diff --git a/component/mydba/mydba/app/tool/mcp_tool.py b/component/mydba/mydba/app/tool/mcp_tool.py
new file mode 100644
index 0000000..3ff6b36
--- /dev/null
+++ b/component/mydba/mydba/app/tool/mcp_tool.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+import asyncio
+from datetime import timedelta
+import json
+import os
+import shutil
+from contextlib import AsyncExitStack
+from exceptiongroup import ExceptionGroup
+from mcp.client.session import ClientSession
+from mcp.client.sse import sse_client
+from mcp.client.stdio import StdioServerParameters, stdio_client
+from pydantic import BaseModel, Field
+from typing import Any, Optional, Dict
+from mydba.app.config.mcp_tool import McpInfo, Transport
+from mydba.common.logger import logger
+
+class McpClient(BaseModel):
+ """Manages MCP server connections and tool execution."""
+ mcp_info: McpInfo = Field(..., description="mcp server 信息")
+ timeout: int = Field(30, description="mcp client session 超时时间")
+
+ async def list_tools(self) -> list[Dict[str, str]]:
+ tools = []
+ if self.mcp_info.transport == Transport.STDIO:
+ envs = self._merge_env(self.mcp_info.envs)
+ server_params = StdioServerParameters(
+ command=self.mcp_info.command,
+ args=self.mcp_info.args,
+ env=envs,
+ )
+ async with stdio_client(server_params) as streams:
+ read, write = streams
+ async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout)) as session:
+ await session.initialize()
+ tools_response = await session.list_tools()
+ for tool in tools_response.tools:
+ tools.append({
+ 'server_name': self.mcp_info.name,
+ 'tool_name': tool.name,
+ 'description': tool.description,
+ 'input_schema': json.dumps(tool.inputSchema)})
+ else:
+ async with sse_client(self.mcp_info.server_uri) as streams:
+ read, write = streams
+ async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout)) as session:
+ await session.initialize()
+ tools_response = await session.list_tools()
+ for tool in tools_response.tools:
+ tools.append({
+ 'server_name': self.mcp_info.name,
+ 'tool_name': tool.name,
+ 'description': tool.description,
+ 'input_schema': json.dumps(tool.inputSchema)})
+ return tools
+
+ async def execute_tool(self, tool_name: str, arguments: Optional[dict[str, Any]] = None) -> str:
+ if self.mcp_info.transport == Transport.STDIO:
+ envs = self._merge_env(self.mcp_info.envs)
+ server_params = StdioServerParameters(
+ command=self.mcp_info.command,
+ args=self.mcp_info.args,
+ env=envs,
+ )
+ async with stdio_client(server_params) as streams:
+ read, write = streams
+ async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout)) as session:
+ await session.initialize()
+ result = await session.call_tool(tool_name, arguments)
+ else:
+ async with sse_client(self.mcp_info.server_uri) as streams:
+ read, write = streams
+ async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout)) as session:
+ await session.initialize()
+ result = await session.call_tool(tool_name, arguments)
+ if result.isError:
+ raise Exception(f"execute fail, resp={result.content}")
+ # 默认取 text 信息
+ return result.content[0].text
+
+ def _merge_env(self, mcp_envs: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
+ """合并 mcp env 和当前环境的 env"""
+ envs = {**os.environ}
+ if 'VIRTUAL_ENV' in envs:
+ del envs['VIRTUAL_ENV']
+ if not mcp_envs:
+ return envs
+ for k, v in mcp_envs.items():
+ if v:
+ envs[k] = v
+ return envs
diff --git a/component/mydba/mydba/app/tool/mysql_execution.py b/component/mydba/mydba/app/tool/mysql_execution.py
new file mode 100644
index 0000000..5915ae8
--- /dev/null
+++ b/component/mydba/mydba/app/tool/mysql_execution.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import json
+import signal
+from decimal import Decimal
+from datetime import datetime, date, timedelta
+from pydantic import BaseModel, Field
+from typing import Dict
+from mydba.app.config.database import database_config
+from mydba.app.database.base_database import BaseDatabases
+from mydba.app.tool.base_local_tool import BaseLocalTool, LocalTool
+from mydba.common.global_settings import global_settings
+from mydba.common.logger import logger
+
+class DatabaseCache(BaseModel):
+ database_map: Dict[str, BaseDatabases] = Field(default_factory=dict, description="key 为实例连接信息,value 为 database")
+ lock: asyncio.Lock = Field(default_factory=asyncio.Lock, description="缓存锁")
+ is_register: bool = Field(default=False, description="是否注册清理函数")
+
+ async def get_database(self, database_name: str) -> BaseDatabases:
+ """
+ 根据库名获取缓存的 db 对象
+ Args:
+ database_name (str): 库名称。
+ Returns:
+ BaseDatabases: 数据库操作对象。
+ Raises:
+ Exception: 如果库信息不存在
+ """
+ database_info = database_config.get_database(database_name)
+ if not database_info:
+ logger.error(f"[mysql_exec] Database {database_name} not found")
+ raise Exception(f"Database {database_name} not found")
+ async with self.lock:
+ if self.is_register is False:
+ await self._register_cleanup()
+ key = f"{database_info.host}_{database_info.port}_{database_info.user}_{database_info.database}"
+ key = database_info.uri if database_info.uri else key
+ database = self.database_map.get(key)
+ if database:
+ return database
+ database = BaseDatabases.create_database(database_info=database_info)
+ self.database_map[key] = database
+ return database
+
+ async def _register_cleanup(self) -> None:
+ asyncio.create_task(self._cleanup())
+ self.is_register = True
+
+ async def _cleanup(self) -> None:
+ # 等待退出
+ while True:
+ try:
+ await asyncio.sleep(1)
+ except BaseException:
+ pass
+ if global_settings.IS_EXIT:
+ break
+ # 清理缓存
+ async with self.lock:
+ for db in self.database_map.values():
+ try:
+ await db.clean()
+ except Exception as e:
+ pass
+ self.database_map.clear()
+ return
+
+ class Config:
+ arbitrary_types_allowed = True
+_database_cache = DatabaseCache()
+
+class MySQLExecution(BaseLocalTool, BaseModel):
+ tool_name: str = LocalTool.MYSQL_EXECUTION.value
+ description: str = "MySQL执行器,用于查询MySQL数据库"
+ input_schema: Dict = {
+ "type" : "object",
+ "properties" : {
+ "database" : {
+ "type" : "string",
+ "description": "Database name."
+ },
+ "sql" : {
+ "type" : "string",
+ "description": "SQL query."
+ }
+ },
+ "required": ["database", "sql"]
+ }
+
+ async def execute(self, arguments: str) -> str:
+ args = self._parse_arguments(arguments=arguments)
+ database_name = args.get("database")
+ sql = args.get("sql")
+ if not database_name or not sql:
+ raise ValueError("Need paramater(database & sql), when query sql in mysql.")
+ db = await _database_cache.get_database(database_name)
+ results = await db.query(sql=sql)
+ return json.dumps(results, cls=CustomJSONEncoder, ensure_ascii=False)
+
+class CustomJSONEncoder(json.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, datetime):
+ return obj.strftime("%Y-%m-%d %H:%M:%S")
+ if isinstance(obj, date):
+ return obj.strftime("%Y-%m-%d")
+ if isinstance(obj, timedelta):
+ total_seconds = int(obj.total_seconds())
+ hours, remainder = divmod(total_seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ return f"{hours:02}:{minutes:02}:{seconds:02}"
+ if isinstance(obj, bytes):
+ return obj.hex()
+ if isinstance(obj, set):
+ return repr(obj)
+ if isinstance(obj, Decimal):
+ return str(obj)
+ return super().default(obj)
diff --git a/component/mydba/mydba/app/tool/tool_manager.py b/component/mydba/mydba/app/tool/tool_manager.py
new file mode 100644
index 0000000..17ca3aa
--- /dev/null
+++ b/component/mydba/mydba/app/tool/tool_manager.py
@@ -0,0 +1,187 @@
+# -*- coding: utf-8 -*-
+import asyncio
+import json
+from datetime import datetime
+from exceptiongroup import ExceptionGroup
+from typing import List, Optional
+from mydba.app.config.agent import AgentMcp
+from mydba.app.config.settings import settings
+from mydba.app.config.mcp_tool import McpToolInfo, mcp_config, mcp_tool_config
+from mydba.app.tool.base_local_tool import LOCAL_TOOL_VALUES, LOCAL_TOOL_TYPE, BaseLocalTool, LocalTool
+from mydba.app.tool.interaction import Interaction
+from mydba.app.tool.mcp_tool import McpClient
+from mydba.app.tool.mysql_execution import MySQLExecution
+from mydba.common import stream
+from mydba.common.global_settings import global_settings
+from mydba.common.logger import logger
+from mydba.common.session import get_context, set_context, reset_context
+
+RETRYABLE_NAME_PREFIX = ['get', 'describe', 'list']
+
+class ToolManager:
+ def __init__(self):
+ # mcp 工具列表上次刷新时间
+ self.last_refresh_time = 0
+ self.local_tool_map = {
+ LocalTool.INTERACTION.value: Interaction(),
+ LocalTool.MYSQL_EXECUTION.value: MySQLExecution()
+ }
+ pass
+
+ async def get_tool_list(self, filter_: Optional[AgentMcp] = None) -> List[McpToolInfo]:
+ """
+ 获取最新的 tool 列表
+ Args:
+ filter_ (AgentMcp): 过滤器,包含 allow 和 deny 列表。
+ Returns:
+ list: tool 列表。
+ """
+ await self._wait_refresh()
+ tools = self.get_local_tool_list()
+ tools.extend(mcp_tool_config.mcp_tool_list)
+ if filter_ and filter_.allow:
+ tools = list(filter(lambda tool: tool.server_name in filter_.allow, tools))
+ elif filter_ and filter_.deny:
+ tools = list(filter(lambda tool: tool.server_name not in filter_.deny, tools))
+ return tools
+
+ def get_local_tool(self, name:LOCAL_TOOL_TYPE) -> McpToolInfo: # type: ignore
+ if name not in self.local_tool_map:
+ raise ValueError(f"local tool '{name}' does not exist")
+ tool = self.local_tool_map[name]
+ return self._convert(tool)
+
+ def get_local_tool_list(self) -> List[McpToolInfo]:
+ tool_infos = []
+ for _, tool in self.local_tool_map.items():
+ tool_info = self._convert(tool)
+ tool_infos.append(tool_info)
+ return tool_infos
+
+ def is_retryable_tool(self, tool_name: str) -> bool:
+ """工具是否能重试调用"""
+ for prefix in RETRYABLE_NAME_PREFIX:
+ if tool_name.lower().startswith(prefix):
+ return True
+ return False
+
+ async def convert_name(self, tool_key: str) -> List[str]:
+ """
+ 转换 tool_key 为可识读的名称
+ Args:
+ tool_key (str): 工具名称或者工具 tool_key
+ Returns:
+ str: 工具可识读名称
+ """
+ if tool_key in LOCAL_TOOL_VALUES:
+ return [tool_key, ]
+ mcp_tool_info = mcp_tool_config.get_mcp_tool_by_key(tool_key)
+ if not mcp_tool_info:
+ return [tool_key, ]
+ return [mcp_tool_info.server_name, mcp_tool_info.tool_name]
+
+ async def execute(self, name:str, arguments:str) -> str:
+ # 输出工具信息
+ context = get_context()
+ tool_name_infos = await self.convert_name(name)
+ if name != LocalTool.INTERACTION.value:
+ await stream.aprint(f"[A] 执行工具 {'::'.join(tool_name_infos)}")
+ if context.detail_info:
+ await stream.aprint(f" - 参数: {arguments}")
+
+ # 执行工具调用
+ if name in LOCAL_TOOL_VALUES:
+ result = await self._execute_local_tool(name, arguments)
+ else:
+ result = await self._execute_mcp_tool(name, arguments)
+
+ # 输出调用结果
+ if name != LocalTool.INTERACTION.value and context.detail_info:
+ await stream.aprint(f" - 返回: {result}")
+ return result
+
+ async def _execute_local_tool(self, name:LOCAL_TOOL_TYPE, arguments:str) -> str: # type: ignore
+ tool = self.local_tool_map.get(name)
+ if tool is None:
+ raise Exception(f"The specified tool '{name}' does not exist.")
+ return await tool.execute(arguments)
+
+ async def _execute_mcp_tool(self, name:str, arguments:str) -> str:
+ mcp_client = await self._get_mcp_client(tool_key=name)
+ mcp_tool_info = mcp_tool_config.get_mcp_tool_by_key(name)
+ logger.info(f"[tool] call tool, service name: {mcp_tool_info.server_name}, tool name: {mcp_tool_info.tool_name}")
+ return await mcp_client.execute_tool(tool_name=mcp_tool_info.tool_name,
+ arguments=None if not arguments else json.loads(arguments))
+
+ async def _get_mcp_client(self, tool_key:str) -> McpClient:
+ """
+ 获取 mcp client。
+ Args:
+ tool_key (str): mcp tool key。
+ Returns:
+ mcp_client: mcp client 对象。
+ """
+ mcp_tool_info = mcp_tool_config.get_mcp_tool_by_key(tool_key)
+ if not mcp_tool_info:
+ logger.error(f"[tool] mcp tool {tool_key} not exist, tool list: {mcp_tool_config.mcp_tool_list}")
+ raise Exception(f"tool {tool_key} not exist")
+ mcp_info = mcp_config.get_mcp_by_name(mcp_tool_info.server_name)
+ if not mcp_info:
+ logger.error(f"[tool] mcp server {mcp_tool_info.server_name} not exist, tool list: {mcp_tool_config.mcp_tool_list}")
+ raise Exception(f"mcp server {mcp_tool_info.server_name} not found")
+ mcp_client = McpClient(mcp_info=mcp_info)
+ return mcp_client
+
+ async def _wait_refresh(self) -> None:
+ if self.last_refresh_time == 0:
+ token = set_context(None)
+ asyncio.create_task(self._refresh_tool_list())
+ reset_context(token)
+ logger.info("[tool] tool list is being refreshed...")
+ while self.last_refresh_time == 0:
+ logger.info("[tool] Waiting for refreshing to complete")
+ await asyncio.sleep(1)
+ return
+
+ async def _fetch_tool_list(self) -> None:
+ """
+ 获取 tool 列表
+ """
+ if not mcp_config.mcp_list:
+ return
+ for mcp_info in mcp_config.mcp_list:
+ try:
+ mcp_client = McpClient(mcp_info=mcp_info)
+ tools = await mcp_client.list_tools()
+ for tool in tools:
+ description = f"{mcp_info.description}\n{tool.get('description')}" if mcp_info.description else tool.get('description')
+ await mcp_tool_config.add_mcp_tool(server_name=tool.get('server_name'), tool_name=tool.get('tool_name'),
+ description=description, input_schema=tool.get('input_schema'))
+ except ExceptionGroup as eg:
+ logger.error(f"[tool] fetch tool exception, mcp: {mcp_info}")
+ for task in eg.exceptions:
+ logger.error(f"[tool] task raised an exception: {task}")
+ except Exception as e:
+ logger.error(f"[tool] fetch tool exception, mcp: {mcp_info}, ex: {e}")
+
+ async def _refresh_tool_list(self) -> None:
+ """
+ 刷新 tool 列表
+ """
+ while True:
+ if global_settings.IS_EXIT:
+ break
+ elif datetime.now().timestamp() - self.last_refresh_time > settings.REFRESH_INTERVAL:
+ await self._fetch_tool_list()
+ logger.info(f"[tool] refresh tool list over")
+ self.last_refresh_time = datetime.now().timestamp()
+ else:
+ try:
+ await asyncio.sleep(1)
+ except BaseException:
+ pass
+
+ def _convert(self, local_tool:BaseLocalTool) -> McpToolInfo:
+ return McpToolInfo(tool_key=local_tool.tool_name, server_name="local-tool", tool_name=local_tool.tool_name,
+ description=local_tool.description, input_schema=json.dumps(local_tool.input_schema, ensure_ascii=False))
+tool_manager = ToolManager()
diff --git a/component/mydba/mydba/common/__init__.py b/component/mydba/mydba/common/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/common/encryption.py b/component/mydba/mydba/common/encryption.py
new file mode 100644
index 0000000..98ca0e5
--- /dev/null
+++ b/component/mydba/mydba/common/encryption.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+import os
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives import padding
+from cryptography.hazmat.backends import default_backend
+
+def encrypt(key: str, plain_data: str) -> str:
+ """
+ AES-CBC 加密
+ Args:
+ key (str): 16/24/32 字节的密钥(对应 AES-128/AES-192/AES-256)
+ plain_data (str): 明文文本
+ Returns:
+ str: iv + 密文
+ """
+ if not plain_data:
+ return plain_data
+ key = key.encode('utf-8')
+ plain_data = plain_data.encode('utf-8')
+ # 生成随机 16 字节 IV
+ iv = os.urandom(16)
+ # 添加 PKCS7 填充
+ padder = padding.PKCS7(128).padder()
+ padded_data = padder.update(plain_data) + padder.finalize()
+ # 创建加密器
+ cipher = Cipher(
+ algorithms.AES(key),
+ modes.CBC(iv),
+ backend=default_backend()
+ )
+ encryptor = cipher.encryptor()
+ # 加密并返回 iv + 密文
+ ciphertext = encryptor.update(padded_data) + encryptor.finalize()
+ return (iv + ciphertext).hex()
+
+def decrypt(key: str, cipher_data: str) -> str:
+ """
+ AES-CBC 解密
+ Args:
+ key (str): 与加密时相同的密钥
+ cipher_data (str): iv + 密文
+ Returns:
+ str: 解密后的明文
+ """
+ if not cipher_data:
+ return cipher_data
+ key = key.encode('utf-8')
+ cipher_data = bytes.fromhex(cipher_data)
+ # 提取 IV 和密文
+ iv = cipher_data[:16]
+ cipher_data = cipher_data[16:]
+ # 创建解密器
+ cipher = Cipher(
+ algorithms.AES(key),
+ modes.CBC(iv),
+ backend=default_backend()
+ )
+ decryptor = cipher.decryptor()
+ # 解密并去除填充
+ padded_plaintext = decryptor.update(cipher_data) + decryptor.finalize()
+ unpadder = padding.PKCS7(128).unpadder()
+ plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
+ return plaintext.decode('utf-8')
diff --git a/component/mydba/mydba/common/global_settings.py b/component/mydba/mydba/common/global_settings.py
new file mode 100644
index 0000000..0003b63
--- /dev/null
+++ b/component/mydba/mydba/common/global_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+
+class GlobalSettings:
+ """
+ 用于控制服务的全局开关。
+ Attributes:
+ IS_EXIT (bool): 服务是否退出
+ """
+ IS_EXIT: bool = False
+global_settings = GlobalSettings()
diff --git a/component/mydba/mydba/common/logger.py b/component/mydba/mydba/common/logger.py
new file mode 100644
index 0000000..62eab20
--- /dev/null
+++ b/component/mydba/mydba/common/logger.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+import os
+from datetime import datetime
+from loguru import logger as _logger
+from mydba.common.session import get_context
+from mydba.common.settings import settings
+
+def _custom_rotation(message, file):
+ file = file.name
+ file_size = os.path.getsize(file)
+ current_date = datetime.now().strftime("%Y-%m-%d")
+ # 如果文件超过 200 MB 或者日期发生变化,则轮转
+ if file_size > 200 * 1024 * 1024 or not file.endswith(f"{current_date}.log"):
+ return True
+ return False
+
+def _formatter(record):
+ context = get_context()
+ if context:
+ record["extra"]["request_id"] = context.request_id
+ record["extra"]["user_name"] = context.user_name
+ else:
+ record["extra"]["request_id"] = ""
+ record["extra"]["user_name"] = ""
+ format_str = "{time:YYYY-MM-DD HH:mm:ss}|{level}"
+ if settings.DEBUG:
+ format_str += "|{file}:{line}"
+ format_str += "|{extra[user_name]}|{extra[request_id]}|"
+ if record["message"] and isinstance(record["message"], str) and not record["message"].startswith("["):
+ record["extra"]["file"] = record["file"].name.split('.')[0]
+ format_str += "[{extra[file]}] "
+ format_str += "{message}\n"
+ return format_str
+
+def init_logger(console_level: str, file_level: str, log_dir: str, log_name: str):
+ """
+ 获取日志记录器
+ Args:
+ console_level (str): 控制台日志等级。
+ file_level (str): 文件日志等级。
+ log_dir (str): 日志文件存放地址。
+ log_name (str): 日志文件名。
+ Returns:
+ logger: 日志记录器。
+ """
+ _logger.remove()
+ _logger.add(
+ log_dir + os.sep + log_name + "_{time:YYYY-MM-DD}.log",
+ level=file_level,
+ rotation=_custom_rotation,
+ retention="7 days",
+ enqueue=True,
+ format=_formatter,
+ )
+ return _logger
+logger = _logger
diff --git a/component/mydba/mydba/common/session.py b/component/mydba/mydba/common/session.py
new file mode 100644
index 0000000..89f6dbc
--- /dev/null
+++ b/component/mydba/mydba/common/session.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+import contextvars
+from pydantic import BaseModel, Field
+from datetime import datetime
+
+class RequestContext(BaseModel):
+ request_id: str = Field(..., description="请求 ID")
+ user_name: str = Field(..., description="用户名")
+ session: str = Field('default', description="会话 ID")
+ detail_info: bool = Field(True, description="信息展示开关,默认展示详细信息")
+ time: datetime = Field(default_factory=datetime.now, description="请求时间")
+_request_context = contextvars.ContextVar('request_context', default=None)
+
+def get_context() -> RequestContext:
+ """
+ 获取当前请求上下文
+ Returns:
+ RequestContext: 当前请求上下文
+ """
+ return _request_context.get()
+
+def set_context(context: RequestContext):
+ """
+ 设置当前请求上下文
+ Args:
+ context (RequestContext): 要设置的请求上下文
+ """
+ return _request_context.set(context)
+
+def reset_context(token: contextvars.Token):
+ """
+ 重置当前请求上下文
+ Args:
+ token (contextvars.Token): 上下文 token,用于恢复上下文
+ """
+ _request_context.reset(token)
\ No newline at end of file
diff --git a/component/mydba/mydba/common/settings.py b/component/mydba/mydba/common/settings.py
new file mode 100644
index 0000000..38d7a7f
--- /dev/null
+++ b/component/mydba/mydba/common/settings.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+import os
+
+class Settings:
+ """
+ 记录 MyDBA 启动时依赖的环境变量。
+ Attributes:
+ DEBUG (bool): 是否开启调试模式。
+ LOG_DIR (str): 日志文件存放地址。
+ LOG_NAME (str): 日志文件名。
+ LOG_FILE_LEVEL (str): 文件日志等级。
+ LOG_CONSOLE_LEVEL (str): 控制台日志等级。
+ """
+ DEBUG = os.getenv("MYDBA_DEBUG", "False") == "True"
+ LOG_DIR = os.getenv("MYDBA_LOG_DIR", "/usr/local/mydba/logs")
+ LOG_NAME = os.getenv("MYDBA_LOG_NAME", "mydba")
+ LOG_FILE_LEVEL = os.getenv("MYDBA_LOG_FILE_LEVEL", "INFO")
+ LOG_CONSOLE_LEVEL = os.getenv("MYDBA_LOG_CONSOLE_LEVEL", "CRITICAL")
+settings = Settings()
\ No newline at end of file
diff --git a/component/mydba/mydba/common/stream.py b/component/mydba/mydba/common/stream.py
new file mode 100644
index 0000000..ec017a8
--- /dev/null
+++ b/component/mydba/mydba/common/stream.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+from prompt_toolkit import PromptSession
+import asyncio
+
+async def ainput(prompt=""):
+ session = PromptSession()
+ input = await asyncio.to_thread(session.prompt, prompt)
+ return input.rstrip("\n")
+
+async def aprint(*values, sep=" ", end="\n"):
+ print(*values, sep=sep, end=end, flush=True)
diff --git a/component/mydba/mydba/mcp/rag/README.md b/component/mydba/mydba/mcp/rag/README.md
new file mode 100644
index 0000000..eebd2bb
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/README.md
@@ -0,0 +1,15 @@
+# RAG MCP Server
+
+RAG MCP server for mydba
+
+## Using Cline
+
+```shell
+# set env
+export MYDBA_RAG_API_KEY=sk-xxx; # model api key, don't set it if you want to use local model
+export MYDBA_RAG_API_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1; # model api base url
+export MYDBA_RAG_EMBEDDING_MODEL=text-embedding-v2; # model name, it will download the specific model file if the api key is not set(default: maidalun/bce-embedding-base_v1), download url: https://modelscope.cn/models
+
+# run mcp server
+uv run rag_server.py
+```
diff --git a/component/mydba/mydba/mcp/rag/embeddings.py b/component/mydba/mydba/mcp/rag/embeddings.py
new file mode 100644
index 0000000..e558488
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/embeddings.py
@@ -0,0 +1,66 @@
+# -*- coding: utf-8 -*-
+from langchain_community.embeddings import OpenAIEmbeddings
+from typing import List, Optional
+
+'''
+CompatibleEmbeddings 兼容 OpenAI Embedding API
+'''
+class CompatibleEmbeddings(OpenAIEmbeddings):
+
+ def _tokenize(self, texts: List[str], chunk_size: int) -> tuple:
+ """
+ 禁用 Tokenization,直接返回原始文本和索引
+ """
+ indices = list(range(len(texts)))
+ return (range(0, len(texts), chunk_size), texts, indices)
+
+ def _get_len_safe_embeddings(
+ self, texts: List[str], *, engine: str, chunk_size: Optional[int] = None
+ ) -> List[List[float]]:
+ """
+ 直接传递原始文本,跳过 Token 化步骤
+ """
+ _chunk_size = chunk_size or self.chunk_size
+ batched_embeddings: List[List[float]] = []
+
+ # 直接遍历原始文本分块
+ for i in range(0, len(texts), _chunk_size):
+ chunk = texts[i: i + _chunk_size]
+
+ # 关键修改:input 直接使用文本列表
+ response = self.client.create(
+ input=chunk, # 直接使用原始文本列表
+ model=self.model, # 显式传递模型参数
+ **{k: v for k, v in self._invocation_params.items() if k != "model"}
+ )
+
+ if not isinstance(response, dict):
+ response = response.model_dump()
+ batched_embeddings.extend(r["embedding"] for r in response["data"])
+
+ # 跳过空文本处理(Ollama 不需要)
+ return batched_embeddings
+
+ async def _aget_len_safe_embeddings(
+ self, texts: List[str], *, engine: str, chunk_size: Optional[int] = None
+ ) -> List[List[float]]:
+ """
+ 异步版本处理逻辑
+ """
+ _chunk_size = chunk_size or self.chunk_size
+ batched_embeddings: List[List[float]] = []
+
+ for i in range(0, len(texts), _chunk_size):
+ chunk = texts[i: i + _chunk_size]
+
+ response = await self.async_client.create(
+ input=chunk,
+ model=self.model,
+ **{k: v for k, v in self._invocation_params.items() if k != "model"}
+ )
+
+ if not isinstance(response, dict):
+ response = response.model_dump()
+ batched_embeddings.extend(r["embedding"] for r in response["data"]) # 注意: 实际应为 "embedding"
+
+ return batched_embeddings
diff --git a/component/mydba/mydba/mcp/rag/mysql_utils.py b/component/mydba/mydba/mcp/rag/mysql_utils.py
new file mode 100644
index 0000000..deb80ba
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/mysql_utils.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+import base64
+import decimal
+import pymysql, time
+from datetime import datetime
+from pydantic import BaseModel, Field
+from typing import List, Dict, Any, Optional
+
+BASE_DATABASES = ["information_schema", "information_schema", "mysql", "performance_schema", "sys"]
+
+class MysqlUtils(BaseModel):
+ """连接 mysql 数据库并执行 sql 语句"""
+ host: str = Field("127.0.0.1", description="mysql 主机地址")
+ username: str = Field(..., description="mysql 用户名")
+ password: str = Field(..., description="mysql 密码")
+ port: str = Field("3306", description="mysql 端口号")
+
+ def save_sql_file(self, filename: str, database: Optional[str] = None, overwrite: bool=False) -> None:
+ """
+ 保存数据库的建表语句到本地文件
+ Args:
+ filename (str): 文件名。
+ """
+ res = self.get_tables(database)
+ if overwrite:
+ mode = 'w'
+ else:
+ mode = 'a'
+ with open(filename, mode=mode) as f:
+ for item in res:
+ f.write(f"## Database: {item['db']}\n")
+ f.write(f"{item['create_table_sql']};\n")
+
+ def get_tables(self, database: Optional[str] = None) -> List[Dict[str, str]]:
+ """
+ 获取数据库的建表语句
+ Args:
+ accountName (str): 用户名。
+ accountPasword (str): 密码。
+ port (str): 端口号。
+ Returns:
+ list: 数据库的建表语句。
+ """
+ res = []
+ databases = self._show_databases(database)
+ for db in databases:
+ db_name = db['db']
+ show_tables_sql = "select table_name from information_schema.tables where table_schema = '%s'" % db_name
+ table_names = self._exec_sql(show_tables_sql)
+ for table in table_names:
+ table_name = table['table_name']
+ show_create_table = "show create table %s.%s" % (db_name, table_name)
+ create_table_sql = self._exec_sql(show_create_table)
+ res.append({
+ "db": db_name,
+ "create_table_sql": create_table_sql[0]['create table']
+ })
+
+ return res
+
+ def _get_conn(self) -> pymysql.connections.Connection:
+ max_retries_count = 3 # 设置最大重试次数
+ conn_retries_count = 0 # 初始重试次数
+ while conn_retries_count <= max_retries_count:
+ try:
+ conn = pymysql.connect(
+ host=self.host,
+ port=int(self.port),
+ user=self.username,
+ password=self.password,
+ charset="utf8mb4",
+ cursorclass=pymysql.cursors.DictCursor,
+ connect_timeout=3,
+ read_timeout=5
+ )
+ return conn
+ except Exception:
+ conn_retries_count += 1
+ time.sleep(3)
+ raise Exception("Can't connect to MySQL server")
+
+ def _show_databases(self, database: Optional[str] = None) -> List[Dict[str, str]]:
+ conn = self._get_conn()
+ where_clause = "'%s'" % ("','".join(BASE_DATABASES), )
+ try:
+ with conn.cursor() as cursor:
+ if database is not None:
+ sql = ("SELECT SCHEMA_NAME,DEFAULT_CHARACTER_SET_NAME FROM information_schema.SCHEMATA "
+ "WHERE SCHEMA_NAME NOT IN (%s) AND SCHEMA_NAME='%s' ORDER BY SCHEMA_NAME") % (
+ where_clause, database)
+ else:
+ sql = ("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA "
+ "WHERE SCHEMA_NAME NOT IN (%s) ORDER BY SCHEMA_NAME") % where_clause
+ cursor.execute(sql)
+ rows = cursor.fetchall()
+ databases = []
+ for row in rows:
+ row_iter = iter(row.values())
+ databases.append({'db': next(row_iter)})
+ finally:
+ conn.close()
+ return databases
+
+ def _exec_sql(self, sql: str) -> List[Dict[str, Any]]:
+ conn = self._get_conn()
+ try:
+ resp_datas = []
+ with conn.cursor() as cursor:
+ cursor.execute(sql)
+ res = cursor.fetchall()
+ if not res:
+ conn.commit()
+ return None
+ for item in res:
+ resp_data = {}
+ for key in item.keys():
+ value = item[key]
+ if isinstance(value, (datetime, decimal.Decimal)):
+ value = str(value)
+ if isinstance(value, set):
+ value = list(value)
+ if isinstance(value, bytes):
+ value = base64.b64encode(value).decode('utf-8')
+ resp_data[key.lower()] = value
+ resp_datas.append(resp_data)
+ return resp_datas
+ finally:
+ conn.close()
diff --git a/component/mydba/mydba/mcp/rag/rag_init.py b/component/mydba/mydba/mcp/rag/rag_init.py
new file mode 100644
index 0000000..f968f80
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/rag_init.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+import aiosqlite
+import argparse
+import asyncio
+import importlib.util
+import os
+import sys
+import subprocess
+from typing import Any, List, Dict, Optional
+from mysql_utils import MysqlUtils
+from settings import settings
+from vector_store import VectorStore
+
+def import_file_as_module(file_path, module_name):
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
+ module = importlib.util.module_from_spec(spec)
+ sys.modules[module_name] = module
+ spec.loader.exec_module(module)
+ return module
+
+# 动态导入加密模块
+parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../common'))
+encryption = import_file_as_module(parent_dir + "/encryption.py", 'encryption')
+
+def kill_process(file_name: str) -> None:
+ """
+ 杀死指定文件名的进程
+ """
+ # 执行 ps 命令并过滤出包含对应文件名的行(排除 grep 自身)
+ cmd = "ps aux | grep '" + file_name + "' | grep -v 'grep'"
+ result = subprocess.run(cmd, shell=True, text=True, capture_output=True)
+ lines = result.stdout.strip().splitlines()
+
+ if len(lines) > 0:
+ for line in lines:
+ pid = line.split()[1]
+ # 使用 kill -9 杀死进程
+ try:
+ print(f"Killing process with PID: {pid}")
+ subprocess.run(f"kill -9 {pid}", shell=True, check=True)
+ except subprocess.CalledProcessError as e:
+ print(f"Failed to kill process {pid}: {e}")
+
+def run_cmd(cmd):
+ try:
+ result = subprocess.run(cmd, shell=True, cwd=os.path.dirname(os.path.abspath(__file__)))
+ except subprocess.CalledProcessError as e:
+ print(f"Error running command: {e}")
+ return None
+
+async def prepare_rag_config(db: str, key: str, vectorstore: VectorStore) -> None:
+ filename = os.path.join(settings.RAG_DATA_DIR, 'create_table_sql.md')
+ if os.path.exists(filename):
+ os.remove(filename)
+ sql = 'SELECT * FROM db_instance'
+ rows = await _query(uri=db[9:], sql=sql)
+ if rows:
+ for row in rows:
+ if row['type'] == 'mysql':
+ password = encryption.decrypt(key, row['password'])
+ username=row['user']
+ host=row['host']
+ port=row['port']
+ database=row['database']
+ mysql_utils = MysqlUtils(host=host, port=str(port), username=username, password=password)
+ mysql_utils.save_sql_file(filename=filename, database=database, overwrite=False)
+ vectorstore.create_vectorstore_by_file(file=filename,
+ vectorstore_name='table_struct')
+
+async def _query(uri: str, sql: str, parameters: Optional[List[Any]]=None) -> Optional[List[Dict[str, Any]]]:
+ async with aiosqlite.connect(uri) as db:
+ db.row_factory = aiosqlite.Row
+ async with db.execute(sql, parameters) as cursor:
+ results = []
+ async for row in cursor:
+ results.append(dict(row))
+ return results
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="MyDBA RAG CLI")
+
+ # Define subparsers for different commands
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
+
+ # Subparser for create_table_struct_vectorstore
+ create_table_parser = subparsers.add_parser('get-table-struct', help='Create table structure vectorstore')
+ create_table_parser.add_argument('--host', required=False, default="127.0.0.1", help='MySQL host address')
+ create_table_parser.add_argument('--port', required=False, default="3306", help='MySQL port number')
+ create_table_parser.add_argument('--username', required=True, help='MySQL username')
+ create_table_parser.add_argument('--password', required=True, help='MySQL password')
+ create_table_parser.add_argument('--database', required=False, default=None, help='MySQL database name')
+ create_table_parser.add_argument('--overwrite', action='store_true', help='overwrite the sql file')
+ create_table_parser.add_argument('--restart_rag', action='store_true', help='restart the rag tool')
+ create_table_parser.add_argument('--config_file', required=False, default="/usr/local/mydba/config_app.ini", help='Path to the configuration file')
+
+ # Subparser for create_vectorstore
+ create_vector_parser = subparsers.add_parser('create-vectorstore', help='Create a vectorstore from a file')
+ create_vector_parser.add_argument('--file', required=True, help='Path to the input file')
+ create_vector_parser.add_argument('--vectorstore_name', required=True, help='Name of the vectorstore directory')
+ create_vector_parser.add_argument('--restart_rag', action='store_true', help='restart the rag tool')
+ create_vector_parser.add_argument('--config_file', required=False, default="/usr/local/mydba/config_app.ini", help='Path to the configuration file')
+
+ init_config_parser = subparsers.add_parser('init-config', help='Initialize the configuration')
+ init_config_parser.add_argument('--config_file', required=False, default="/usr/local/mydba/config_app.ini", help='Path to the configuration file')
+ init_config_parser.add_argument('--restart_rag', action='store_true', help='restart the rag tool')
+
+ args = parser.parse_args()
+ settings.load_config(args.config_file)
+ vector_store = VectorStore(model_name=settings.RAG_EMBEDDING_MODEL, api_key=settings.RAG_API_KEY,
+ base_url=settings.RAG_API_BASE_URL, dir_path=settings.RAG_DATA_DIR)
+
+ if args.command == 'get-table-struct':
+ filename = os.path.join(settings.RAG_DATA_DIR, 'create_table_sql.md')
+ mysql_utils = MysqlUtils(host=args.host, port=args.port, username=args.username, password=args.password)
+ mysql_utils.save_sql_file(filename=filename, database=args.database, overwrite=args.overwrite)
+ vector_store.create_vectorstore_by_file(
+ file=filename,
+ vectorstore_name='table_struct'
+ )
+ if args.restart_rag:
+ kill_process('rag_server.py')
+ run_cmd('nohup uv run rag_server.py >> /usr/local/mydba/logs/rag.log 2>&1 &')
+
+ elif args.command == 'create-vectorstore':
+ vector_store.create_vectorstore_by_file(
+ file=args.file,
+ vectorstore_name=args.vectorstore_name
+ )
+ if args.restart_rag:
+ kill_process('rag_server.py')
+ run_cmd('nohup uv run rag_server.py >> /usr/local/mydba/logs/rag.log 2>&1 &')
+ elif args.command == 'init-config':
+ asyncio.run(prepare_rag_config(db=settings.CONFIG_DATABASE, key=settings.SECURITY_KEY, vectorstore=vector_store))
+ if args.restart_rag:
+ kill_process('rag_server.py')
+ run_cmd('nohup uv run rag_server.py >> /usr/local/mydba/logs/rag.log 2>&1 &')
+ else:
+ parser.print_help()
\ No newline at end of file
diff --git a/component/mydba/mydba/mcp/rag/rag_server.py b/component/mydba/mydba/mcp/rag/rag_server.py
new file mode 100644
index 0000000..de42f72
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/rag_server.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+from mcp.server.fastmcp import FastMCP
+from mcp.server.session import ServerSession
+from settings import settings
+from vector_store import VectorStore
+
+####################################################################################
+# Temporary monkeypatch which avoids crashing when a POST message is received
+# before a connection has been initialized, e.g: after a deployment.
+# pylint: disable-next=protected-access
+old__received_request = ServerSession._received_request
+
+
+async def _received_request(self, *args, **kwargs):
+ try:
+ return await old__received_request(self, *args, **kwargs)
+ except RuntimeError:
+ pass
+
+
+# pylint: disable-next=protected-access
+ServerSession._received_request = _received_request
+####################################################################################
+
+settings.load_config(settings.CONFIG_FILE)
+vectorestores = VectorStore(
+ model_name=settings.RAG_EMBEDDING_MODEL,
+ api_key=settings.RAG_API_KEY,
+ base_url=settings.RAG_API_BASE_URL,
+ dir_path=settings.RAG_DATA_DIR
+)
+
+mcp = FastMCP(
+ name="MyBase Table Struct Tool",
+ host="0.0.0.0",
+ port=8006,
+ description="根据用户的查询返回对应的表结构信息。",
+ sse_path='/sse'
+)
+
+@mcp.tool()
+async def get_table_struct(query: str, topk: int=10) -> str:
+ """
+ 根据用户的输入获取与之关联的 k 个数据库表结构信息。
+ Args:
+ query (str): 用户的查询。
+ topk (int): 返回的记录数。
+ Return:
+ res: 返回的表结构内容。
+ """
+ res = ""
+ vectorestore = vectorestores.load_vectorstore_by_name("table_struct")
+ if vectorestore is not None:
+ docs = vectorestore.similarity_search(query, k=topk)
+ res = "\n".join(doc.page_content for doc in docs)
+ return res
+
+if __name__ == "__main__":
+ # 初始化并运行服务器
+ try:
+ print("Starting server...")
+ mcp.run(transport='sse')
+ except Exception as e:
+ print(f"Error: {e}")
\ No newline at end of file
diff --git a/component/mydba/mydba/mcp/rag/settings.py b/component/mydba/mydba/mcp/rag/settings.py
new file mode 100644
index 0000000..e21e665
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/settings.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+import configparser
+import os
+
+class Settings:
+ """
+ 记录 RAG MCP 启动时依赖的环境变量。
+ Attributes:
+ CONFIG_FILE (str): 配置文件路径。
+ CONFIG_DATABASE (str): 配置数据库 URI。
+ SECURITY_KEY (str): 数据加密 key。
+ RAG_API_KEY (str): 向量模型的 api key。
+ RAG_API_BASE_URL (str): 向量模型的 base url。
+ RAG_EMBEDDING_MODEL (str): 向量模型名称。
+ RAG_DATA_DIR (str): 向量库数据存储目录。
+ """
+ CONFIG_FILE = os.getenv("MYDBA_CONFIG_FILE", "/usr/local/mydba/config_app.ini")
+ CONFIG_DATABASE = os.getenv("MYDBA_CONFIG_DATABASE", "sqlite:///usr/local/mydba/sqlite_app.db")
+ SECURITY_KEY = os.getenv("MYDBA_SECURITY_KEY", "")
+ RAG_API_KEY = os.getenv("MYDBA_RAG_API_KEY", "")
+ RAG_API_BASE_URL = os.getenv("MYDBA_RAG_API_BASE_URL", "")
+ RAG_EMBEDDING_MODEL = os.getenv("MYDBA_RAG_EMBEDDING_MODEL", "")
+ RAG_DATA_DIR = os.getenv("MYDBA_RAG_DATA_DIR", "/usr/local/mydba/vector_store")
+
+ def load_config(self, config_file: str) -> None:
+ config = configparser.ConfigParser()
+ config.read(config_file)
+ config_value = config.get('common', 'config_database')
+ if config_value:
+ self.CONFIG_DATABASE = config_value
+ config_value = config.get('app', 'security_key')
+ if config_value:
+ self.SECURITY_KEY = config_value
+ config_value = config.get('rag', 'api_key')
+ if config_value:
+ self.RAG_API_KEY = config_value
+ config_value = config.get('rag', 'base_url')
+ if config_value:
+ self.RAG_API_BASE_URL = config_value
+ config_value = config.get('rag', 'embedding')
+ if config_value:
+ self.RAG_EMBEDDING_MODEL = config_value
+ config_value = config.get('rag', 'data_dir')
+ if config_value:
+ self.RAG_DATA_DIR = config_value
+settings = Settings()
\ No newline at end of file
diff --git a/component/mydba/mydba/mcp/rag/vector_store.py b/component/mydba/mydba/mcp/rag/vector_store.py
new file mode 100644
index 0000000..a58809e
--- /dev/null
+++ b/component/mydba/mydba/mcp/rag/vector_store.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+import os
+from langchain_community.vectorstores import FAISS
+from langchain_core.documents import Document
+from pydantic import BaseModel, Field
+from typing import Any, List, Dict
+
+current_dir = os.path.dirname(os.path.abspath(__file__))
+
+class VectorStore(BaseModel):
+ embedding: Any = Field(None, description="文本向量化模型")
+ dir_path: str = Field("", description="向量库保存路径")
+ vectorstores: Dict[str, Any] = Field({}, description="向量库集合")
+
+ def __init__(self, model_name: str, api_key: str, base_url: str, dir_path: str):
+ """
+ embedding 模型初始化,若传入了 openai 的 api_key,则使用 openai 的 Embeddings 模型
+ 若传入了其他模型的 api_key,则使用兼容的 Embeddings 模型
+ 否则使用本地 Embeddings 模型
+ Args:
+ model_name (str): embedding 模型名称。
+ api_key (str): API key。
+ base_url (str): API base URL。
+ dir_path (str): 向量库保存路径。
+ """
+ super().__init__()
+ if "openai" in base_url and api_key:
+ from langchain.embeddings import OpenAIEmbeddings
+ if not model_name:
+ model_name = "text-embedding-ada-002"
+ embedding = OpenAIEmbeddings(model=model_name, openai_api_key=api_key)
+ elif api_key and base_url:
+ from embeddings import CompatibleEmbeddings
+ if not model_name:
+ model_name = "text-embedding-v2"
+ embedding = CompatibleEmbeddings(model=model_name, openai_api_key=api_key, base_url=base_url, chunk_size=10)
+ else:
+ # 使用本地 Embeddings 模型,从 https://www.modelscope.cn/ 上下载
+ from langchain_huggingface import HuggingFaceEmbeddings
+ from modelscope.hub.snapshot_download import snapshot_download
+ if not model_name:
+ model_name = 'maidalun/bce-embedding-base_v1'
+ model_dir = os.path.join(current_dir, model_name)
+ snapshot_download(model_name, local_dir=model_dir)
+ embedding = HuggingFaceEmbeddings(model_name=model_dir)
+ self.embedding = embedding
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+ self.dir_path = dir_path
+ self.vectorstores = {}
+
+ def load_vectorstore_by_name(self, vectorstore_name: str) -> FAISS:
+ """
+ 根据 vectorstore_name 返回对应的向量库对象
+ """
+ if vectorstore_name in self.vectorstores:
+ return self.vectorstores[vectorstore_name]
+ vectorstore_dir = os.path.join(self.dir_path, vectorstore_name)
+ if os.path.isdir(vectorstore_dir) and os.path.exists(os.path.join(vectorstore_dir, 'index.faiss')):
+ vectorstore = FAISS.load_local(vectorstore_dir, embeddings=self.embedding, allow_dangerous_deserialization=True)
+ self.vectorstores[vectorstore_name] = vectorstore
+ return vectorstore
+ return None
+
+ def create_vectorstore_by_file(self, file: str, vectorstore_name: str) -> None:
+ """
+ 根据传入的文件名,创建向量库
+ Args:
+ file (str): 文件名。
+ vectorstore_name (str): 向量库保存路径。
+ Returns:
+ vectorstore (FAISS): 向量库。
+ """
+ data = self._load_document(file)
+ chunks = self._chunk_data(data)
+ vectorstore = FAISS.from_documents(chunks, embedding=self.embedding)
+ vectorstore_dir = os.path.join(self.dir_path, vectorstore_name)
+ vectorstore.save_local(vectorstore_dir)
+
+ def _load_document(self, file: str) -> List[Document]:
+ """
+ 读取文件
+ Args:
+ file (str): 文件名。
+ Returns:
+ data (List[Document]): 文档数据。
+ """
+ name, extension = os.path.splitext(file)
+
+ if extension == '.pdf':
+ from langchain_community.document_loaders import PyPDFLoader
+ print(f'Loading {file}')
+ loader = PyPDFLoader(file)
+ elif extension == '.docx':
+ from langchain_community.document_loaders import Docx2txtLoader
+ print(f'Loading {file}')
+ loader = Docx2txtLoader(file)
+ elif extension == '.txt' or extension == '.md':
+ from langchain_community.document_loaders import TextLoader
+ loader = TextLoader(file)
+ else:
+ print('Document format is not supported!')
+ return None
+
+ data = loader.load()
+ return data
+
+ def _chunk_data(self, data: List[Document], chunk_size : int=20, chunk_overlap: int=0) -> List[Document]:
+ """
+ 对数据进行切分,以 ; 进行分割
+ Args:
+ data (List[Document]): 文档数据。
+ chunk_size (int): 切分大小,设置成一个较小的数,可以将完整语句切分出来。
+ chunk_overlap (int): 切分重叠。
+ Returns
+ chunks (List[Document]): 切分后的数据。
+ """
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
+ text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap, separators=[";\n"], keep_separator=False)
+ chunks = text_splitter.split_documents(data)
+ return chunks
diff --git a/component/mydba/mydba/provider/__init__.py b/component/mydba/mydba/provider/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/component/mydba/mydba/provider/base.py b/component/mydba/mydba/provider/base.py
new file mode 100644
index 0000000..a337945
--- /dev/null
+++ b/component/mydba/mydba/provider/base.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from abc import ABC, abstractmethod
+from pydantic import BaseModel, Field
+
+class BaseProvider(ABC, BaseModel):
+ name: str = Field(..., description="名称")
+ description: str = Field(..., description="描述")
+
+ @abstractmethod
+ async def run(self) -> None:
+ return NotImplementedError("Subclasses must implement this method")
+
+ @abstractmethod
+ def get_user_info(self) -> str:
+ return NotImplementedError("Subclasses must implement this method")
+
+ @abstractmethod
+ def get_session(self) -> str:
+ return NotImplementedError("Subclasses must implement this method")
+
+ @abstractmethod
+ def get_request_info(self) -> str:
+ return NotImplementedError("Subclasses must implement this method")
+
\ No newline at end of file
diff --git a/component/mydba/mydba/provider/command_line.py b/component/mydba/mydba/provider/command_line.py
new file mode 100644
index 0000000..a352c7c
--- /dev/null
+++ b/component/mydba/mydba/provider/command_line.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+import argparse
+import os
+import pwd
+import sys
+from pydantic import BaseModel, Field
+from tenacity import RetryError
+from typing import Optional, Tuple
+from contextlib import asynccontextmanager
+from mydba.app.agent.base import BaseAgent
+from mydba.app.config.agent import agent_config
+from mydba.app.config.settings import settings
+from mydba.app.llm import LLM
+from mydba.common import stream
+from mydba.common.logger import logger
+from mydba.common.session import get_context, set_context, reset_context, RequestContext
+from mydba.provider.base import BaseProvider
+
+class CommandLineProvider(BaseProvider, BaseModel):
+ name: str = Field("CommandLine", description="名称")
+ description: str = Field("通过命令行接入", description="描述")
+
+ async def run(self) -> None:
+ await self._welcome_message()
+ async with self._get_context() as context:
+ while True:
+ query = await self._get_query()
+ if query is None:
+ await stream.aprint("[A] 退出助手")
+ return
+ logger.info(f"[cmd] get user query: {query}")
+ agent = self._get_main_agent()
+ context_memory = await agent.get_history_memory()
+ has_error = True
+ try:
+ content = await agent.run(query=query, context_memory=context_memory)
+ has_error = False
+ except RetryError as re:
+ content = str(re.last_attempt.exception())
+ except Exception as e:
+ content = str(e)
+ if has_error:
+ await stream.aprint(f"[A] 查询异常: \n{content}")
+ elif not context.detail_info:
+ await self._send_response(content)
+
+ def get_user_info(self) -> str:
+ return pwd.getpwuid(os.getuid()).pw_name
+
+ def get_session(self) -> str:
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-s",
+ type=str,
+ default="default",
+ help="会话名称,默认: %(default)s"
+ )
+ args = parser.parse_args()
+ return args.s
+
+ def get_request_info(self) -> str:
+ try:
+ tty_name = os.ttyname(sys.stdin.fileno())
+ except OSError:
+ tty_name = 'unknown'
+ tty_name = tty_name.split("/")[-1]
+ sid = os.getsid(os.getpid())
+ return f'{tty_name}_{sid}'
+
+ def _get_main_agent(self) -> BaseAgent:
+ main_agent_info = agent_config.get_main_agent()
+ if main_agent_info is None:
+ logger.error("[cmd] main agent not found")
+ raise Exception("main agent not found")
+ llm = LLM(model=settings.LLM_MODEL, base_url=settings.API_BASE_URL,
+ api_key=settings.API_KEY, max_tokens=settings.MAX_TOKENS,
+ temperature=settings.TEMPERATURE)
+ return BaseAgent.create_agent(main_agent_info, llm)
+
+ async def _get_query(self) -> Optional[str]:
+ while True:
+ await stream.aprint("[A] 输入指令:")
+ try:
+ query = await stream.ainput(">> ")
+ except KeyboardInterrupt:
+ query = None
+ except BaseException:
+ await stream.aprint()
+ query = None
+ retry, query = await self._handle_query(query)
+ if not retry:
+ return query
+
+ async def _handle_query(self, query: Optional[str]) -> Tuple[bool, Optional[str]]:
+ """处理查询
+ Args:
+ query (str): 用户输入的查询内容
+ Returns:
+ bool: 是否继续输入
+ str: 处理后的查询内容
+ """
+ if not query.strip():
+ return True, None
+ if await self._handle_quit(query):
+ return False, None
+ if await self._handle_detail_info(query):
+ return True, None
+ if await self._handle_session(query):
+ return True, None
+ if await self._handle_help(query):
+ return True, None
+ return False, query.strip()
+
+ async def _handle_quit(self, query: str) -> bool:
+ query = query.strip().lower()
+ if query in ["/exit", "/quit", "/e", "/q"]:
+ return True
+ return False
+
+ async def _handle_detail_info(self, query: str) -> bool:
+ context = get_context()
+ query = query.strip().lower()
+ if query in ["/i", "/info"]:
+ context.detail_info = not context.detail_info
+ if context.detail_info:
+ await stream.aprint(f"开启详细信息")
+ else:
+ await stream.aprint(f"关闭详细信息")
+ return True
+ return False
+
+ async def _handle_session(self, query: str) -> bool:
+ query = query.strip().lower()
+ if query.startswith('/s ') or query == '/s':
+ items = query.split(" ")
+ session = next((s for s in items[1:] if s), None)
+ context = get_context()
+ if session:
+ await stream.aprint(f"切换会话: {session}")
+ context.session = session
+ else:
+ usage = f"当前会话: {context.session}\n切换方法: /s [session_name]"
+ await stream.aprint(f"{usage}")
+ return True
+ return False
+
+ async def _handle_help(self, query: str) -> bool:
+ query = query.strip().lower()
+ if query in ["/help", "/h", "/?", "/?"]:
+ await self._welcome_message()
+ return True
+ if query.startswith('/'):
+ await stream.aprint("快捷命令有误")
+ await stream.aprint("输入 [/h] 或 [/?] 查看帮助")
+ await stream.aprint("输入 [/e] 或 [/q] 退出助手")
+ await stream.aprint("输入 [/i] 关闭或打开详细信息")
+ await stream.aprint("输入 [/s] 切换会话,默认 `default`")
+ return True
+ return False
+
+ async def _welcome_message(self) -> None:
+ functions = "、".join([agent.intent for agent in filter(lambda agent: not agent.is_main and not agent.is_default, agent_config.agent_list)])
+ await stream.aprint("欢迎使用阿里云数据库智能助手 MyDBA")
+ await stream.aprint(f"我能帮您:{functions}")
+ await stream.aprint("快捷命令:")
+ await stream.aprint("输入 [/h] 或 [/?] 查看帮助")
+ await stream.aprint("输入 [/e] 或 [/q] 退出助手")
+ await stream.aprint("输入 [/i] 关闭或打开详细信息")
+ await stream.aprint("输入 [/s session] 切换会话,默认 `default`")
+
+ async def _send_response(self, content: str) -> None:
+ await stream.aprint(f"[A] 查询结果: \n{content}")
+ pass
+
+ @asynccontextmanager
+ async def _get_context(self):
+ context = RequestContext(request_id=self.get_request_info(),
+ user_name=self.get_user_info(),
+ session=self.get_session())
+ token = set_context(context)
+ yield context
+ reset_context(token)
diff --git a/component/mydba/pyproject.toml b/component/mydba/pyproject.toml
new file mode 100644
index 0000000..8d56cf6
--- /dev/null
+++ b/component/mydba/pyproject.toml
@@ -0,0 +1,25 @@
+[project]
+name = "mydba"
+version = "0.1.0"
+description = "阿里云数据库 DBA 智能体"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "aiomysql>=0.2.0",
+ "aiosqlite>=0.21.0",
+ "asyncio>=3.4.3",
+ "cryptography>=45.0.2",
+ "exceptiongroup>=1.3.0",
+ "faiss-cpu>=1.11.0",
+ "langchain>=0.3.25",
+ "langchain-community>=0.3.24",
+ "langchain-openai>=0.3.17",
+ "loguru>=0.7.3",
+ "mcp[cli]>=1.9.0",
+ "modelscope>=1.26.0",
+ "openai>=1.79.0",
+ "prompt-toolkit>=3.0.51",
+ "pydantic>=2.11.4",
+ "pymysql>=1.1.1",
+ "tenacity>=9.1.2",
+]
diff --git a/component/mydba/shell/init_env.sh b/component/mydba/shell/init_env.sh
new file mode 100755
index 0000000..0cb6618
--- /dev/null
+++ b/component/mydba/shell/init_env.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+
+set -e
+
+if [ -f /etc/os-release ]; then
+ . /etc/os-release
+ OS=$ID
+else
+ echo "无法确定操作系统类型"
+ exit 1
+fi
+
+# check distro
+if [ -f /etc/os-release ]; then
+ . /etc/os-release
+ DISTRO_VERSION=$(echo "$VERSION_ID" | cut -d '.' -f 1)
+else
+ echo "Unable to determine the distribution. Please check /etc/os-release."
+ exit 1
+fi
+
+case $OS in
+ ubuntu|debian)
+ ;;
+ alinux)
+ ;;
+ centos|rhel)
+ echo "CentOS $DISTRO_VERSION"
+ if [[ "$DISTRO_VERSION" -lt 8 && "$DISTRO_VERSION" -ge 7 ]]; then
+ sed -i \
+ -e 's/faiss-cpu>=1.11.0/faiss-cpu==1.9.0.post1/' /usr/local/mydba/pyproject.toml
+ echo "修改pyproject.toml完成"
+ fi
+ ;;
+ *)
+ echo "不支持的操作系统: $OS"
+ exit 1
+ ;;
+esac
+
+HOME_DIR="/usr/local/mydba"
+
+# starting
+echo "执行初始化环境命令"
+
+# mkdir logs
+if ! [ -e "/usr/local/mydba/logs" ]; then
+ mkdir /usr/local/mydba/logs
+fi
+
+# install
+echo "安装uv"
+if ! command -v uv &> /dev/null; then
+ /usr/local/mydba/shell/uv-installer.sh
+ ln -s /root/.local/bin/uv /usr/bin/uv
+else
+ echo "uv 已安装"
+fi
+
+# install python3.12
+export UV_PYTHON_INSTALL_MIRROR="https://ghproxy.cn/https://github.com/indygreg/python-build-standalone/releases/download"
+uv python install 3.12
+
+# init agent env
+echo "设置镜像源"
+export UV_DEFAULT_INDEX="https://mirrors.aliyun.com/pypi/simple"
+
+echo "拉取Agent依赖"
+cd "$HOME_DIR"
+uv sync --inexact
+
+# init mcp env
+echo "拉取RDS MCP依赖"
+cd mydba/mcp/alibabacloud-rds-openapi-mcp-server
+uv sync --inexact
diff --git a/component/mydba/shell/mydba.sh b/component/mydba/shell/mydba.sh
new file mode 100755
index 0000000..27a2af8
--- /dev/null
+++ b/component/mydba/shell/mydba.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+
+set -e
+
+usage() {
+ echo "Usage: $0 [-h] [-s session]\n启动 mydba 智能体"
+ echo "Options:"
+ echo " -h 展示帮助信息"
+ echo " -s session 指定会话 ID,默认 default"
+ exit 1
+}
+
+session=""
+while getopts ":hs:" opt; do
+ case $opt in
+ h) usage ;;
+ s)
+ session="$OPTARG"
+ ;;
+ \?) echo "Invalid option: -$OPTARG" >&2; usage ;;
+ :) echo "Option -$OPTARG requires an argument." >&2; usage ;;
+ esac
+done
+
+HOME_DIR="/usr/local/mydba"
+LOG_DIR="$HOME_DIR/logs"
+AGENT_SCRIPT="main.py"
+RAG_DIR="$HOME_DIR/mydba/mcp/rag"
+RAG_SCRIPT="rag_server.py"
+RAG_LOG="$LOG_DIR/rag.log"
+RDS_DIR="$HOME_DIR/mydba/mcp/alibabacloud-rds-openapi-mcp-server/src/alibabacloud_rds_openapi_mcp_server"
+RDS_SCRIPT="server.py"
+
+if ! [ -e "${HOME_DIR}/main.py" ]; then
+ echo "请确认 mydba 是否正确安装!"
+ exit 1
+fi
+
+if ! [ -e "${RDS_DIR}/${RDS_SCRIPT}" ]; then
+ echo "RDS 服务未安装,阿里云RDS管理功能将无法正常使用!"
+ exit 1
+fi
+
+if ! [ -e "${RAG_DIR}/${RAG_SCRIPT}" ]; then
+ echo "RAG 服务未安装,问询数据功能将无法正常使用!"
+ exit 1
+fi
+
+if ! pgrep -f "${RAG_SCRIPT}" > /dev/null; then
+ cd "$RAG_DIR"
+ nohup uv run "${RAG_SCRIPT}" >> "$RAG_LOG" 2>&1 &
+fi
+
+cd "$HOME_DIR"
+if [ -z "$session" ]; then
+ uv run "${AGENT_SCRIPT}"
+else
+ echo "使用会话: $session"
+ uv run "${AGENT_SCRIPT}" -s "$session"
+fi
diff --git a/component/mydba/uv.lock b/component/mydba/uv.lock
new file mode 100644
index 0000000..f7aa3c1
--- /dev/null
+++ b/component/mydba/uv.lock
@@ -0,0 +1,1560 @@
+version = 1
+revision = 2
+requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.13'",
+ "python_full_version >= '3.12.4' and python_full_version < '3.13'",
+ "python_full_version < '3.12.4'",
+]
+
+[[package]]
+name = "aiohappyeyeballs"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
+]
+
+[[package]]
+name = "aiohttp"
+version = "3.11.18"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohappyeyeballs" },
+ { name = "aiosignal" },
+ { name = "attrs" },
+ { name = "frozenlist" },
+ { name = "multidict" },
+ { name = "propcache" },
+ { name = "yarl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" },
+ { url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" },
+ { url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" },
+ { url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" },
+ { url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" },
+ { url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" },
+ { url = "https://files.pythonhosted.org/packages/97/97/d1248cd6d02b9de6aa514793d0dcb20099f0ec47ae71a933290116c070c5/aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", size = 412442, upload-time = "2025-04-21T09:41:55.689Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9a/e34e65506e06427b111e19218a99abf627638a9703f4b8bcc3e3021277ed/aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", size = 439444, upload-time = "2025-04-21T09:41:57.977Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/18/be8b5dd6b9cf1b2172301dbed28e8e5e878ee687c21947a6c81d6ceaa15d/aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", size = 699833, upload-time = "2025-04-21T09:42:00.298Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/84/ecdc68e293110e6f6f6d7b57786a77555a85f70edd2b180fb1fafaff361a/aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", size = 462774, upload-time = "2025-04-21T09:42:02.015Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/85/f07718cca55884dad83cc2433746384d267ee970e91f0dcc75c6d5544079/aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", size = 454429, upload-time = "2025-04-21T09:42:03.728Z" },
+ { url = "https://files.pythonhosted.org/packages/82/02/7f669c3d4d39810db8842c4e572ce4fe3b3a9b82945fdd64affea4c6947e/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", size = 1670283, upload-time = "2025-04-21T09:42:06.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/79/b82a12f67009b377b6c07a26bdd1b81dab7409fc2902d669dbfa79e5ac02/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", size = 1717231, upload-time = "2025-04-21T09:42:07.953Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/38/d5a1f28c3904a840642b9a12c286ff41fc66dfa28b87e204b1f242dbd5e6/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", size = 1769621, upload-time = "2025-04-21T09:42:09.855Z" },
+ { url = "https://files.pythonhosted.org/packages/53/2d/deb3749ba293e716b5714dda06e257f123c5b8679072346b1eb28b766a0b/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", size = 1678667, upload-time = "2025-04-21T09:42:11.741Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a8/04b6e11683a54e104b984bd19a9790eb1ae5f50968b601bb202d0406f0ff/aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", size = 1601592, upload-time = "2025-04-21T09:42:14.137Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/9d/c33305ae8370b789423623f0e073d09ac775cd9c831ac0f11338b81c16e0/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", size = 1621679, upload-time = "2025-04-21T09:42:16.056Z" },
+ { url = "https://files.pythonhosted.org/packages/56/45/8e9a27fff0538173d47ba60362823358f7a5f1653c6c30c613469f94150e/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", size = 1656878, upload-time = "2025-04-21T09:42:18.368Z" },
+ { url = "https://files.pythonhosted.org/packages/84/5b/8c5378f10d7a5a46b10cb9161a3aac3eeae6dba54ec0f627fc4ddc4f2e72/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", size = 1620509, upload-time = "2025-04-21T09:42:20.141Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/2f/99dee7bd91c62c5ff0aa3c55f4ae7e1bc99c6affef780d7777c60c5b3735/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", size = 1680263, upload-time = "2025-04-21T09:42:21.993Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0a/378745e4ff88acb83e2d5c884a4fe993a6e9f04600a4560ce0e9b19936e3/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", size = 1715014, upload-time = "2025-04-21T09:42:23.87Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/0b/b5524b3bb4b01e91bc4323aad0c2fcaebdf2f1b4d2eb22743948ba364958/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", size = 1666614, upload-time = "2025-04-21T09:42:25.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b7/3d7b036d5a4ed5a4c704e0754afe2eef24a824dfab08e6efbffb0f6dd36a/aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78", size = 411358, upload-time = "2025-04-21T09:42:27.558Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/3c/143831b32cd23b5263a995b2a1794e10aa42f8a895aae5074c20fda36c07/aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01", size = 437658, upload-time = "2025-04-21T09:42:29.209Z" },
+]
+
+[[package]]
+name = "aiomysql"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pymysql" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/76/2c5b55e4406a1957ffdfd933a94c2517455291c97d2b81cec6813754791a/aiomysql-0.2.0.tar.gz", hash = "sha256:558b9c26d580d08b8c5fd1be23c5231ce3aeff2dadad989540fee740253deb67", size = 114706, upload-time = "2023-06-11T19:57:53.608Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/87/c982ee8b333c85b8ae16306387d703a1fcdfc81a2f3f15a24820ab1a512d/aiomysql-0.2.0-py3-none-any.whl", hash = "sha256:b7c26da0daf23a5ec5e0b133c03d20657276e4eae9b73e040b72787f6f6ade0a", size = 44215, upload-time = "2023-06-11T19:57:51.09Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
+]
+
+[[package]]
+name = "aiosqlite"
+version = "0.21.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
+]
+
+[[package]]
+name = "asyncio"
+version = "3.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411, upload-time = "2015-03-10T14:11:26.494Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767, upload-time = "2015-03-10T14:05:10.959Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.4.26"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
+ { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
+ { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
+ { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
+ { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
+ { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
+ { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "45.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/47/92a8914716f2405f33f1814b97353e3cfa223cd94a77104075d42de3099e/cryptography-45.0.2.tar.gz", hash = "sha256:d784d57b958ffd07e9e226d17272f9af0c41572557604ca7554214def32c26bf", size = 743865, upload-time = "2025-05-18T02:46:34.986Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/2f/46b9e715157643ad16f039ec3c3c47d174da6f825bf5034b1c5f692ab9e2/cryptography-45.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:61a8b1bbddd9332917485b2453d1de49f142e6334ce1d97b7916d5a85d179c84", size = 7043448, upload-time = "2025-05-18T02:45:12.495Z" },
+ { url = "https://files.pythonhosted.org/packages/90/52/49e6c86278e1b5ec226e96b62322538ccc466306517bf9aad8854116a088/cryptography-45.0.2-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cc31c66411e14dd70e2f384a9204a859dc25b05e1f303df0f5326691061b839", size = 4201098, upload-time = "2025-05-18T02:45:15.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/3a/201272539ac5b66b4cb1af89021e423fc0bfacb73498950280c51695fb78/cryptography-45.0.2-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:463096533acd5097f8751115bc600b0b64620c4aafcac10c6d0041e6e68f88fe", size = 4429839, upload-time = "2025-05-18T02:45:17.614Z" },
+ { url = "https://files.pythonhosted.org/packages/99/89/fa1a84832b8f8f3917875cb15324bba98def5a70175a889df7d21a45dc75/cryptography-45.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdafb86eb673c3211accffbffdb3cdffa3aaafacd14819e0898d23696d18e4d3", size = 4205154, upload-time = "2025-05-18T02:45:19.874Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c5/5225d5230d538ab461725711cf5220560a813d1eb68bafcfb00131b8f631/cryptography-45.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05c2385b1f5c89a17df19900cfb1345115a77168f5ed44bdf6fd3de1ce5cc65b", size = 3897145, upload-time = "2025-05-18T02:45:22.209Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/24/f19aae32526cc55ae17d473bc4588b1234af2979483d99cbfc57e55ffea6/cryptography-45.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e9e4bdcd70216b08801e267c0b563316b787f957a46e215249921f99288456f9", size = 4462192, upload-time = "2025-05-18T02:45:24.773Z" },
+ { url = "https://files.pythonhosted.org/packages/19/18/4a69ac95b0b3f03355970baa6c3f9502bbfc54e7df81fdb179654a00f48e/cryptography-45.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b2de529027579e43b6dc1f805f467b102fb7d13c1e54c334f1403ee2b37d0059", size = 4208093, upload-time = "2025-05-18T02:45:27.028Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/54/2dea55ccc9558b8fa14f67156250b6ee231e31765601524e4757d0b5db6b/cryptography-45.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10d68763892a7b19c22508ab57799c4423c7c8cd61d7eee4c5a6a55a46511949", size = 4461819, upload-time = "2025-05-18T02:45:29.39Z" },
+ { url = "https://files.pythonhosted.org/packages/37/f1/1b220fcd5ef4b1f0ff3e59e733b61597505e47f945606cc877adab2c1a17/cryptography-45.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2a90ce2f0f5b695e4785ac07c19a58244092f3c85d57db6d8eb1a2b26d2aad6", size = 4329202, upload-time = "2025-05-18T02:45:31.925Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e0/51d1dc4f96f819a56db70f0b4039b4185055bbb8616135884c3c3acc4c6d/cryptography-45.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:59c0c8f043dd376bbd9d4f636223836aed50431af4c5a467ed9bf61520294627", size = 4570412, upload-time = "2025-05-18T02:45:34.348Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/44/88efb40a3600d15277a77cdc69eeeab45a98532078d2a36cffd9325d3b3f/cryptography-45.0.2-cp311-abi3-win32.whl", hash = "sha256:80303ee6a02ef38c4253160446cbeb5c400c07e01d4ddbd4ff722a89b736d95a", size = 2933584, upload-time = "2025-05-18T02:45:36.198Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/a1/bc9f82ba08760442cc8346d1b4e7b769b86d197193c45b42b3595d231e84/cryptography-45.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7429936146063bd1b2cfc54f0e04016b90ee9b1c908a7bed0800049cbace70eb", size = 3408537, upload-time = "2025-05-18T02:45:38.184Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bc/1b6acb1dca366f9c0b3880888ecd7fcfb68023930d57df854847c6da1d10/cryptography-45.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e86c8d54cd19a13e9081898b3c24351683fd39d726ecf8e774aaa9d8d96f5f3a", size = 7025581, upload-time = "2025-05-18T02:45:40.632Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a3/a3e4a298d3db4a04085728f5ae6c8cda157e49c5bb784886d463b9fbff70/cryptography-45.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e328357b6bbf79928363dbf13f4635b7aac0306afb7e5ad24d21d0c5761c3253", size = 4189148, upload-time = "2025-05-18T02:45:42.538Z" },
+ { url = "https://files.pythonhosted.org/packages/53/90/100dfadd4663b389cb56972541ec1103490a19ebad0132af284114ba0868/cryptography-45.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49af56491473231159c98c2c26f1a8f3799a60e5cf0e872d00745b858ddac9d2", size = 4424113, upload-time = "2025-05-18T02:45:44.316Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/40/e2b9177dbed6f3fcbbf1942e1acea2fd15b17007204b79d675540dd053af/cryptography-45.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f169469d04a23282de9d0be349499cb6683b6ff1b68901210faacac9b0c24b7d", size = 4189696, upload-time = "2025-05-18T02:45:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ae/ec29c79f481e1767c2ff916424ba36f3cf7774de93bbd60428a3c52d1357/cryptography-45.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9cfd1399064b13043082c660ddd97a0358e41c8b0dc7b77c1243e013d305c344", size = 3881498, upload-time = "2025-05-18T02:45:48.884Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4a/72937090e5637a232b2f73801c9361cd08404a2d4e620ca4ec58c7ea4b70/cryptography-45.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f8084b7ca3ce1b8d38bdfe33c48116edf9a08b4d056ef4a96dceaa36d8d965", size = 4451678, upload-time = "2025-05-18T02:45:50.706Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/fa/1377fced81fd67a4a27514248261bb0d45c3c1e02169411fe231583088c8/cryptography-45.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2cb03a944a1a412724d15a7c051d50e63a868031f26b6a312f2016965b661942", size = 4192296, upload-time = "2025-05-18T02:45:52.422Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/cf/b6fe837c83a08b9df81e63299d75fc5b3c6d82cf24b3e1e0e331050e9e5c/cryptography-45.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a9727a21957d3327cf6b7eb5ffc9e4b663909a25fea158e3fcbc49d4cdd7881b", size = 4451749, upload-time = "2025-05-18T02:45:55.025Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d8/5a655675cc635c7190bfc8cffb84bcdc44fc62ce945ad1d844adaa884252/cryptography-45.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddb8d01aa900b741d6b7cc585a97aff787175f160ab975e21f880e89d810781a", size = 4317601, upload-time = "2025-05-18T02:45:56.911Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d4/75d2375a20d80aa262a8adee77bf56950e9292929e394b9fae2481803f11/cryptography-45.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c0c000c1a09f069632d8a9eb3b610ac029fcc682f1d69b758e625d6ee713f4ed", size = 4560535, upload-time = "2025-05-18T02:45:59.33Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/18/c3a94474987ebcfb88692036b2ec44880d243fefa73794bdcbf748679a6e/cryptography-45.0.2-cp37-abi3-win32.whl", hash = "sha256:08281de408e7eb71ba3cd5098709a356bfdf65eebd7ee7633c3610f0aa80d79b", size = 2922045, upload-time = "2025-05-18T02:46:01.012Z" },
+ { url = "https://files.pythonhosted.org/packages/63/63/fb28b30c144182fd44ce93d13ab859791adbf923e43bdfb610024bfecda1/cryptography-45.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:48caa55c528617fa6db1a9c3bf2e37ccb31b73e098ac2b71408d1f2db551dde4", size = 3393321, upload-time = "2025-05-18T02:46:03.441Z" },
+]
+
+[[package]]
+name = "dataclasses-json"
+version = "0.6.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "marshmallow" },
+ { name = "typing-inspect" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "faiss-cpu"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/9a/e33fc563f007924dd4ec3c5101fe5320298d6c13c158a24a9ed849058569/faiss_cpu-1.11.0.tar.gz", hash = "sha256:44877b896a2b30a61e35ea4970d008e8822545cb340eca4eff223ac7f40a1db9", size = 70218, upload-time = "2025-04-28T07:48:30.459Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/d3/7178fa07047fd770964a83543329bb5e3fc1447004cfd85186ccf65ec3ee/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:356437b9a46f98c25831cdae70ca484bd6c05065af6256d87f6505005e9135b9", size = 3313807, upload-time = "2025-04-28T07:47:54.533Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/71/25f5f7b70a9f22a3efe19e7288278da460b043a3b60ad98e4e47401ed5aa/faiss_cpu-1.11.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c4a3d35993e614847f3221c6931529c0bac637a00eff0d55293e1db5cb98c85f", size = 7913537, upload-time = "2025-04-28T07:47:56.723Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c8/a5cb8466c981ad47750e1d5fda3d4223c82f9da947538749a582b3a2d35c/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8f9af33e0b8324e8199b93eb70ac4a951df02802a9dcff88e9afc183b11666f0", size = 3785180, upload-time = "2025-04-28T07:47:59.004Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eaf15a7d80e1aad74f56cf737b31b4547a1a664ad3c6e4cfaf90e82454a8/faiss_cpu-1.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:48b7e7876829e6bdf7333041800fa3c1753bb0c47e07662e3ef55aca86981430", size = 31287630, upload-time = "2025-04-28T07:48:01.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/5c/902a78347e9c47baaf133e47863134e564c39f9afe105795b16ee986b0df/faiss_cpu-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bdc199311266d2be9d299da52361cad981393327b2b8aa55af31a1b75eaaf522", size = 15005398, upload-time = "2025-04-28T07:48:04.232Z" },
+ { url = "https://files.pythonhosted.org/packages/92/90/d2329ce56423cc61f4c20ae6b4db001c6f88f28bf5a7ef7f8bbc246fd485/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0c98e5feff83b87348e44eac4d578d6f201780dae6f27f08a11d55536a20b3a8", size = 3313807, upload-time = "2025-04-28T07:48:06.486Z" },
+ { url = "https://files.pythonhosted.org/packages/24/14/8af8f996d54e6097a86e6048b1a2c958c52dc985eb4f935027615079939e/faiss_cpu-1.11.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:796e90389427b1c1fb06abdb0427bb343b6350f80112a2e6090ac8f176ff7416", size = 7913539, upload-time = "2025-04-28T07:48:08.338Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2b/437c2f36c3aa3cffe041479fced1c76420d3e92e1f434f1da3be3e6f32b1/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2b6e355dda72b3050991bc32031b558b8f83a2b3537a2b9e905a84f28585b47e", size = 3785181, upload-time = "2025-04-28T07:48:10.594Z" },
+ { url = "https://files.pythonhosted.org/packages/66/75/955527414371843f558234df66fa0b62c6e86e71e4022b1be9333ac6004c/faiss_cpu-1.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6c482d07194638c169b4422774366e7472877d09181ea86835e782e6304d4185", size = 31287635, upload-time = "2025-04-28T07:48:12.93Z" },
+ { url = "https://files.pythonhosted.org/packages/50/51/35b7a3f47f7859363a367c344ae5d415ea9eda65db0a7d497c7ea2c0b576/faiss_cpu-1.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:13eac45299532b10e911bff1abbb19d1bf5211aa9e72afeade653c3f1e50e042", size = 15005455, upload-time = "2025-04-28T07:48:16.173Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831, upload-time = "2025-04-17T22:38:53.099Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193, upload-time = "2025-04-17T22:36:47.382Z" },
+ { url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831, upload-time = "2025-04-17T22:36:49.401Z" },
+ { url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862, upload-time = "2025-04-17T22:36:51.899Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361, upload-time = "2025-04-17T22:36:53.402Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115, upload-time = "2025-04-17T22:36:55.016Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505, upload-time = "2025-04-17T22:36:57.12Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666, upload-time = "2025-04-17T22:36:58.735Z" },
+ { url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119, upload-time = "2025-04-17T22:37:00.512Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226, upload-time = "2025-04-17T22:37:02.102Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788, upload-time = "2025-04-17T22:37:03.578Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914, upload-time = "2025-04-17T22:37:05.213Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283, upload-time = "2025-04-17T22:37:06.985Z" },
+ { url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264, upload-time = "2025-04-17T22:37:08.618Z" },
+ { url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482, upload-time = "2025-04-17T22:37:10.196Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248, upload-time = "2025-04-17T22:37:12.284Z" },
+ { url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161, upload-time = "2025-04-17T22:37:13.902Z" },
+ { url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548, upload-time = "2025-04-17T22:37:15.326Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e5/04c7090c514d96ca00887932417f04343ab94904a56ab7f57861bf63652d/frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", size = 158182, upload-time = "2025-04-17T22:37:16.837Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/8f/60d0555c61eec855783a6356268314d204137f5e0c53b59ae2fc28938c99/frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", size = 122838, upload-time = "2025-04-17T22:37:18.352Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a7/d0ec890e3665b4b3b7c05dc80e477ed8dc2e2e77719368e78e2cd9fec9c8/frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", size = 120980, upload-time = "2025-04-17T22:37:19.857Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/19/9b355a5e7a8eba903a008579964192c3e427444752f20b2144b10bb336df/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", size = 305463, upload-time = "2025-04-17T22:37:21.328Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/8d/5b4c758c2550131d66935ef2fa700ada2461c08866aef4229ae1554b93ca/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", size = 297985, upload-time = "2025-04-17T22:37:23.55Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2c/537ec09e032b5865715726b2d1d9813e6589b571d34d01550c7aeaad7e53/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", size = 311188, upload-time = "2025-04-17T22:37:25.221Z" },
+ { url = "https://files.pythonhosted.org/packages/31/2f/1aa74b33f74d54817055de9a4961eff798f066cdc6f67591905d4fc82a84/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", size = 311874, upload-time = "2025-04-17T22:37:26.791Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f0/cfec18838f13ebf4b37cfebc8649db5ea71a1b25dacd691444a10729776c/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f", size = 291897, upload-time = "2025-04-17T22:37:28.958Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/a5/deb39325cbbea6cd0a46db8ccd76150ae2fcbe60d63243d9df4a0b8c3205/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", size = 305799, upload-time = "2025-04-17T22:37:30.889Z" },
+ { url = "https://files.pythonhosted.org/packages/78/22/6ddec55c5243a59f605e4280f10cee8c95a449f81e40117163383829c241/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", size = 302804, upload-time = "2025-04-17T22:37:32.489Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b7/d9ca9bab87f28855063c4d202936800219e39db9e46f9fb004d521152623/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", size = 316404, upload-time = "2025-04-17T22:37:34.59Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3a/1255305db7874d0b9eddb4fe4a27469e1fb63720f1fc6d325a5118492d18/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", size = 295572, upload-time = "2025-04-17T22:37:36.337Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/f2/8d38eeee39a0e3a91b75867cc102159ecccf441deb6ddf67be96d3410b84/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", size = 307601, upload-time = "2025-04-17T22:37:37.923Z" },
+ { url = "https://files.pythonhosted.org/packages/38/04/80ec8e6b92f61ef085422d7b196822820404f940950dde5b2e367bede8bc/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", size = 314232, upload-time = "2025-04-17T22:37:39.669Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/58/93b41fb23e75f38f453ae92a2f987274c64637c450285577bd81c599b715/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", size = 308187, upload-time = "2025-04-17T22:37:41.662Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a2/e64df5c5aa36ab3dee5a40d254f3e471bb0603c225f81664267281c46a2d/frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", size = 114772, upload-time = "2025-04-17T22:37:43.132Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/77/fead27441e749b2d574bb73d693530d59d520d4b9e9679b8e3cb779d37f2/frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", size = 119847, upload-time = "2025-04-17T22:37:45.118Z" },
+ { url = "https://files.pythonhosted.org/packages/df/bd/cc6d934991c1e5d9cafda83dfdc52f987c7b28343686aef2e58a9cf89f20/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", size = 174937, upload-time = "2025-04-17T22:37:46.635Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/a2/daf945f335abdbfdd5993e9dc348ef4507436936ab3c26d7cfe72f4843bf/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", size = 136029, upload-time = "2025-04-17T22:37:48.192Z" },
+ { url = "https://files.pythonhosted.org/packages/51/65/4c3145f237a31247c3429e1c94c384d053f69b52110a0d04bfc8afc55fb2/frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", size = 134831, upload-time = "2025-04-17T22:37:50.485Z" },
+ { url = "https://files.pythonhosted.org/packages/77/38/03d316507d8dea84dfb99bdd515ea245628af964b2bf57759e3c9205cc5e/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", size = 392981, upload-time = "2025-04-17T22:37:52.558Z" },
+ { url = "https://files.pythonhosted.org/packages/37/02/46285ef9828f318ba400a51d5bb616ded38db8466836a9cfa39f3903260b/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", size = 371999, upload-time = "2025-04-17T22:37:54.092Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/64/1212fea37a112c3c5c05bfb5f0a81af4836ce349e69be75af93f99644da9/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", size = 392200, upload-time = "2025-04-17T22:37:55.951Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ce/9a6ea1763e3366e44a5208f76bf37c76c5da570772375e4d0be85180e588/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", size = 390134, upload-time = "2025-04-17T22:37:57.633Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/939738b0b495b2c6d0c39ba51563e453232813042a8d908b8f9544296c29/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", size = 365208, upload-time = "2025-04-17T22:37:59.742Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/8b/939e62e93c63409949c25220d1ba8e88e3960f8ef6a8d9ede8f94b459d27/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", size = 385548, upload-time = "2025-04-17T22:38:01.416Z" },
+ { url = "https://files.pythonhosted.org/packages/62/38/22d2873c90102e06a7c5a3a5b82ca47e393c6079413e8a75c72bff067fa8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", size = 391123, upload-time = "2025-04-17T22:38:03.049Z" },
+ { url = "https://files.pythonhosted.org/packages/44/78/63aaaf533ee0701549500f6d819be092c6065cb5c577edb70c09df74d5d0/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", size = 394199, upload-time = "2025-04-17T22:38:04.776Z" },
+ { url = "https://files.pythonhosted.org/packages/54/45/71a6b48981d429e8fbcc08454dc99c4c2639865a646d549812883e9c9dd3/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", size = 373854, upload-time = "2025-04-17T22:38:06.576Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/f3/dbf2a5e11736ea81a66e37288bf9f881143a7822b288a992579ba1b4204d/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", size = 395412, upload-time = "2025-04-17T22:38:08.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/f1/c63166806b331f05104d8ea385c4acd511598568b1f3e4e8297ca54f2676/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", size = 394936, upload-time = "2025-04-17T22:38:10.056Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/ea/4f3e69e179a430473eaa1a75ff986526571215fefc6b9281cdc1f09a4eb8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", size = 391459, upload-time = "2025-04-17T22:38:11.826Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/c3/0fc2c97dea550df9afd072a37c1e95421652e3206bbeaa02378b24c2b480/frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", size = 128797, upload-time = "2025-04-17T22:38:14.013Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f5/79c9320c5656b1965634fe4be9c82b12a3305bdbc58ad9cb941131107b20/frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", size = 134709, upload-time = "2025-04-17T22:38:15.551Z" },
+ { url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797, upload-time = "2025-05-09T19:47:35.066Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413, upload-time = "2025-05-09T14:51:32.455Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242, upload-time = "2025-05-09T15:24:02.63Z" },
+ { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444, upload-time = "2025-05-09T15:24:49.856Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067, upload-time = "2025-05-09T15:29:24.989Z" },
+ { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153, upload-time = "2025-05-09T14:53:34.716Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865, upload-time = "2025-05-09T14:53:45.738Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575, upload-time = "2025-05-09T15:27:04.248Z" },
+ { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460, upload-time = "2025-05-09T14:54:00.315Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239, upload-time = "2025-05-09T14:57:17.633Z" },
+ { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150, upload-time = "2025-05-09T14:50:30.784Z" },
+ { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381, upload-time = "2025-05-09T15:24:12.893Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427, upload-time = "2025-05-09T15:24:51.074Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795, upload-time = "2025-05-09T15:29:26.673Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398, upload-time = "2025-05-09T14:53:36.61Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795, upload-time = "2025-05-09T14:53:47.039Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976, upload-time = "2025-05-09T15:27:06.542Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509, upload-time = "2025-05-09T14:54:02.223Z" },
+ { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023, upload-time = "2025-05-09T14:53:24.157Z" },
+ { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911, upload-time = "2025-05-09T15:24:22.376Z" },
+ { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251, upload-time = "2025-05-09T15:24:52.205Z" },
+ { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620, upload-time = "2025-05-09T15:29:28.051Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851, upload-time = "2025-05-09T14:53:38.472Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718, upload-time = "2025-05-09T14:53:48.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752, upload-time = "2025-05-09T15:27:08.217Z" },
+ { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170, upload-time = "2025-05-09T14:54:04.082Z" },
+ { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899, upload-time = "2025-05-09T14:54:01.581Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/b5/348b3313c58f5fbfb2194eb4d07e46a35748ba6e5b3b3046143f3040bafa/jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b", size = 312262, upload-time = "2025-05-18T19:03:44.637Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/4a/6a2397096162b21645162825f058d1709a02965606e537e3304b02742e9b/jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744", size = 320124, upload-time = "2025-05-18T19:03:46.341Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/85/1ce02cade7516b726dd88f59a4ee46914bf79d1676d1228ef2002ed2f1c9/jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2", size = 345330, upload-time = "2025-05-18T19:03:47.596Z" },
+ { url = "https://files.pythonhosted.org/packages/75/d0/bb6b4f209a77190ce10ea8d7e50bf3725fc16d3372d0a9f11985a2b23eff/jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026", size = 369670, upload-time = "2025-05-18T19:03:49.334Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/f5/a61787da9b8847a601e6827fbc42ecb12be2c925ced3252c8ffcb56afcaf/jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c", size = 489057, upload-time = "2025-05-18T19:03:50.66Z" },
+ { url = "https://files.pythonhosted.org/packages/12/e4/6f906272810a7b21406c760a53aadbe52e99ee070fc5c0cb191e316de30b/jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959", size = 389372, upload-time = "2025-05-18T19:03:51.98Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ba/77013b0b8ba904bf3762f11e0129b8928bff7f978a81838dfcc958ad5728/jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a", size = 352038, upload-time = "2025-05-18T19:03:53.703Z" },
+ { url = "https://files.pythonhosted.org/packages/67/27/c62568e3ccb03368dbcc44a1ef3a423cb86778a4389e995125d3d1aaa0a4/jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95", size = 391538, upload-time = "2025-05-18T19:03:55.046Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/72/0d6b7e31fc17a8fdce76164884edef0698ba556b8eb0af9546ae1a06b91d/jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea", size = 523557, upload-time = "2025-05-18T19:03:56.386Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/09/bc1661fbbcbeb6244bd2904ff3a06f340aa77a2b94e5a7373fd165960ea3/jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b", size = 514202, upload-time = "2025-05-18T19:03:57.675Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/84/5a5d5400e9d4d54b8004c9673bbe4403928a00d28529ff35b19e9d176b19/jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01", size = 211781, upload-time = "2025-05-18T19:03:59.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/52/7ec47455e26f2d6e5f2ea4951a0652c06e5b995c291f723973ae9e724a65/jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49", size = 206176, upload-time = "2025-05-18T19:04:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
+ { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" },
+ { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
+ { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
+ { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
+ { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
+ { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
+ { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "langchain"
+version = "0.3.25"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "langchain-text-splitters" },
+ { name = "langsmith" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f9/a256609096a9fc7a1b3a6300a97000091efabdf21555a97988f93d4d9258/langchain-0.3.25.tar.gz", hash = "sha256:a1d72aa39546a23db08492d7228464af35c9ee83379945535ceef877340d2a3a", size = 10225045, upload-time = "2025-05-02T18:39:04.353Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/5c/5c0be747261e1f8129b875fa3bfea736bc5fe17652f9d5e15ca118571b6f/langchain-0.3.25-py3-none-any.whl", hash = "sha256:931f7d2d1eaf182f9f41c5e3272859cfe7f94fc1f7cef6b3e5a46024b4884c21", size = 1011008, upload-time = "2025-05-02T18:39:02.21Z" },
+]
+
+[[package]]
+name = "langchain-community"
+version = "0.3.24"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "dataclasses-json" },
+ { name = "httpx-sse" },
+ { name = "langchain" },
+ { name = "langchain-core" },
+ { name = "langsmith" },
+ { name = "numpy" },
+ { name = "pydantic-settings" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sqlalchemy" },
+ { name = "tenacity" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/f6/4892d1f1cf6d3e89da6ee6cfb0eb82b908c706c58bde7df28367ee76a93f/langchain_community-0.3.24.tar.gz", hash = "sha256:62d9e8cf9aadf35182ec3925f9ec1c8e5e84fb4f199f67a01aee496d289dc264", size = 33233643, upload-time = "2025-05-12T13:26:39.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/cb/582f22d74d69f4dbd41e98d361ee36922b79a245a9411383327bd4b63747/langchain_community-0.3.24-py3-none-any.whl", hash = "sha256:b6cdb376bf1c2f4d2503aca20f8f35f2d5b3d879c52848277f20ce1950e7afaf", size = 2528335, upload-time = "2025-05-12T13:26:37.375Z" },
+]
+
+[[package]]
+name = "langchain-core"
+version = "0.3.60"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpatch" },
+ { name = "langsmith" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "tenacity" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/75/95129aaada92980a002a31e002610a80af3c8967ae7884710372e89cdde0/langchain_core-0.3.60.tar.gz", hash = "sha256:63dd1bdf7939816115399522661ca85a2f3686a61440f2f46ebd86d1b028595b", size = 557456, upload-time = "2025-05-15T15:23:23.642Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/bc/344f5b11fdfe0e27f7064d2e829921a791461dc32e5ed285fe6325518c26/langchain_core-0.3.60-py3-none-any.whl", hash = "sha256:2ccdf06b12e699b1b0962bc02837056c075b4981c3d13f82a4d4c30bb22ea3dc", size = 437890, upload-time = "2025-05-15T15:23:22.278Z" },
+]
+
+[[package]]
+name = "langchain-openai"
+version = "0.3.17"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+ { name = "openai" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/85/4c5f34d177a442a48273688c52b8e2d285e6fa77329ef3de62ca8cdaecfb/langchain_openai-0.3.17.tar.gz", hash = "sha256:10bcdfac3edb3dea4a8aabb12f01566e5ff8756634cc52aa169c62e4c4b73801", size = 271556, upload-time = "2025-05-15T13:35:04.162Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/60/886dc53c91031e26542f7ac1ea4062b7ebe542d22970996acaee59aa1cab/langchain_openai-0.3.17-py3-none-any.whl", hash = "sha256:d4d9cf945e2453ee5895ccd12fd8a3ea9131a0f6130dcc21427c77cc2206b1c0", size = 62891, upload-time = "2025-05-15T13:35:02.817Z" },
+]
+
+[[package]]
+name = "langchain-text-splitters"
+version = "0.3.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "langchain-core" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e7/ac/b4a25c5716bb0103b1515f1f52cc69ffb1035a5a225ee5afe3aed28bf57b/langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e", size = 42128, upload-time = "2025-04-04T14:03:51.521Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/a3/3696ff2444658053c01b6b7443e761f28bb71217d82bb89137a978c5f66f/langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02", size = 32440, upload-time = "2025-04-04T14:03:50.6Z" },
+]
+
+[[package]]
+name = "langsmith"
+version = "0.3.42"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "requests-toolbelt" },
+ { name = "zstandard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/44/fe171c0b0fb0377b191aebf0b7779e0c7b2a53693c6a01ddad737212495d/langsmith-0.3.42.tar.gz", hash = "sha256:2b5cbc450ab808b992362aac6943bb1d285579aa68a3a8be901d30a393458f25", size = 345619, upload-time = "2025-05-03T03:07:17.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/8e/e8a58e0abaae3f3ac4702e9ca35d1fc6159711556b64ffd0e247771a3f12/langsmith-0.3.42-py3-none-any.whl", hash = "sha256:18114327f3364385dae4026ebfd57d1c1cb46d8f80931098f0f10abe533475ff", size = 360334, upload-time = "2025-05-03T03:07:15.491Z" },
+]
+
+[[package]]
+name = "loguru"
+version = "0.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
+]
+
+[[package]]
+name = "marshmallow"
+version = "3.26.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/8d/0f4468582e9e97b0a24604b585c651dfd2144300ecffd1c06a680f5c8861/mcp-1.9.0.tar.gz", hash = "sha256:905d8d208baf7e3e71d70c82803b89112e321581bcd2530f9de0fe4103d28749", size = 281432, upload-time = "2025-05-15T18:51:06.615Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/d5/22e36c95c83c80eb47c83f231095419cf57cf5cca5416f1c960032074c78/mcp-1.9.0-py3-none-any.whl", hash = "sha256:9dfb89c8c56f742da10a5910a1f64b0d2ac2c3ed2bd572ddb1cfab7f35957178", size = 125082, upload-time = "2025-05-15T18:51:04.916Z" },
+]
+
+[package.optional-dependencies]
+cli = [
+ { name = "python-dotenv" },
+ { name = "typer" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "modelscope"
+version = "1.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/48/3c/76d1701761590012b331742d6c67babc3b048df166d5ef99a8c6b0c80c2f/modelscope-1.26.0.tar.gz", hash = "sha256:3178d82ce795ba1a5472eacefda517e1f2162186e4f9f9641cf6587d2c368bc1", size = 4390303, upload-time = "2025-05-14T12:55:09.891Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/6e/50fed6b37503accb0199153d3c18884380797b40491b11bd03279cd2d698/modelscope-1.26.0-py3-none-any.whl", hash = "sha256:fdb23191cdf8d5811accefc09ef4594f3b7c5f1d165af9194dc64eba96780c3a", size = 5855598, upload-time = "2025-05-14T12:55:05.055Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183, upload-time = "2025-05-19T14:16:37.381Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293, upload-time = "2025-05-19T14:14:44.724Z" },
+ { url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096, upload-time = "2025-05-19T14:14:45.95Z" },
+ { url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214, upload-time = "2025-05-19T14:14:47.158Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686, upload-time = "2025-05-19T14:14:48.366Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061, upload-time = "2025-05-19T14:14:49.952Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412, upload-time = "2025-05-19T14:14:51.812Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563, upload-time = "2025-05-19T14:14:53.262Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811, upload-time = "2025-05-19T14:14:55.232Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524, upload-time = "2025-05-19T14:14:57.226Z" },
+ { url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012, upload-time = "2025-05-19T14:14:58.597Z" },
+ { url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765, upload-time = "2025-05-19T14:15:00.048Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888, upload-time = "2025-05-19T14:15:01.568Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041, upload-time = "2025-05-19T14:15:03.759Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046, upload-time = "2025-05-19T14:15:05.698Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106, upload-time = "2025-05-19T14:15:07.124Z" },
+ { url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351, upload-time = "2025-05-19T14:15:08.556Z" },
+ { url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791, upload-time = "2025-05-19T14:15:09.825Z" },
+ { url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123, upload-time = "2025-05-19T14:15:11.044Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049, upload-time = "2025-05-19T14:15:12.902Z" },
+ { url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078, upload-time = "2025-05-19T14:15:14.282Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097, upload-time = "2025-05-19T14:15:15.566Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768, upload-time = "2025-05-19T14:15:17.308Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331, upload-time = "2025-05-19T14:15:18.73Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169, upload-time = "2025-05-19T14:15:20.179Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947, upload-time = "2025-05-19T14:15:21.714Z" },
+ { url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761, upload-time = "2025-05-19T14:15:23.242Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605, upload-time = "2025-05-19T14:15:24.763Z" },
+ { url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144, upload-time = "2025-05-19T14:15:26.249Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100, upload-time = "2025-05-19T14:15:28.303Z" },
+ { url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731, upload-time = "2025-05-19T14:15:30.263Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637, upload-time = "2025-05-19T14:15:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594, upload-time = "2025-05-19T14:15:34.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359, upload-time = "2025-05-19T14:15:36.246Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903, upload-time = "2025-05-19T14:15:37.507Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895, upload-time = "2025-05-19T14:15:38.856Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183, upload-time = "2025-05-19T14:15:40.197Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592, upload-time = "2025-05-19T14:15:41.508Z" },
+ { url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071, upload-time = "2025-05-19T14:15:42.877Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597, upload-time = "2025-05-19T14:15:44.412Z" },
+ { url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253, upload-time = "2025-05-19T14:15:46.474Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146, upload-time = "2025-05-19T14:15:48.003Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585, upload-time = "2025-05-19T14:15:49.546Z" },
+ { url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080, upload-time = "2025-05-19T14:15:51.151Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558, upload-time = "2025-05-19T14:15:52.665Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168, upload-time = "2025-05-19T14:15:55.279Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970, upload-time = "2025-05-19T14:15:56.806Z" },
+ { url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980, upload-time = "2025-05-19T14:15:58.313Z" },
+ { url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641, upload-time = "2025-05-19T14:15:59.866Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728, upload-time = "2025-05-19T14:16:01.535Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913, upload-time = "2025-05-19T14:16:03.199Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112, upload-time = "2025-05-19T14:16:04.909Z" },
+ { url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481, upload-time = "2025-05-19T14:16:36.024Z" },
+]
+
+[[package]]
+name = "mydba"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "aiomysql" },
+ { name = "aiosqlite" },
+ { name = "asyncio" },
+ { name = "cryptography" },
+ { name = "exceptiongroup" },
+ { name = "faiss-cpu" },
+ { name = "langchain" },
+ { name = "langchain-community" },
+ { name = "langchain-openai" },
+ { name = "loguru" },
+ { name = "mcp", extra = ["cli"] },
+ { name = "modelscope" },
+ { name = "openai" },
+ { name = "prompt-toolkit" },
+ { name = "pydantic" },
+ { name = "pymysql" },
+ { name = "tenacity" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aiomysql", specifier = ">=0.2.0" },
+ { name = "aiosqlite", specifier = ">=0.21.0" },
+ { name = "asyncio", specifier = ">=3.4.3" },
+ { name = "cryptography", specifier = ">=45.0.2" },
+ { name = "exceptiongroup", specifier = ">=1.3.0" },
+ { name = "faiss-cpu", specifier = ">=1.11.0" },
+ { name = "langchain", specifier = ">=0.3.25" },
+ { name = "langchain-community", specifier = ">=0.3.24" },
+ { name = "langchain-openai", specifier = ">=0.3.17" },
+ { name = "loguru", specifier = ">=0.7.3" },
+ { name = "mcp", extras = ["cli"], specifier = ">=1.9.0" },
+ { name = "modelscope", specifier = ">=1.26.0" },
+ { name = "openai", specifier = ">=1.79.0" },
+ { name = "prompt-toolkit", specifier = ">=3.0.51" },
+ { name = "pydantic", specifier = ">=2.11.4" },
+ { name = "pymysql", specifier = ">=1.1.1" },
+ { name = "tenacity", specifier = ">=9.1.2" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.2.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
+ { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
+ { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
+ { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
+ { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
+ { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
+]
+
+[[package]]
+name = "openai"
+version = "1.79.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/cf/4901077dbbfd0d82a814d721600fa0c3a61a093d7f0bf84d0e4732448dc9/openai-1.79.0.tar.gz", hash = "sha256:e3b627aa82858d3e42d16616edc22aa9f7477ee5eb3e6819e9f44a961d899a4c", size = 444736, upload-time = "2025-05-16T19:49:59.738Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/d2/e3992bb7c6641b765c1008e3c96e076e0b50381be2cce344e6ff177bad80/openai-1.79.0-py3-none-any.whl", hash = "sha256:d5050b92d5ef83f869cb8dcd0aca0b2291c3413412500eec40c66981b3966992", size = 683334, upload-time = "2025-05-16T19:49:57.445Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.10.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/0b/fea456a3ffe74e70ba30e01ec183a9b26bec4d497f61dcfce1b601059c60/orjson-3.10.18.tar.gz", hash = "sha256:e8da3947d92123eda795b68228cafe2724815621fe35e8e320a9e9593a4bcd53", size = 5422810, upload-time = "2025-04-29T23:30:08.423Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/1a/67236da0916c1a192d5f4ccbe10ec495367a726996ceb7614eaa687112f2/orjson-3.10.18-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:50c15557afb7f6d63bc6d6348e0337a880a04eaa9cd7c9d569bcb4e760a24753", size = 249184, upload-time = "2025-04-29T23:28:53.612Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/bc/c7f1db3b1d094dc0c6c83ed16b161a16c214aaa77f311118a93f647b32dc/orjson-3.10.18-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:356b076f1662c9813d5fa56db7d63ccceef4c271b1fb3dd522aca291375fcf17", size = 133279, upload-time = "2025-04-29T23:28:55.055Z" },
+ { url = "https://files.pythonhosted.org/packages/af/84/664657cd14cc11f0d81e80e64766c7ba5c9b7fc1ec304117878cc1b4659c/orjson-3.10.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559eb40a70a7494cd5beab2d73657262a74a2c59aff2068fdba8f0424ec5b39d", size = 136799, upload-time = "2025-04-29T23:28:56.828Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/bb/f50039c5bb05a7ab024ed43ba25d0319e8722a0ac3babb0807e543349978/orjson-3.10.18-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3c29eb9a81e2fbc6fd7ddcfba3e101ba92eaff455b8d602bf7511088bbc0eae", size = 132791, upload-time = "2025-04-29T23:28:58.751Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8c/ee74709fc072c3ee219784173ddfe46f699598a1723d9d49cbc78d66df65/orjson-3.10.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6612787e5b0756a171c7d81ba245ef63a3533a637c335aa7fcb8e665f4a0966f", size = 137059, upload-time = "2025-04-29T23:29:00.129Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/e6d3109ee004296c80426b5a62b47bcadd96a3deab7443e56507823588c5/orjson-3.10.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ac6bd7be0dcab5b702c9d43d25e70eb456dfd2e119d512447468f6405b4a69c", size = 138359, upload-time = "2025-04-29T23:29:01.704Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/5d/387dafae0e4691857c62bd02839a3bf3fa648eebd26185adfac58d09f207/orjson-3.10.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f72f100cee8dde70100406d5c1abba515a7df926d4ed81e20a9730c062fe9ad", size = 142853, upload-time = "2025-04-29T23:29:03.576Z" },
+ { url = "https://files.pythonhosted.org/packages/27/6f/875e8e282105350b9a5341c0222a13419758545ae32ad6e0fcf5f64d76aa/orjson-3.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dca85398d6d093dd41dc0983cbf54ab8e6afd1c547b6b8a311643917fbf4e0c", size = 133131, upload-time = "2025-04-29T23:29:05.753Z" },
+ { url = "https://files.pythonhosted.org/packages/48/b2/73a1f0b4790dcb1e5a45f058f4f5dcadc8a85d90137b50d6bbc6afd0ae50/orjson-3.10.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22748de2a07fcc8781a70edb887abf801bb6142e6236123ff93d12d92db3d406", size = 134834, upload-time = "2025-04-29T23:29:07.35Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f5/7ed133a5525add9c14dbdf17d011dd82206ca6840811d32ac52a35935d19/orjson-3.10.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a83c9954a4107b9acd10291b7f12a6b29e35e8d43a414799906ea10e75438e6", size = 413368, upload-time = "2025-04-29T23:29:09.301Z" },
+ { url = "https://files.pythonhosted.org/packages/11/7c/439654221ed9c3324bbac7bdf94cf06a971206b7b62327f11a52544e4982/orjson-3.10.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:303565c67a6c7b1f194c94632a4a39918e067bd6176a48bec697393865ce4f06", size = 153359, upload-time = "2025-04-29T23:29:10.813Z" },
+ { url = "https://files.pythonhosted.org/packages/48/e7/d58074fa0cc9dd29a8fa2a6c8d5deebdfd82c6cfef72b0e4277c4017563a/orjson-3.10.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:86314fdb5053a2f5a5d881f03fca0219bfdf832912aa88d18676a5175c6916b5", size = 137466, upload-time = "2025-04-29T23:29:12.26Z" },
+ { url = "https://files.pythonhosted.org/packages/57/4d/fe17581cf81fb70dfcef44e966aa4003360e4194d15a3f38cbffe873333a/orjson-3.10.18-cp312-cp312-win32.whl", hash = "sha256:187ec33bbec58c76dbd4066340067d9ece6e10067bb0cc074a21ae3300caa84e", size = 142683, upload-time = "2025-04-29T23:29:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/22/469f62d25ab5f0f3aee256ea732e72dc3aab6d73bac777bd6277955bceef/orjson-3.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:f9f94cf6d3f9cd720d641f8399e390e7411487e493962213390d1ae45c7814fc", size = 134754, upload-time = "2025-04-29T23:29:15.338Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b0/1040c447fac5b91bc1e9c004b69ee50abb0c1ffd0d24406e1350c58a7fcb/orjson-3.10.18-cp312-cp312-win_arm64.whl", hash = "sha256:3d600be83fe4514944500fa8c2a0a77099025ec6482e8087d7659e891f23058a", size = 131218, upload-time = "2025-04-29T23:29:17.324Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f0/8aedb6574b68096f3be8f74c0b56d36fd94bcf47e6c7ed47a7bd1474aaa8/orjson-3.10.18-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:69c34b9441b863175cc6a01f2935de994025e773f814412030f269da4f7be147", size = 249087, upload-time = "2025-04-29T23:29:19.083Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/f7/7118f965541aeac6844fcb18d6988e111ac0d349c9b80cda53583e758908/orjson-3.10.18-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:1ebeda919725f9dbdb269f59bc94f861afbe2a27dce5608cdba2d92772364d1c", size = 133273, upload-time = "2025-04-29T23:29:20.602Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d9/839637cc06eaf528dd8127b36004247bf56e064501f68df9ee6fd56a88ee/orjson-3.10.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5adf5f4eed520a4959d29ea80192fa626ab9a20b2ea13f8f6dc58644f6927103", size = 136779, upload-time = "2025-04-29T23:29:22.062Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/6d/f226ecfef31a1f0e7d6bf9a31a0bbaf384c7cbe3fce49cc9c2acc51f902a/orjson-3.10.18-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7592bb48a214e18cd670974f289520f12b7aed1fa0b2e2616b8ed9e069e08595", size = 132811, upload-time = "2025-04-29T23:29:23.602Z" },
+ { url = "https://files.pythonhosted.org/packages/73/2d/371513d04143c85b681cf8f3bce743656eb5b640cb1f461dad750ac4b4d4/orjson-3.10.18-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f872bef9f042734110642b7a11937440797ace8c87527de25e0c53558b579ccc", size = 137018, upload-time = "2025-04-29T23:29:25.094Z" },
+ { url = "https://files.pythonhosted.org/packages/69/cb/a4d37a30507b7a59bdc484e4a3253c8141bf756d4e13fcc1da760a0b00cb/orjson-3.10.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0315317601149c244cb3ecef246ef5861a64824ccbcb8018d32c66a60a84ffbc", size = 138368, upload-time = "2025-04-29T23:29:26.609Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/ae/cd10883c48d912d216d541eb3db8b2433415fde67f620afe6f311f5cd2ca/orjson-3.10.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0da26957e77e9e55a6c2ce2e7182a36a6f6b180ab7189315cb0995ec362e049", size = 142840, upload-time = "2025-04-29T23:29:28.153Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/4c/2bda09855c6b5f2c055034c9eda1529967b042ff8d81a05005115c4e6772/orjson-3.10.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb70d489bc79b7519e5803e2cc4c72343c9dc1154258adf2f8925d0b60da7c58", size = 133135, upload-time = "2025-04-29T23:29:29.726Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4a/35971fd809a8896731930a80dfff0b8ff48eeb5d8b57bb4d0d525160017f/orjson-3.10.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9e86a6af31b92299b00736c89caf63816f70a4001e750bda179e15564d7a034", size = 134810, upload-time = "2025-04-29T23:29:31.269Z" },
+ { url = "https://files.pythonhosted.org/packages/99/70/0fa9e6310cda98365629182486ff37a1c6578e34c33992df271a476ea1cd/orjson-3.10.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c382a5c0b5931a5fc5405053d36c1ce3fd561694738626c77ae0b1dfc0242ca1", size = 413491, upload-time = "2025-04-29T23:29:33.315Z" },
+ { url = "https://files.pythonhosted.org/packages/32/cb/990a0e88498babddb74fb97855ae4fbd22a82960e9b06eab5775cac435da/orjson-3.10.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8e4b2ae732431127171b875cb2668f883e1234711d3c147ffd69fe5be51a8012", size = 153277, upload-time = "2025-04-29T23:29:34.946Z" },
+ { url = "https://files.pythonhosted.org/packages/92/44/473248c3305bf782a384ed50dd8bc2d3cde1543d107138fd99b707480ca1/orjson-3.10.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d808e34ddb24fc29a4d4041dcfafbae13e129c93509b847b14432717d94b44f", size = 137367, upload-time = "2025-04-29T23:29:36.52Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fd/7f1d3edd4ffcd944a6a40e9f88af2197b619c931ac4d3cfba4798d4d3815/orjson-3.10.18-cp313-cp313-win32.whl", hash = "sha256:ad8eacbb5d904d5591f27dee4031e2c1db43d559edb8f91778efd642d70e6bea", size = 142687, upload-time = "2025-04-29T23:29:38.292Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/03/c75c6ad46be41c16f4cfe0352a2d1450546f3c09ad2c9d341110cd87b025/orjson-3.10.18-cp313-cp313-win_amd64.whl", hash = "sha256:aed411bcb68bf62e85588f2a7e03a6082cc42e5a2796e06e72a962d7c6310b52", size = 134794, upload-time = "2025-04-29T23:29:40.349Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/f53038a5a72cc4fd0b56c1eafb4ef64aec9685460d5ac34de98ca78b6e29/orjson-3.10.18-cp313-cp313-win_arm64.whl", hash = "sha256:f54c1385a0e6aba2f15a40d703b858bedad36ded0491e55d35d905b2c34a4cc3", size = 131186, upload-time = "2025-04-29T23:29:41.922Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "24.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.51"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651, upload-time = "2025-03-26T03:06:12.05Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430, upload-time = "2025-03-26T03:04:26.436Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637, upload-time = "2025-03-26T03:04:27.932Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123, upload-time = "2025-03-26T03:04:30.659Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031, upload-time = "2025-03-26T03:04:31.977Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100, upload-time = "2025-03-26T03:04:33.45Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170, upload-time = "2025-03-26T03:04:35.542Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000, upload-time = "2025-03-26T03:04:37.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262, upload-time = "2025-03-26T03:04:39.532Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772, upload-time = "2025-03-26T03:04:41.109Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133, upload-time = "2025-03-26T03:04:42.544Z" },
+ { url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741, upload-time = "2025-03-26T03:04:44.06Z" },
+ { url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047, upload-time = "2025-03-26T03:04:45.983Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467, upload-time = "2025-03-26T03:04:47.699Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022, upload-time = "2025-03-26T03:04:49.195Z" },
+ { url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647, upload-time = "2025-03-26T03:04:50.595Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784, upload-time = "2025-03-26T03:04:51.791Z" },
+ { url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865, upload-time = "2025-03-26T03:04:53.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452, upload-time = "2025-03-26T03:04:54.624Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800, upload-time = "2025-03-26T03:04:55.844Z" },
+ { url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804, upload-time = "2025-03-26T03:04:57.158Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650, upload-time = "2025-03-26T03:04:58.61Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235, upload-time = "2025-03-26T03:05:00.599Z" },
+ { url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249, upload-time = "2025-03-26T03:05:02.11Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964, upload-time = "2025-03-26T03:05:03.599Z" },
+ { url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501, upload-time = "2025-03-26T03:05:05.107Z" },
+ { url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917, upload-time = "2025-03-26T03:05:06.59Z" },
+ { url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089, upload-time = "2025-03-26T03:05:08.1Z" },
+ { url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102, upload-time = "2025-03-26T03:05:09.982Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122, upload-time = "2025-03-26T03:05:11.408Z" },
+ { url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818, upload-time = "2025-03-26T03:05:12.909Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112, upload-time = "2025-03-26T03:05:14.289Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034, upload-time = "2025-03-26T03:05:15.616Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613, upload-time = "2025-03-26T03:05:16.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763, upload-time = "2025-03-26T03:05:18.607Z" },
+ { url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175, upload-time = "2025-03-26T03:05:19.85Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265, upload-time = "2025-03-26T03:05:21.654Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412, upload-time = "2025-03-26T03:05:23.147Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290, upload-time = "2025-03-26T03:05:24.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926, upload-time = "2025-03-26T03:05:26.459Z" },
+ { url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808, upload-time = "2025-03-26T03:05:28.188Z" },
+ { url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916, upload-time = "2025-03-26T03:05:29.757Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661, upload-time = "2025-03-26T03:05:31.472Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384, upload-time = "2025-03-26T03:05:32.984Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420, upload-time = "2025-03-26T03:05:34.496Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880, upload-time = "2025-03-26T03:05:36.256Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407, upload-time = "2025-03-26T03:05:37.799Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573, upload-time = "2025-03-26T03:05:39.193Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757, upload-time = "2025-03-26T03:05:40.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.11.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540, upload-time = "2025-04-29T20:38:55.02Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900, upload-time = "2025-04-29T20:38:52.724Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.33.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
+]
+
+[[package]]
+name = "pymysql"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678, upload-time = "2024-05-21T11:03:43.722Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972, upload-time = "2024-05-21T11:03:41.216Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
+ { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
+ { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2024.11.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781, upload-time = "2024-11-06T20:10:07.07Z" },
+ { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455, upload-time = "2024-11-06T20:10:09.117Z" },
+ { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759, upload-time = "2024-11-06T20:10:11.155Z" },
+ { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976, upload-time = "2024-11-06T20:10:13.24Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077, upload-time = "2024-11-06T20:10:15.37Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160, upload-time = "2024-11-06T20:10:19.027Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896, upload-time = "2024-11-06T20:10:21.85Z" },
+ { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997, upload-time = "2024-11-06T20:10:24.329Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725, upload-time = "2024-11-06T20:10:28.067Z" },
+ { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481, upload-time = "2024-11-06T20:10:31.612Z" },
+ { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896, upload-time = "2024-11-06T20:10:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138, upload-time = "2024-11-06T20:10:36.142Z" },
+ { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692, upload-time = "2024-11-06T20:10:38.394Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135, upload-time = "2024-11-06T20:10:40.367Z" },
+ { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
+ { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" },
+ { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" },
+ { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" },
+ { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" },
+ { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
+]
+
+[[package]]
+name = "requests-toolbelt"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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 = "rich"
+version = "14.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.41"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" },
+ { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "2.3.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/5f/28f45b1ff14bee871bacafd0a97213f7ec70e389939a80c60c0fb72a9fc9/sse_starlette-2.3.5.tar.gz", hash = "sha256:228357b6e42dcc73a427990e2b4a03c023e2495ecee82e14f07ba15077e334b2", size = 17511, upload-time = "2025-05-12T18:23:52.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/48/3e49cf0f64961656402c0023edbc51844fe17afe53ab50e958a6dbbbd499/sse_starlette-2.3.5-py3-none-any.whl", hash = "sha256:251708539a335570f10eaaa21d1848a10c42ee6dc3a9cf37ef42266cdb1c52a8", size = 10233, upload-time = "2025-05-12T18:23:50.722Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.46.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "tiktoken"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" },
+ { url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919, upload-time = "2025-02-14T06:02:37.494Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877, upload-time = "2025-02-14T06:02:39.516Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095, upload-time = "2025-02-14T06:02:41.791Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649, upload-time = "2025-02-14T06:02:43Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465, upload-time = "2025-02-14T06:02:45.046Z" },
+ { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.15.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559, upload-time = "2025-05-14T16:34:57.704Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258, upload-time = "2025-05-14T16:34:55.583Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+]
+
+[[package]]
+name = "typing-inspect"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
+]
+
+[[package]]
+name = "win32-setctime"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258, upload-time = "2025-04-17T00:45:14.661Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089, upload-time = "2025-04-17T00:42:39.602Z" },
+ { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706, upload-time = "2025-04-17T00:42:41.469Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719, upload-time = "2025-04-17T00:42:43.666Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972, upload-time = "2025-04-17T00:42:45.391Z" },
+ { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639, upload-time = "2025-04-17T00:42:47.552Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745, upload-time = "2025-04-17T00:42:49.406Z" },
+ { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178, upload-time = "2025-04-17T00:42:51.588Z" },
+ { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219, upload-time = "2025-04-17T00:42:53.674Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266, upload-time = "2025-04-17T00:42:55.49Z" },
+ { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873, upload-time = "2025-04-17T00:42:57.895Z" },
+ { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524, upload-time = "2025-04-17T00:43:00.094Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370, upload-time = "2025-04-17T00:43:02.242Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297, upload-time = "2025-04-17T00:43:04.189Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771, upload-time = "2025-04-17T00:43:06.609Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000, upload-time = "2025-04-17T00:43:09.01Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355, upload-time = "2025-04-17T00:43:11.311Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904, upload-time = "2025-04-17T00:43:13.087Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6f/514c9bff2900c22a4f10e06297714dbaf98707143b37ff0bcba65a956221/yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", size = 145030, upload-time = "2025-04-17T00:43:15.083Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9d/f88da3fa319b8c9c813389bfb3463e8d777c62654c7168e580a13fadff05/yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", size = 96894, upload-time = "2025-04-17T00:43:17.372Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/57/92e83538580a6968b2451d6c89c5579938a7309d4785748e8ad42ddafdce/yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", size = 94457, upload-time = "2025-04-17T00:43:19.431Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/ee/7ee43bd4cf82dddd5da97fcaddb6fa541ab81f3ed564c42f146c83ae17ce/yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", size = 343070, upload-time = "2025-04-17T00:43:21.426Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/12/b5eccd1109e2097bcc494ba7dc5de156e41cf8309fab437ebb7c2b296ce3/yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", size = 337739, upload-time = "2025-04-17T00:43:23.634Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/6b/0eade8e49af9fc2585552f63c76fa59ef469c724cc05b29519b19aa3a6d5/yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", size = 351338, upload-time = "2025-04-17T00:43:25.695Z" },
+ { url = "https://files.pythonhosted.org/packages/45/cb/aaaa75d30087b5183c7b8a07b4fb16ae0682dd149a1719b3a28f54061754/yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", size = 353636, upload-time = "2025-04-17T00:43:27.876Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9d/d9cb39ec68a91ba6e66fa86d97003f58570327d6713833edf7ad6ce9dde5/yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", size = 348061, upload-time = "2025-04-17T00:43:29.788Z" },
+ { url = "https://files.pythonhosted.org/packages/72/6b/103940aae893d0cc770b4c36ce80e2ed86fcb863d48ea80a752b8bda9303/yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", size = 334150, upload-time = "2025-04-17T00:43:31.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b2/986bd82aa222c3e6b211a69c9081ba46484cffa9fab2a5235e8d18ca7a27/yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", size = 362207, upload-time = "2025-04-17T00:43:34.099Z" },
+ { url = "https://files.pythonhosted.org/packages/14/7c/63f5922437b873795d9422cbe7eb2509d4b540c37ae5548a4bb68fd2c546/yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", size = 361277, upload-time = "2025-04-17T00:43:36.202Z" },
+ { url = "https://files.pythonhosted.org/packages/81/83/450938cccf732466953406570bdb42c62b5ffb0ac7ac75a1f267773ab5c8/yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", size = 364990, upload-time = "2025-04-17T00:43:38.551Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/de/af47d3a47e4a833693b9ec8e87debb20f09d9fdc9139b207b09a3e6cbd5a/yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", size = 374684, upload-time = "2025-04-17T00:43:40.481Z" },
+ { url = "https://files.pythonhosted.org/packages/62/0b/078bcc2d539f1faffdc7d32cb29a2d7caa65f1a6f7e40795d8485db21851/yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", size = 382599, upload-time = "2025-04-17T00:43:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/74/a9/4fdb1a7899f1fb47fd1371e7ba9e94bff73439ce87099d5dd26d285fffe0/yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", size = 378573, upload-time = "2025-04-17T00:43:44.797Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/be/29f5156b7a319e4d2e5b51ce622b4dfb3aa8d8204cd2a8a339340fbfad40/yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", size = 86051, upload-time = "2025-04-17T00:43:47.076Z" },
+ { url = "https://files.pythonhosted.org/packages/52/56/05fa52c32c301da77ec0b5f63d2d9605946fe29defacb2a7ebd473c23b81/yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", size = 92742, upload-time = "2025-04-17T00:43:49.193Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/2f/422546794196519152fc2e2f475f0e1d4d094a11995c81a465faf5673ffd/yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", size = 163575, upload-time = "2025-04-17T00:43:51.533Z" },
+ { url = "https://files.pythonhosted.org/packages/90/fc/67c64ddab6c0b4a169d03c637fb2d2a212b536e1989dec8e7e2c92211b7f/yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", size = 106121, upload-time = "2025-04-17T00:43:53.506Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/00/29366b9eba7b6f6baed7d749f12add209b987c4cfbfa418404dbadc0f97c/yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", size = 103815, upload-time = "2025-04-17T00:43:55.41Z" },
+ { url = "https://files.pythonhosted.org/packages/28/f4/a2a4c967c8323c03689383dff73396281ced3b35d0ed140580825c826af7/yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", size = 408231, upload-time = "2025-04-17T00:43:57.825Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a1/66f7ffc0915877d726b70cc7a896ac30b6ac5d1d2760613603b022173635/yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", size = 390221, upload-time = "2025-04-17T00:44:00.526Z" },
+ { url = "https://files.pythonhosted.org/packages/41/15/cc248f0504610283271615e85bf38bc014224122498c2016d13a3a1b8426/yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", size = 411400, upload-time = "2025-04-17T00:44:02.853Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/f0823d7e092bfb97d24fce6c7269d67fcd1aefade97d0a8189c4452e4d5e/yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", size = 411714, upload-time = "2025-04-17T00:44:04.904Z" },
+ { url = "https://files.pythonhosted.org/packages/83/70/be418329eae64b9f1b20ecdaac75d53aef098797d4c2299d82ae6f8e4663/yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", size = 404279, upload-time = "2025-04-17T00:44:07.721Z" },
+ { url = "https://files.pythonhosted.org/packages/19/f5/52e02f0075f65b4914eb890eea1ba97e6fd91dd821cc33a623aa707b2f67/yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", size = 384044, upload-time = "2025-04-17T00:44:09.708Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/36/b0fa25226b03d3f769c68d46170b3e92b00ab3853d73127273ba22474697/yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", size = 416236, upload-time = "2025-04-17T00:44:11.734Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3a/54c828dd35f6831dfdd5a79e6c6b4302ae2c5feca24232a83cb75132b205/yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", size = 402034, upload-time = "2025-04-17T00:44:13.975Z" },
+ { url = "https://files.pythonhosted.org/packages/10/97/c7bf5fba488f7e049f9ad69c1b8fdfe3daa2e8916b3d321aa049e361a55a/yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", size = 407943, upload-time = "2025-04-17T00:44:16.052Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a4/022d2555c1e8fcff08ad7f0f43e4df3aba34f135bff04dd35d5526ce54ab/yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", size = 423058, upload-time = "2025-04-17T00:44:18.547Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/f6/0873a05563e5df29ccf35345a6ae0ac9e66588b41fdb7043a65848f03139/yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", size = 423792, upload-time = "2025-04-17T00:44:20.639Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/35/43fbbd082708fa42e923f314c24f8277a28483d219e049552e5007a9aaca/yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", size = 422242, upload-time = "2025-04-17T00:44:22.851Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/f7/f0f2500cf0c469beb2050b522c7815c575811627e6d3eb9ec7550ddd0bfe/yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", size = 93816, upload-time = "2025-04-17T00:44:25.491Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/93/f73b61353b2a699d489e782c3f5998b59f974ec3156a2050a52dfd7e8946/yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", size = 101093, upload-time = "2025-04-17T00:44:27.418Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload-time = "2025-04-17T00:45:12.199Z" },
+]
+
+[[package]]
+name = "zstandard"
+version = "0.23.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" },
+ { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" },
+ { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" },
+ { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" },
+ { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" },
+ { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975, upload-time = "2024-07-15T00:16:16.005Z" },
+ { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448, upload-time = "2024-07-15T00:16:17.897Z" },
+ { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269, upload-time = "2024-07-15T00:16:20.136Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228, upload-time = "2024-07-15T00:16:23.398Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891, upload-time = "2024-07-15T00:16:26.391Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310, upload-time = "2024-07-15T00:16:29.018Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912, upload-time = "2024-07-15T00:16:31.871Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946, upload-time = "2024-07-15T00:16:34.593Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994, upload-time = "2024-07-15T00:16:36.887Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681, upload-time = "2024-07-15T00:16:39.709Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239, upload-time = "2024-07-15T00:16:41.83Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149, upload-time = "2024-07-15T00:16:44.287Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392, upload-time = "2024-07-15T00:16:46.423Z" },
+ { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299, upload-time = "2024-07-15T00:16:49.053Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862, upload-time = "2024-07-15T00:16:51.003Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578, upload-time = "2024-07-15T00:16:53.135Z" },
+]
diff --git a/pyproject.toml b/pyproject.toml
index cfd0307..104abe5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,19 @@
[project]
name = "alibabacloud-rds-openapi-mcp-server"
-version = "1.7.4"
+version = "3.0.0"
description = "MCP server for RDS Services via OPENAPI."
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"alibabacloud-bssopenapi20171214>=5.0.0",
+ "alibabacloud-das20200116==2.7.1",
"alibabacloud-rds20140815>=11.0.0",
"alibabacloud-vpc20160428>=6.11.4",
"httpx>=0.28.1",
"mcp[cli]>=1.6.0",
+# "psycopg2>=2.9.10",
+ "pymysql>=1.1.1",
+ "pyodbc>=5.2.0",
]
license = "Apache-2.0"
diff --git a/src/alibabacloud_rds_openapi_mcp_server/core/__init__.py b/src/alibabacloud_rds_openapi_mcp_server/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/alibabacloud_rds_openapi_mcp_server/core/context.py b/src/alibabacloud_rds_openapi_mcp_server/core/context.py
new file mode 100644
index 0000000..a6de789
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/core/context.py
@@ -0,0 +1,31 @@
+"""
+Global Singleton Accessor for the RdsMCP Instance.
+
+This module provides a controlled, application-wide access point to the single
+RdsMCP instance. It implements a singleton-like pattern to ensure that all
+components, such as tools and prompts, can interact with the same central engine.
+
+ All other modules (e.g., tool or prompt definition files) MUST use the
+ `global_mcp_instance()` function to retrieve the shared instance. This is
+ the only approved way to access the central MCP engine from a component.
+"""
+
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from .mcp import RdsMCP
+
+_mcp_instance: Optional["RdsMCP"] = None
+
+def set_mcp_instance(mcp: "RdsMCP") -> None:
+ """Sets the global instance of the RdsMCP server."""
+ global _mcp_instance
+ if _mcp_instance is not None:
+ print("Warning: RdsMCP instance is being reset.")
+ _mcp_instance = mcp
+
+def global_mcp_instance() -> "RdsMCP":
+ """Retrieves the globally available RdsMCP server instance."""
+ if _mcp_instance is None:
+ raise RuntimeError("The RdsMCP instance has not been set yet.")
+ return _mcp_instance
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/core/mcp.py b/src/alibabacloud_rds_openapi_mcp_server/core/mcp.py
new file mode 100644
index 0000000..fe0a68b
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/core/mcp.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any, Callable, Dict, List, Tuple
+
+from mcp.server.fastmcp import FastMCP
+import os
+from enum import Enum
+
+from mcp.server.fastmcp.prompts import Prompt
+from .context import set_mcp_instance
+
+
+class _ComponentType(Enum):
+ """Defines the valid types of components that can be registered."""
+ TOOL = 'tool'
+ PROMPT = 'prompt'
+ RESOURCE = 'resource'
+
+@dataclass
+class _RegistrableItem:
+ func: Callable
+ args: Tuple[Any, ...]
+ kwargs: Dict[str, Any]
+ group: str
+ item_type: _ComponentType
+
+
+
+class RdsMCP(FastMCP):
+ """ An enhanced FastMCP that supports grouping and delayed registration of
+ components like tools, prompts, and resources.
+
+ This class introduces a two-phase workflow for component management:
+ 1. **Definition Phase:** Use decorators like `@mcp.tool()` to *define* a
+ component and assign it to a logical group (e.g., 'database', 'api').
+ At this stage, the component is only cataloged internally and is NOT yet
+ active in the underlying FastMCP system.
+ 2. **Activation Phase:** Call the `.activate()` method with a list of group
+ names. Only the components from these specified groups are then validated
+ and formally registered with the FastMCP, making them live.
+
+ **Usage Workflow:**
+ The expected lifecycle is as follows:
+ 1. Instantiate `RdsMCP`: `mcp = RdsMCP()`
+ 2. Define components using decorators: `@mcp.tool(...)`
+ 3. Finalize the setup by calling the activation method:
+ `mcp.activate(enabled_groups=['group1', 'group2'])`
+ """
+
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
+ """Initializes the engine with an internal list for pending registrations."""
+ self._pending_registrations: List[_RegistrableItem] = []
+ self._is_activated = False
+ super().__init__(*args, **kwargs)
+ set_mcp_instance(self)
+
+
+ '''
+ Decorate a tool and store it for later registration.
+
+ This method overrides the mcp.tool() registration mechanism.
+ All tools not explicitly assigned to specific groups will be automatically categorized into the
+ "rds" group. This ensures that when launching without tools parameters, these default tools are automatically loaded.
+ '''
+ def tool(self, *dargs: Any, group: str = 'rds', **dkwargs: Any) -> Callable:
+ #Decorator used without parentheses, e.g., @mcp.tool
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
+ func = dargs[0]
+ item = _RegistrableItem(
+ func=func, group=group, item_type=_ComponentType.TOOL,
+ args=(), kwargs={}
+ )
+ self._pending_registrations.append(item)
+ return func
+
+ #Decorator used with parentheses, e.g., @mcp.tool(group='db')
+ def decorator(fn: Callable) -> Callable:
+ item = _RegistrableItem(
+ func=fn, group=group, item_type=_ComponentType.TOOL,
+ args=dargs, kwargs=dkwargs
+ )
+ self._pending_registrations.append(item)
+ return fn
+
+ return decorator
+
+ def prompt(self, *dargs: Any, group: str = 'rds', **dkwargs: Any) -> Callable:
+ if len(dargs) == 1 and callable(dargs[0]) and not dkwargs:
+ func = dargs[0]
+ item = _RegistrableItem(
+ func=func, group=group, item_type=_ComponentType.PROMPT,
+ args=(), kwargs={}
+ )
+ self._pending_registrations.append(item)
+ return func
+
+ def decorator(fn: Callable) -> Callable:
+ item = _RegistrableItem(
+ func=fn, group=group, item_type=_ComponentType.PROMPT,
+ args=dargs, kwargs=dkwargs
+ )
+ self._pending_registrations.append(item)
+ return fn
+
+ return decorator
+
+ def activate(self, enabled_groups: list[str]) -> None:
+ """
+ Finalizes the setup by activating all deferred components.
+ """
+ if self._is_activated:
+ print("Warning: MCP engine has already been activated. Ignoring subsequent calls.")
+ return
+
+ self._validate_groups(enabled_groups)
+ print(f"\n--- Activating Component Groups: {enabled_groups} ---")
+
+ activated_items: List[_RegistrableItem] = []
+ for item in self._pending_registrations:
+ if item.group in enabled_groups:
+ print(f"Activating {item.item_type.value} '{item.func.__name__}' from group '{item.group}'...")
+
+ final_kwargs = item.kwargs.copy()
+ final_kwargs.setdefault('name', item.func.__name__)
+
+ if item.item_type == _ComponentType.TOOL:
+ super().add_tool(item.func, *item.args, **final_kwargs)
+
+ elif item.item_type == _ComponentType.PROMPT:
+ prompt_object = Prompt(
+ fn=item.func,
+ **final_kwargs
+ )
+ super().add_prompt(prompt_object)
+
+ activated_items.append(item)
+
+ self._is_activated = True
+ print("--- Activation Complete ---")
+ self._run_debug_output(enabled_groups, activated_items)
+
+ def _validate_groups(self, enabled_groups: list[str]) -> None:
+ """Checks if all requested groups are valid before activation."""
+ all_defined_groups = {item.group for item in self._pending_registrations}
+ invalid_groups = set(enabled_groups) - all_defined_groups
+ if invalid_groups:
+ raise ValueError(
+ f"Unknown group(s): {sorted(list(invalid_groups))}. "
+ f"Available groups: {sorted(list(all_defined_groups))}"
+ )
+
+ def _run_debug_output(self, enabled_groups: list[str], activated_items: list[_RegistrableItem]):
+ """Prints debug information for all component types if the env var is set."""
+ if os.getenv('TOOLSET_DEBUG', '').lower() in ('1', 'true', 'yes', 'on'):
+ all_groups = sorted(list({item.group for item in self._pending_registrations}))
+
+ print("\n--- COMPONENT DEBUG OUTPUT ---")
+ print(f"All defined groups: {all_groups}")
+ print(f"Enabled groups for this activation: {sorted(enabled_groups)}")
+
+ output_by_type: Dict[str, List[_RegistrableItem]] = {}
+ for item in activated_items:
+ output_by_type.setdefault(item.item_type.value.upper() + 'S', []).append(item)
+
+ if not output_by_type:
+ print("No components were activated.")
+
+ for item_type_str, items in sorted(output_by_type.items()):
+ print(f"Activated {item_type_str}:")
+ grouped_items: Dict[str, List[str]] = {}
+ for item in items:
+ grouped_items.setdefault(item.group, []).append(item.func.__name__)
+
+ for group in sorted(grouped_items.keys()):
+ print(f" - Group: {group}")
+ for item_name in sorted(grouped_items[group]):
+ print(f" • {item_name}")
+
+ print("----------------------------\n")
+
diff --git a/src/alibabacloud_rds_openapi_mcp_server/db_service.py b/src/alibabacloud_rds_openapi_mcp_server/db_service.py
new file mode 100644
index 0000000..a7ac415
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/db_service.py
@@ -0,0 +1,213 @@
+import json
+import random
+import socket
+import string
+
+import pymysql
+from alibabacloud_rds20140815 import models as rds_20140815_models
+
+from utils import get_rds_client
+
+
+def random_str(length=8):
+ chars = string.ascii_lowercase + string.digits
+ return ''.join(random.choice(chars) for _ in range(length))
+
+
+def random_password(length=32):
+ U = string.ascii_uppercase
+ L = string.ascii_lowercase
+ D = string.digits
+ S = '_!@#$%^&*()-+='
+ pool = U + L + D + S
+ for _ in range(1000):
+ # 确保至少三类
+ chosen = [
+ random.choice(U),
+ random.choice(L),
+ random.choice(D),
+ random.choice(S)
+ ]
+ rest = [random.choice(pool) for _ in range(length - len(chosen))]
+ pw = ''.join(random.sample(chosen + rest, length))
+ return pw
+
+
+def test_connect(host, port, timeout=1):
+ try:
+ with socket.create_connection((host, int(port)), timeout):
+ return True
+ except Exception:
+ return False
+
+
+class DBService:
+ """
+ Create a read-only account, execute the SQL statements, and automatically delete the account afterward.
+ """
+ def __init__(self,
+ region_id,
+ instance_id,
+ database=None, ):
+ self.instance_id = instance_id
+ self.database = database
+ self.region_id = region_id
+
+ self.__db_type = None
+ self.__account_name = None
+ self.__account_password = None
+ self.__host = None
+ self.__port = None
+ self.__client = get_rds_client(region_id)
+ self.__db_conn = None
+
+ def __enter__(self):
+ self._get_db_instance_info()
+ self._create_temp_account()
+ if self.database:
+ self._grant_privilege()
+ self.__db_conn = DBConn(self)
+ self.__db_conn.connect()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.__db_conn is not None:
+ self.__db_conn.close()
+ self._delete_account()
+ self.__client = None
+
+ def _get_db_instance_info(self):
+ req = rds_20140815_models.DescribeDBInstanceAttributeRequest(
+ dbinstance_id=self.instance_id,
+ )
+ self.__client.describe_dbinstance_attribute(req)
+ resp = self.__client.describe_dbinstance_attribute(req)
+ self.db_type = resp.body.items.dbinstance_attribute[0].engine.lower()
+
+ req = rds_20140815_models.DescribeDBInstanceNetInfoRequest(
+ dbinstance_id=self.instance_id,
+ )
+ resp = self.__client.describe_dbinstance_net_info(req)
+
+ # 取支持的地址:
+ vpc_host, vpc_port, public_host, public_port, dbtype = None, None, None, None, None
+ net_infos = resp.body.dbinstance_net_infos.dbinstance_net_info
+ for item in net_infos:
+ if 'Private' == item.iptype:
+ vpc_host = item.connection_string
+ vpc_port = int(item.port)
+ elif 'Public' in item.iptype:
+ public_host = item.connection_string
+ public_port = int(item.port)
+
+ if vpc_host and test_connect(vpc_host, vpc_port):
+ self.host = vpc_host
+ self.port = vpc_port
+ elif public_host and test_connect(public_host, public_port):
+ self.host = public_host
+ self.port = public_port
+ else:
+ raise Exception('connection db failed.')
+
+ def _create_temp_account(self):
+ self.account_name = 'mcp_' + random_str(10)
+ self.account_password = random_password(32)
+ request = rds_20140815_models.CreateAccountRequest(
+ dbinstance_id=self.instance_id,
+ account_name=self.account_name,
+ account_password=self.account_password,
+ account_description="Created by mcp for execute sql."
+ )
+ self.__client.create_account(request)
+
+ def _grant_privilege(self):
+ req = rds_20140815_models.GrantAccountPrivilegeRequest(
+ dbinstance_id=self.instance_id,
+ account_name=self.account_name,
+ dbname=self.database,
+ account_privilege="ReadOnly" if self.db_type.lower() in ('mysql', 'postgresql') else "DBOwner"
+ )
+ self.__client.grant_account_privilege(req)
+
+ def _delete_account(self):
+ if not self.account_name:
+ return
+ req = rds_20140815_models.DeleteAccountRequest(
+ dbinstance_id=self.instance_id,
+ account_name=self.account_name
+ )
+ self.__client.delete_account(req)
+
+ def execute_sql(self, sql):
+ return self.__db_conn.execute_sql(sql)
+
+ @property
+ def user(self):
+ return self.account_name
+
+ @property
+ def password(self):
+ return self.account_password
+
+
+class DBConn:
+ def __init__(self, db_service: DBService):
+ self.dbtype = db_service.db_type
+ self.host = db_service.host
+ self.port = db_service.port
+ self.user = db_service.user
+ self.password = db_service.password
+ self.database = db_service.database
+ self.conn = None
+
+ def connect(self):
+ if self.conn is not None:
+ return
+
+ if self.dbtype == 'mysql':
+ self.conn = pymysql.connect(
+ host=self.host, port=self.port,
+ user=self.user, password=self.password,
+ db=self.database, charset='utf8mb4',
+ cursorclass=pymysql.cursors.DictCursor
+ )
+ elif self.dbtype == 'postgresql' or self.dbtype == 'pg':
+ import psycopg2
+ self.conn = psycopg2.connect(
+ host=self.host, port=self.port,
+ user=self.user, password=self.password,
+ dbname=self.database
+ )
+ elif self.dbtype == 'sqlserver':
+ import pyodbc
+ driver = 'ODBC Driver 17 for SQL Server'
+ conn_str = (
+ f'DRIVER={{{driver}}};SERVER={self.host},{self.port};'
+ f'UID={self.user};PWD={self.password};DATABASE={self.database}'
+ )
+ self.conn = pyodbc.connect(conn_str)
+ else:
+ raise ValueError('Unsupported dbtype')
+
+ def close(self):
+ if self.conn is not None:
+ try:
+ self.conn.close()
+ except Exception as e:
+ print(e)
+ self.conn = None
+
+ def execute_sql(self, sql):
+ cursor = self.conn.cursor()
+ cursor.execute(sql)
+ columns = [desc[0] for desc in cursor.description]
+ rows = cursor.fetchall()
+ if self.dbtype == 'mysql':
+ result = [dict(row) for row in rows]
+ elif self.dbtype == 'postgresql' or self.dbtype == 'pg':
+ result = [dict(zip(columns, row)) for row in rows]
+ elif self.dbtype == 'sqlserver':
+ result = [dict(zip(columns, row)) for row in rows]
+ else:
+ result = []
+ return json.dumps(result, ensure_ascii=False)
diff --git a/src/alibabacloud_rds_openapi_mcp_server/prompts/__init__.py b/src/alibabacloud_rds_openapi_mcp_server/prompts/__init__.py
new file mode 100644
index 0000000..a09c98e
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/prompts/__init__.py
@@ -0,0 +1,20 @@
+import os
+import pkgutil
+import importlib
+from typing import Any, Callable
+
+from ..core.context import global_mcp_instance
+
+
+def prompt(*dargs: Any, **dkwargs: Any) -> Callable:
+ mcp_instance = global_mcp_instance()
+ return mcp_instance.prompt(*dargs, **dkwargs)
+
+print("Initializing and discovering 'prompts' package...")
+
+for _, module_name, _ in pkgutil.iter_modules(__path__, __name__ + '.'):
+ try:
+ importlib.import_module(module_name)
+ print(f" ✓ Discovered prompt module: {module_name}")
+ except Exception as e:
+ print(f" ✗ Failed to discover prompt module {module_name}: {e}")
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/prompts/system_prompts.py b/src/alibabacloud_rds_openapi_mcp_server/prompts/system_prompts.py
new file mode 100644
index 0000000..1ed9e6b
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/prompts/system_prompts.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+from . import prompt
+from typing import List, Dict
+
+
+@prompt(group="rds_custom")
+def rds_custom_system_prompt() -> List[Dict[str, str]]:
+ """
+ Provides a system prompt that defines the role, goals, and core instructions
+ for the CloudOps-Assistant AI. This sets the context and operating rules for the assistant.
+
+ Returns:
+ A list containing a dictionary that defines the 'system' role and its content.
+ """
+ return [
+ {
+ "role": "system",
+ "content": r'''
+# Role and Goal
+You are CloudOps-Assistant, an AI assistant specializing in Alibaba Cloud RDS Custom instance management. Your primary goal is to help users manage their database infrastructure safely and efficiently.
+
+# Core Instructions
+1. **Clarity First:** Before executing any command, repeat the user's intention and the exact parameters you will use.
+2. **Safety Confirmation:** For any action that modifies or deletes a resource (e.g., stop, reboot, resize, modify), you MUST ask for the user's explicit confirmation before proceeding.
+3. **Structured Output:** Present complex information, like lists of instances or metrics, in a clean, readable format using Markdown tables or lists.
+4. **Parameter Requirement:** If a user's request is missing mandatory parameters (like `region_id`), you must ask for them clearly. Do not make assumptions about the region.
+
+'''
+ }
+ ]
+
+
+@prompt(group="rds_custom")
+def rds_custom_sql_server_health_check_template(instance_id: str, region_id: str) -> str:
+ """
+ Generates a structured, multi-step instructional string to perform
+ a comprehensive health check on a specified RDS instance.
+
+ Args:
+ instance_id: The ID of the RDS instance to be checked.
+ region_id: The region where the RDS instance is located.
+
+ Returns:
+ A formatted string that serves as a detailed input prompt for the user,
+ outlining the steps for a health check.
+ """
+ # The function returns a string that will be used as the 'input' from the user.
+ # {instance_id} and {region_id} are placeholders that the MCP framework will populate.
+ return f"""
+Please perform a complete health check for the RDS instance with the following steps:
+
+1. **Query Basic Instance Information**: Use the `describe_rc_instance_attribute` tool to query the detailed attributes of instance `{instance_id}` in region `{region_id}`, focusing on its status, specifications, and creation time.
+2. **Check CPU and Memory**: Use the `describe_rc_metric_list` tool to retrieve the `CPUUtilization` and `MemoryUsage` metrics for the instance over the last 3 hours.
+3. **Check Disk Space**: Use the `describe_rc_metric_list` tool to retrieve the `DiskUsage` metric for the instance.
+4. **Summary Report**: Based on all the information gathered above, generate a health summary report for me and point out any potential risks.
+"""
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/server.py b/src/alibabacloud_rds_openapi_mcp_server/server.py
index 1169c78..b155f2e 100644
--- a/src/alibabacloud_rds_openapi_mcp_server/server.py
+++ b/src/alibabacloud_rds_openapi_mcp_server/server.py
@@ -2,29 +2,45 @@
import logging
import os
import sys
-import csv
+import argparse
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(current_dir)
+import time
from datetime import datetime
-from typing import Dict, Any, List
+from typing import Dict, Any, List, Optional
from alibabacloud_bssopenapi20171214 import models as bss_open_api_20171214_models
+from alibabacloud_openapi_util.client import Client as OpenApiUtilClient
from alibabacloud_rds20140815 import models as rds_20140815_models
+from alibabacloud_tea_openapi import models as open_api_models
+from alibabacloud_tea_util import models as util_models
from alibabacloud_vpc20160428 import models as vpc_20160428_models
-from mcp.server.fastmcp import FastMCP
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
+
+src_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if src_path not in sys.path:
+ sys.path.insert(0, src_path)
+
+from db_service import DBService
from utils import (transform_to_iso_8601,
transform_to_datetime,
transform_perf_key,
json_array_to_csv,
get_rds_client,
get_vpc_client,
- get_bill_client)
+ get_bill_client, get_das_client, convert_datetime_to_timestamp)
+from alibabacloud_rds_openapi_mcp_server.core.mcp import RdsMCP
+DEFAULT_TOOL_GROUP = 'rds'
logger = logging.getLogger(__name__)
-
-mcp = FastMCP("Alibaba Cloud RDS OPENAPI")
-
+mcp = RdsMCP("Alibaba Cloud RDS OPENAPI", port=os.getenv("SERVER_PORT", 8000))
+try:
+ import alibabacloud_rds_openapi_mcp_server.tools
+ import alibabacloud_rds_openapi_mcp_server.prompts
+except Exception as e:
+ print(f"ERROR: Failed to import component packages: {e}")
class OpenAPIError(Exception):
"""Custom exception for RDS OpenAPI related errors."""
@@ -46,7 +62,11 @@ async def describe_db_instances(region_id: str):
page_size=100
)
response = client.describe_dbinstances(request)
- return json_array_to_csv(response.body.items.dbinstance)
+
+ res = json_array_to_csv(response.body.items.dbinstance)
+ if not res:
+ return "No RDS instances found."
+ return res
except Exception as e:
raise e
@@ -378,7 +398,8 @@ async def create_db_instance(
serverless_config: Dict[str, Any] = None,
table_names_case_sensitive: bool = False,
db_time_zone: str = None,
- connection_string: str = None
+ connection_string: str = None,
+ db_param_group_id: str = None,
) -> Dict[str, Any]:
"""Create an RDS instance.
@@ -410,6 +431,7 @@ async def create_db_instance(
table_names_case_sensitive: Are table names case-sensitive.
db_time_zone: the db instance time zone.
connection_string: the connection string for db instance.
+ db_param_group_id: the db param group id for db instance.
Returns:
Dict[str, Any]: Response containing the created instance details.
"""
@@ -426,7 +448,8 @@ async def create_db_instance(
instance_network_type=instance_network_type,
dbis_ignore_case=str(not table_names_case_sensitive).lower(),
dbtime_zone=db_time_zone,
- connection_string=connection_string
+ connection_string=connection_string,
+ dbparam_group_id=db_param_group_id
)
# Add optional parameters
@@ -1232,10 +1255,284 @@ async def get_current_time() -> Dict[str, Any]:
raise Exception(f"Failed to get the current time: {str(e)}")
-def main():
- mcp.run(transport=os.getenv('SERVER_TRANSPORT', 'stdio'))
+@mcp.tool()
+async def modify_security_ips(
+ region_id: str,
+ dbinstance_id: str,
+ security_ips: str,
+ whitelist_network_type: str = "MIX",
+ security_ip_type: str = None,
+ dbinstance_ip_array_name: str = None,
+ dbinstance_ip_array_attribute: str = None,
+ client_token: str = None
+) -> Dict[str, Any]:
+ """modify security ips。
+
+ Args:
+ region_id (str): RDS instance region id.
+ dbinstance_id (str): RDS instance id.
+ security_ips (str): security ips list, separated by commas.
+ whitelist_network_type (str, optional): whitelist network type.
+ - MIX: mixed network type
+ - Classic: classic network
+ - VPC: vpc
+ default value: MIX
+ security_ip_type (str, optional): security ip type.
+ - normal: normal security ip
+ - hidden: hidden security ip
+ dbinstance_ip_array_name (str, optional): security ip array name.
+ dbinstance_ip_array_attribute (str, optional): security ip array attribute.
+ - hidden: hidden security ip
+ - normal: normal security ip
+ client_token (str, optional): idempotency token, max 64 ascii characters.
+
+ Returns:
+ Dict[str, Any]: response contains request id.
+ """
+ try:
+ # initialize client
+ client = get_rds_client(region_id)
+
+ # create request
+ request = rds_20140815_models.ModifySecurityIpsRequest(
+ dbinstance_id=dbinstance_id,
+ security_ips=security_ips,
+ whitelist_network_type=whitelist_network_type
+ )
+
+ # add optional parameters
+ if security_ip_type:
+ request.security_ip_type = security_ip_type
+ if dbinstance_ip_array_name:
+ request.dbinstance_ip_array_name = dbinstance_ip_array_name
+ if dbinstance_ip_array_attribute:
+ request.dbinstance_ip_array_attribute = dbinstance_ip_array_attribute
+ if client_token:
+ request.client_token = client_token
+
+ # send api request
+ response = client.modify_security_ips(request)
+ return response.body.to_map()
+
+ except Exception as e:
+ logger.error(f"modify security ips error: {str(e)}")
+ raise OpenAPIError(f"modify rds instance security ips failed: {str(e)}")
+
+
+@mcp.tool()
+async def restart_db_instance(
+ region_id: str,
+ dbinstance_id: str,
+ effective_time: str = "Immediate",
+ switch_time: str = None,
+ client_token: str = None
+) -> Dict[str, Any]:
+ """Restart an RDS instance.
+
+ Args:
+ region_id (str): The region ID of the RDS instance.
+ dbinstance_id (str): The ID of the RDS instance.
+ effective_time (str, optional): When to restart the instance. Options:
+ - Immediate: Restart immediately
+ - MaintainTime: Restart during maintenance window
+ - ScheduleTime: Restart at specified time
+ Default: Immediate
+ switch_time (str, optional): The scheduled restart time in format: yyyy-MM-ddTHH:mm:ssZ (UTC time).
+ Required when effective_time is ScheduleTime.
+ client_token (str, optional): Idempotency token, max 64 ASCII characters.
+
+ Returns:
+ Dict[str, Any]: Response containing the request ID.
+ """
+ try:
+ # Initialize client
+ client = get_rds_client(region_id)
+
+ # Create request
+ request = rds_20140815_models.RestartDBInstanceRequest(
+ dbinstance_id=dbinstance_id
+ )
+
+ # Add optional parameters
+ if effective_time:
+ request.effective_time = effective_time
+ if switch_time:
+ request.switch_time = switch_time
+ if client_token:
+ request.client_token = client_token
+
+ # Make the API request
+ response = client.restart_dbinstance(request)
+ return response.body.to_map()
+
+ except Exception as e:
+ logger.error(f"Error occurred while restarting instance: {str(e)}")
+ raise OpenAPIError(f"Failed to restart RDS instance: {str(e)}")
+
+
+@mcp.tool()
+async def describe_sql_insight_statistic(
+ dbinstance_id: str,
+ start_time: str,
+ end_time: str,
+) -> Dict[str, Any]:
+ """
+ Query SQL Log statistics, including SQL cost time, execution times, and account.
+ Args:
+ dbinstance_id (str): The ID of the RDS instance.
+ start_time(str): the start time of sql insight statistic. e.g. 2025-06-06 20:00:00
+ end_time(str): the end time of sql insight statistic. e.g. 2025-06-06 20:10:00
+ Returns:
+ the sql insight statistic information in csv format.
+ """
+ def _descirbe(order_by: str):
+ try:
+ # Initialize client
+ client = get_das_client()
+ page_no = 1
+ page_size = 50
+ total = page_no * page_size + 1
+ result = []
+ while total > page_no * page_size:
+ state = "RUNNING"
+ job_id = ""
+ while state == "RUNNING":
+ body = {
+ "InstanceId": dbinstance_id,
+ "OrderBy": order_by,
+ "Asc": False,
+ "PageNo": 1,
+ "PageSize": 10,
+ "TemplateId": "",
+ "DbName": "",
+ "StartTime": convert_datetime_to_timestamp(start_time),
+ "EndTime": convert_datetime_to_timestamp(end_time),
+ "JobId": job_id
+ }
+ req = open_api_models.OpenApiRequest(
+ query=OpenApiUtilClient.query({}),
+ body=OpenApiUtilClient.parse_to_map(body)
+ )
+ params = open_api_models.Params(
+ action='DescribeSqlInsightStatistic',
+ version='2020-01-16',
+ protocol='HTTPS',
+ pathname='/',
+ method='POST',
+ auth_type='AK',
+ style='RPC',
+ req_body_type='formData',
+ body_type='json'
+ )
+ response = client.call_api(params, req, util_models.RuntimeOptions())
+ response_data = response['body']['Data']
+ state = response_data['State']
+ if state == "RUNNING":
+ job_id = response_data['ResultId']
+ time.sleep(1)
+ continue
+ if state == "SUCCESS":
+ result.extend(response_data['Data']['List'])
+ total = response_data['Data']['Total']
+ page_no = page_no + 1
+
+ return json_array_to_csv(result)
+ except Exception as e:
+ logger.error(f"Error occurred: {str(e)}")
+ raise e
+ rt_rate = _descirbe("rtRate")
+ count_rate = _descirbe("countRate")
+ return {
+ "sql_log_order_by_rt_rate": rt_rate,
+ "sql_log_order_by_count_rate": count_rate
+ }
+
+
+@mcp.tool()
+async def show_engine_innodb_status(
+ dbinstance_id: str,
+ region_id: str
+) -> str:
+ """
+ show engine innodb status in db.
+ Args:
+ dbinstance_id (str): The ID of the RDS instance.
+ region_id(str): the region id of instance.
+ Returns:
+ the sql result.
+ """
+ try:
+ with DBService(region_id, dbinstance_id) as service:
+ return service.execute_sql("show engine innodb status")
+ except Exception as e:
+ logger.error(f"Error occurred: {str(e)}")
+ raise e
+
+@mcp.tool()
+async def show_create_table(
+ region_id: str,
+ dbinstance_id: str,
+ db_name: str,
+ table_name: str
+) -> str:
+ """
+ show create table db_name.table_name
+ Args:
+ dbinstance_id (str): The ID of the RDS instance.
+ region_id(str): the region id of instance.
+ db_name(str): the db name for table.
+ table_name(str): the table name.
+ Returns:
+ the sql result.
+ """
+ try:
+ with DBService(region_id, dbinstance_id, db_name) as service:
+ return service.execute_sql(f"show create table {db_name}.{table_name}")
+ except Exception as e:
+ logger.error(f"Error occurred: {str(e)}")
+ raise e
+
+
+def main(toolsets: Optional[str] = None) -> None:
+ """
+ Initializes, activates, and runs the MCP server engine.
+
+ This function serves as the main entry point for the application. It
+ orchestrates the entire server lifecycle: determining which component
+ groups to activate based on a clear precedence order, activating them,
+ and finally starting the server's transport layer.
+
+ The component groups to be loaded are determined with the following priority:
+ 1. --toolsets command-line argument.
+ 2. MCP_TOOLSETS environment variable.
+ 3. A default group ('rds') if neither of the above is provided.
+
+ Args:
+ toolsets: A comma-separated string of group names passed from
+ the command line.
+ """
+ source_string = toolsets or os.getenv("MCP_TOOLSETS")
+
+ enabled_groups = _parse_groups_from_source(source_string)
+
+ mcp.activate(enabled_groups=enabled_groups)
+
+ transport = os.getenv("SERVER_TRANSPORT", "stdio")
+ mcp.run(transport=transport)
+
+
+def _parse_groups_from_source(source: str | None) -> List[str]:
+ if not source:
+ return [DEFAULT_TOOL_GROUP]
+ groups = [g.strip() for g in source.split(",") if g.strip()]
+ return groups or [DEFAULT_TOOL_GROUP]
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--toolsets",
+ help="Comma-separated list of toolset groups to enable (e.g., 'rds,rds_custom')."
+ )
+ args = parser.parse_args()
+ main(toolsets=args.toolsets)
-if __name__ == '__main__':
- # Initialize and run the server
- main()
diff --git a/src/alibabacloud_rds_openapi_mcp_server/tools/__init__.py b/src/alibabacloud_rds_openapi_mcp_server/tools/__init__.py
new file mode 100644
index 0000000..7ea5b2e
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/tools/__init__.py
@@ -0,0 +1,20 @@
+import os
+import pkgutil
+import importlib
+from typing import Any, Callable
+
+from ..core.context import global_mcp_instance
+
+
+def tool(*dargs: Any, **dkwargs: Any) -> Callable:
+ mcp_instance = global_mcp_instance()
+ return mcp_instance.tool(*dargs, **dkwargs)
+
+print("Initializing and discovering 'tools' package...")
+
+for _, module_name, _ in pkgutil.iter_modules(__path__, __name__ + '.'):
+ try:
+ importlib.import_module(module_name)
+ print(f" ✓ Discovered tool module: {module_name}")
+ except Exception as e:
+ print(f" ✗ Failed to discover tool module {module_name}: {e}")
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/tools/aliyun_openapi_gateway.py b/src/alibabacloud_rds_openapi_mcp_server/tools/aliyun_openapi_gateway.py
new file mode 100644
index 0000000..8dd8126
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/tools/aliyun_openapi_gateway.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+import os
+import logging
+from typing import Type, TypeVar, Dict, Any
+from functools import wraps
+
+# --- Core SDK Imports ---
+import alibabacloud_tea_openapi.models as OpenApiModels
+from alibabacloud_tea_util.models import RuntimeOptions
+
+try:
+ from alibabacloud_rds20140815.client import Client as RdsApiClient
+except ImportError:
+ RdsApiClient = None
+
+try:
+ from alibabacloud_ecs20140526.client import Client as EcsApiClient
+except ImportError:
+ EcsApiClient = None
+
+try:
+ from alibabacloud_das20200116.client import Client as DasApiClient
+except ImportError:
+ DasApiClient = None
+
+logger = logging.getLogger(__name__)
+
+# To add support for a new service, import its client and add it here.
+SERVICE_CLIENT_MAP = {
+ 'rds': RdsApiClient,
+ 'ecs': EcsApiClient,
+ 'das': DasApiClient,
+}
+
+T = TypeVar('T')
+
+
+def _api_call_wrapper(func):
+ """
+ A decorator that encapsulates repetitive API call logic:
+ 1. Provides default RuntimeOptions.
+ 2. Automatically calls .body.to_map() on the response.
+ 3. Provides unified exception logging and handling.
+ """
+
+ @wraps(func)
+ def wrapper(request_model: T, runtime: RuntimeOptions = None) -> Dict[str, Any]:
+ try:
+ if runtime is None:
+ runtime = RuntimeOptions()
+
+ response = func(request_model, runtime)
+
+ return response.body.to_map()
+ except Exception as e:
+ logger.error(f"Aliyun API call to '{func.__name__}' failed: {e}", exc_info=True)
+ raise
+
+ return wrapper
+
+
+class _ServiceProxy:
+ """
+ A private proxy class that dynamically intercepts calls to an SDK client.
+ For example, a call to proxy.describe_db_instances(...) will be forwarded
+ to the real client's method, with all boilerplate logic handled automatically
+ by the _api_call_wrapper.
+ """
+
+ def __init__(self, service_client):
+ self._service_client = service_client
+
+ def __getattr__(self, method_name: str):
+ if hasattr(self._service_client, method_name) and callable(getattr(self._service_client, method_name)):
+ actual_method = getattr(self._service_client, method_name)
+ return _api_call_wrapper(actual_method)
+
+ raise AttributeError(
+ f"'{type(self._service_client).__name__}' object has no callable attribute '{method_name}'")
+
+
+class AliyunServiceGateway:
+ """
+ hides all SDK implementation details, providing a clean, explicit, and
+ discoverable interface for each service.
+ """
+
+ def __init__(self, region_id: str):
+ self._config = OpenApiModels.Config(
+ access_key_id=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_ID'),
+ access_key_secret=os.environ.get('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
+ security_token=os.environ.get('ALIBABA_CLOUD_SECURITY_TOKEN'),
+ region_id=region_id
+ )
+ self._config.validate()
+ self._clients_cache: Dict[str, Any] = {} # Cache for client instances.
+
+ def rds(self) -> _ServiceProxy:
+ """
+ Provides access to RDS (ApsaraDB for RDS) service APIs.
+
+ Returns:
+ A proxy object for chain-calling RDS API methods.
+ """
+ return self._get_service_proxy('rds')
+
+ def ecs(self) -> _ServiceProxy:
+ """
+ Provides access to ECS (Elastic Compute Service) APIs.
+
+ Returns:
+ A proxy object for chain-calling ECS API methods.
+ """
+ return self._get_service_proxy('ecs')
+
+ def das(self) -> _ServiceProxy:
+ """
+ Provides access to DAS (Database Autonomy Service) APIs.
+
+ Returns:
+ A proxy object for chain-calling DAS API methods.
+ """
+ return self._get_service_proxy('das')
+
+ def _get_service_proxy(self, service_name: str) -> _ServiceProxy:
+ """
+ Private method to create, cache, and wrap a service client in a proxy.
+ """
+ if service_name in self._clients_cache:
+ client = self._clients_cache[service_name]
+ else:
+ client_class = SERVICE_CLIENT_MAP.get(service_name)
+ if not client_class:
+ raise ValueError(
+ f"Service '{service_name}' is not supported or its SDK (e.g., alibabacloud_{service_name}...) is not installed.")
+
+ client = client_class(self._config)
+ self._clients_cache[service_name] = client
+
+ return _ServiceProxy(client)
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_common.py b/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_common.py
new file mode 100644
index 0000000..2f7631c
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_common.py
@@ -0,0 +1,432 @@
+# -*- coding: utf-8 -*-
+"""Provides core functionalities for the "rds_custom" MCP toolset.
+
+This module contains the engine-agnostic logic and serves as the **required
+base dependency** for all engine-specific tools. It can also be loaded
+stand-alone for basic operations.
+
+Toolsets are loaded at runtime via the `--toolsets` command-line argument
+or a corresponding environment variable.
+
+Command-Line Usage:
+-------------------
+1. **Base Usage Only:**
+ To load only the base functionalities, specify `rds_custom` by itself.
+
+2. **Single-Engine Usage:**
+ To use tools for a specific engine (e.g., SQL Server), you MUST include
+ **both** the base toolset `rds_custom` AND the engine-specific toolset
+ `rds_custom_mssql` in the list, separated by a comma.
+
+Command-Line Examples:
+----------------------
+# Scenario 1: Basic usage with only the base toolset
+# python server.py --toolsets rds_custom
+
+# Scenario 2: Usage for SQL Server
+# python server.py --toolsets rds_custom,rds_custom_mssql
+"""
+
+import logging
+from typing import Dict, Any, Optional, List
+import alibabacloud_rds20140815.models as RdsApiModels
+from .aliyun_openapi_gateway import AliyunServiceGateway
+from . import tool
+
+
+logger = logging.getLogger(__name__)
+
+RDS_CUSTOM_GROUP_NAME = 'rds_custom'
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_instances(region_id: str, instance_id: str|None = None) -> Dict[str, Any]:
+ """
+ describe rds custom instances.
+
+ Args:
+ region_id: The region ID of the RDS Custom instances.
+ instance_id: The ID of a specific instance. If omitted, all instances in the region are returned.
+
+ Returns:
+ dict[str, Any]: The response containing instance metadata.
+ """
+ request = RdsApiModels.DescribeRCInstancesRequest(
+ region_id=region_id,
+ instance_id=instance_id
+ )
+ rds_client = AliyunServiceGateway(region_id).rds()
+ return rds_client.describe_rcinstances_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_instance_attribute(region_id: str,instance_id: str) -> Dict[str, Any]:
+ """
+ describe a single rds custom instance's details.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance.
+
+ Returns:
+ dict[str, Any]: The response containing the instance details.
+ """
+ request = RdsApiModels.DescribeRCInstanceAttributeRequest(
+ region_id=region_id,
+ instance_id=instance_id
+ )
+ return AliyunServiceGateway(region_id).rds().describe_rcinstance_attribute_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def resize_rc_instance_disk(
+ region_id: str,
+ instance_id: str,
+ new_size: int,
+ disk_id: str,
+ auto_pay: bool = False,
+ dry_run: bool = False,
+) -> Dict[str, Any]:
+ """
+ resize a specific rds custom instance's disk.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance.
+ new_size: The target size of the disk in GiB.
+ disk_id: The ID of the cloud disk.
+ auto_pay: Specifies whether to enable automatic payment. Default is false.
+ dry_run: Specifies whether to perform a dry run. Default is false.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.ResizeRCInstanceDiskRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ new_size=new_size,
+ disk_id=disk_id,
+ auto_pay=auto_pay,
+ dry_run=dry_run,
+ type='online'
+ )
+ return AliyunServiceGateway(region_id).rds().resize_rcinstance_disk_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_instance_vnc_url(
+ region_id: str,
+ instance_id: str,
+ db_type: str
+) -> Dict[str, Any]:
+ """
+ describe the vnc login url for a specific rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the instance.
+ db_type: The database type, e.g., 'mysql' or 'mssql'.
+
+ Returns:
+ dict[str, Any]: The response containing the VNC login URL.
+ """
+ request = RdsApiModels.DescribeRCInstanceVncUrlRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ db_type=db_type
+ )
+ return AliyunServiceGateway(region_id).rds().describe_rcinstance_vnc_url_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def modify_rc_instance_attribute(
+ region_id: str,
+ instance_id: str,
+ password: Optional[str] = None,
+ reboot: Optional[bool] = None,
+ host_name: Optional[str] = None,
+ security_group_id: Optional[str] = None,
+ deletion_protection: Optional[bool] = None
+) -> Dict[str, Any]:
+ """
+ modify attributes of a specific rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance to modify.
+ password: The new password for the instance.
+ reboot: Specifies whether to restart the instance after modification.
+ host_name: The new hostname for the instance.
+ security_group_id: The ID of the new security group for the instance.
+ deletion_protection: Specifies whether to enable the deletion protection feature.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.ModifyRCInstanceAttributeRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ password=password,
+ reboot=reboot,
+ host_name=host_name,
+ security_group_id=security_group_id,
+ deletion_protection=deletion_protection
+ )
+ return AliyunServiceGateway(region_id).rds().modify_rcinstance_attribute_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def modify_rc_instance_description(
+ region_id: str,
+ instance_id: str,
+ instance_description: str
+) -> Dict[str, Any]:
+ """
+ modify the description of a specific rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the instance to modify.
+ instance_description: The new description for the instance.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+
+ request = RdsApiModels.ModifyRCInstanceDescriptionRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ instance_description=instance_description
+ )
+ return AliyunServiceGateway(region_id).rds().modify_rcinstance_description_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_snapshots(
+ region_id: str,
+ disk_id: Optional[str] = None,
+ snapshot_ids: Optional[List[str]] = None,
+ page_number: Optional[int] = None,
+ page_size: Optional[int] = None
+) -> Dict[str, Any]:
+ """
+ Query the list of RDS Custom snapshots information.
+
+ Args:
+ region_id: The region ID. You can call DescribeRegions to obtain the latest region list.
+ disk_id: The specified cloud disk ID.
+ snapshot_ids: The list of snapshot IDs.
+ page_number: The page number to return.
+ page_size: The number of entries to return on each page. Value range: 30~100. Default value: 30.
+
+ Returns:
+ dict[str, Any]: The response containing the list of snapshots and pagination information.
+ """
+
+ request = RdsApiModels.DescribeRCSnapshotsRequest(
+ region_id=region_id,
+ disk_id=disk_id,
+ snapshot_ids=snapshot_ids,
+ page_number=page_number,
+ page_size=page_size
+ )
+
+ return AliyunServiceGateway(region_id).rds().describe_rcsnapshots_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def create_rc_snapshot(
+ region_id: str,
+ disk_id: str,
+ description: Optional[str] = None,
+ retention_days: Optional[int] = None
+) -> Dict[str, Any]:
+ """
+ Create a manual snapshot for a specific cloud disk of an RDS Custom instance.
+
+ Args:
+ region_id: The region ID. You can call DescribeRegions to obtain the latest region list.
+ disk_id: The ID of the cloud disk for which to create a snapshot.
+ description: The description of the snapshot. It must be 2 to 256 characters in length and cannot start with http:// or https://.
+ retention_days: The retention period of the snapshot, in days. After the retention period expires, the snapshot is automatically released. Value range: 1 to 65536.
+
+ Returns:
+ dict[str, Any]: A dictionary containing the RequestId and the ID of the new snapshot.
+ """
+ request = RdsApiModels.CreateRCSnapshotRequest(
+ region_id=region_id,
+ disk_id=disk_id,
+ description=description,
+ retention_days=retention_days
+ )
+
+ return AliyunServiceGateway(region_id).rds().create_rcsnapshot_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_disks(
+ region_id: str,
+ instance_id: Optional[str] = None,
+ disk_ids: Optional[List[str]] = None,
+ page_number: Optional[int] = None,
+ page_size: Optional[int] = None,
+ tag: Optional[List[Dict[str, str]]] = None
+) -> Dict[str, Any]:
+ """
+ Query the list of disks for an RDS Custom instance.
+
+ Args:
+ region_id: The region ID. You can call DescribeRegions to obtain the latest region list.
+ instance_id: The ID of the instance to which the disks belong.
+ disk_ids: The list of disk IDs to query. Supports up to 100 IDs.
+ page_number: The page number to return.
+ page_size: The number of entries to return on each page. Value range: 30 to 100. Default value: 30.
+ tag: A list of tags to filter results. For example: [{"Key": "your_key", "Value": "your_value"}].
+
+ Returns:
+ dict[str, Any]: A dictionary containing the list of disks and pagination information.
+ """
+ request = RdsApiModels.DescribeRCDisksRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ disk_ids=disk_ids,
+ page_number=page_number,
+ page_size=page_size,
+ tag=tag
+ )
+ return AliyunServiceGateway(region_id).rds().describe_rcdisks_with_options(request)
+
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def run_rc_instances(
+ region_id: str,
+ instance_type: str,
+ password: str,
+ vswitch_id: str,
+ security_group_id: str,
+ zone_id: str,
+ image_id: str,
+ # --- Optional Parameters ---
+ instance_charge_type: Optional[str] = None,
+ amount: Optional[int] = None,
+ period: Optional[int] = None,
+ period_unit: Optional[str] = None,
+ auto_renew: Optional[bool] = None,
+ auto_pay: Optional[bool] = None,
+ client_token: Optional[str] = None,
+ auto_use_coupon: Optional[bool] = None,
+ promotion_code: Optional[str] = None,
+ data_disk: Optional[List[Dict[str, Any]]] = None,
+ system_disk: Optional[Dict[str, Any]] = None,
+ deployment_set_id: Optional[str] = None,
+ internet_max_bandwidth_out: Optional[int] = None,
+ description: Optional[str] = None,
+ key_pair_name: Optional[str] = None,
+ dry_run: Optional[bool] = None,
+ tag: Optional[List[Dict[str, str]]] = None,
+ resource_group_id: Optional[str] = None,
+ create_mode: Optional[str] = None,
+ host_name: Optional[str] = None,
+ spot_strategy: Optional[str] = None,
+ support_case: Optional[str] = None,
+ create_ack_edge_param: Optional[Dict[str, Any]] = None,
+ user_data: Optional[str] = None,
+ user_data_in_base_64: Optional[bool] = None,
+ deletion_protection: Optional[bool] = None
+) -> Dict[str, Any]:
+ """
+ Creates one or more RDS Custom instances by converting dicts to model objects internally.
+
+ Args:
+ region_id: The region ID.
+ instance_type: The instance specification. See RDS Custom instance specification list for details.
+ password: The password for the instance. It must be 8 to 30 characters long and contain at least three of the following character types: uppercase letters, lowercase letters, digits, and special characters.
+ vswitch_id: The vSwitch ID for the target instance.
+ security_group_id: The ID of the security group to which the instance belongs.
+ zone_id: The zone ID to which the instance belongs.
+ image_id: The image ID used by the instance.
+ instance_charge_type: The billing method. Valid values: Prepaid (subscription), PostPaid (pay-as-you-go).
+ amount: The number of RDS Custom instances to create. Default is 1.
+ period: The subscription duration of the resource. Used when instance_charge_type is 'Prepaid'.
+ period_unit: The unit of the subscription duration. Valid values: Month, Year.
+ auto_renew: Specifies whether to enable auto-renewal for the subscription.
+ auto_pay: Specifies whether to enable automatic payment.
+ client_token: A client token used to ensure the idempotence of the request.
+ auto_use_coupon: Specifies whether to automatically use coupons.
+ promotion_code: The coupon code.
+ data_disk: The list of data disks. Example: [{"Size": 50, "Category": "cloud_essd"}]
+ system_disk: The system disk specification. Example: {"Size": 60, "Category": "cloud_essd"}
+ deployment_set_id: The deployment set ID.
+ internet_max_bandwidth_out: The maximum public outbound bandwidth in Mbit/s for Custom for SQL Server.
+ description: The description of the instance.
+ key_pair_name: The name of the key pair.
+ dry_run: Specifies whether to perform a dry run to check the request.
+ tag: A list of tags to attach to the instance. Example: [{"Key": "your_key", "Value": "your_value"}].
+ resource_group_id: The resource group ID.
+ create_mode: Whether to allow joining an ACK cluster. '1' means allowed.
+ host_name: The hostname of the instance.
+ spot_strategy: The bidding strategy for the pay-as-you-go instance.
+ support_case: The RDS Custom edition. 'share' or 'exclusive'.
+ create_ack_edge_param: Information for the ACK Edge cluster.
+ user_data: Custom data for the instance, up to 32 KB in raw format.
+ user_data_in_base_64: Whether the custom data is Base64 encoded.
+ deletion_protection: Specifies whether to enable release protection.
+
+ Returns:
+ dict[str, Any]: A dictionary containing the OrderId, RequestId, and the set of created instance IDs.
+ """
+ system_disk_obj = None
+ if system_disk:
+ system_disk_obj = RdsApiModels.RunRCInstancesRequestSystemDisk(**system_disk)
+ data_disk_objs = None
+ if data_disk:
+ data_disk_objs = [RdsApiModels.RunRCInstancesRequestDataDisk(**disk) for disk in data_disk]
+ tag_objs = None
+ if tag:
+ tag_objs = [RdsApiModels.RunRCInstancesRequestTag(**t) for t in tag]
+ request = RdsApiModels.RunRCInstancesRequest(
+ region_id=region_id,
+ instance_type=instance_type,
+ password=password,
+ v_switch_id=vswitch_id,
+ security_group_id=security_group_id,
+ zone_id=zone_id,
+ image_id=image_id,
+ instance_charge_type=instance_charge_type,
+ amount=amount,
+ period=period,
+ period_unit=period_unit,
+ auto_renew=auto_renew,
+ auto_pay=auto_pay,
+ client_token=client_token,
+ auto_use_coupon=auto_use_coupon,
+ promotion_code=promotion_code,
+ deployment_set_id=deployment_set_id,
+ internet_max_bandwidth_out=internet_max_bandwidth_out,
+ description=description,
+ key_pair_name=key_pair_name,
+ dry_run=dry_run,
+ resource_group_id=resource_group_id,
+ create_mode=create_mode,
+ host_name=host_name,
+ spot_strategy=spot_strategy,
+ support_case=support_case,
+ create_ack_edge_param=create_ack_edge_param,
+ user_data=user_data,
+ user_data_in_base_64=user_data_in_base_64,
+ deletion_protection=deletion_protection,
+ # 传入转换后的模型对象
+ system_disk=system_disk_obj,
+ data_disk=data_disk_objs,
+ tag=tag_objs
+ )
+ return AliyunServiceGateway(region_id).rds().run_rcinstances_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def get_current_time() -> Dict[str, Any]:
+ """Get the current time.
+
+ Returns:
+ Dict[str, Any]: The response containing the current time.
+ """
+ import datetime
+ try:
+ current_time = datetime.datetime.now()
+ formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S")
+ return {
+ "current_time": formatted_time
+ }
+ except Exception as e:
+ logger.error(f"Error occurred while getting the current time: {str(e)}")
+ raise Exception(f"Failed to get the current time: {str(e)}")
diff --git a/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_mssql.py b/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_mssql.py
new file mode 100644
index 0000000..4614628
--- /dev/null
+++ b/src/alibabacloud_rds_openapi_mcp_server/tools/rds_custom_mssql.py
@@ -0,0 +1,325 @@
+# -*- coding: utf-8 -*-
+"""
+Provides SQL Server-specific MCP tools for the "rds_custom" product.
+
+This toolset requires the base `rds_custom` toolset to be loaded
+simultaneously. See the base module's docstring for detailed usage.
+"""
+import logging
+from typing import Dict, Any, Optional, List
+import alibabacloud_rds20140815.models as RdsApiModels
+
+from .aliyun_openapi_gateway import AliyunServiceGateway
+from . import tool
+
+logger = logging.getLogger(__name__)
+
+RDS_CUSTOM_GROUP_NAME = 'rds_custom_mssql'
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_instance_ip_address(
+ region_id: str,
+ instance_id: str,
+ ddos_region_id: str,
+ instance_type: str = 'ecs',
+ resource_type: str = 'ecs',
+ ddos_status: Optional[str] = None,
+ instance_ip: Optional[str] = None,
+ current_page: Optional[int] = None,
+ page_size: Optional[int] = None,
+ instance_name: Optional[str] = None
+) -> Dict[str, Any]:
+ """
+ describe the ddos protection details for an rds custom instance.
+ Args:
+ region_id: The region ID where the Custom instance is located.
+ instance_id: The ID of the Custom instance.
+ ddos_region_id: The region ID of the public IP asset.
+ instance_type: The instance type of the public IP asset, fixed value 'ecs'.
+ resource_type: The resource type, fixed value 'ecs'.
+ ddos_status: The DDoS protection status of the public IP asset.
+ instance_ip: The IP address of the public IP asset to query.
+ current_page: The page number of the results to display.
+ page_size: The number of instances per page.
+ instance_name: The name of the Custom instance.
+
+ Returns:
+ dict[str, Any]: The response containing the DDoS protection details.
+ """
+ request = RdsApiModels.DescribeRCInstanceIpAddressRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ ddos_region_id=ddos_region_id,
+ instance_type=instance_type,
+ resource_type=resource_type,
+ ddos_status=ddos_status,
+ instance_ip=instance_ip,
+ current_page=current_page,
+ page_size=page_size,
+ instance_name=instance_name
+ )
+ return AliyunServiceGateway(region_id).rds().describe_rcinstance_ip_address_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def stop_rc_instances(
+ region_id: str,
+ instance_ids: List[str],
+ force_stop: bool = False,
+ batch_optimization: Optional[str] = None
+) -> Dict[str, Any]:
+ """
+ stop one or more rds custom instances in batch.
+
+ Args:
+ region_id: The region ID of the RDS Custom instances.
+ instance_ids: A list of instance IDs to be stopped.
+ force_stop: Specifies whether to force stop the instances. Default is false.
+ batch_optimization: The batch operation mode.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.StopRCInstancesRequest(
+ region_id=region_id,
+ instance_ids=instance_ids,
+ force_stop=force_stop,
+ batch_optimization=batch_optimization
+ )
+ return AliyunServiceGateway(region_id).rds().stop_rcinstances_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def start_rc_instances(
+ region_id: str,
+ instance_ids: List[str],
+ batch_optimization: Optional[str] = None
+) -> Dict[str, Any]:
+ """
+ start one or more rds custom instances in batch.
+
+ Args:
+ region_id: The region ID of the RDS Custom instances.
+ instance_ids: A list of instance IDs to be started.
+ batch_optimization: The batch operation mode.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.StartRCInstancesRequest(
+ region_id=region_id,
+ instance_ids=instance_ids,
+ batch_optimization=batch_optimization
+ )
+ return AliyunServiceGateway(region_id).rds().start_rcinstances_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def reboot_rc_instance(
+ region_id: str,
+ instance_id: str,
+ force_stop: bool = False,
+ dry_run: bool = False
+) -> Dict[str, Any]:
+ """
+ reboot a specific rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the instance to reboot.
+ force_stop: Specifies whether to force shutdown before rebooting. Default is false.
+ dry_run: Specifies whether to perform a dry run only. Default is false.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.RebootRCInstanceRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ force_stop=force_stop,
+ dry_run=dry_run
+ )
+ return AliyunServiceGateway(region_id).rds().reboot_rcinstance_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_image_list(
+ region_id: str,
+ page_number: Optional[int] = None,
+ page_size: Optional[int] = None,
+ type: Optional[str] = None,
+ architecture: Optional[str] = None,
+ image_id: Optional[str] = None,
+ image_name: Optional[str] = None,
+ instance_type: Optional[str] = None
+) -> Dict[str, Any]:
+ """
+ describe the list of custom images for creating rds custom instances.
+
+ Args:
+ region_id: The region ID to query for images.
+ page_number: The page number of the results.
+ page_size: The number of records per page.
+ type: The image type, currently only 'self' is supported.
+ architecture: The system architecture of the image, e.g., 'x86_64'.
+ image_id: The ID of a specific image to query.
+ image_name: The name of a specific image to query.
+ instance_type: The instance type to query usable images for.
+
+ Returns:
+ dict[str, Any]: The response containing the list of custom images.
+ """
+ request = RdsApiModels.DescribeRCImageListRequest(
+ region_id=region_id,
+ page_number=page_number,
+ page_size=page_size,
+ type=type,
+ architecture=architecture,
+ image_id=image_id,
+ image_name=image_name,
+ instance_type=instance_type
+ )
+
+ return AliyunServiceGateway(region_id).rds().describe_rcimage_list_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_metric_list(
+ region_id: str,
+ instance_id: str,
+ metric_name: str,
+ start_time: str,
+ end_time: str,
+ period: Optional[str] = None,
+ length: Optional[str] = None,
+ next_token: Optional[str] = None,
+ dimensions: Optional[str] = None,
+ express: Optional[str] = None
+) -> Dict[str, Any]:
+ """
+ describe monitoring data for a specific metric of an rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the instance to query.
+ metric_name: The metric to be monitored, e.g., 'CPUUtilization'.
+ start_time: The start time of the query, format 'YYYY-MM-DD HH:MM:SS'.
+ end_time: The end time of the query, format 'YYYY-MM-DD HH:MM:SS'.
+ period: The statistical period of the monitoring data in seconds.
+ length: The number of entries to return on each page.
+ next_token: The pagination token.
+ dimensions: The dimensions to query data for multiple resources in batch.
+ express: A reserved parameter.
+
+ Returns:
+ dict[str, Any]: The response containing the list of monitoring data.
+ """
+ request = RdsApiModels.DescribeRCMetricListRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ metric_name=metric_name,
+ start_time=start_time,
+ end_time=end_time,
+ period=period,
+ length=length,
+ next_token=next_token,
+ dimensions=dimensions,
+ express=express
+ )
+
+ return AliyunServiceGateway(region_id).rds().describe_rcmetric_list_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def sync_rc_security_group(
+ region_id: str,
+ instance_id: str,
+ security_group_id: str
+) -> Dict[str, Any]:
+ """
+ synchronize the security group rules for an rds sql server custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance.
+ security_group_id: The ID of the security group.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.SyncRCSecurityGroupRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ security_group_id=security_group_id
+ )
+
+ return AliyunServiceGateway(region_id).rds().sync_rcsecurity_group_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def associate_eip_address_with_rc_instance(
+ region_id: str,
+ instance_id: str,
+ allocation_id: str
+) -> Dict[str, Any]:
+ """
+ associate an elastic ip address (eip) with an rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance.
+ allocation_id: The ID of the Elastic IP Address.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.AssociateEipAddressWithRCInstanceRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ allocation_id=allocation_id
+ )
+
+ return AliyunServiceGateway(region_id).rds().associate_eip_address_with_rcinstance_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def unassociate_eip_address_with_rc_instance(
+ region_id: str,
+ instance_id: str,
+ allocation_id: str
+) -> Dict[str, Any]:
+ """
+ unassociate an elastic ip address (eip) from an rds custom instance.
+
+ Args:
+ region_id: The region ID of the RDS Custom instance.
+ instance_id: The ID of the RDS Custom instance.
+ allocation_id: The ID of the Elastic IP Address to unassociate.
+
+ Returns:
+ dict[str, Any]: The response containing the result of the operation.
+ """
+ request = RdsApiModels.UnassociateEipAddressWithRCInstanceRequest(
+ region_id=region_id,
+ instance_id=instance_id,
+ allocation_id=allocation_id
+ )
+
+ return AliyunServiceGateway(region_id).rds().unassociate_eip_address_with_rcinstance_with_options(request)
+
+@tool(group=RDS_CUSTOM_GROUP_NAME)
+async def describe_rc_instance_ddos_count(
+ region_id: str,
+ ddos_region_id: str,
+ instance_type: str = 'ecs'
+) -> Dict[str, Any]:
+ """
+ describe the count of ddos attacks on rds custom instances.
+
+ Args:
+ region_id: The region ID where the Custom instance is located.
+ ddos_region_id: The region ID of the public IP asset to query.
+ instance_type: The instance type of the public IP asset, fixed value 'ecs'.
+
+ Returns:
+ dict[str, Any]: The response containing the count of ddos attacks.
+ """
+ request = RdsApiModels.DescribeRCInstanceDdosCountRequest(
+ region_id=region_id,
+ ddos_region_id=ddos_region_id,
+ instance_type=instance_type
+ )
+
+ return AliyunServiceGateway(region_id).rds().describe_rcinstance_ddos_count_with_options(request)
\ No newline at end of file
diff --git a/src/alibabacloud_rds_openapi_mcp_server/utils.py b/src/alibabacloud_rds_openapi_mcp_server/utils.py
index 790a93d..fd4acb1 100644
--- a/src/alibabacloud_rds_openapi_mcp_server/utils.py
+++ b/src/alibabacloud_rds_openapi_mcp_server/utils.py
@@ -2,11 +2,14 @@
import os
from datetime import datetime, timezone
from io import StringIO
+import time
from alibabacloud_bssopenapi20171214.client import Client as BssOpenApi20171214Client
from alibabacloud_rds20140815.client import Client as RdsClient
from alibabacloud_tea_openapi.models import Config
from alibabacloud_vpc20160428.client import Client as VpcClient
+from alibabacloud_das20200116.client import Client as DAS20200116Client
+
PERF_KEYS = {
"mysql": {
@@ -46,6 +49,22 @@
}
+def parse_args(argv):
+ args = {}
+ i = 1
+ while i < len(argv):
+ arg = argv[i]
+ if arg.startswith('--'):
+ key = arg[2:]
+ if i + 1 < len(argv) and not argv[i + 1].startswith('--'):
+ args[key] = argv[i+1]
+ i += 2
+ else:
+ args[key] = True
+ i += 1
+ return args
+
+
def transform_to_iso_8601(dt: datetime, timespec: str):
return dt.astimezone(timezone.utc).isoformat(timespec=timespec).replace("+00:00", "Z")
@@ -95,6 +114,13 @@ def json_array_to_csv(data):
return output.getvalue()
+def convert_datetime_to_timestamp(date_str):
+ dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
+ timestamp_seconds = time.mktime(dt.timetuple())
+ timestamp_milliseconds = int(timestamp_seconds) * 1000
+ return timestamp_milliseconds
+
+
def get_rds_client(region_id: str):
config = Config(
access_key_id=os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'),
@@ -142,3 +168,17 @@ def get_bill_client(region_id: str):
)
client = BssOpenApi20171214Client(config)
return client
+
+
+def get_das_client():
+ config = Config(
+ access_key_id=os.getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'),
+ access_key_secret=os.getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'),
+ security_token=os.getenv('ALIBABA_CLOUD_SECURITY_TOKEN'),
+ region_id='cn-shanghai',
+ protocol="https",
+ connect_timeout=10 * 1000,
+ read_timeout=300 * 1000
+ )
+ client = DAS20200116Client(config)
+ return client
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/test_toolsets.py b/test/test_toolsets.py
new file mode 100644
index 0000000..76b07ce
--- /dev/null
+++ b/test/test_toolsets.py
@@ -0,0 +1,145 @@
+import sys
+from pathlib import Path
+from unittest.mock import MagicMock
+
+import pytest
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
+
+from alibabacloud_rds_openapi_mcp_server.core.mcp import RdsMCP, FastMCP, Prompt
+from alibabacloud_rds_openapi_mcp_server.core.context import set_mcp_instance
+
+DEFAULT_TOOL_GROUP = 'rds'
+
+
+@pytest.fixture
+def mcp_instance(monkeypatch) -> RdsMCP:
+ mocked_add_tool = MagicMock()
+ mocked_add_prompt = MagicMock()
+
+ monkeypatch.setattr(FastMCP, "add_tool", mocked_add_tool)
+ monkeypatch.setattr(FastMCP, "add_prompt", mocked_add_prompt)
+
+ mcp = RdsMCP("dummy_server")
+
+ mcp.add_tool = mocked_add_tool
+ mcp.add_prompt = mocked_add_prompt
+
+ set_mcp_instance(mcp)
+ yield mcp
+ # Teardown: Clear the global context
+ set_mcp_instance(None)
+
+
+@pytest.fixture
+def populated_mcp(mcp_instance: RdsMCP):
+ """
+ A fixture that populates the MCP instance with several deferred
+ component definitions.
+ """
+
+ def rds_tool_a(): pass
+
+ def rds_tool_b(): pass
+
+ def custom_tool_a(): pass
+
+ def custom_prompt_a(): return "prompt content"
+
+ mcp_instance.tool(rds_tool_a)
+ mcp_instance.tool(group="rds")(rds_tool_b)
+ mcp_instance.tool(group="rds_custom")(custom_tool_a)
+ mcp_instance.prompt(group="rds_custom", name="custom_prompt")(custom_prompt_a)
+
+ mcp_instance._test_funcs = {
+ "rds_tool_a": rds_tool_a,
+ "rds_tool_b": rds_tool_b,
+ "custom_tool_a": custom_tool_a,
+ "custom_prompt_a": custom_prompt_a,
+ }
+ return mcp_instance
+
+
+# --- Corrected Test Cases ---
+
+def test_activate_should_register_default_tools_when_default_group_is_passed(populated_mcp: RdsMCP):
+ mcp = populated_mcp
+ test_funcs = mcp._test_funcs
+
+ mcp.activate(enabled_groups=[DEFAULT_TOOL_GROUP])
+
+ # Assert that the mock attached to the instance was called correctly
+ assert mcp.add_tool.call_count == 2
+ mcp.add_tool.assert_any_call(test_funcs["rds_tool_a"], name="rds_tool_a")
+ mcp.add_tool.assert_any_call(test_funcs["rds_tool_b"], name="rds_tool_b")
+ mcp.add_prompt.assert_not_called()
+
+
+def test_activate_should_register_only_specified_groups(populated_mcp: RdsMCP):
+ mcp = populated_mcp
+ test_funcs = mcp._test_funcs
+
+ mcp.activate(enabled_groups=["rds_custom"])
+
+ assert mcp.add_tool.call_count == 1
+ mcp.add_tool.assert_called_once_with(test_funcs["custom_tool_a"], name="custom_tool_a")
+
+ assert mcp.add_prompt.call_count == 1
+ registered_prompt_obj = mcp.add_prompt.call_args.args[0]
+ assert isinstance(registered_prompt_obj, Prompt)
+ assert registered_prompt_obj.name == "custom_prompt"
+
+
+def test_activate_should_raise_value_error_when_an_unknown_group_is_passed(populated_mcp: RdsMCP):
+ with pytest.raises(ValueError, match="Unknown group\\(s\\): \\['invalid_group'\\]"):
+ populated_mcp.activate(enabled_groups=["rds", "invalid_group"])
+
+
+def test_activate_should_do_nothing_when_called_a_second_time(populated_mcp: RdsMCP):
+ """
+ Tests that the internal activation logic only runs once.
+ """
+ mcp = populated_mcp
+
+ mcp.activate(enabled_groups=["rds"])
+ # Initial registration count should be 2
+ assert mcp.add_tool.call_count == 2
+ assert mcp.add_prompt.call_count == 0
+
+ # Call activate again with a different group
+ mcp.activate(enabled_groups=["rds_custom"])
+
+ # Assert that the call counts have NOT changed, because it was already activated
+ assert mcp.add_tool.call_count == 2
+ assert mcp.add_prompt.call_count == 0
+
+def test_activate_should_not_register_duplicates_when_enabled_groups_contain_duplicates(populated_mcp: RdsMCP):
+ mcp = populated_mcp
+
+ # Enable 'rds' group twice.
+ mcp.activate(enabled_groups=['rds', 'rds_custom', 'rds'])
+
+ assert mcp.add_tool.call_count == 3
+ # We expect 1 prompt from 'rds_custom'.
+ assert mcp.add_prompt.call_count == 1
+
+
+def test_activate_should_succeed_gracefully_when_activating_an_empty_group(mcp_instance: RdsMCP):
+ """
+ Tests that the system runs without error when an enabled group
+ is valid but contains no components.
+ """
+ mcp = mcp_instance
+
+ # Define a tool in 'group_a' but leave 'group_b' empty
+ def my_tool():
+ pass
+
+ mcp.tool(group='group_a')(my_tool)
+
+ try:
+ mcp.activate(enabled_groups=['group_a'])
+ except ValueError:
+ pytest.fail("activate() raised ValueError unexpectedly for a valid, non-empty group.")
+ assert mcp.add_tool.call_count == 1
+ mcp.add_tool.assert_called_once_with(my_tool, name='my_tool')
diff --git a/uv.lock b/uv.lock
index 4c09304..fc04476 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2,6 +2,15 @@ version = 1
revision = 1
requires-python = ">=3.12"
+[[package]]
+name = "aiofiles"
+version = "24.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 },
+]
+
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
@@ -89,12 +98,36 @@ wheels = [
[[package]]
name = "alibabacloud-credentials"
-version = "0.3.6"
+version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "aiofiles" },
+ { name = "alibabacloud-credentials-api" },
{ name = "alibabacloud-tea" },
+ { name = "apscheduler" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b7/0c/1b0c5f4c2170165719b336616ac0a88f1666fd8690fda41e2e8ae3139fd9/alibabacloud-credentials-1.0.2.tar.gz", hash = "sha256:d2368eb70bd02db9143b2bf531a27a6fecd2cde9601db6e5b48cd6dbe25720ce", size = 30804 }
+
+[[package]]
+name = "alibabacloud-credentials-api"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/87/1d7019d23891897cb076b2f7e3c81ab3c2ba91de3bb067196f675d60d34c/alibabacloud-credentials-api-1.0.0.tar.gz", hash = "sha256:8c340038d904f0218d7214a8f4088c31912bfcf279af2cbc7d9be4897a97dd2f", size = 2330 }
+
+[[package]]
+name = "alibabacloud-das20200116"
+version = "2.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "alibabacloud-endpoint-util" },
+ { name = "alibabacloud-openapi-util" },
+ { name = "alibabacloud-tea-openapi" },
+ { name = "alibabacloud-tea-util" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/c5/28048b23fd6e53033ca058ac5eb7b8655fec55fad8e72504ee264be3f3f5/alibabacloud_das20200116-2.7.1.tar.gz", hash = "sha256:a9a74f10304c647fd6b2402aecd74e66c9499ceaf7917b89f065ff85d552ee1d", size = 151209 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/60/20fa1397f43d793d072d93d0179a8ed11874d86a2f30781737820b2d9487/alibabacloud_das20200116-2.7.1-py3-none-any.whl", hash = "sha256:e2579c5bbc97294d7300e116daa7d9e824ff3a277e4472da17d5ba4be7e57e71", size = 152448 },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/92/7cb0807d6d380fa09cbad6d4fe983781e657dcc16d60fc559d6575bf98be/alibabacloud_credentials-0.3.6.tar.gz", hash = "sha256:caa82cf258648dcbe1ca14aeba50ba21845567d6ac3cd48d318e0a445fff7f96", size = 18771 }
[[package]]
name = "alibabacloud-endpoint-util"
@@ -126,23 +159,31 @@ sdist = { url = "https://files.pythonhosted.org/packages/f6/50/5f41ab550d7874c62
[[package]]
name = "alibabacloud-rds-openapi-mcp-server"
-version = "1.6.2"
+version = "1.8.0"
source = { virtual = "." }
dependencies = [
{ name = "alibabacloud-bssopenapi20171214" },
+ { name = "alibabacloud-das20200116" },
{ name = "alibabacloud-rds20140815" },
{ name = "alibabacloud-vpc20160428" },
{ name = "httpx" },
{ name = "mcp", extra = ["cli"] },
+ { name = "psycopg2" },
+ { name = "pymysql" },
+ { name = "pyodbc" },
]
[package.metadata]
requires-dist = [
{ name = "alibabacloud-bssopenapi20171214", specifier = ">=5.0.0" },
+ { name = "alibabacloud-das20200116", specifier = "==2.7.1" },
{ name = "alibabacloud-rds20140815", specifier = ">=11.0.0" },
{ name = "alibabacloud-vpc20160428", specifier = ">=6.11.4" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.6.0" },
+ { name = "psycopg2", specifier = ">=2.9.10" },
+ { name = "pymysql", specifier = ">=1.1.1" },
+ { name = "pyodbc", specifier = ">=5.2.0" },
]
[[package]]
@@ -172,7 +213,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/9a/7d/b22cb9a0d4f396ee0
[[package]]
name = "alibabacloud-tea-openapi"
-version = "0.3.13"
+version = "0.3.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "alibabacloud-credentials" },
@@ -181,7 +222,7 @@ dependencies = [
{ name = "alibabacloud-tea-util" },
{ name = "alibabacloud-tea-xml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/21/02/f5a6519efee96141a6e5af4e79b768bdae77dd62992bca02a710e06c36b1/alibabacloud_tea_openapi-0.3.13.tar.gz", hash = "sha256:77034911dbed41de440e9b6de38cb24646723aa1d0059cefeb3906f8c0a4523e", size = 12918 }
+sdist = { url = "https://files.pythonhosted.org/packages/be/cb/f1b10b1da37e4c0de2aa9ca1e7153a6960a7f2dc496664e85fdc8b621f84/alibabacloud_tea_openapi-0.3.15.tar.gz", hash = "sha256:56a0aa6d51d8cf18c0cf3d219d861f4697f59d3e17fa6726b1101826d93988a2", size = 13021 }
[[package]]
name = "alibabacloud-tea-util"
@@ -239,6 +280,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
+[[package]]
+name = "apscheduler"
+version = "3.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzlocal" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004 },
+]
+
[[package]]
name = "attrs"
version = "25.3.0"
@@ -632,6 +685,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376 },
]
+[[package]]
+name = "psycopg2"
+version = "2.9.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 },
+ { url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 },
+ { url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 },
+]
+
[[package]]
name = "pycparser"
version = "2.22"
@@ -720,6 +784,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
+[[package]]
+name = "pymysql"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972 },
+]
+
+[[package]]
+name = "pyodbc"
+version = "5.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/36/a1ac7d23a1611e7ccd4d27df096f3794e8d1e7faa040260d9d41b6fc3185/pyodbc-5.2.0.tar.gz", hash = "sha256:de8be39809c8ddeeee26a4b876a6463529cd487a60d1393eb2a93e9bcd44a8f5", size = 116908 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/26/104525b728fedfababd3143426b9d0008c70f0d604a3bf5d4773977d83f4/pyodbc-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be43d1ece4f2cf4d430996689d89a1a15aeb3a8da8262527e5ced5aee27e89c3", size = 73014 },
+ { url = "https://files.pythonhosted.org/packages/4f/7d/bb632488b603bcd2a6753b858e8bc7dd56146dd19bd72003cc09ae6e3fc0/pyodbc-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9f7badd0055221a744d76c11440c0856fd2846ed53b6555cf8f0a8893a3e4b03", size = 72515 },
+ { url = "https://files.pythonhosted.org/packages/ab/38/a1b9bfe5a7062672268553c2d6ff93676173b0fb4bd583e8c4f74a0e296f/pyodbc-5.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad633c52f4f4e7691daaa2278d6e6ebb2fe4ae7709e610e22c7dd1a1d620cf8b", size = 348561 },
+ { url = "https://files.pythonhosted.org/packages/71/82/ddb1c41c682550116f391aa6cab2052910046a30d63014bbe6d09c4958f4/pyodbc-5.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d086a8f7a302b74c9c2e77bedf954a603b19168af900d4d3a97322e773df63", size = 353962 },
+ { url = "https://files.pythonhosted.org/packages/e5/29/fec0e739d0c1cab155843ed71d0717f5e1694effe3f28d397168f48bcd92/pyodbc-5.2.0-cp312-cp312-win32.whl", hash = "sha256:0e4412f8e608db2a4be5bcc75f9581f386ed6a427dbcb5eac795049ba6fc205e", size = 63050 },
+ { url = "https://files.pythonhosted.org/packages/21/7f/3a47e022a97b017ffb73351a1061e4401bcb5aa4fc0162d04f4e5452e4fc/pyodbc-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1f5686b142759c5b2bdbeaa0692622c2ebb1f10780eb3c174b85f5607fbcf55", size = 69485 },
+ { url = "https://files.pythonhosted.org/packages/90/be/e5f8022ec57a7ea6aa3717a3f307a44c3b012fce7ad6ec91aad3e2a56978/pyodbc-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:26844d780045bbc3514d5c2f0d89e7fda7df7db0bd24292eb6902046f5730885", size = 72982 },
+ { url = "https://files.pythonhosted.org/packages/5c/0e/71111e4f53936b0b99731d9b6acfc8fc95660533a1421447a63d6e519112/pyodbc-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26d2d8fd53b71204c755abc53b0379df4e23fd9a40faf211e1cb87e8a32470f0", size = 72515 },
+ { url = "https://files.pythonhosted.org/packages/a5/09/3c06bbc1ebb9ae15f53cefe10774809b67da643883287ba1c44ba053816a/pyodbc-5.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a27996b6d27e275dfb5fe8a34087ba1cacadfd1439e636874ef675faea5149d9", size = 347470 },
+ { url = "https://files.pythonhosted.org/packages/a4/35/1c7efd4665e7983169d20175014f68578e0edfcbc4602b0bafcefa522c4a/pyodbc-5.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaf42c4bd323b8fd01f1cd900cca2d09232155f9b8f0b9bcd0be66763588ce64", size = 353025 },
+ { url = "https://files.pythonhosted.org/packages/6d/c9/736d07fa33572abdc50d858fd9e527d2c8281f3acbb90dff4999a3662edd/pyodbc-5.2.0-cp313-cp313-win32.whl", hash = "sha256:207f16b7e9bf09c591616429ebf2b47127e879aad21167ac15158910dc9bbcda", size = 63052 },
+ { url = "https://files.pythonhosted.org/packages/73/2a/3219c8b7fa3788fc9f27b5fc2244017223cf070e5ab370f71c519adf9120/pyodbc-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:96d3127f28c0dacf18da7ae009cd48eac532d3dcc718a334b86a3c65f6a5ef5c", size = 69486 },
+]
+
[[package]]
name = "python-dotenv"
version = "1.1.0"
@@ -836,6 +929,27 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
+]
+
+[[package]]
+name = "tzlocal"
+version = "5.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 },
+]
+
[[package]]
name = "urllib3"
version = "2.3.0"