first commit

This commit is contained in:
2026-05-10 10:56:52 +08:00
commit 9dec38db60
35 changed files with 8206 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# 虚拟机连接信息
VM_NAME=ubuntu20.04
VM_SSH_HOST=127.0.0.1
VM_SSH_PORT=2222
VM_SSH_USER=chakcy
VM_SSH_PASS=020709djh
# 大模型 API(OpenAI 或兼容接口)
LLM_API_KEY=sk-7b300b4948d04f208d1781c830f773ca
LLM_BASE_URL=https://api.deepseek.com # 若用本地模型可改为 http://localhost:11434/v1
LLM_MODEL=deepseek-v4-flash
+14
View File
@@ -0,0 +1,14 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
*.log
output.md
# Virtual environments
.venv
.sdk
+1
View File
@@ -0,0 +1 @@
3.14
+314
View File
@@ -0,0 +1,314 @@
# VirtualBox Client
**VirtualBox 虚拟机自动化管理工具** — 通过 SSH 和 VirtualBox SDK 实现对 Ubuntu 20.04 虚拟机的全生命周期管理,支持 AI 智能任务执行、文件传输、脚本部署和图形化管理。
---
## ✨ 功能特性
- 🖥️ **虚拟机管理** — 启动、关闭、状态检测(VirtualBox SDK)
- 🔌 **纯 SSH 模式** — 不依赖 VirtualBox,直接通过 SSH 管理远程服务器
- 🤖 **AI 智能代理** — 接入大语言模型(OpenAI API),通过自然语言描述自动执行运维任务
- 📁 **文件传输** — 支持上传/下载文件、上传整个文件夹
- 📂 **工作区浏览器** — 树形文件管理器,支持浏览、新建、重命名、复制、删除、下载文件夹(zip 打包)
- ⚡ **脚本执行** — 上传并执行 Shell 脚本,自动区分部署脚本(sudo)和普通脚本
- 🎨 **图形界面** — 基于 PySide6 构建的现代化暗色主题 GUI
- 📋 **日志面板** — 带等级标识的彩色日志输出,支持自动滚动
---
## 📦 系统要求
| 依赖 | 说明 |
| -------------- | ------------------------ |
| Python | >= 3.14 |
| VirtualBox | 含 VirtualBox SDK(可选) |
| 操作系统 | Windows(主控端) |
| 目标虚拟机 | Ubuntu 20.04(受控端) |
---
## 🚀 快速开始
### 1. 安装依赖
```powershell
# 同步 Python 依赖
uv sync
# 安装 VirtualBox SDK(仅 VirtualBox 模式需要)
# 确保已安装 VirtualBox,找到 sdk 路径后执行:
cd ./sdk/installer/python/
uv run ./vboxapisetup.py install
```
> **📥 VirtualBox SDK 下载**
>
> 如果项目中尚未包含 SDK,请先下载 VirtualBox SDK 并解压到项目目录:
>
> ```powershell
> # 下载 VirtualBox SDK 7.2.8
> curl -L https://download.virtualbox.org/virtualbox/7.2.8/VirtualBoxSDK-7.2.8-173730.zip -o VirtualBoxSDK.zip
> # 解压到项目根目录
> tar -xf VirtualBoxSDK.zip
> ```
>
> SDK 目录结构应包含 `sdk/installer/python/vboxapisetup.py`。
>
> *注意:SDK 版本需与本地安装的 VirtualBox 版本匹配。*
### 2. 配置环境变量
复制 `.env.example` 为 `.env`(或直接在 GUI 中填写):
```env
# 虚拟机配置
VM_NAME=ubuntu20.04
VM_SSH_HOST=127.0.0.1
VM_SSH_PORT=2222
VM_SSH_USER=your_username
VM_SSH_PASS=your_password
# 大模型配置(AI 代理模式需要)
LLM_API_KEY=sk-your-api-key
LLM_BASE_URL=https://api.openai.com
LLM_MODEL=gpt-4o
```
### 3. 启动方式
#### 🖥️ 图形界面
```powershell
uv run gui
```
![GUI Screenshot](docs/images/gui.png) *(screenshot placeholder)*
#### ⌨️ 命令行示例
```powershell
uv run example
```
---
## 📖 使用指南
### 配置面板
左侧配置面板包含三个区域:
1. **运行模式** — 切换「纯 SSH 模式」或「VirtualBox 模式」
2. **虚拟机配置** — SSH 连接信息(主机、端口、用户名、密码)
3. **大模型配置** — AI 代理的 API Key、Base URL 和模型名称
### 任务执行面板
在「任务执行」Tab 中,输入自然语言描述,AI 代理将自动解析并执行:
```
在 /home/user/myproject 下有一个 "myproject" 项目,
请安装 nginx 并确认服务状态
```
系统会自动:
1. 启动虚拟机(VirtualBox 模式)
2. 连接 SSH
3. 调用 LLM 生成执行计划
4. 逐步执行命令并反馈日志
5. 任务完成后询问是否关闭虚拟机
### 文件传输
在「文件传输」Tab 中:
- **上传** — 选择本地文件,指定远程路径,点击上传
- **下载** — 输入远程路径,选择本地保存位置,点击下载
### 脚本执行
在「Script」Tab 中:
1. 点击「Browse」选择本地的 `.sh` 脚本文件
2. 点击「Upload and Execute Script」
3. 脚本会自动上传到 `/tmp/`,执行完成后下载日志并清理临时文件
4. 以 `deploy_` 开头的脚本自动使用 `sudo` 权限执行
### 工作区浏览器
在「工作区」Tab 中,可以像本地文件管理器一样操作远程文件:
- **双击文件夹** — 懒加载展开子目录
- **单击文件** — 显示文件详情
- **右键菜单** — 重命名、复制、删除、下载
- **工具栏** — 上传文件、上传文件夹、新建文件夹、回到根目录
---
## 🛠️ 脚本开发
### 添加新脚本
在 `src/virtualbox_client/scripts/` 下创建 Python 文件:
```python
# src/virtualbox_client/scripts/my_script.py
def run():
print("Hello from my script!")
```
在 `pyproject.toml` 中注册:
```toml
[project.scripts]
my-script = "virtualbox_client.scripts.my_script:run"
```
重新构建并执行:
```powershell
uv sync
uv run my-script
```
### 命令式脚本执行
```powershell
# 直接通过命令行运行脚本模块
uv run -m virtualbox_client.scripts.my_script
```
---
## 📁 项目结构
```
virtualbox_client/
├── src/virtualbox_client/
│ ├── __init__.py
│ ├── gui.py # PySide6 图形界面
│ ├── scripts/
│ │ ├── __init__.py
│ │ ├── agent_task.py # AI 代理任务执行
│ │ ├── example.py # 使用示例
│ │ └── run_script.py # 脚本运行器
│ └── utils/
│ ├── __init__.py
│ ├── ssh_manager.py # 纯 SSH 管理器
│ ├── vbox_ssh_manager.py # VirtualBox + SSH 管理器
│ └── vm_agent.py # AI 代理核心
├── resources/
│ ├── chongming_dev.sh
│ ├── deploy_docker.sh
│ ├── deploy_git.sh
│ ├── deploy_nvm.sh
│ └── deploy_uv.sh
├── scripts/
│ ├── add_script_runner.py
│ ├── add_script_tab.py
│ ├── check_syntax.py
│ ├── clean_pycache.py
│ ├── code_content.py
│ ├── fix_paths.py
│ └── fix_upload_folder.py
├── docs/
│ ├── docker使用手册.md
│ ├── git使用手册.md
│ ├── nvm使用文档.md
│ └── uv使用文档.md
├── .env.example # 环境变量模板
├── .gitignore
├── .python-version
├── gui.spec # PyInstaller 打包配置
├── pyproject.toml # 项目配置与依赖
└── README.md
```
---
## 🧩 技术架构
### 后端
- **VirtualBox SDK** (`vboxapi`) — 通过 Windows COM 接口管理虚拟机生命周期
- **paramiko** — SSH 连接与远程命令执行
- **openai** — 大语言模型 API 调用(AI 代理模式)
- **pywin32** — Windows COM 互操作
### 前端
- **PySide6** — Qt6 的 Python 绑定,提供现代化 GUI
- **QThread** — 后台线程管理,避免 UI 阻塞
- **QTreeWidget** — 带懒加载的远程文件浏览器
### 工作模式
```
┌─────────────────────────────────────────┐
│ VirtualBox Client │
├─────────────────┬───────────────────────┤
│ VirtualBox 模式 │ 纯 SSH 模式 │
│ │ │
│ VirtualBox │ SSH │
│ SDK (COM) │ Manager │
│ ↓ │ ↓ │
│ VBoxSSHManager │ SSHManager │
│ ↓ │ ↓ │
│ Ubuntu VM │ 远程服务器 │
└─────────────────┴───────────────────────┘
```
---
## 🐳 部署与打包
### 使用 PyInstaller 打包为可执行文件
```powershell
uv run pyinstaller gui.spec
```
打包后的可执行文件位于 `dist/` 目录。
---
## ⚙️ 环境搭建详情
| 命令 | 说明 |
| ----------------------------------------- | ----------------------------- |
| `uv sync` | 同步所有 Python 依赖 |
| `uv run ./sdk/installer/python/vboxapisetup.py install` | 安装 VirtualBox SDK |
| `uv run gui` | 启动图形界面 |
| `uv run example` | 运行命令行示例 |
| `uv run agent-task` | 运行 AI 代理任务 |
| `uv run run-script` | 运行脚本 |
| `uv run pyinstaller gui.spec` | 打包为 EXE |
---
## 📝 文档
详细使用文档请参考 `docs/` 目录:
- [Docker 使用手册](docs/docker使用手册.md)
- [Git 使用手册](docs/git使用手册.md)
- [NVM 使用文档](docs/nvm使用文档.md)
- [UV 使用文档](docs/uv使用文档.md)
---
## 🔧 常见问题
**Q: 启动报错 `CoInitialize` 相关错误?**
A: 这是 Windows COM 初始化问题,通常出现在 VirtualBox 模式下。确保已正确安装 VirtualBox SDK。
**Q: SSH 连接失败?**
A: 检查目标虚拟机是否已启动、SSH 服务是否运行、端口转发是否配置正确(默认 2222)。
**Q: AI 代理无响应?**
A: 确保 API Key、Base URL 和模型名称配置正确,且网络可以访问 LLM 服务。
---
## 📄 许可证
本项目仅供学习和参考使用。
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

+754
View File
@@ -0,0 +1,754 @@
# Docker 使用手册
## 📖 目录
- [1. Docker 简介](#1-docker-简介)
- [2. 安装 Docker](#2-安装-docker)
- [3. Docker 基本概念](#3-docker-基本概念)
- [4. 镜像管理](#4-镜像管理)
- [5. 容器管理](#5-容器管理)
- [6. Dockerfile 构建](#6-dockerfile-构建)
- [7. Docker Compose](#7-docker-compose)
- [8. 数据管理(Volume)](#8-数据管理volume)
- [9. 网络管理](#9-网络管理)
- [10. 常用命令速查](#10-常用命令速查)
- [11. 最佳实践与注意事项](#11-最佳实践与注意事项)
---
## 1. Docker 简介
Docker 是一个开源的应用容器引擎,让开发者可以打包应用及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 或 Windows 机器上。
### 核心优势
- **轻量级**:容器共享宿主机内核,无需虚拟化完整 OS
- **快速启动**:秒级启动,相比虚拟机分钟级启动更快
- **环境一致**:消除"在我机器上能跑"的问题
- **高效资源**:内存、硬盘、CPU 占用远小于虚拟机
- **易于迁移**:一次构建,随处运行
---
## 2. 安装 Docker
### 2.1 Ubuntu / Debian
```bash
# 更新软件包索引
sudo apt update
# 安装依赖
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
# 添加 Docker 官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# 添加 Docker 源
sudo add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
# 安装 Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
# 启动并设置开机自启
sudo systemctl enable docker
sudo systemctl start docker
# 将当前用户加入 docker 组(免 sudo 执行)
sudo usermod -aG docker $USER
```
### 2.2 CentOS / RHEL
```bash
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io
sudo systemctl enable docker
sudo systemctl start docker
```
### 2.3 macOS
下载 [Docker Desktop for Mac](https://hub.docker.com/editions/community/docker-ce-desktop-mac/)
### 2.4 Windows
下载 [Docker Desktop for Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows/)
### 2.5 验证安装
```bash
docker --version
docker run hello-world
```
---
## 3. Docker 基本概念
| 概念 | 说明 |
|------|------|
| **镜像(Image)** | 一个只读的模板,包含创建容器的说明 |
| **容器(Container)** | 镜像的运行实例,可被启动、停止、删除 |
| **仓库(Repository)** | 集中存放镜像的地方,如 Docker Hub |
| **Dockerfile** | 自动化构建镜像的脚本文件 |
| **Volume** | 持久化存储数据卷 |
| **网络(Network)** | 容器间的通信通道 |
### 三者的关系
```
Dockerfile (构建) --> Image (镜像) --> run --> Container (容器)
↑
Docker Hub (仓库)
```
---
## 4. 镜像管理
### 4.1 搜索镜像
```bash
docker search nginx
docker search --filter stars=1000 ubuntu
```
### 4.2 拉取镜像
```bash
docker pull nginx:latest # 拉取最新版
docker pull ubuntu:20.04 # 拉取指定标签
docker pull registry.cn-hangzhou.aliyuncs.com/library/nginx # 国内镜像加速
```
### 4.3 查看本地镜像
```bash
docker images # 查看所有镜像
docker images -q # 只显示镜像 ID
docker image ls --filter dangling=true # 查看悬空镜像
```
### 4.4 删除镜像
```bash
docker rmi nginx:latest # 删除指定镜像
docker rmi -f <image_id> # 强制删除
docker image prune # 删除悬空镜像
docker image prune -a # 删除所有未使用的镜像
```
### 4.5 镜像标签与导出
```bash
# 打标签
docker tag ubuntu:20.04 myubuntu:v1
# 导出镜像为 tar 文件
docker save -o ubuntu.tar ubuntu:20.04
# 从 tar 文件导入镜像
docker load -i ubuntu.tar
```
### 4.6 登录与推送
```bash
docker login # 登录 Docker Hub
docker push username/myapp:v1 # 推送镜像
```
---
## 5. 容器管理
### 5.1 创建并启动容器
```bash
# 基本启动
docker run nginx
# 后台运行(-d)
docker run -d nginx
# 端口映射(-p 宿主机端口:容器端口)
docker run -d -p 8080:80 nginx
# 命名容器(--name)
docker run -d --name my-nginx -p 8080:80 nginx
# 设置环境变量(-e)
docker run -d --name my-mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7
# 挂载数据卷(-v)
docker run -d -v /host/data:/container/data nginx
# 自动删除(--rm,容器停止后自动删除)
docker run --rm -it ubuntu bash
# 交互模式(-it)
docker run -it ubuntu bash
```
### 5.2 查看容器
```bash
docker ps # 查看运行中的容器
docker ps -a # 查看所有容器(含已停止)
docker ps -q # 只显示容器 ID
docker container ls -a # 同 docker ps -a
```
### 5.3 停止、启动、重启容器
```bash
docker stop <container_id/name> # 停止容器
docker start <container_id/name> # 启动已停止的容器
docker restart <container_id/name> # 重启容器
docker pause <container_id/name> # 暂停容器
docker unpause <container_id/name> # 恢复暂停的容器
```
### 5.4 进入容器
```bash
# 推荐方式(exit 不会导致容器停止)
docker exec -it <container_id/name> bash
# 如果容器没有 bash,可以用 sh
docker exec -it <container_id/name> sh
# attach 方式(exit 会导致容器停止)
docker attach <container_id/name>
```
### 5.5 查看日志
```bash
docker logs <container_id/name> # 查看所有日志
docker logs -f <container_id/name> # 实时跟踪日志
docker logs --tail 100 <container_id/name> # 查看最近 100 行
docker logs -t <container_id/name> # 显示时间戳
```
### 5.6 容器内部文件操作
```bash
# 从宿主机拷贝文件到容器
docker cp /host/file.txt container_id:/container/path/
# 从容器拷贝文件到宿主机
docker cp container_id:/container/file.txt /host/path/
```
### 5.7 删除容器
```bash
docker rm <container_id/name> # 删除已停止的容器
docker rm -f <container_id/name> # 强制删除运行中的容器
docker container prune # 删除所有已停止的容器
docker rm $(docker ps -aq) # 删除所有容器
```
### 5.8 查看容器详细信息
```bash
docker inspect <container_id/name> # 查看容器详细信息(JSON)
docker inspect --format '{{.NetworkSettings.IPAddress}}' <container_id>
docker top <container_id/name> # 查看容器中的进程
docker stats # 查看所有容器的资源使用
```
---
## 6. Dockerfile 构建
### 6.1 Dockerfile 指令详解
```dockerfile
# 指定基础镜像
FROM ubuntu:20.04
# 维护者信息
LABEL maintainer="yourname@example.com"
# 设置环境变量
ENV APP_HOME /app
ENV TZ Asia/Shanghai
# 设置工作目录
WORKDIR $APP_HOME
# 运行命令(构建时执行)
RUN apt update && apt install -y python3 python3-pip nginx
# 复制文件到镜像
COPY ./app /app
ADD ./archive.tar.gz /tmp/ # ADD 支持自动解压 tar 和远程 URL
# 暴露端口(仅声明,不实际映射)
EXPOSE 80 443
# 设置卷
VOLUME ["/data", "/config"]
# 运行用户
USER appuser
# 启动命令
CMD ["python3", "app.py"]
# 或使用 ENTRYPOINT(可追加参数)
ENTRYPOINT ["python3"]
CMD ["app.py"] # 作为 ENTRYPOINT 的默认参数
```
### 6.2 构建镜像
```bash
docker build -t myapp:v1 . # 当前目录构建
docker build -t myapp:v1 -f Dockerfile.prod . # 指定 Dockerfile
docker build --no-cache -t myapp:v1 . # 不使用缓存
```
### 6.3 多阶段构建
```dockerfile
# 阶段一:编译
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 阶段二:运行(极小镜像)
FROM alpine:latest
RUN apk add --no-cache ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
```
### 6.4 优化建议
```dockerfile
# ✅ 合并 RUN 命令,减少层数
RUN apt update && \
apt install -y curl nginx && \
rm -rf /var/lib/apt/lists/*
# ✅ 使用 .dockerignore 忽略不必要的文件
# .dockerignore 文件内容:
# node_modules
# .git
# *.log
# Dockerfile
```
---
## 7. Docker Compose
### 7.1 安装 Docker Compose
```bash
# Linux
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
# Ubuntu 通过 apt
sudo apt install -y docker-compose
# 验证
docker-compose --version
```
### 7.2 docker-compose.yml 示例
```yaml
version: '3.8'
services:
# Web 服务
web:
image: nginx:latest
container_name: my-web
ports:
- "8080:80"
- "443:443"
volumes:
- ./html:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- app-net
depends_on:
- api
restart: always
# API 服务
api:
build:
context: ./api
dockerfile: Dockerfile
container_name: my-api
environment:
- DB_HOST=db
- DB_PORT=3306
volumes:
- ./api:/app
networks:
- app-net
depends_on:
db:
condition: service_healthy
restart: unless-stopped
# 数据库
db:
image: mysql:5.7
container_name: my-db
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-root123}
MYSQL_DATABASE: myapp
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-net
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
restart: always
# Redis 缓存
redis:
image: redis:7-alpine
container_name: my-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- app-net
restart: always
volumes:
mysql-data:
driver: local
redis-data:
driver: local
networks:
app-net:
driver: bridge
```
### 7.3 Compose 常用命令
```bash
# 启动所有服务(-d 后台运行)
docker-compose up -d
# 构建并启动
docker-compose up -d --build
# 停止并删除容器
docker-compose down
# 停止并删除容器 + 数据卷
docker-compose down -v
# 查看日志
docker-compose logs -f
# 查看服务状态
docker-compose ps
# 重启服务
docker-compose restart
# 执行命令
docker-compose exec web bash
# 查看镜像
docker-compose images
# 拉取最新镜像
docker-compose pull
# 指定配置文件
docker-compose -f docker-compose.prod.yml up -d
```
### 7.4 环境变量管理
```bash
# 创建 .env 文件
echo "DB_PASSWORD=mysecret" > .env
echo "APP_ENV=production" >> .env
# 在 compose 文件中使用 ${VAR_NAME} 引用
# 默认值语法:${VAR_NAME:-default_value}
```
---
## 8. 数据管理(Volume)
### 8.1 Volume 类型
| 类型 | 说明 | 适用场景 |
|------|------|----------|
| **命名卷** | docker 管理的卷,存储在 `/var/lib/docker/volumes/` | 持久化数据库数据 |
| **绑定挂载** | 挂载宿主机目录到容器 | 开发时热更新代码 |
| **tmpfs 挂载** | 存储在内存中 | 敏感数据,临时文件 |
### 8.2 管理 Volume
```bash
# 创建卷
docker volume create my-volume
# 查看卷
docker volume ls
docker volume inspect my-volume
# 删除卷
docker volume rm my-volume
docker volume prune # 删除所有未使用的卷
# 使用卷运行容器
docker run -d -v my-volume:/data nginx
# 绑定挂载
docker run -d -v /host/path:/container/path nginx
# 只读挂载
docker run -d -v /host/path:/container/path:ro nginx
# tmpfs 挂载
docker run -d --tmpfs /tmp:rw,noexec,nosuid,size=100m nginx
```
### 8.3 容器间共享数据
```bash
# 创建数据卷容器
docker create -v /shared-data --name data-container ubuntu
# 其他容器使用 --volumes-from 共享
docker run -d --volumes-from data-container --name app1 nginx
docker run -d --volumes-from data-container --name app2 nginx
```
---
## 9. 网络管理
### 9.1 网络类型
| 网络模式 | 说明 | 适用场景 |
|----------|------|----------|
| **bridge** | 默认,通过虚拟网桥通信 | 单机容器间通信 |
| **host** | 直接使用宿主机网络 | 高性能场景 |
| **none** | 无网络 | 安全隔离 |
| **overlay** | 跨主机通信 | 多节点集群 |
### 9.2 管理网络
```bash
# 创建网络
docker network create --driver bridge my-network
# 创建带子网的网络
docker network create --subnet=172.20.0.0/16 --ip-range=172.20.10.0/24 my-network
# 查看网络
docker network ls
docker network inspect my-network
# 连接容器到网络
docker network connect my-network container_name
# 断开容器网络
docker network disconnect my-network container_name
# 删除网络
docker network rm my-network
docker network prune # 删除所有未使用的网络
```
### 9.3 指定 IP 运行
```bash
docker run -d --network my-network --ip 172.20.10.10 nginx
```
### 9.4 容器间 DNS 解析
同一自定义网络下的容器,可以通过容器名直接通信:
```bash
docker run -d --network my-network --name web nginx
docker run -d --network my-network --name app alpine ping web
```
---
## 10. 常用命令速查
### 10.1 镜像操作
| 命令 | 说明 |
|------|------|
| `docker images` | 列出镜像 |
| `docker pull <image>` | 拉取镜像 |
| `docker push <image>` | 推送镜像 |
| `docker rmi <image>` | 删除镜像 |
| `docker build -t <tag> .` | 构建镜像 |
| `docker tag <old> <new>` | 打标签 |
| `docker save -o file.tar <image>` | 导出镜像 |
| `docker load -i file.tar` | 导入镜像 |
| `docker image prune` | 清理悬空镜像 |
### 10.2 容器操作
| 命令 | 说明 |
|------|------|
| `docker ps -a` | 查看所有容器 |
| `docker run -d --name <name> <image>` | 后台运行容器 |
| `docker exec -it <name> bash` | 进入容器 |
| `docker logs -f <name>` | 查看日志 |
| `docker stop <name>` | 停止容器 |
| `docker start <name>` | 启动容器 |
| `docker restart <name>` | 重启容器 |
| `docker rm -f <name>` | 强制删除容器 |
| `docker cp <name>:/path /local` | 拷贝文件 |
| `docker inspect <name>` | 查看详情 |
| `docker stats` | 查看资源占用 |
| `docker container prune` | 清理已停止容器 |
### 10.3 系统维护
| 命令 | 说明 |
|------|------|
| `docker info` | Docker 系统信息 |
| `docker version` | 版本信息 |
| `docker system df` | 查看磁盘占用 |
| `docker system prune -a` | 清理所有未使用资源 |
| `docker system prune -a --volumes` | 清理全部(含卷) |
---
## 11. 最佳实践与注意事项
### 11.1 安全建议
1. **不使用 root 用户运行容器**:在 Dockerfile 中使用 `USER` 指定非 root 用户
2. **镜像最小化**:使用 alpine 等精简基础镜像
3. **限制资源**:
```bash
docker run -d \
--memory="512m" \
--cpus="1.0" \
--memory-swap="1g" \
--read-only \
nginx
```
4. **定期扫描漏洞**:使用 `docker scan` 或 Trivy 扫描镜像
### 11.2 性能优化
1. **使用 `.dockerignore`**:避免将不必要的文件构建到镜像中
2. **合并 RUN 指令**:减少镜像层数
3. **利用构建缓存**:将不常变化的指令放在 Dockerfile 前面
4. **选择合适的基础镜像**:alpine (~5MB) vs ubuntu (~200MB)
### 11.3 日志与调试
```bash
# 限制容器日志大小(在 docker-compose.yml 中)
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
# 全局设置(daemon.json)
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
```
### 11.4 镜像加速器配置
```bash
# /etc/docker/daemon.json
{
"registry-mirrors": [
"https://registry.docker-cn.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
# 重启 Docker
sudo systemctl restart docker
```
### 11.5 清理空间
```bash
# 一键清理(慎用!会删除未使用的镜像、容器、网络、构建缓存)
docker system prune -a
# 只清理构建缓存
docker builder prune
# 查看空间占用
docker system df
```
### 11.6 常见问题排查
```bash
# 1. 容器无法启动 -> 查看日志
docker logs <container_id>
# 2. 端口冲突 -> 查看占用
sudo netstat -tlnp | grep 80
# 3. 磁盘空间不足 -> 清理
docker system prune -a
docker volume prune
# 4. 容器间无法通信 -> 检查网络
docker network inspect <network_name>
# 5. 进入排查模式
docker run -it --rm --network container:<目标容器名> nicolaka/netshoot
```
---
## 附录:资源链接
- [Docker 官方文档](https://docs.docker.com/)
- [Docker Hub](https://hub.docker.com/)
- [Docker Compose 官方文档](https://docs.docker.com/compose/)
- [Dockerfile 最佳实践](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/)
- [Play With Docker(在线实验)](https://labs.play-with-docker.com/)
---
*本文档由 Docker 虚拟机管理助手生成,持续更新中...*
+431
View File
@@ -0,0 +1,431 @@
# Git 使用手册
> 版本: 1.0
> 适用系统: Ubuntu 20.04 / 通用
> Git 版本: 2.25.1+
---
## 目录
1. [Git 简介](#1-git-简介)
2. [安装与配置](#2-安装与配置)
3. [基础操作](#3-基础操作)
4. [分支管理](#4-分支管理)
5. [远程仓库](#5-远程仓库)
6. [撤销与回退](#6-撤销与回退)
7. [标签管理](#7-标签管理)
8. [常见工作流程](#8-常见工作流程)
9. [常用命令速查表](#9-常用命令速查表)
---
## 1. Git 简介
Git 是一个**分布式版本控制系统**,由 Linus Torvalds 于 2005 年创建。它能够高效地处理从小型到大型项目的版本管理,是目前最流行的版本控制工具。
### 核心特点
- **分布式**: 每个开发者本地都有完整的代码仓库
- **高效**: 针对文件差异和分支操作进行了优化
- **数据完整性**: 所有数据在存储前都会计算校验和
- **支持非线性开发**: 轻量级分支,鼓励分支开发
---
## 2. 安装与配置
### 2.1 在 Ubuntu/Debian 上安装
```bash
# 更新软件包列表
sudo apt update
# 安装 Git
sudo apt install -y git
# 验证安装
git --version
```
### 2.2 在 CentOS/RHEL 上安装
```bash
sudo yum install -y git
```
### 2.3 基本配置
```bash
# 配置用户名(提交时记录)
git config --global user.name "Your Name"
# 配置邮箱
git config --global user.email "your_email@example.com"
# 配置默认分支名(Git 2.28+)
git config --global init.defaultBranch main
# 配置默认文本编辑器
git config --global core.editor "vim"
# 查看当前配置
git config --list
```
> 配置文件的优先级: 项目级 `.git/config` > 全局 `~/.gitconfig` > 系统级 `/etc/gitconfig`
---
## 3. 基础操作
### 3.1 创建仓库
```bash
# 在当前目录初始化新仓库
git init
# 克隆远程仓库
git clone <仓库地址>
git clone <仓库地址> <本地目录名>
```
### 3.2 文件状态
Git 文件有三种状态:**已修改 (modified)**、**已暂存 (staged)**、**已提交 (committed)**
```bash
# 查看文件状态
git status
# 以简洁模式查看
git status -s
```
状态标识说明:
- `??` - 未跟踪的文件
- `M` - 已修改
- `A` - 已暂存
- `D` - 已删除
### 3.3 添加与提交
```bash
# 添加文件到暂存区
git add <文件名> # 添加指定文件
git add . # 添加所有变更
git add -A # 添加所有变更(包括删除)
git add -p # 交互式分段添加
# 提交到本地仓库
git commit -m "提交说明"
git commit -am "提交说明" # 跳过 git add(仅对已跟踪文件有效)
# 修改最后一次提交
git commit --amend -m "新的提交说明"
```
### 3.4 查看历史
```bash
# 查看提交历史
git log
git log --oneline # 简洁模式
git log --graph # 图形化显示
git log --all # 显示所有分支
git log -p # 显示差异内容
git log --author="名字" # 按作者筛选
git log --since="2024-01-01" # 按时间筛选
# 查看某个文件的修改历史
git log -p <文件名>
git blame <文件名> # 每行最后修改者
```
---
## 4. 分支管理
### 4.1 创建与切换
```bash
# 查看分支列表
git branch # 本地分支
git branch -r # 远程分支
git branch -a # 所有分支
# 创建分支
git branch <分支名>
# 切换分支
git checkout <分支名>
git switch <分支名> # 新式语法
# 创建并切换分支
git checkout -b <分支名>
git switch -c <分支名>
# 重命名分支
git branch -m <旧名> <新名>
```
### 4.2 合并与变基
```bash
# 合并指定分支到当前分支
git merge <分支名>
# 变基操作(将当前分支的提交移至目标分支顶端)
git rebase <分支名>
# 交互式变基(合并、修改提交记录)
git rebase -i HEAD~3 # 修改最近 3 个提交
```
> **合并 vs 变基**: 合并保留完整历史,变基使历史更线性。公共分支推荐使用合并,个人分支可使用变基。
### 4.3 删除分支
```bash
# 删除本地分支
git branch -d <分支名> # 安全删除(会检查是否已合并)
git branch -D <分支名> # 强制删除
# 删除远程分支
git push origin --delete <分支名>
```
---
## 5. 远程仓库
### 5.1 管理远程仓库
```bash
# 查看远程仓库
git remote -v
# 添加远程仓库
git remote add origin <仓库地址>
# 修改远程仓库地址
git remote set-url origin <新地址>
# 删除远程仓库
git remote remove origin
```
### 5.2 推送与拉取
```bash
# 推送到远程仓库
git push origin <分支名>
git push -u origin <分支名> # 首次推送,建立关联
git push --all # 推送所有分支
git push origin --tags # 推送所有标签
# 拉取远程更新
git pull # 拉取并合并(相当于 fetch + merge)
git pull --rebase # 拉取并变基(推荐)
git fetch # 仅拉取,不自动合并
```
### 5.3 处理冲突
```bash
# 当合并或拉取产生冲突时:
# 1. 编辑冲突文件,保留需要的内容
# 2. 标记为已解决
git add <已解决冲突的文件>
# 3. 完成合并
git commit
# 中止合并
git merge --abort
```
---
## 6. 撤销与回退
### 6.1 撤销工作区修改
```bash
# 恢复文件到最近一次提交的状态
git checkout -- <文件名>
git restore <文件名> # 新式语法
```
### 6.2 撤销暂存区
```bash
# 将文件从暂存区移出(保留工作区修改)
git reset HEAD <文件名>
git restore --staged <文件名> # 新式语法
```
### 6.3 回退提交
```bash
# 软重置(保留工作区和暂存区修改)
git reset --soft HEAD~1
# 混合重置(保留工作区,清空暂存区)- 默认模式
git reset --mixed HEAD~1
# 硬重置(丢弃所有修改)- 谨慎使用!
git reset --hard HEAD~1
# 回退到指定提交
git reset --hard <提交哈希>
# 生成一个新的反向提交来撤销修改
git revert HEAD # 撤销最近一次提交
git revert <提交哈希>
```
> `git reset` 与 `git revert` 的区别:`reset` 会删除提交历史,`revert` 会保留历史并新增一个反向提交。公共分支应使用 `revert`。
---
## 7. 标签管理
```bash
# 创建标签
git tag <标签名> # 轻量标签
git tag -a <标签名> -m "说明" # 附注标签
# 查看标签
git tag
git tag -l "v1.*" # 匹配模式
# 推送标签到远程
git push origin <标签名>
git push origin --tags
# 删除标签
git tag -d <标签名>
git push origin --delete <标签名>
```
---
## 8. 常见工作流程
### 8.1 Git Flow 工作流
适用于有固定发布周期的项目:
- `main` - 主分支,存放稳定版本
- `develop` - 开发分支
- `feature/*` - 功能分支
- `release/*` - 发布分支
- `hotfix/*` - 紧急修复分支
### 8.2 GitHub Flow
适用于持续交付的项目:
1. 从 `main` 创建功能分支
2. 提交修改
3. 发起 Pull Request
4. 代码评审 + 自动化测试
5. 合并回 `main`
6. 立即部署
### 8.3 日常开发流程示例
```bash
# 1. 更新主分支
git checkout main
git pull
# 2. 创建功能分支
git checkout -b feature/my-feature
# 3. 开发并提交
git add .
git commit -m "feat: 添加新功能"
# 4. 同步上游变更
git fetch origin
git rebase origin/main
# 5. 推送并创建 Pull Request
git push -u origin feature/my-feature
```
---
## 9. 常用命令速查表
| 操作 | 命令 |
|------|------|
| 初始化仓库 | `git init` |
| 克隆仓库 | `git clone <url>` |
| 查看状态 | `git status` |
| 添加文件 | `git add <file>` |
| 提交 | `git commit -m "msg"` |
| 查看日志 | `git log --oneline --graph` |
| 创建分支 | `git branch <name>` |
| 切换分支 | `git switch <name>` |
| 创建+切换 | `git switch -c <name>` |
| 合并分支 | `git merge <name>` |
| 推送 | `git push origin <branch>` |
| 拉取 | `git pull --rebase` |
| 查看差异 | `git diff` |
| 暂存修改 | `git stash` |
| 恢复暂存 | `git stash pop` |
| 查看远程 | `git remote -v` |
---
## 附录
### .gitignore 文件示例
```gitignore
# 编译输出
*.o
*.class
*.exe
*.dll
# 依赖目录
node_modules/
vendor/
__pycache__/
# IDE 配置
.vscode/
.idea/
*.swp
# 系统文件
.DS_Store
Thumbs.db
# 环境配置
.env
*.local
```
### 提交信息规范
推荐使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
```
<类型>(<范围>): <简短描述>
<详细描述>
<尾部信息>
```
类型包括:`feat`、`fix`、`docs`、`style`、`refactor`、`test`、`chore` 等。
---
> 本文档持续更新中...
> 更多信息请访问 [Git 官方文档](https://git-scm.com/doc)
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

+189
View File
@@ -0,0 +1,189 @@
# NVM (Node Version Manager) 使用文档
## 一、什么是 NVM?
NVM(Node Version Manager)是一个用于管理多个 Node.js 版本的工具。它可以让你在同一台机器上安装、切换和使用不同版本的 Node.js,非常适合需要兼顾不同项目 Node.js 版本需求的开发者。
---
## 二、安装 NVM
### 2.1 一键安装
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
```
### 2.2 加载 NVM
安装完成后,需要重新加载终端或者执行以下命令使 nvm 生效:
```bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
```
### 2.3 验证安装
```bash
nvm --version
```
---
## 三、配置镜像源(加速 Node.js 下载)
在国内使用 nvm 下载 Node.js 时,建议配置淘宝镜像源以提高下载速度。
### 临时配置(仅当前会话生效)
```bash
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
```
### 永久配置(写入 shell 配置文件)
```bash
echo 'export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node' >> ~/.bashrc
source ~/.bashrc
```
---
## 四、NVM 常用命令
### 4.1 安装 Node.js
```bash
# 安装最新稳定版
nvm install node
# 安装指定版本
nvm install 20.11.0
# 安装最新的 LTS 版本
nvm install --lts
```
### 4.2 查看可安装的版本
```bash
# 列出所有可安装的版本
nvm ls-remote
# 列出所有 LTS 版本
nvm ls-remote --lts
```
### 4.3 切换 Node.js 版本
```bash
# 使用指定版本
nvm use 20.11.0
# 使用最新稳定版
nvm use node
# 使用最新的 LTS 版本
nvm use --lts
```
### 4.4 查看已安装的版本
```bash
nvm ls
```
### 4.5 设置默认版本
```bash
# 设置系统默认使用的 Node.js 版本
nvm alias default 20.11.0
# 将最新稳定版设为默认
nvm alias default node
```
### 4.6 卸载 Node.js 版本
```bash
nvm uninstall 20.11.0
```
---
## 五、版本管理实践
### 5.1 为项目指定 Node.js 版本
在项目根目录创建 `.nvmrc` 文件:
```bash
# .nvmrc 文件内容示例
20.11.0
```
然后在该目录下运行 `nvm use` 即可自动切换到对应版本。
### 5.2 查看当前使用的版本
```bash
node -v
npm -v
```
### 5.3 版本别名管理
```bash
# 创建别名
nvm alias myapp 20.11.0
# 使用别名切换
nvm use myapp
# 删除别名
nvm unalias myapp
```
---
## 六、常见问题
### 6.1 命令找不到(command not found)
如果重启终端后出现 `nvm: command not found`,请确保已将 nvm 加载命令添加到 `~/.bashrc`、`~/.zshrc` 或 `~/.profile` 文件中。
### 6.2 下载速度慢
配置淘宝镜像源即可解决:
```bash
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
```
### 6.3 权限问题
使用 nvm 安装的 Node.js 不需要 `sudo` 权限,如果遇到权限问题,请检查是否使用了系统自带的 Node.js。
---
## 七、常用命令速查表
| 命令 | 说明 |
|------|------|
| `nvm --version` | 查看 nvm 版本 |
| `nvm install <version>` | 安装指定版本 |
| `nvm install node` | 安装最新稳定版 |
| `nvm install --lts` | 安装最新 LTS 版本 |
| `nvm use <version>` | 切换使用指定版本 |
| `nvm ls` | 列出已安装版本 |
| `nvm ls-remote` | 列出远程所有版本 |
| `nvm alias default <version>` | 设置默认版本 |
| `nvm uninstall <version>` | 卸载指定版本 |
| `nvm current` | 显示当前使用的版本 |
---
> 文档版本: v1.0
> 最后更新: 2025年
+424
View File
@@ -0,0 +1,424 @@
# uv 使用文档
> uv 是一个用 Rust 编写的极速 Python 包管理与工具链管理器,由 Astral 公司(Ruff 作者)开发。
> 它可替代 pip、pip-tools、pipx、poetry、pyenv、virtualenv 等多个工具。
---
## 目录
1. [简介](#1-简介)
2. [安装与配置](#2-安装与配置)
3. [核心命令速查](#3-核心命令速查)
4. [Python 版本管理](#4-python-版本管理)
5. [虚拟环境管理](#5-虚拟环境管理)
6. [包管理](#6-包管理)
7. [项目工作流](#7-项目工作流)
8. [镜像源配置](#8-镜像源配置)
9. [常见问题](#9-常见问题)
---
## 1. 简介
### 1.1 什么是 uv?
uv 是一个"大一统"的 Python 工具链,具备以下核心能力:
| 功能 | 对应传统工具 |
|------|------------|
| Python 版本安装与管理 | pyenv |
| 虚拟环境创建 | venv / virtualenv |
| 包安装 | pip |
| 依赖锁定 | pip-tools / poetry |
| 工具包运行 | pipx |
| 脚本依赖管理 | 原生支持 |
### 1.2 核心优势
- **极快速度**:比 pip 快 10-100 倍
- **单一二进制**:无需 Python 即可安装 uv
- **兼容性强**:兼容 pip 的工作流和 requirements.txt
- **全面替代**:一个工具解决 Python 环境所有问题
---
## 2. 安装与配置
### 2.1 一键安装(推荐)
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
安装后重启终端或执行:
```bash
source $HOME/.local/bin/env
```
### 2.2 其他安装方式
**macOS (Homebrew):**
```bash
brew install uv
```
**Linux (apt/手动):**
```bash
# 下载二进制
curl -LsSf https://astral.sh/uv/install.sh | sh
```
**Windows (PowerShell):**
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
### 2.3 验证安装
```bash
uv --version
# 输出示例: uv 0.11.11 (x86_64-unknown-linux-gnu)
```
### 2.4 配置国内镜像源(加速)
创建配置文件 `~/.config/uv/uv.toml`:
```toml
# Python 版本下载镜像源
python-install-mirror = "https://registry.npmmirror.com/-/binary/python-build-standalone/"
# pip 包下载镜像源(阿里云)
[[index]]
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true
```
也可通过环境变量临时设置:
```bash
export UV_PYTHON_INSTALL_MIRROR="https://registry.npmmirror.com/-/binary/python-build-standalone/"
```
---
## 3. 核心命令速查
| 命令 | 说明 |
|------|------|
| `uv python install <版本>` | 安装指定 Python 版本 |
| `uv python list` | 列出可用的 Python 版本 |
| `uv venv` | 创建虚拟环境 |
| `uv pip install <包>` | 安装 Python 包 |
| `uv pip sync requirements.txt` | 同步依赖 |
| `uv run <文件>` | 在虚拟环境中运行脚本 |
| `uv add <包>` | 向项目添加依赖 |
| `uv lock` | 生成锁定文件 |
| `uv tool run <工具>` | 运行工具包(类似 pipx) |
| `uv init` | 初始化新项目 |
| `uv build` | 构建分发包 |
---
## 4. Python 版本管理
### 4.1 查看可安装的 Python 版本
```bash
# 列出所有可下载的版本
uv python list --only-downloads
# 列出已安装的版本
uv python list
```
### 4.2 安装指定 Python 版本
```bash
# 安装最新 Python 3.12
uv python install 3.12
# 安装具体版本
uv python install 3.11.11
# 安装多个版本
uv python install 3.10 3.11 3.12
```
### 4.3 查看与切换当前 Python
```bash
# 查看当前使用的 Python
uv python
# 查找特定版本的路径
uv python find 3.11
# 为项目固定 Python 版本
uv python pin 3.12
```
### 4.4 卸载 Python 版本
```bash
# uv 安装的 Python 存储在 ~/.local/share/uv/python/
# 直接删除对应目录即可
rm -rf ~/.local/share/uv/python/cpython-3.10*
```
---
## 5. 虚拟环境管理
### 5.1 创建虚拟环境
```bash
# 在当前目录创建 .venv(使用默认 Python)
uv venv
# 指定 Python 版本创建
uv venv --python 3.12
# 指定名称
uv venv myenv
```
### 5.2 激活虚拟环境
```bash
# Linux / macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
```
> **提示**:uv 会自动检测并使用当前目录下的 `.venv`,无需手动激活也可通过 `uv run` 使用。
### 5.3 删除虚拟环境
```bash
# 直接删除 .venv 目录即可
rm -rf .venv
```
---
## 6. 包管理
uv 兼容 pip 的全部功能,且速度更快。
### 6.1 安装包
```bash
# 安装单个包
uv pip install requests
# 安装指定版本
uv pip install "flask>=2.0"
# 从 requirements.txt 安装
uv pip install -r requirements.txt
# 从 pyproject.toml 安装
uv pip install -e .
```
### 6.2 管理已安装的包
```bash
# 列出已安装的包
uv pip list
# 查看包信息
uv pip show requests
# 检查依赖冲突
uv pip check
# 卸载包
uv pip uninstall requests
```
### 6.3 依赖锁定
```bash
# 根据 requirements.in 编译出锁定文件
uv pip compile requirements.in -o requirements.txt
# 同步环境到锁定状态
uv pip sync requirements.txt
```
---
## 7. 项目工作流
### 7.1 初始化新项目
```bash
# 创建新项目
uv init my_project
cd my_project
# 添加依赖
uv add requests flask
# 安装所有依赖
uv sync
# 运行项目
uv run python main.py
```
### 7.2 脚本依赖管理(Shebang 方式)
在 Python 脚本顶部添加内联元数据,自动管理依赖:
```python
# /// script
# dependencies = [
# "requests>=2.28",
# "rich>=13.0",
# ]
# ///
import requests
from rich import print
response = requests.get("https://api.github.com")
print(response.status_code)
```
运行方式:
```bash
uv run script.py
```
> uv 会自动创建隔离环境并安装依赖,无需手动管理!
### 7.3 工具包管理(替代 pipx)
```bash
# 安装工具包
uv tool install ruff
# 运行工具(无需安装)
uv tool run black --help
# 列出已安装的工具
uv tool list
# 升级工具
uv tool upgrade ruff
# 卸载工具
uv tool uninstall ruff
```
---
## 8. 镜像源配置
### 8.1 配置文件方式
编辑 `~/.config/uv/uv.toml`:
```toml
# ========== Python 版本下载镜像 ==========
python-install-mirror = "https://registry.npmmirror.com/-/binary/python-build-standalone/"
# ========== 包下载镜像源 ==========
[[index]]
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true
# 可配置多个备用镜像源
[[index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple/"
```
### 8.2 环境变量方式
```bash
# Python 版本下载镜像
export UV_PYTHON_INSTALL_MIRROR="https://registry.npmmirror.com/-/binary/python-build-standalone/"
# 包下载镜像(临时覆盖)
export UV_INDEX_URL="https://mirrors.aliyun.com/pypi/simple/"
```
### 8.3 常用国内镜像源
| 镜像源 | Python 版本镜像 URL | PyPI 镜像 URL |
|--------|-------------------|--------------|
| 华为云 | `https://repo.huaweicloud.com/python` | `https://mirrors.huaweicloud.com/repository/pypi/simple` |
| 阿里云 | - | `https://mirrors.aliyun.com/pypi/simple/` |
| 清华大学 | `https://mirrors.tuna.tsinghua.edu.cn/python` | `https://pypi.tuna.tsinghua.edu.cn/simple/` |
| 中科大 | `https://mirrors.ustc.edu.cn/python` | `https://pypi.mirrors.ustc.edu.cn/simple/` |
---
## 9. 常见问题
### Q1: 安装时遇到权限问题?
```bash
# 确保 ~/.local/bin 在 PATH 中
export PATH="$HOME/.local/bin:$PATH"
```
### Q2: 如何更新 uv?
```bash
uv self update
```
### Q3: uv 和 pip 能否共存?
完全可以。uv 兼容 pip 的工作流,两者可共存使用。建议逐步迁移到 uv。
### Q4: 如何查看帮助?
```bash
# 通用帮助
uv help
# 子命令帮助
uv python --help
uv pip --help
```
### Q5: 如何卸载 uv?
```bash
# 删除二进制文件
rm ~/.local/bin/uv ~/.local/bin/uvx
# 删除配置
rm -rf ~/.config/uv
# 删除缓存
rm -rf ~/.cache/uv
# 删除安装的 Python 版本
rm -rf ~/.local/share/uv
```
### Q6: 为什么我执行 `uv python install` 下载很慢?
请检查是否已配置国内镜像源(参见第 8 节),或手动设置:
```bash
export UV_PYTHON_INSTALL_MIRROR="https://registry.npmmirror.com/-/binary/python-build-standalone/"
```
---
## 参考资源
- [uv 官方文档](https://docs.astral.sh/uv/)
- [uv GitHub 仓库](https://github.com/astral-sh/uv)
- [华为云 Python 镜像](https://repo.huaweicloud.com/python)
- [阿里云 PyPI 镜像](https://mirrors.aliyun.com/pypi/simple/)
+55
View File
@@ -0,0 +1,55 @@
# -*- mode: python ; coding: utf-8 -*-
import os
import sys
# Python 的 base_prefix 路径
python_dir = sys.base_prefix # e.g., C:\Users\chakcy\AppData\Roaming\uv\python\cpython-3.14-windows-x86_64-none
# DLL 目录
dlls_dir = os.path.join(python_dir, "DLLs")
# 收集 SSL 相关的二进制文件
ssl_binaries = []
for f in ["_ssl.pyd", "libcrypto-3-x64.dll", "libssl-3-x64.dll"]:
src = os.path.join(dlls_dir, f)
if os.path.exists(src):
ssl_binaries.append((src, "."))
print(f"Found: {src}")
a = Analysis(
['src\\virtualbox_client\\gui.py'],
pathex=[],
binaries=ssl_binaries,
datas=[],
hiddenimports=["ssl", "pywin32", "win32api", "win32security"],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='VirtualBoxClient',
icon=['chongming.ico'],
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
+32
View File
@@ -0,0 +1,32 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
[project]
name = "virtualbox-client"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
"openai>=2.35.1",
"paramiko>=4.0.0",
"python-dotenv>=1.2.2",
"pywin32>=311",
"PySide6>=6.8.0",
]
[dependency-groups]
dev = [
"pyinstaller>=6.20.0",
"setuptools>=82.0.1",
]
[tool.hatch.build.targets.wheel]
packages = ["src/virtualbox_client"]
[project.scripts]
example = "virtualbox_client.scripts.example:run"
agent-task = "virtualbox_client.scripts.agent_task:run"
run-script = "virtualbox_client.scripts.run_script:run"
gui = "virtualbox_client.gui:run"
+116
View File
@@ -0,0 +1,116 @@
#!/bin/bash
# chongming 开发环境一键启动脚本 (远程执行版)
# 用法: 上传到虚拟机 /tmp/ 后执行,会自动定位到项目根目录
# 项目路径: ~/workspace/chongming
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${GREEN}==> 初始化 chongming 开发环境${NC}"
# 1. 定位项目根目录
PROJECT_DIR=~/workspace/chongming
if [ ! -d "$PROJECT_DIR" ]; then
echo -e "${RED}错误:项目目录不存在: $PROJECT_DIR${NC}"
echo -e "${YELLOW}请先在虚拟机中创建项目目录并拉取代码:${NC}"
echo " mkdir -p ~/workspace && cd ~/workspace"
echo " git clone <仓库地址> chongming"
exit 1
fi
cd "$PROJECT_DIR"
echo -e "${GREEN}✔ 切换到项目目录: $(pwd)${NC}"
# 确认 pyproject.toml 存在(防御性检查)
if [ ! -f "pyproject.toml" ]; then
echo -e "${RED}错误:项目目录中未找到 pyproject.toml:$(pwd)${NC}"
exit 1
fi
# 2. 加载 nvm(SSH 非交互式 shell 需要手动加载)
export NVM_DIR="$HOME/.nvm"
if [ -s "$NVM_DIR/nvm.sh" ]; then
. "$NVM_DIR/nvm.sh" 2>/dev/null
fi
# 3. 检查 Node.js (版本要求 >=20.19)
if ! command -v node &> /dev/null; then
echo -e "${RED}错误:未找到 node,请安装 Node.js >= 20.19${NC}"
echo "推荐使用 nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"
exit 1
fi
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 20 ]; then
echo -e "${RED}错误:Node.js 版本过低 (当前 $(node -v)),需要 >=20.19${NC}"
exit 1
fi
echo -e "${GREEN}✔ Node.js $(node -v)${NC}"
# 4. 安装/检查 uv (项目推荐)
# SSH 非交互式 shell 的 PATH 可能不含 ~/.local/bin,手动添加
export PATH="$HOME/.local/bin:$PATH"
if ! command -v uv &> /dev/null; then
echo -e "${YELLOW}未找到 uv,正在自动安装...${NC}"
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
if ! command -v uv &> /dev/null; then
echo -e "${RED}uv 安装失败,请手动安装:https://docs.astral.sh/uv/${NC}"
exit 1
fi
echo -e "${GREEN}uv 安装成功${NC}"
fi
# 5. 安装后端依赖
echo -e "${GREEN}==> 安装后端依赖 (uv sync)${NC}"
cd ~/workspace/chongming
uv sync
# 6. 安装前端依赖
echo -e "${GREEN}==> 安装前端依赖 (npm install)${NC}"
cd ~/workspace/chongming/src/chongming-web
npm install
cd ../..
# 7. 启动后端服务(后台运行,使用 nohup 防止 SSH 断开后被 SIGHUP 杀死)
echo -e "${GREEN}==> 启动后端服务 (uv run serve)${NC}"
mkdir -p ~/workspace/chongming/logs
nohup uv run serve > ~/workspace/chongming/logs/backend.log 2>&1 &
BACKEND_PID=$!
# 等待后端完全启动(健康检查)
echo "等待后端服务就绪(约3秒)..."
sleep 10
# 可选:检查端口8000是否监听
if ! ss -tln | grep -q ':8000'; then
echo -e "${YELLOW}警告:后端可能尚未完全启动,请稍后手动检查${NC}"
fi
# 8. 生成前端 API 客户端(需要后端运行)
echo -e "${GREEN}==> 生成前端 API 客户端 (npm run openapi)${NC}"
cd src/chongming-web
npm run openapi || echo -e "${YELLOW}注意:生成 openapi 失败,请确认后端已运行在 http://localhost:8000${NC}"
cd ../..
# 9. 启动前端开发服务器(后台运行,使用 nohup 防止 SSH 断开后被 SIGHUP 杀死)
echo -e "${GREEN}==> 启动前端开发服务器 (npm run dev)${NC}"
cd src/chongming-web
nohup npm run dev > ~/workspace/chongming/logs/frontend.log 2>&1 &
FRONTEND_PID=$!
cd ../..
echo -e "前端地址: ${GREEN}http://localhost:5173${NC}"
echo -e "后端地址: ${GREEN}http://localhost:8000${NC}"
echo -e "后端日志: ~/workspace/chongming/logs/backend.log"
echo -e "前端日志: ~/workspace/chongming/logs/frontend.log"
echo -e "后端 PID: $BACKEND_PID"
echo -e "前端 PID: $FRONTEND_PID"
# 记录 PID 到文件,方便后续停止服务
echo -e "$BACKEND_PID\n$FRONTEND_PID" > ~/workspace/chongming/.dev_pids
echo -e "${GREEN}✅ 开发环境已全部启动,脚本将退出,服务在后台持续运行${NC}"
echo -e "${YELLOW}要停止服务,请执行: kill \$(cat ~/workspace/chongming/.dev_pids)${NC}"
+189
View File
@@ -0,0 +1,189 @@
#!/bin/bash
#
# deploy_docker.sh - Docker 一键部署脚本
# 适用系统: Ubuntu 20.04 (Focal)
# 功能: 安装 Docker Engine 并配置国内镜像加速器
#
set -e
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 检查是否以 root 运行
check_root() {
if [ "$EUID" -ne 0 ]; then
log_warn "未以 root 身份运行,将使用 sudo 提权"
SUDO="sudo"
else
SUDO=""
fi
}
# 步骤1: 清理旧 Docker 仓库配置(避免残余配置干扰)
cleanup_old_repo() {
log_info "步骤1: 清理旧 Docker 仓库配置..."
# 移除所有旧的 docker 源文件
$SUDO rm -f /etc/apt/sources.list.d/docker.list /etc/apt/sources.list.d/docker*.list
# 移除旧的 docker gpg 密钥
$SUDO rm -f /usr/share/keyrings/docker-archive-keyring.gpg
$SUDO apt-key del "7EA0A9C3F273FCD8" 2>/dev/null || true
log_info "旧 Docker 仓库配置已清理"
}
# 步骤2: 添加 Docker 官方 GPG 密钥(使用阿里云镜像)
add_gpg_key() {
log_info "步骤2: 添加 Docker GPG 密钥..."
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg -o /tmp/docker.gpg
# 安装到 keyrings 目录(供 signed-by 使用)
$SUDO mkdir -p /usr/share/keyrings
$SUDO gpg --batch --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg /tmp/docker.gpg 2>/dev/null || true
# 用 apt-key 添加(传统方式兼容)
$SUDO apt-key add /tmp/docker.gpg
rm -f /tmp/docker.gpg
log_info "GPG 密钥添加完成"
}
# 步骤3: 添加 Docker APT 源(使用阿里云镜像,带 signed-by 引用)
add_docker_repo() {
log_info "步骤3: 添加 Docker APT 源..."
# 使用 signed-by 添加(指定密钥文件路径,避免 NO_PUBKEY 错误)
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | $SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null
log_info "Docker APT 源添加完成"
}
# 步骤4: 更新 apt 并安装依赖(此时 Docker 源已配置好密钥)
install_dependencies() {
log_info "步骤4: 更新 apt 包索引并安装依赖..."
$SUDO apt update -y
$SUDO apt install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release
log_info "依赖安装完成"
}
# 步骤5: 安装 Docker Engine
install_docker() {
log_info "步骤5: 安装 Docker Engine(先装核心组件,再试可选组件)..."
# 核心组件(必须成功)
$SUDO apt install -y docker-ce docker-ce-cli containerd.io || {
log_error "Docker 核心组件安装失败!"
exit 1
}
# 可选组件(失败不影响核心功能)
for pkg in docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
if $SUDO apt install -y "$pkg" 2>/dev/null; then
log_info "✔ $pkg 安装成功"
else
log_warn "$pkg 安装失败(可选组件,可跳过)"
fi
done
log_info "Docker Engine 安装完成"
}
# 步骤6: 配置 Docker 镜像加速器
configure_mirrors() {
log_info "步骤6: 配置 Docker 镜像加速器..."
$SUDO mkdir -p /etc/docker
cat > /tmp/daemon.json << 'EOF'
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://dockerproxy.com",
"https://docker.nju.edu.cn",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
$SUDO cp /tmp/daemon.json /etc/docker/daemon.json
rm -f /tmp/daemon.json
log_info "镜像加速器配置完成"
}
# 步骤7: 重启 Docker 服务
restart_docker() {
log_info "步骤7: 重启 Docker 服务..."
$SUDO systemctl restart docker
log_info "Docker 服务已重启"
}
# 步骤8: 验证安装
verify_installation() {
log_info "步骤8: 验证安装..."
# 检查 Docker 版本
if docker --version &>/dev/null; then
log_info "$(docker --version)"
else
log_error "Docker 未正确安装"
exit 1
fi
# 检查 Docker 服务状态
if $SUDO systemctl is-active docker &>/dev/null; then
log_info "Docker 服务运行正常"
else
log_error "Docker 服务未运行"
exit 1
fi
# 检查镜像加速器
if $SUDO docker info 2>/dev/null | grep -q "Registry Mirrors"; then
log_info "镜像加速器配置如下:"
$SUDO docker info 2>/dev/null | grep -A5 "Registry Mirrors"
fi
# 运行 hello-world 测试
log_info "运行 hello-world 测试..."
if $SUDO docker run --rm hello-world &>/dev/null; then
log_info "Docker 安装验证通过!"
else
log_warn "hello-world 测试未通过,请检查网络连接"
fi
}
# 主函数
main() {
echo "=========================================="
echo " Docker 一键部署脚本"
echo " 适用系统: Ubuntu 20.04"
echo "=========================================="
echo ""
check_root
cleanup_old_repo
add_gpg_key
add_docker_repo
install_dependencies
install_docker
configure_mirrors
restart_docker
verify_installation
echo ""
echo "=========================================="
echo -e "${GREEN} Docker 部署完成!${NC}"
echo "=========================================="
echo ""
echo "常用命令:"
echo " docker ps - 查看运行中的容器"
echo " docker images - 查看本地镜像"
echo " docker info - 查看 Docker 信息"
echo " docker run hello-world - 测试 Docker 安装"
echo ""
}
main "$@"
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# ============================================
# deploy_git.sh - Git 安装部署脚本
# 适用系统: Ubuntu 20.04
# 功能: 一键安装 Git 并验证
# ============================================
set -e # 遇到错误立即退出
echo "=========================================="
echo " 开始部署 Git ..."
echo "=========================================="
# 步骤 1: 更新软件包列表
echo "[Step 1/4] 更新软件包列表..."
sudo apt update -y
# 步骤 2: 安装 Git
echo "[Step 2/4] 安装 Git..."
sudo apt install -y git
# 步骤 3: 验证安装
echo "[Step 3/4] 验证 Git 安装..."
if command -v git &>/dev/null; then
echo " Git 安装成功!"
git --version
else
echo " Git 安装失败!"
exit 1
fi
# 步骤 4: 配置基本信息(可选)
echo "[Step 4/4] 配置 Git 基本信息(可选)..."
echo " 提示:您可以使用以下命令配置用户名和邮箱:"
echo " git config --global user.name \"Your Name\""
echo " git config --global user.email \"your_email@example.com\""
echo ""
echo "=========================================="
echo " Git 部署完成!"
echo " 版本: $(git --version 2>/dev/null || echo 'unknown')"
echo "=========================================="
+133
View File
@@ -0,0 +1,133 @@
#!/bin/bash
# ==============================================================
# NVM 部署脚本 - 一键安装 nvm + Node.js LTS 稳定版 + 换源加速
# ==============================================================
# 适用系统: Ubuntu 20.04+
# 说明: 使用国内镜像源加速,避免 GitHub raw 下载缓慢
# 安装 Node.js LTS 版(比 latest 更小更快)
#
# 注意:脚本可能通过 sudo 以 root 身份执行,
# 因此需要检测原始用户并切换到该用户安装 nvm,
# 避免 nvm 装到 /root 下导致原用户看不到。
# =============================================================
set -e
echo "========================================"
echo " NVM 部署脚本 - 开始安装..."
echo "========================================"
# ---------- 检测原始用户 ----------
# 如果通过 sudo 运行,SUDO_USER 会保留原用户名
if [ -n "$SUDO_USER" ]; then
REAL_USER="$SUDO_USER"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
if [ -z "$REAL_HOME" ]; then
REAL_HOME="/home/$REAL_USER"
fi
else
REAL_USER=$(whoami)
REAL_HOME="$HOME"
fi
echo " 检测到实际用户: $REAL_USER"
echo " 用户目录: $REAL_HOME"
echo ""
NVM_VERSION="v0.39.7"
NODE_LTS_VERSION="--lts" # 安装 LTS 最新版(比 install node 的 latest 版更小)
# ---------- 辅助函数:以原始用户身份执行命令 ----------
run_as_user() {
if [ "$(whoami)" != "$REAL_USER" ]; then
sudo -u "$REAL_USER" bash -c "$*"
else
bash -c "$*"
fi
}
# 1. 安装 nvm(使用 gitee 镜像加速)
echo "[1/4] 正在安装 nvm ${NVM_VERSION}..."
# 优先用 gitee 镜像(国内快),失败则回滚到 GitHub 官方
NVM_INSTALL_URL="https://gitee.com/mirrors/nvm/raw/${NVM_VERSION}/install.sh"
echo " 下载源: ${NVM_INSTALL_URL}"
# 以原始用户身份下载并执行安装脚本
rm -f /tmp/install_nvm.sh
if timeout 30 curl -fsSL "${NVM_INSTALL_URL}" -o /tmp/install_nvm.sh 2>/dev/null; then
echo " ✅ 从 Gitee 镜像下载成功"
else
echo " ⚠ Gitee 镜像下载失败,切换到 GitHub 源..."
NVM_INSTALL_URL="https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh"
curl -fsSL "${NVM_INSTALL_URL}" -o /tmp/install_nvm.sh
fi
# 以原始用户身份执行安装(不要用 sudo)
chown "$REAL_USER":"$REAL_USER" /tmp/install_nvm.sh 2>/dev/null || true
sudo -u "$REAL_USER" bash /tmp/install_nvm.sh
rm -f /tmp/install_nvm.sh
# 2. 加载 nvm 到当前 shell(以原始用户身份)
echo "[2/4] 正在加载 nvm 环境变量..."
export NVM_DIR="$REAL_HOME/.nvm"
if [ -s "$NVM_DIR/nvm.sh" ]; then
\. "$NVM_DIR/nvm.sh"
echo " nvm 已加载"
else
echo " ⚠ nvm.sh 未找到,可能安装过程中出现问题"
ls -la "$NVM_DIR" 2>/dev/null || echo " .nvm 目录不存在"
fi
# 3. 配置淘宝镜像源(加速 Node.js 下载)
echo "[3/4] 正在配置淘宝镜像源..."
echo 'export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node' >> "$REAL_HOME/.bashrc"
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
echo " 镜像源已配置: NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node"
# 4. 安装 Node.js LTS 版
echo "[4/4] 正在安装 Node.js LTS 版本(稳定版)..."
echo " 下载源: ${NVM_NODEJS_ORG_MIRROR}"
# nvm install 必须以原始用户身份执行,否则 $NVM_DIR 不匹配
sudo -u "$REAL_USER" bash -c "
export NVM_DIR='$REAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
export NVM_NODEJS_ORG_MIRROR='$NVM_NODEJS_ORG_MIRROR'
nvm install $NODE_LTS_VERSION
"
# 验证安装结果
echo ""
echo "========================================"
echo " 安装完成!验证信息如下:"
echo "========================================"
# 以原始用户身份验证
sudo -u "$REAL_USER" bash -c "
export NVM_DIR='$REAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\"
if command -v nvm &> /dev/null; then
echo ' nvm 版本 : \$(nvm --version)'
else
echo ' nvm 版本 : n/a'
fi
if command -v node &> /dev/null; then
echo ' node 版本: \$(node -v)'
else
echo ' node 版本: n/a'
fi
if command -v npm &> /dev/null; then
echo ' npm 版本 : \$(npm -v)'
else
echo ' npm 版本 : n/a'
fi
echo ''
echo ' 当前默认使用的 Node 版本:'
nvm ls 2>/dev/null || echo ' nvm ls 不可用'
"
echo ""
echo "========================================"
echo " 提示:重新登录终端或执行:"
echo " source ~/.bashrc"
echo " 然后: nvm --version"
echo "========================================"
+151
View File
@@ -0,0 +1,151 @@
#!/bin/bash
# =============================================================
# deploy_uv.sh - uv (Python 包管理工具) 一键安装与配置脚本
# =============================================================
# 适用系统: Ubuntu 20.04+
# 功能: 安装 uv、配置国内镜像源加速
# 说明: 先将安装脚本下载到本地再执行,避免管道问题;
# 如果官方源慢,自动切 GitHub Release 镜像。
# =============================================================
set -e
echo "========================================"
echo " 开始部署 uv - Python 包管理工具"
echo "========================================"
# ---------- 1. 安装 uv ----------
install_uv() {
echo "[1/4] 正在安装 uv..."
# 先将安装脚本下载到 /tmp(而非管道直接执行)
echo " 正在下载安装脚本..."
# 尝试从官方源下载(带超时)
if timeout 30 curl -fsSL https://astral.sh/uv/install.sh -o /tmp/uv-install.sh 2>/dev/null; then
echo " 下载源: https://astral.sh/uv/install.sh"
else
# 官方源超时,换 GitHub Release(国内 CDN 更快)
echo " 官方源下载超时,尝试 GitHub Release..."
UV_VERSION=$(curl -fsSL https://api.github.com/repos/astral-sh/uv/releases/latest 2>/dev/null | grep '"tag_name"' | cut -d'"' -f4)
if [ -z "$UV_VERSION" ]; then
UV_VERSION="0.6.14" # fallback 固定版本
fi
echo " 正在下载 uv ${UV_VERSION} 预编译包..."
curl -fsSL "https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${UV_VERSION}-x86_64-unknown-linux-gnu.tar.gz" -o /tmp/uv.tar.gz
# 解压并安装
mkdir -p /tmp/uv-extract
tar xzf /tmp/uv.tar.gz -C /tmp/uv-extract
cp /tmp/uv-extract/uv-${UV_VERSION}-x86_64-unknown-linux-gnu/uv /tmp/uv-install-dir/ 2>/dev/null || true
# 直接复制到 local/bin
mkdir -p "$HOME/.local/bin"
cp /tmp/uv-extract/uv-${UV_VERSION}-x86_64-unknown-linux-gnu/uv "$HOME/.local/bin/uv"
chmod +x "$HOME/.local/bin/uv"
ln -sf "$HOME/.local/bin/uv" "$HOME/.local/bin/uvx" 2>/dev/null || true
rm -rf /tmp/uv.tar.gz /tmp/uv-extract
echo "✅ uv 安装成功(来自 GitHub Release)!"
return 0
fi
# 执行安装脚本
echo " 正在执行安装..."
bash /tmp/uv-install.sh
rm -f /tmp/uv-install.sh
if [ $? -ne 0 ]; then
echo "❌ uv 安装失败,请检查网络连接后重试。"
exit 1
fi
echo "✅ uv 安装成功!"
}
# ---------- 2. 配置环境变量 ----------
setup_env() {
echo "[2/4] 正在配置环境变量..."
# 检测 shell 并添加 uv 到 PATH
SHELL_CONFIG=""
if [ -n "$BASH_VERSION" ]; then
SHELL_CONFIG="$HOME/.bashrc"
elif [ -n "$ZSH_VERSION" ]; then
SHELL_CONFIG="$HOME/.zshrc"
else
SHELL_CONFIG="$HOME/.profile"
fi
# 如果 PATH 中还没有 uv 路径,则添加
if ! echo "$PATH" | grep -q "$HOME/.local/bin"; then
echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$SHELL_CONFIG"
echo "已添加到 $SHELL_CONFIG"
else
echo "$HOME/.local/bin 已在 PATH 中"
fi
# 临时生效
export PATH="$HOME/.local/bin:$PATH"
echo "✅ 环境变量配置完成!"
}
# ---------- 3. 创建 uv 配置文件(换源) ----------
config_mirror() {
echo "[3/4] 正在配置国内镜像源(加速下载)..."
CONFIG_DIR="$HOME/.config/uv"
CONFIG_FILE="$CONFIG_DIR/uv.toml"
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_FILE" << 'EOF'
# uv 全局配置 - 使用国内镜像源加速
# Python 版本下载镜像源(提高 `uv python install` 速度)
python-install-mirror = "https://registry.npmmirror.com/-/binary/python-build-standalone/"
# pip 包下载镜像源(提高 `uv pip install` 速度)
[[index]]
url = "https://mirrors.aliyun.com/pypi/simple/"
default = true
EOF
echo "配置文件已创建: $CONFIG_FILE"
echo "✅ 镜像源配置完成!"
}
# ---------- 4. 验证安装 ----------
verify_install() {
echo "[4/4] 正在验证安装..."
if command -v uv &> /dev/null; then
UV_VER=$(uv --version)
echo "✅ uv 版本: $UV_VER"
else
echo "❌ uv 命令未找到,请重启终端或执行: source ~/.bashrc"
exit 1
fi
if command -v uvx &> /dev/null; then
echo "✅ uvx 已安装(用于运行工具包)"
fi
echo ""
echo "========================================"
echo " 🎉 uv 部署完成!"
echo "========================================"
echo ""
echo "快速上手:"
echo " uv python install 3.12 # 安装 Python 3.12"
echo " uv venv # 创建虚拟环境"
echo " uv pip install requests # 安装包"
echo " uv run main.py # 运行脚本"
echo ""
echo "重启终端或执行: source $SHELL_CONFIG"
}
# ---------- 主流程 ----------
install_uv
setup_env
config_mirror
verify_install
+281
View File
@@ -0,0 +1,281 @@
import os
gui_path = 'src/virtualbox_client/gui.py'
with open(gui_path, 'r', encoding='utf-8') as f:
content = f.read()
# ===== 1. 添加 ScriptWorker 类(放在 DownloadWorker 之后) =====
old = """class WorkspaceWorker(QObject):"""
new = """class ScriptWorker(QObject):
"""上传本地脚本到虚拟机、执行并下载日志"""
log_signal = Signal(str, str)
finished_signal = Signal(bool)
error_signal = Signal(str)
def __init__(self, config: dict, local_path: str):
super().__init__()
self.config = config
self.local_path = local_path
self.cancel_event = threading.Event()
def cancel(self):
self.cancel_event.set()
def run(self):
ssh_only = self.config.get("ssh_only", False)
_com_initialized = False
if not ssh_only:
try:
import ctypes
ctypes.windll.ole32.CoInitializeEx(None, 0)
_com_initialized = True
except Exception:
pass
try:
if ssh_only:
from virtualbox_client.utils.ssh_manager import SSHManager
vm = SSHManager(
self.config["ssh_host"], self.config["ssh_port"],
self.config["ssh_user"], self.config["ssh_pass"],
)
else:
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
vm = VBoxSSHManager(
self.config["vm_name"], self.config["ssh_host"],
self.config["ssh_port"], self.config["ssh_user"],
self.config["ssh_pass"],
)
vm.set_cancel_event(self.cancel_event)
if self.cancel_event.is_set():
return
import builtins
original_print = print
def custom_print(*args, **kwargs):
msg = " ".join(str(a) for a in args)
self.log_signal.emit(msg, "info")
original_print(*args, **kwargs)
builtins.print = custom_print
try:
script_name = os.path.basename(self.local_path)
remote_path = f"/tmp/{script_name}"
log_path = f"/tmp/{script_name}.log"
local_log_path = f"{script_name}.log"
# 1. 上传脚本
self.log_signal.emit(f"📤 [1/3] 上传脚本: {self.local_path} -> {remote_path}", "info")
if not vm.upload_file(self.local_path, remote_path):
self.log_signal.emit("❌ 上传失败", "error")
return
# 2. 执行脚本
self.log_signal.emit(f"⚡ [2/3] 执行脚本: {remote_path}", "info")
cmd = (
f"chmod +x {remote_path} && "
f"echo '{self.config['ssh_pass']}' | sudo -S {remote_path} 2>&1 | "
f"tee {log_path}"
)
out, err, code = vm.exec_command(cmd, get_pty=False)
self.log_signal.emit(f"✔ 脚本执行完成,退出码: {code}", "info")
# 3. 下载日志
self.log_signal.emit(f"📥 [3/3] 下载日志: {log_path} -> {local_log_path}", "info")
if vm.download_file(log_path, local_log_path):
self.log_signal.emit(f"✔ 日志已保存到本地: {local_log_path}", "success")
else:
self.log_signal.emit("⚠ tee 日志下载失败,尝试保存终端输出", "warning")
if out:
with open(local_log_path, "w") as f:
f.write(out)
self.log_signal.emit(f"✔ 终端输出已保存到: {local_log_path}", "success")
# 4. 清理远程临时文件
vm.exec_command(f'rm -f "{remote_path}" "{log_path}"')
self.log_signal.emit("🧹 远程临时文件已清理", "info")
self.log_signal.emit("🎉 脚本执行全部完成!", "success")
self.finished_signal.emit(True)
finally:
builtins.print = original_print
except ImportError as e:
self.log_signal.emit(f"❌ 缺少依赖: {e}", "error")
self.error_signal.emit(str(e))
except Exception as e:
self.log_signal.emit(f"❌ 脚本执行错误: {str(e)}", "error")
self.error_signal.emit(str(e))
finally:
if _com_initialized:
try:
import ctypes
ctypes.windll.ole32.CoUninitialize()
except Exception:
pass
class WorkspaceWorker(QObject):"""
content = content.replace(old, new)
# ===== 2. 在 TaskPanel.setup_ui 的文件传输 tab 后面添加脚本执行 tab =====
old_file_tab_end = """ file_layout.addStretch()
self.tab_widget.addTab(file_tab, "📁 文件传输")"""
new_file_tab_end = """ file_layout.addStretch()
self.tab_widget.addTab(file_tab, "📁 文件传输")
# ====== 脚本执行 Tab ======
script_tab = QWidget()
script_layout = QVBoxLayout(script_tab)
script_layout.setContentsMargins(8, 8, 8, 8)
script_group = QGroupBox("⚡ 上传并执行脚本")
script_group_layout = QVBoxLayout(script_group)
script_path_layout = QHBoxLayout()
self.script_path_edit = QLineEdit(placeholderText="选择本地 .sh 脚本文件...")
script_path_layout.addWidget(self.script_path_edit)
self.script_browse_btn = QPushButton("📂 浏览")
self.script_browse_btn.setFixedWidth(80)
self.script_browse_btn.clicked.connect(self._browse_script)
script_path_layout.addWidget(self.script_browse_btn)
script_group_layout.addWidget(QLabel("本地脚本路径:"))
script_group_layout.addLayout(script_path_layout)
self.run_script_btn = QPushButton("⚡ 上传并执行脚本")
self.run_script_btn.setStyleSheet(\"""
QPushButton { background-color: #8e44ad; color: white; padding: 10px 30px;
font-size: 14px; font-weight: bold; border: none; border-radius: 6px; }
QPushButton:hover { background-color: #9b59b6; }
QPushButton:disabled { background-color: #555; color: #999; }
\""")
self.run_script_btn.clicked.connect(self._run_script)
script_group_layout.addWidget(self.run_script_btn)
script_layout.addWidget(script_group)
# 执行说明
desc_group = QGroupBox("📖 说明")
desc_layout = QVBoxLayout(desc_group)
desc_label = QLabel(
"1. 选择一个本地 .sh 脚本文件\\n"
"2. 点击按钮自动完成:\\n"
" 📤 上传脚本到虚拟机 /tmp/ 目录\\n"
" ⚡ 以 sudo 权限执行脚本\\n"
" 📥 下载执行日志到本地\\n"
" 🧹 清理远程临时文件"
)
desc_label.setStyleSheet("color: #aaa; font-size: 12px; line-height: 1.6;")
desc_layout.addWidget(desc_label)
script_layout.addWidget(desc_group)
script_layout.addStretch()
self.tab_widget.addTab(script_tab, "⚡ 脚本执行")"""
content = content.replace(old_file_tab_end, new_file_tab_end)
# ===== 3. 在 TaskPanel 的 _set_ui_running 中禁用脚本按钮 =====
old_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
new_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
self.run_script_btn.setEnabled(not running)
self.script_browse_btn.setEnabled(not running)
self.script_path_edit.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
content = content.replace(old_running, new_running)
# ===== 4. 在 TaskPanel 中添加 _browse_script 和 _run_script 方法 =====
# 放在 _on_download_error 之后、class MainWindow 之前
old_mainwindow = """class MainWindow(QMainWindow):"""
new_methods = """ _script_thread = None
_script_worker = None
def _cleanup_script(self):
if self._script_thread:
try:
self._script_thread.finished.disconnect()
except (TypeError, RuntimeError):
pass
if self._script_thread.isRunning():
self._script_thread.quit()
if not self._script_thread.wait(5000):
self._script_thread.terminate()
self._script_thread.wait(3000)
self._script_thread = None
if self._script_worker:
try:
self._script_worker.log_signal.disconnect()
self._script_worker.finished_signal.disconnect()
self._script_worker.error_signal.disconnect()
except (TypeError, RuntimeError):
pass
self._script_worker = None
def _browse_script(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "选择要执行的脚本", "", "Shell 脚本 (*.sh);;所有文件 (*)"
)
if file_path:
self.script_path_edit.setText(file_path)
def _run_script(self):
if not self._config_panel or not self._config_panel.validate():
return
local_path = self.script_path_edit.text().strip()
if not local_path:
QMessageBox.warning(self, "提示", "请选择要执行的脚本文件")
return
if not os.path.isfile(local_path):
QMessageBox.warning(self, "提示", f"本地脚本不存在: {local_path}")
return
self._cleanup_script()
self._set_ui_running(True)
self.tab_widget.setCurrentIndex(0) # 切换到任务日志 tab
self.log_panel.clear_log()
config = self._config_panel.get_config()
self._script_worker = ScriptWorker(config, local_path)
self._script_thread = QThread(self)
self._script_worker.moveToThread(self._script_thread)
self._script_worker.log_signal.connect(self.log_panel.append_log)
self._script_worker.finished_signal.connect(self._on_script_finished)
self._script_worker.error_signal.connect(self._on_script_error)
self._script_thread.started.connect(self._script_worker.run)
self._script_thread.finished.connect(self._cleanup_script)
self._script_thread.start()
def _on_script_finished(self, success: bool):
self._set_ui_running(False)
if success:
self.log_panel.append_log("✅ 脚本执行已完成", "success")
def _on_script_error(self, error_msg: str):
self._set_ui_running(False)
self.log_panel.append_log(f"❌ 脚本执行失败: {error_msg}", "error")
""" + old_mainwindow
content = content.replace(old_mainwindow, new_methods)
with open(gui_path, 'w', encoding='utf-8') as f:
f.write(content)
print('✅ gui.py 已集成脚本执行功能')
+275
View File
@@ -0,0 +1,275 @@
# This script adds ScriptWorker and a script execution tab to gui.py
import os
gui_path = "src/virtualbox_client/gui.py"
with open(gui_path, "r", encoding="utf-8") as f:
content = f.read()
# 1. Add ScriptWorker before WorkspaceWorker
old_worker = "class WorkspaceWorker(QObject):"
new_worker = """class ScriptWorker(QObject):
log_signal = Signal(str, str)
finished_signal = Signal(bool)
error_signal = Signal(str)
def __init__(self, config: dict, local_path: str):
super().__init__()
self.config = config
self.local_path = local_path
self.cancel_event = threading.Event()
def cancel(self):
self.cancel_event.set()
def run(self):
ssh_only = self.config.get("ssh_only", False)
_com_initialized = False
if not ssh_only:
try:
import ctypes
ctypes.windll.ole32.CoInitializeEx(None, 0)
_com_initialized = True
except Exception:
pass
try:
if ssh_only:
from virtualbox_client.utils.ssh_manager import SSHManager
vm = SSHManager(
self.config["ssh_host"], self.config["ssh_port"],
self.config["ssh_user"], self.config["ssh_pass"],
)
else:
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
vm = VBoxSSHManager(
self.config["vm_name"], self.config["ssh_host"],
self.config["ssh_port"], self.config["ssh_user"],
self.config["ssh_pass"],
)
vm.set_cancel_event(self.cancel_event)
if self.cancel_event.is_set():
return
import builtins
original_print = print
def custom_print(*args, **kwargs):
msg = " ".join(str(a) for a in args)
self.log_signal.emit(msg, "info")
original_print(*args, **kwargs)
builtins.print = custom_print
try:
import os as _os
script_name = _os.path.basename(self.local_path)
remote_path = "/tmp/" + script_name
log_path = "/tmp/" + script_name + ".log"
local_log_path = script_name + ".log"
self.log_signal.emit("[1/3] Upload: " + self.local_path + " -> " + remote_path, "info")
if not vm.upload_file(self.local_path, remote_path):
self.log_signal.emit("Upload failed", "error")
return
self.log_signal.emit("[2/3] Execute: " + remote_path, "info")
cmd = (
"chmod +x " + remote_path + " && "
"echo '" + self.config["ssh_pass"] + "' | sudo -S " + remote_path + " 2>&1 | "
"tee " + log_path
)
out, err, code = vm.exec_command(cmd, get_pty=False)
self.log_signal.emit("Exit code: " + str(code), "info")
self.log_signal.emit("[3/3] Download log: " + log_path + " -> " + local_log_path, "info")
if vm.download_file(log_path, local_log_path):
self.log_signal.emit("Log saved: " + local_log_path, "success")
else:
self.log_signal.emit("Tee log download failed, saving terminal output", "warning")
if out:
with open(local_log_path, "w") as f:
f.write(out)
self.log_signal.emit("Output saved: " + local_log_path, "success")
vm.exec_command("rm -f \\"" + remote_path + "\\" \\"" + log_path + "\\"")
self.log_signal.emit("Remote temp files cleaned", "info")
self.log_signal.emit("Script execution complete!", "success")
self.finished_signal.emit(True)
finally:
builtins.print = original_print
except ImportError as e:
self.log_signal.emit("Missing dependency: " + str(e), "error")
self.error_signal.emit(str(e))
except Exception as e:
self.log_signal.emit("Script error: " + str(e), "error")
self.error_signal.emit(str(e))
finally:
if _com_initialized:
try:
import ctypes
ctypes.windll.ole32.CoUninitialize()
except Exception:
pass
""" + old_worker
content = content.replace(old_worker, new_worker)
# 2. Add script tab after file transfer tab
old_tab_end = ' file_layout.addStretch()\n self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")'
new_tab_end = ''' file_layout.addStretch()
self.tab_widget.addTab(file_tab, "\U0001f4c1 \u6587\u4ef6\u4f20\u8f93")
# ====== Script Execution Tab ======
script_tab = QWidget()
script_layout = QVBoxLayout(script_tab)
script_layout.setContentsMargins(8, 8, 8, 8)
script_group = QGroupBox("Upload and Execute Script")
script_group_layout = QVBoxLayout(script_group)
script_path_layout = QHBoxLayout()
self.script_path_edit = QLineEdit(placeholderText="Select local .sh script file...")
script_path_layout.addWidget(self.script_path_edit)
self.script_browse_btn = QPushButton("Browse")
self.script_browse_btn.setFixedWidth(80)
self.script_browse_btn.clicked.connect(self._browse_script)
script_path_layout.addWidget(self.script_browse_btn)
script_group_layout.addWidget(QLabel("Local Script:"))
script_group_layout.addLayout(script_path_layout)
self.run_script_btn = QPushButton("Upload and Execute Script")
self.run_script_btn.setStyleSheet("""
QPushButton { background-color: #8e44ad; color: white; padding: 10px 30px;
font-size: 14px; font-weight: bold; border: none; border-radius: 6px; }
QPushButton:hover { background-color: #9b59b6; }
QPushButton:disabled { background-color: #555; color: #999; }
""")
self.run_script_btn.clicked.connect(self._run_script)
script_group_layout.addWidget(self.run_script_btn)
script_layout.addWidget(script_group)
desc_group = QGroupBox("How it works")
desc_layout = QVBoxLayout(desc_group)
desc_label = QLabel(
"1. Select a local .sh script file\\n"
"2. Click button to auto:\\n"
" Upload script to /tmp/\\n"
" Execute with sudo\\n"
" Download execution log\\n"
" Clean remote temp files"
)
desc_label.setStyleSheet("color: #aaa; font-size: 12px; line-height: 1.6;")
desc_layout.addWidget(desc_label)
script_layout.addWidget(desc_group)
script_layout.addStretch()
self.tab_widget.addTab(script_tab, "Script")'''
content = content.replace(old_tab_end, new_tab_end)
# 3. Update _set_ui_running
old_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
new_running = """ def _set_ui_running(self, running: bool):
self.run_btn.setEnabled(not running)
self.cancel_btn.setEnabled(running)
self.task_input.setEnabled(not running)
self.run_script_btn.setEnabled(not running)
self.script_browse_btn.setEnabled(not running)
self.script_path_edit.setEnabled(not running)
if self._config_panel:
self._config_panel.set_enabled(not running)"""
content = content.replace(old_running, new_running)
# 4. Add methods before MainWindow
old_mw = "class MainWindow(QMainWindow):"
new_mw = """ _script_thread = None
_script_worker = None
def _cleanup_script(self):
if self._script_thread:
try:
self._script_thread.finished.disconnect()
except (TypeError, RuntimeError):
pass
if self._script_thread.isRunning():
self._script_thread.quit()
if not self._script_thread.wait(5000):
self._script_thread.terminate()
self._script_thread.wait(3000)
self._script_thread = None
if self._script_worker:
try:
self._script_worker.log_signal.disconnect()
self._script_worker.finished_signal.disconnect()
self._script_worker.error_signal.disconnect()
except (TypeError, RuntimeError):
pass
self._script_worker = None
def _browse_script(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Select script", "", "Shell scripts (*.sh);;All files (*)"
)
if file_path:
self.script_path_edit.setText(file_path)
def _run_script(self):
if not self._config_panel or not self._config_panel.validate():
return
local_path = self.script_path_edit.text().strip()
if not local_path:
QMessageBox.warning(self, "Hint", "Please select a script file")
return
if not os.path.isfile(local_path):
QMessageBox.warning(self, "Hint", "File not found: " + local_path)
return
self._cleanup_script()
self._set_ui_running(True)
self.tab_widget.setCurrentIndex(0)
self.log_panel.clear_log()
config = self._config_panel.get_config()
self._script_worker = ScriptWorker(config, local_path)
self._script_thread = QThread(self)
self._script_worker.moveToThread(self._script_thread)
self._script_worker.log_signal.connect(self.log_panel.append_log)
self._script_worker.finished_signal.connect(self._on_script_finished)
self._script_worker.error_signal.connect(self._on_script_error)
self._script_thread.started.connect(self._script_worker.run)
self._script_thread.finished.connect(self._cleanup_script)
self._script_thread.start()
def _on_script_finished(self, success: bool):
self._set_ui_running(False)
if success:
self.log_panel.append_log("Script execution completed", "success")
def _on_script_error(self, error_msg: str):
self._set_ui_running(False)
self.log_panel.append_log("Script error: " + error_msg, "error")
""" + old_mw
content = content.replace(old_mw, new_mw)
with open(gui_path, "w", encoding="utf-8") as f:
f.write(content)
print("Done")
+18
View File
@@ -0,0 +1,18 @@
import ast
import sys
files = [
"src/virtualbox_client/gui.py",
"src/virtualbox_client/utils/vbox_ssh_manager.py",
"src/virtualbox_client/utils/vm_agent.py",
]
for f in files:
try:
tree = ast.parse(open(f, encoding="utf-8").read())
print(f"OK: {f.split(chr(47))[-1]} ({len(tree.body)} nodes)")
except SyntaxError as e:
print(f"FAIL: {f.split(chr(47))[-1]}: {e}")
sys.exit(1)
print("All files pass syntax check")
+13
View File
@@ -0,0 +1,13 @@
import os
import shutil
# 需要遍历的目录
root_dir = "./"
# 遍历目录
for dirpath, dirnames, filenames in os.walk(root_dir):
if "__pycache__" in dirnames:
# 获取 __pycache__ 目录的全路径
pycache_dir = os.path.join(dirpath, "__pycache__")
# 删除目录
shutil.rmtree(pycache_dir)
print(f"Removed: {pycache_dir}")
+45
View File
@@ -0,0 +1,45 @@
import os
import sys
def generate_markdown_from_py_files(directory, output_file):
with open(output_file, "w", encoding="utf-8") as md_file:
for root, dirs, files in os.walk(directory):
# 排除 venv 目录
dirs[:] = [d for d in dirs if d != ".venv"]
dirs[:] = [d for d in dirs if d != ".vscode"]
dirs[:] = [d for d in dirs if d != "node_modules"]
dirs[:] = [d for d in dirs if d != "build"]
dirs[:] = [d for d in dirs if d != "dist"]
dirs[:] = [d for d in dirs if d != ".sdk"]
for file in files:
if (
file.endswith(".py")
or file.endswith(".rs")
or file.endswith(".toml")
or file.endswith(".md")
or file.endswith(".css")
or file.endswith(".vue")
or file.endswith(".ts")
or file.endswith(".json")
or file.endswith(".html")
or file.endswith(".sh")
or file == "Dockerfile"
):
file_path = os.path.join(root, file)
if file == "output.md" or file == "package-lock.json":
continue
md_file.write(f"`{file_path}`\n")
md_file.write("```\n")
with open(file_path, "r", encoding="utf-8") as py_file:
md_file.write(py_file.read())
md_file.write("\n```\n\n")
if __name__ == "__main__":
# 指定目录和输出文件名
target_directory = sys.argv[1] # 替换为你的目标目录
output_markdown_file = "output.md" # 输出的 Markdown 文件名
generate_markdown_from_py_files(target_directory, output_markdown_file)
print(f"Markdown 文件已生成:{output_markdown_file}")
+30
View File
@@ -0,0 +1,30 @@
content = open("src/virtualbox_client/gui.py", encoding="utf-8").read()
# 1. 修复 except 语法
content = content.replace(
"except TypeError, RuntimeError:", "except (TypeError, RuntimeError):"
)
# 2. 修复 _rename_item 中的路径分隔符
old = """ dst_dir = os.path.dirname(src_path)
dst_path = os.path.join(dst_dir, new_name.strip())"""
new = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
dst_path = dst_dir.rstrip('/') + '/' + new_name.strip()"""
content = content.replace(old, new)
# 3. 修复 _copy_item 中的路径分隔符
old2 = """ dst_dir = os.path.dirname(src_path)
dst_path = os.path.join(dst_dir, new_name)"""
new2 = """ dst_dir = src_path.rsplit('/', 1)[0] if '/' in src_path else src_path.rsplit('\\\\', 1)[0]
dst_path = dst_dir.rstrip('/') + '/' + new_name"""
content = content.replace(old2, new2)
# 4. 修复 _upload_file 中的 remote_dir 路径
old3 = """ remote_dir = os.path.dirname(remote_path)
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
new3 = """ remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')"""
content = content.replace(old3, new3)
open("src/virtualbox_client/gui.py", "w", encoding="utf-8").write(content)
print("✅ 所有修复完成")
+226
View File
@@ -0,0 +1,226 @@
import os
# 1. 给 SSHManager 添加 upload_directory 方法
ssh_path = "src/virtualbox_client/utils/ssh_manager.py"
with open(ssh_path, "r", encoding="utf-8") as f:
content = f.read()
old_method = """ def upload_file(self, local_path, remote_path):
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 上传失败: {e}\")
return False
"""
new_method = """ def upload_file(self, local_path, remote_path):
\"\"\"上传本地文件到远程服务器,返回是否成功\"\"\"
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f\"✔ 上传成功: {local_path} -> {remote_path}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 上传失败: {e}\")
return False
def upload_directory(self, local_dir, remote_dir):
\"\"\"递归上传整个文件夹到远程服务器,返回是否成功\"\"\"
import os as _os
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
local_dir = _os.path.normpath(local_dir)
remote_dir = remote_dir.rstrip('/')
# 创建远程根目录
self.exec_command(f'mkdir -p \"{remote_dir}\"')
for root, dirs, files in _os.walk(local_dir):
# 计算相对路径
rel_path = _os.path.relpath(root, local_dir)
if rel_path == '.':
rel_path = ''
# 创建远程子目录
target_dir = remote_dir + ('/' + rel_path.replace('\\\\', '/') if rel_path else '')
self.exec_command(f'mkdir -p \"{target_dir}\"')
for fname in files:
if self._cancelled.is_set():
sftp.close()
return False
local_file = _os.path.join(root, fname)
remote_file = target_dir + '/' + fname
sftp.put(local_file, remote_file)
print(f\" ✔ {local_file} -> {remote_file}\")
sftp.close()
print(f\"✔ 文件夹上传成功: {local_dir} -> {remote_dir}\")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f\"✘ 文件夹上传失败: {e}\")
return False
"""
content = content.replace(old_method, new_method)
with open(ssh_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ SSHManager 已添加 upload_directory")
# 2. 给 VBoxSSHManager 添加 upload_directory 方法
vbox_path = "src/virtualbox_client/utils/vbox_ssh_manager.py"
with open(vbox_path, "r", encoding="utf-8") as f:
content = f.read()
content = content.replace(old_method, new_method)
with open(vbox_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ VBoxSSHManager 已添加 upload_directory")
# 3. 修改 gui.py - 添加上传文件夹功能
gui_path = "src/virtualbox_client/gui.py"
with open(gui_path, "r", encoding="utf-8") as f:
content = f.read()
# 在 WorkspaceWorker 中添加 OPERATION_UPLOAD_DIR
old_op = ' OPERATION_UPLOAD = "upload"\n'
new_op = ' OPERATION_UPLOAD = "upload"\n OPERATION_UPLOAD_DIR = "upload_dir"\n'
content = content.replace(old_op, new_op)
# 在 run 方法中添加分支
old_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
result = self._upload_file(vm)"""
new_run = """ elif self.operation == WorkspaceWorker.OPERATION_UPLOAD:
result = self._upload_file(vm)
elif self.operation == WorkspaceWorker.OPERATION_UPLOAD_DIR:
result = self._upload_directory(vm)"""
content = content.replace(old_run, new_run)
# 在 _upload_file 之后添加 _upload_directory 方法
old_upload = """ def _upload_file(self, vm) -> bool:
local_path = self.kwargs.get("local_path", "")
remote_path = self.kwargs.get("remote_path", "")
if not local_path or not remote_path:
return False
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')
return vm.upload_file(local_path, remote_path)"""
new_upload = """ def _upload_file(self, vm) -> bool:
local_path = self.kwargs.get("local_path", "")
remote_path = self.kwargs.get("remote_path", "")
if not local_path or not remote_path:
return False
remote_dir = remote_path.rsplit('/', 1)[0] if '/' in remote_path else remote_path.rsplit('\\\\', 1)[0]
vm.exec_command(f'mkdir -p "{remote_dir}"')
return vm.upload_file(local_path, remote_path)
def _upload_directory(self, vm) -> bool:
local_dir = self.kwargs.get("local_path", "")
remote_dir = self.kwargs.get("remote_path", "")
if not local_dir or not remote_dir:
return False
return vm.upload_directory(local_dir, remote_dir)"""
content = content.replace(old_upload, new_upload)
# 修改 _on_worker_finished 添加 UPLOAD_DIR 处理
old_finish = """ elif (
self._worker and self._worker.operation == WorkspaceWorker.OPERATION_UPLOAD
):
if result:
self.log_panel.append_log("✅ 上传成功", "success")
else:
self.log_panel.append_log("❌ 上传失败", "error")
QTimer.singleShot(500, self._soft_refresh)"""
new_finish = """ elif (
self._worker
and self._worker.operation
in (WorkspaceWorker.OPERATION_UPLOAD, WorkspaceWorker.OPERATION_UPLOAD_DIR)
):
if result:
self.log_panel.append_log("✅ 上传成功", "success")
else:
self.log_panel.append_log("❌ 上传失败", "error")
QTimer.singleShot(500, self._soft_refresh)"""
content = content.replace(old_finish, new_finish)
# 在 WorkspacePanel.setup_ui 的工具栏中添加"上传文件夹"按钮
old_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
new_upload_btn = """ self.upload_btn = QPushButton("📤 上传文件")"""
# 保持原样,在后面添加新按钮
# 在 upload_btn 后面添加 upload_dir_btn
old_toolbar = """ toolbar.addWidget(self.upload_btn)
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
new_toolbar = """ toolbar.addWidget(self.upload_btn)
self.upload_dir_btn = QPushButton("📁 上传文件夹")
self.upload_dir_btn.setStyleSheet(\"\"\"
QPushButton { background-color: #16a085; color: white; padding: 6px 14px;
font-size: 12px; font-weight: bold; border: none; border-radius: 4px; }
QPushButton:hover { background-color: #1abc9c; }
QPushButton:disabled { background-color: #555; color: #999; }
\"\"\")
self.upload_dir_btn.clicked.connect(self._upload_folder_to_workspace)
toolbar.addWidget(self.upload_dir_btn)
self.mkdir_btn = QPushButton("📂 新建文件夹")"""
content = content.replace(old_toolbar, new_toolbar)
# 更新 _set_busy 禁用 upload_dir_btn
old_set_busy = """ self.upload_btn.setEnabled(not busy)
self.mkdir_btn.setEnabled(not busy)
self.home_btn.setEnabled(not busy)"""
new_set_busy = """ self.upload_btn.setEnabled(not busy)
self.upload_dir_btn.setEnabled(not busy)
self.mkdir_btn.setEnabled(not busy)
self.home_btn.setEnabled(not busy)"""
content = content.replace(old_set_busy, new_set_busy)
# 在 _upload_to_workspace 方法之后添加 _upload_folder_to_workspace
old_upload_workspace = """ def _upload_to_workspace(self):"""
new_upload_workspace = """ def _upload_folder_to_workspace(self):
if not self._config_panel or not self._validate_ssh_config():
return
local_dir = QFileDialog.getExistingDirectory(self, "选择要上传到工作区的文件夹")
if not local_dir:
return
folder_name = os.path.basename(local_dir.rstrip('/\\\\'))
remote_path = f"~/workspace/{folder_name}"
self._start_worker(
WorkspaceWorker.OPERATION_UPLOAD_DIR,
local_path=local_dir,
remote_path=remote_path,
)
def _upload_to_workspace(self):"""
content = content.replace(old_upload_workspace, new_upload_workspace)
with open(gui_path, "w", encoding="utf-8") as f:
f.write(content)
print("✅ gui.py 已添加上传文件夹功能")
print("\\n所有修改完成!")
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
import os
import sys
# 尝试加载 .env 文件(如果存在)
try:
from dotenv import load_dotenv
load_dotenv("./.env.example")
except ImportError:
pass
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
from virtualbox_client.utils.vm_agent import VMAgent
def run():
# 从环境变量读取配置
vm_name = os.getenv("VM_NAME", "ubuntu20.04")
ssh_host = os.getenv("VM_SSH_HOST", "127.0.0.1")
ssh_port = int(os.getenv("VM_SSH_PORT", "2222"))
ssh_user = os.getenv("VM_SSH_USER")
ssh_password = os.getenv("VM_SSH_PASS")
api_key = os.getenv("LLM_API_KEY")
base_url = os.getenv("LLM_BASE_URL") # 可选,用于本地模型
model = os.getenv("LLM_MODEL", "gpt-4o")
# 参数校验
if not ssh_user or not ssh_password:
print("❌ 请设置环境变量 VM_SSH_USER 和 VM_SSH_PASS")
sys.exit(1)
if not api_key:
print("❌ 请设置环境变量 LLM_API_KEY")
sys.exit(1)
if len(sys.argv) < 2:
print("用法: uv run agent-task <任务描述>")
print("示例: uv run agent-task '在虚拟机中安装 nginx 并确认服务状态'")
sys.exit(1)
task = " ".join(sys.argv[1:])
# 创建管理器和 Agent
vm = VBoxSSHManager(
vm_name,
ssh_host,
ssh_port,
ssh_user,
ssh_password,
)
agent = VMAgent(
vm,
api_key=api_key,
base_url=base_url,
model=model,
sudo_password=os.getenv("VM_SSH_PASS"),
)
final_msg = agent.run(task)
print(final_msg)
+49
View File
@@ -0,0 +1,49 @@
import sys
import os
from ..utils.vbox_ssh_manager import VBoxSSHManager
# ---------- 使用示例 ----------
def run():
# 配置参数
VM_NAME = "ubuntu20.04"
SSH_HOST = "127.0.0.1" # 根据网络配置修改
SSH_PORT = 2222 # 默认 SSH 端口或转发端口
SSH_USER = "chakcy"
SSH_PASS = "020709djh"
# 创建管理工具实例
vm = VBoxSSHManager(VM_NAME, SSH_HOST, SSH_PORT, SSH_USER, SSH_PASS)
try:
# 启动虚拟机并等待 SSH 就绪
if not vm.start_vm():
sys.exit(1)
# 1. 上传测试文件
print("\n--- 上传文件测试 ---")
local_test_file = "test_upload.txt"
with open(local_test_file, "w") as f:
f.write("Hello from host!")
vm.upload_file(local_test_file, "/home/chakcy/test_upload.txt")
os.remove(local_test_file)
# 2. 执行命令
print("\n--- 执行命令测试 ---")
out, err, code = vm.exec_command("ls -l /home/chakcy && echo '--- done ---'")
if out:
print(out)
if err:
print("[STDERR]", err)
print(f"退出码: {code}")
# 3. 下载文件
print("\n--- 下载文件测试 ---")
vm.download_file("/home/chakcy/test_upload.txt", "downloaded.txt")
if os.path.exists("downloaded.txt"):
with open("downloaded.txt") as f:
print("下载内容:", f.read())
finally:
# 关闭虚拟机并清理资源
vm.stop_vm()
+137
View File
@@ -0,0 +1,137 @@
"""
上传本地 sh 脚本到虚拟机 → 执行脚本 → 下载执行日志
用法:
uv run run-script <本地脚本路径>
uv run run-script resources/deploy_docker.sh
uv run run-script resources/chongming_dev.sh
脚本会将本地 sh 文件上传到 /tmp/ 目录,执行,并将 stdout + stderr
保存到 /tmp/<脚本名>.log,最后下载回本地。
权限说明:
- deploy_*.sh → 自动以 sudo 权限执行(系统级安装脚本)
- 其他脚本 → 以普通用户权限执行(如开发环境脚本)
"""
import sys
import os
# 尝试加载 .env 文件(如果存在)
try:
from dotenv import load_dotenv
load_dotenv("./.env.example")
except ImportError:
pass
from virtualbox_client.utils.vbox_ssh_manager import VBoxSSHManager
def run():
# 从环境变量读取配置
vm_name = os.getenv("VM_NAME", "ubuntu20.04")
ssh_host = os.getenv("VM_SSH_HOST", "127.0.0.1")
ssh_port = int(os.getenv("VM_SSH_PORT", "2222"))
ssh_user = os.getenv("VM_SSH_USER")
ssh_password = os.getenv("VM_SSH_PASS")
# 参数校验
if not ssh_user or not ssh_password:
print("❌ 请设置环境变量 VM_SSH_USER 和 VM_SSH_PASS")
sys.exit(1)
if len(sys.argv) < 2:
print("用法: uv run run-script <本地脚本路径>")
print("示例: uv run run-script resources/deploy_docker.sh")
sys.exit(1)
local_path = sys.argv[1]
# 检查本地脚本是否存在
if not os.path.isfile(local_path):
print(f"❌ 本地脚本不存在: {local_path}")
sys.exit(1)
# 获取脚本文件名
script_name = os.path.basename(local_path)
remote_path = f"/tmp/{script_name}"
log_path = f"/tmp/{script_name}.log"
local_log_path = f"{script_name}.log"
# 创建管理器和连接
vm = VBoxSSHManager(
vm_name,
ssh_host,
ssh_port,
ssh_user,
ssh_password,
)
try:
# 启动虚拟机
if not vm.start_vm():
print("❌ 虚拟机启动失败")
sys.exit(1)
# 1. 上传脚本
print(f"\n{'='*50}")
print(f"📤 [1/3] 上传脚本: {local_path} -> {remote_path}")
print(f"{'='*50}")
if not vm.upload_file(local_path, remote_path):
print("❌ 上传失败")
sys.exit(1)
# 2. 执行脚本
print(f"\n{'='*50}")
print(f"⚡ [2/3] 执行脚本: {remote_path}")
print(f"{'='*50}")
print(f" 日志将保存到: {log_path}")
print()
# 赋予执行权限并执行,输出同时写入终端(exec_command 实时打印)和日志文件
# deploy_* 脚本需要 sudo 权限(系统级安装),其余脚本以普通用户执行
if script_name.startswith("deploy_"):
cmd = (
f"chmod +x {remote_path} && "
f"echo '{ssh_password}' | sudo -S {remote_path} 2>&1 | "
f"tee {log_path}"
)
print(" 🔑 检测到部署脚本,以 sudo 权限执行")
else:
cmd = f"chmod +x {remote_path} && " f"{remote_path} 2>&1 | tee {log_path}"
print(" 👤 检测到开发脚本,以普通用户权限执行")
out, err, code = vm.exec_command(cmd, get_pty=False)
print(f"\n✔ 脚本执行完成,退出码: {code}")
# 3. 下载日志
print(f"\n{'='*50}")
print(f"📥 [3/3] 下载执行日志: {log_path} -> {local_log_path}")
print(f"{'='*50}")
if vm.download_file(log_path, local_log_path):
print(f"✔ 日志已保存到本地: {local_log_path}")
else:
# 如果 tee 方式没拿到日志,尝试直接读取
print("⚠ 尝试直接下载日志...")
if vm.download_file(log_path, local_log_path):
print(f"✔ 日志已保存到本地: {local_log_path}")
else:
print("⚠ 日志文件未生成,将终端输出保存到本地")
if out:
with open(local_log_path, "w") as f:
f.write(out)
print(f"✔ 终端输出已保存到: {local_log_path}")
# 清理远程临时文件
print(f"\n{'='*50}")
print("🧹 清理远程临时文件...")
vm.exec_command(f"rm -f {remote_path} {log_path}")
print("✔ 清理完成")
print(f"\n{'='*50}")
print("🎉 全部完成!")
print(f"{'='*50}")
finally:
vm.stop_vm()
+264
View File
@@ -0,0 +1,264 @@
"""
纯 SSH 连接管理工具(不依赖 VirtualBox/vboxapi)
提供 SSH 连接、命令执行、文件上传/下载功能。
与 VBoxSSHManager 保持兼容的接口,但剥离了所有 VirtualBox 依赖。
"""
import paramiko
import time
import socket
import threading
class SSHManager:
"""纯 SSH 管理工具,不依赖 VirtualBox"""
def __init__(
self,
ssh_host,
ssh_port,
ssh_user,
ssh_password,
ssh_timeout=180,
cmd_timeout=120,
):
self.ssh_host = ssh_host
self.ssh_port = ssh_port
self.ssh_user = ssh_user
self.ssh_pass = ssh_password
self.ssh_timeout = ssh_timeout
self.cmd_timeout = cmd_timeout
self._ssh_client = None
self._cancelled = threading.Event()
def set_cancel_event(self, event: threading.Event):
"""设置外部取消事件,与 GUI 层的取消信号同步"""
self._cancelled = event
def cancel_operations(self):
"""标记取消所有正在进行的操作"""
self._cancelled.set()
if self._ssh_client:
try:
self._ssh_client.close()
except Exception:
pass
self._ssh_client = None
def reset_cancel(self):
"""重置取消标记"""
self._cancelled.clear()
def is_cancelled(self):
"""检查是否已取消"""
return self._cancelled.is_set()
# ---------- SSH 连接管理 ----------
def get_ssh_client(self):
"""获取或创建 SSH 连接(带重试)"""
if self._ssh_client is not None:
return self._ssh_client
for i in range(5):
if self._cancelled.is_set():
return None
if not self._is_port_open(self.ssh_host, self.ssh_port):
print(f"SSH 端口 {self.ssh_host}:{self.ssh_port} 不可达,重试 {i+1}/5")
time.sleep(10)
continue
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.ssh_host,
port=self.ssh_port,
username=self.ssh_user,
password=self.ssh_pass,
timeout=10,
)
self._ssh_client = client
return client
except Exception as e:
print(f"SSH 连接失败 ({i+1}/5): {e}")
if self._cancelled.is_set():
return None
time.sleep(10)
return None
def close_ssh(self):
"""关闭 SSH 连接"""
if self._ssh_client:
self._ssh_client.close()
self._ssh_client = None
# ---------- 虚拟机生命周期(空实现,兼容接口) ----------
def is_running(self) -> bool:
"""SSH-only 模式下始终返回 True(不管理虚拟机生命周期)"""
return True
def start_vm(self, wait_for_ssh=True):
"""SSH-only 模式下直接等待 SSH 就绪"""
if wait_for_ssh:
return self._wait_for_ssh_ready(self.ssh_timeout)
return True
def stop_vm(self):
"""SSH-only 模式下不执行任何操作"""
self.close_ssh()
def ensure_vm_stopped(self):
"""SSH-only 模式下不执行任何操作"""
return False
# ---------- 核心功能 ----------
def exec_command(self, command, get_pty=False):
"""
执行命令并返回 (stdout_str, stderr_str, exit_code)
边执行边打印输出,方便实时查看进度
如果连接失败返回 (None, None, -1)
"""
client = self.get_ssh_client()
if not client:
return None, None, -1
try:
stdin, stdout, stderr = client.exec_command(
command, timeout=self.cmd_timeout, get_pty=get_pty
)
out_lines = []
for line in stdout:
if self._cancelled.is_set():
stdout.channel.close()
break
if isinstance(line, bytes):
decoded = line.decode("utf-8", errors="replace").rstrip("\n")
else:
decoded = str(line).rstrip("\n")
print(f" {decoded}")
out_lines.append(decoded)
err_lines = []
for line in stderr:
if self._cancelled.is_set():
stderr.channel.close()
break
if isinstance(line, bytes):
decoded = line.decode("utf-8", errors="replace").rstrip("\n")
else:
decoded = str(line).rstrip("\n")
print(f" [stderr] {decoded}")
err_lines.append(decoded)
out = "\n".join(out_lines)
err = "\n".join(err_lines)
exit_code = stdout.channel.recv_exit_status()
return out, err, exit_code
except Exception as e:
if self._cancelled.is_set():
return None, None, -1
print(f"执行命令失败: {e}")
return None, None, -1
def upload_file(self, local_path, remote_path):
"""上传本地文件到远程服务器,返回是否成功"""
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f"✔ 上传成功: {local_path} -> {remote_path}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 上传失败: {e}")
return False
def upload_directory(self, local_dir, remote_dir):
"""递归上传整个文件夹到远程服务器,返回是否成功"""
import os as _os
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
local_dir = _os.path.normpath(local_dir)
remote_dir = remote_dir.rstrip("/")
# 展开 ~ 为真实路径(SFTP 不会自动展开 ~)
if remote_dir.startswith("~/"):
out, _, _ = self.exec_command("echo ~")
if out and out.strip():
remote_dir = out.strip() + remote_dir[1:]
# 创建远程根目录
self.exec_command(f'mkdir -p "{remote_dir}"')
for root, dirs, files in _os.walk(local_dir):
# 计算相对路径
rel_path = _os.path.relpath(root, local_dir)
if rel_path == ".":
rel_path = ""
# 创建远程子目录
target_dir = remote_dir + (
"/" + rel_path.replace("\\", "/") if rel_path else ""
)
self.exec_command(f'mkdir -p "{target_dir}"')
for fname in files:
if self._cancelled.is_set():
sftp.close()
return False
local_file = _os.path.join(root, fname)
remote_file = target_dir + "/" + fname
sftp.put(local_file, remote_file)
print(f" ✔ {local_file} -> {remote_file}")
sftp.close()
print(f"✔ 文件夹上传成功: {local_dir} -> {remote_dir}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 文件夹上传失败: {e}")
return False
def download_file(self, remote_path, local_path):
"""从远程服务器下载文件到本地,返回是否成功"""
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.get(remote_path, local_path)
sftp.close()
print(f"✔ 下载成功: {remote_path} -> {local_path}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 下载失败: {e}")
return False
# ---------- 内部辅助方法 ----------
def _wait_for_ssh_ready(self, timeout=180):
"""等待 SSH 端口可连接"""
start = time.time()
while time.time() - start < timeout:
if self._cancelled.is_set():
return False
if self._is_port_open(self.ssh_host, self.ssh_port):
return True
time.sleep(2)
return False
@staticmethod
def _is_port_open(host, port, timeout=5):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((host, port))
s.close()
return True
except Exception:
return False
@@ -0,0 +1,385 @@
import paramiko
import time
import socket
import threading
from vboxapi import VirtualBoxManager
class VBoxSSHManager:
"""VirtualBox 虚拟机 SSH 管理工具,支持上传、执行、下载文件"""
def __init__(
self,
vm_name,
ssh_host,
ssh_port,
ssh_user,
ssh_password,
ssh_timeout=180,
cmd_timeout=120,
):
self.vm_name = vm_name
self.ssh_host = ssh_host
self.ssh_port = ssh_port
self.ssh_user = ssh_user
self.ssh_pass = ssh_password
self.ssh_timeout = ssh_timeout
self.cmd_timeout = cmd_timeout
self.mgr = VirtualBoxManager(None, None)
self.vbox = self.mgr.getVirtualBox()
self.machine = self.vbox.findMachine(self.vm_name)
self.session = None
self.console = None
self.guest = None
self._ssh_client = None
self._cancelled = threading.Event()
def set_cancel_event(self, event: threading.Event):
"""设置外部取消事件,与 GUI 层的取消信号同步"""
self._cancelled = event
def cancel_operations(self):
"""标记取消所有正在进行的操作"""
self._cancelled.set()
# 强制关闭 SSH 连接,让阻塞的操作立即返回
if self._ssh_client:
try:
self._ssh_client.close()
except:
pass
self._ssh_client = None
def reset_cancel(self):
"""重置取消标记"""
self._cancelled.clear()
def is_cancelled(self):
"""检查是否已取消"""
return self._cancelled.is_set()
# ---------- 虚拟机生命周期 ----------
def is_running(self) -> bool:
"""检查虚拟机是否正在运行"""
try:
state = self.machine.state
return state == self.mgr.constants.MachineState_Running
except Exception as e:
print(f"检查虚拟机状态失败: {e}")
return False
def start_vm(self, wait_for_ssh=True):
"""启动虚拟机,并返回是否成功。如果虚拟机已在运行则跳过。"""
if self.is_running():
print("✔ 虚拟机已在运行,跳过启动")
# 等待 SSH 就绪
if wait_for_ssh and not self._wait_for_ssh_ready(self.ssh_timeout):
print("SSH 服务在超时时间内未就绪")
return False
return True
if self._cancelled.is_set():
return False
self.session = self.mgr.getSessionObject(self.vbox)
progress = self.machine.launchVMProcess(self.session, "headless", [])
progress.waitForCompletion(-1)
if progress.resultCode != 0:
error = progress.errorInfo.text if progress.errorInfo else "Unknown"
print(f"启动失败: {error}")
self.session.unlockMachine()
self.session = None
return False
self.console = self.session.console
self.guest = self.console.guest
# 等待 Running 状态
if not self._wait_for_vm_state(self.mgr.constants.MachineState_Running, 120):
print("虚拟机未能进入 Running 状态")
self.stop_vm()
return False
print("✔ 虚拟机已运行")
if self._cancelled.is_set():
return False
# 等待 Guest Additions 基本就绪
self._wait_for_guest_ready(60)
print("等待 SSH 服务启动...")
time.sleep(15)
if wait_for_ssh and not self._wait_for_ssh_ready(self.ssh_timeout):
print("SSH 服务在超时时间内未就绪")
self.stop_vm()
return False
return True
def stop_vm(self):
"""关闭虚拟机并清理会话"""
# 先关闭 SSH 连接
if self._ssh_client:
try:
self._ssh_client.close()
except:
pass
self._ssh_client = None
# 如果虚拟机没在运行,只需要解锁会话
if not self.is_running():
print("虚拟机未运行,跳过关机")
if self.session:
try:
self.session.unlockMachine()
except:
pass
self.session = None
return
if self.console:
print("正在关闭虚拟机...")
try:
self.console.powerButton()
except:
pass
# 等待虚拟机进入 Stopped 状态
self._wait_for_vm_state(self.mgr.constants.MachineState_Stopped, 30)
if self.session:
try:
self.session.unlockMachine()
except:
pass
self.session = None
print("虚拟机已关闭")
def ensure_vm_stopped(self):
"""确保虚拟机已关闭(供启动前调用)"""
if self.is_running():
print("检测到虚拟机正在运行,正在关闭...")
self.stop_vm()
# 等待完全停止
time.sleep(2)
return True
return False
# ---------- SSH 连接管理 ----------
def get_ssh_client(self):
"""获取或创建 SSH 连接(带重试)"""
if self._ssh_client is not None:
return self._ssh_client
for i in range(5):
if self._cancelled.is_set():
return None
if not self._is_port_open(self.ssh_host, self.ssh_port):
print(f"SSH 端口 {self.ssh_host}:{self.ssh_port} 不可达,重试 {i+1}/5")
time.sleep(10)
continue
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
self.ssh_host,
port=self.ssh_port,
username=self.ssh_user,
password=self.ssh_pass,
timeout=10,
)
self._ssh_client = client
return client
except Exception as e:
print(f"SSH 连接失败 ({i+1}/5): {e}")
if self._cancelled.is_set():
return None
time.sleep(10)
return None
def close_ssh(self):
"""关闭 SSH 连接"""
if self._ssh_client:
self._ssh_client.close()
self._ssh_client = None
# ---------- 核心功能 ----------
def exec_command(self, command, get_pty=False):
"""
执行命令并返回 (stdout_str, stderr_str, exit_code)
边执行边打印输出,方便实时查看进度
如果连接失败返回 (None, None, -1)
"""
client = self.get_ssh_client()
if not client:
return None, None, -1
try:
stdin, stdout, stderr = client.exec_command(
command, timeout=self.cmd_timeout, get_pty=get_pty
)
# 实时读取并打印 stdout
out_lines = []
for line in stdout:
if self._cancelled.is_set():
# 取消时主动关闭通道
stdout.channel.close()
break
if isinstance(line, bytes):
decoded = line.decode("utf-8", errors="replace").rstrip("\n")
else:
decoded = str(line).rstrip("\n")
print(f" {decoded}")
out_lines.append(decoded)
# 实时读取并打印 stderr
err_lines = []
for line in stderr:
if self._cancelled.is_set():
stderr.channel.close()
break
if isinstance(line, bytes):
decoded = line.decode("utf-8", errors="replace").rstrip("\n")
else:
decoded = str(line).rstrip("\n")
print(f" [stderr] {decoded}")
err_lines.append(decoded)
out = "\n".join(out_lines)
err = "\n".join(err_lines)
exit_code = stdout.channel.recv_exit_status()
return out, err, exit_code
except Exception as e:
if self._cancelled.is_set():
return None, None, -1
print(f"执行命令失败: {e}")
return None, None, -1
def upload_file(self, local_path, remote_path):
"""上传本地文件到虚拟机,返回是否成功"""
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
print(f"✔ 上传成功: {local_path} -> {remote_path}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 上传失败: {e}")
return False
def download_file(self, remote_path, local_path):
"""从虚拟机下载文件到本地,返回是否成功"""
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
sftp.get(remote_path, local_path)
sftp.close()
print(f"✔ 下载成功: {remote_path} -> {local_path}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 下载失败: {e}")
return False
def upload_directory(self, local_dir, remote_dir):
"""递归上传整个文件夹到远程服务器,返回是否成功"""
import os as _os
client = self.get_ssh_client()
if not client:
return False
try:
sftp = client.open_sftp()
local_dir = _os.path.normpath(local_dir)
remote_dir = remote_dir.rstrip("/")
# 展开 ~ 为真实路径(SFTP 不会自动展开 ~)
if remote_dir.startswith("~/"):
out, _, _ = self.exec_command("echo ~")
if out and out.strip():
remote_dir = out.strip() + remote_dir[1:]
self.exec_command(f'mkdir -p "{remote_dir}"')
for root, dirs, files in _os.walk(local_dir):
rel_path = _os.path.relpath(root, local_dir)
if rel_path == ".":
rel_path = ""
target_dir = remote_dir + (
"/" + rel_path.replace("\\", "/") if rel_path else ""
)
self.exec_command(f'mkdir -p "{target_dir}"')
for fname in files:
if self._cancelled.is_set():
sftp.close()
return False
local_file = _os.path.join(root, fname)
remote_file = target_dir + "/" + fname
sftp.put(local_file, remote_file)
print(f" ✔ {local_file} -> {remote_file}")
sftp.close()
print(f"✔ 文件夹上传成功: {local_dir} -> {remote_dir}")
return True
except Exception as e:
if self._cancelled.is_set():
return False
print(f"✘ 文件夹上传失败: {e}")
return False
# ---------- 内部辅助方法 ----------
def _wait_for_vm_state(self, target_state, timeout=120):
start = time.time()
while time.time() - start < timeout:
if self._cancelled.is_set():
return False
try:
if self.console.state == target_state: # type: ignore
return True
except:
pass
time.sleep(1)
return False
def _wait_for_guest_ready(self, timeout=60):
start = time.time()
while time.time() - start < timeout:
if self._cancelled.is_set():
return False
try:
if (
self.guest.additionsRunLevel # type: ignore
>= self.mgr.constants.AdditionsRunLevelType_System
):
return True
except:
pass
time.sleep(2)
return False
def _wait_for_ssh_ready(self, timeout=180):
"""等待 SSH 端口可连接"""
start = time.time()
while time.time() - start < timeout:
if self._cancelled.is_set():
return False
if self._is_port_open(self.ssh_host, self.ssh_port):
return True
time.sleep(2)
return False
@staticmethod
def _is_port_open(host, port, timeout=5):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
s.connect((host, port))
s.close()
return True
except:
return False
+336
View File
@@ -0,0 +1,336 @@
import json
import os
import time
from openai import OpenAI
from .vbox_ssh_manager import VBoxSSHManager
from .ssh_manager import SSHManager
from typing import Union
class VMAgent:
"""大模型驱动的虚拟机自动化 Agent"""
TOOLS = [
{
"type": "function",
"function": {
"name": "run_command",
"description": "在虚拟机中执行一条 shell 命令并等待执行完成(如需 sudo,在命令中使用 $SUDO_PASSWORD 占位符,例如 'echo $SUDO_PASSWORD | sudo -S apt update')。重要:所有命令必须是非交互式的,对于 apt install、add-apt-repository 等一定记得加 -y 参数,否则命令会卡住。不要用此工具启动后台服务(如 Web 服务器),请用 start_daemon。",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要执行的命令,例如 'ls -la /home';如需提权,用 echo $SUDO_PASSWORD | sudo -S <命令> 的格式。重要:命令必须是非交互式的,apt install/add-apt-repository 等必须加 -y 参数!",
},
"get_pty": {
"type": "boolean",
"description": "是否需要伪终端(如交互式 sudo),默认 false;用 echo $SUDO_PASSWORD | sudo -S 的方式通常不需要",
},
},
"required": ["command"],
},
},
},
{
"type": "function",
"function": {
"name": "start_daemon",
"description": "在虚拟机中以后台守护进程方式启动一个服务(如 Web 服务器),不会阻塞后续命令。stdout/stderr 将重定向到日志文件,可通过 check_daemon 查看状态。",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "要以后台服务方式运行的命令,例如 'cd /home/chakcy/chongming && uv run serve'。不要加 & 或 nohup,系统会自动处理。",
},
"logfile": {
"type": "string",
"description": "日志文件路径,例如 '/tmp/serve.log'。该文件会包含服务的 stdout 和 stderr 输出。",
},
"workdir": {
"type": "string",
"description": "工作目录,可选。例如 '/home/chakcy/chongming'。",
},
},
"required": ["command", "logfile"],
},
},
},
{
"type": "function",
"function": {
"name": "check_daemon",
"description": "检查后台服务的运行状态和最近日志。如果服务正在运行,会返回最后 N 行日志;如果已退出,会返回退出码和完整日志。",
"parameters": {
"type": "object",
"properties": {
"pid": {
"type": "string",
"description": "后台进程的 PID(来自 start_daemon 的返回结果)",
},
"logfile": {
"type": "string",
"description": "日志文件路径(来自 start_daemon 时指定的 logfile)",
},
},
"required": ["pid", "logfile"],
},
},
},
{
"type": "function",
"function": {
"name": "stop_daemon",
"description": "停止一个后台服务进程",
"parameters": {
"type": "object",
"properties": {
"pid": {
"type": "string",
"description": "要停止的后台进程 PID",
},
},
"required": ["pid"],
},
},
},
{
"type": "function",
"function": {
"name": "upload_file",
"description": "上传本地文件到虚拟机",
"parameters": {
"type": "object",
"properties": {
"local_path": {"type": "string"},
"remote_path": {"type": "string"},
},
"required": ["local_path", "remote_path"],
},
},
},
{
"type": "function",
"function": {
"name": "download_file",
"description": "从虚拟机下载文件到本地",
"parameters": {
"type": "object",
"properties": {
"remote_path": {"type": "string"},
"local_path": {"type": "string"},
},
"required": ["remote_path", "local_path"],
},
},
},
{
"type": "function",
"function": {
"name": "finish_task",
"description": "任务完成,返回结果总结",
"parameters": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "任务执行总结",
}
},
"required": ["summary"],
},
},
},
]
def __init__(
self,
vm_manager: Union[VBoxSSHManager, SSHManager],
api_key=None,
base_url=None,
model="gpt-4o",
sudo_password=None,
):
self.vm = vm_manager
self.client = OpenAI(
api_key=api_key or os.getenv("LLM_API_KEY"),
base_url=base_url or os.getenv("LLM_BASE_URL"),
)
self.model = model or os.getenv("LLM_MODEL", "gpt-4o")
self.sudo_password = sudo_password or os.getenv("VM_SSH_PASS")
# 记录虚拟机是否是由本任务启动的(True=我们启动的,False=执行前已在运行)
self.vm_started_by_us = False
def run(self, task: str) -> tuple:
"""
执行一个自然语言任务,返回 (summary, vm_started_by_us)
vm_started_by_us 表示虚拟机是否是由本次任务启动的(供 GUI 决定是否关机)
"""
system_prompt = (
"你是一个 VirtualBox 虚拟机管理助手。"
"你可以通过提供的工具在 Ubuntu 20.04 虚拟机中执行操作。"
"请一步步完成任务,每次只调用一个工具,观察结果后决定下一步。"
"不要同时调用多个工具。"
"完成任务后调用 finish_task 总结。"
"\n\n重要: 当需要 sudo 提权时,请在命令中使用 $SUDO_PASSWORD 占位符,"
"例如: 'echo $SUDO_PASSWORD | sudo -S apt update'"
"这样系统会自动替换为正确的密码。不要试图用别的方式传密码。"
"\n\n重要: 所有命令必须是非交互式的,因为 SSH 无法处理交互式输入。"
"对于 apt、add-apt-repository 等可能弹出确认提示的命令,务必加上 -y 标志。"
"例如,使用 'add-apt-repository -y <repo>' 而不是 'add-apt-repository <repo>',"
"使用 'apt install -y <包名>' 而不是 'apt install <包名>'。"
"使用 'apt-get install -y <包名>' 而不是 'apt-get install <包名>'。"
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": task},
]
# 判断虚拟机是否已在运行:如果已在运行则直接 SSH 连接,不重启
# start_vm() 内部已处理「如果已在运行则跳过启动」的逻辑
# 但我们还需要知道「是否是本次启动的」,以便让用户决定是否关机
was_running_before = self.vm.is_running()
if was_running_before:
print("✔ 检测到虚拟机已在运行,直接进行 SSH 连接")
else:
print("🔌 虚拟机未运行,需要启动")
if not self.vm.start_vm():
return "❌ 虚拟机启动失败,无法执行任务", False
# 记录虚拟机是否是由我们启动的
self.vm_started_by_us = not was_running_before
try:
while True:
if self.vm.is_cancelled():
return "⏹️ 任务已取消", self.vm_started_by_us
response = self.client.chat.completions.create(
model=self.model,
messages=messages, # type: ignore
tools=self.TOOLS, # type: ignore
tool_choice="auto",
)
msg = response.choices[0].message
# 输出模型文本(若有)
if msg.content:
print("🤖", msg.content)
# 无工具调用即结束对话
if not msg.tool_calls:
break
# 先追加 assistant 消息到历史(包含所有 tool_calls)
messages.append(msg) # type: ignore
# 依次处理每个 tool_call
for tool_call in msg.tool_calls:
if self.vm.is_cancelled():
return "⏹️ 任务已取消", self.vm_started_by_us
func_name = tool_call.function.name # type: ignore
func_args = json.loads(tool_call.function.arguments) # type: ignore
print(f"🔧 执行 {func_name}: {func_args}")
# finish_task 特殊处理:直接跳出循环,不追加到消息
if func_name == "finish_task":
summary = func_args.get("summary", "任务完成")
print("✅", summary)
return summary, self.vm_started_by_us
result = self._execute_tool(func_name, func_args)
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
}
)
finally:
# 注意:不再 here 自动关闭 VM!
# VM 的生命周期由 GUI 层根据用户选择来决定是否关闭
pass
return "任务执行完成", self.vm_started_by_us
def stop_vm_if_we_started(self):
"""如果 VM 是本任务启动的则关闭,否则保持运行"""
if self.vm_started_by_us:
print("🛑 关闭由本任务启动的虚拟机")
self.vm.stop_vm()
else:
print("⏭️ 虚拟机并非本任务启动,保持运行状态")
def _execute_tool(self, func_name: str, args: dict) -> str:
if func_name == "run_command":
cmd = args["command"]
cmd = cmd.replace("$SUDO_PASSWORD", self.sudo_password or "")
get_pty = args.get("get_pty", False)
out, err, code = self.vm.exec_command(cmd, get_pty=get_pty)
return f"退出码: {code}\n标准输出:\n{out}\n标准错误:\n{err}"
elif func_name == "start_daemon":
cmd = args["command"]
logfile = args["logfile"]
workdir = args.get("workdir", "")
cmd = cmd.replace("$SUDO_PASSWORD", self.sudo_password or "")
# 构造后台执行命令:重定向 stdout/stderr 到日志文件
cd_prefix = f"cd {workdir} && " if workdir else ""
bg_cmd = (
f"{cd_prefix}nohup bash -c '{cmd}' " f"> {logfile} 2>&1 & echo PID=$!"
)
out, err, code = self.vm.exec_command(bg_cmd, get_pty=False)
# 提取 PID
pid = ""
if out:
for line in out.splitlines():
if line.startswith("PID="):
pid = line.split("=", 1)[1].strip()
break
if pid:
return f"后台服务已启动, PID: {pid}, 日志: {logfile}"
else:
return f"启动失败(或未获取到 PID)\n输出: {out}\n错误: {err}"
elif func_name == "check_daemon":
pid = args["pid"]
logfile = args["logfile"]
check_cmd = (
f"if ps -p {pid} > /dev/null 2>&1; then "
f' echo "STATUS=running"; '
f' echo "---最后20行日志---"; '
f' tail -20 "{logfile}" 2>/dev/null || echo "日志文件为空"; '
f"else "
f' echo "STATUS=exited"; '
f' echo "---完整日志---"; '
f' cat "{logfile}" 2>/dev/null || echo "日志文件不存在"; '
f"fi"
)
out, err, code = self.vm.exec_command(check_cmd)
return f"{out}\n{err}" if err else out or "无输出"
elif func_name == "stop_daemon":
pid = args["pid"]
stop_cmd = (
f'kill {pid} 2>/dev/null && echo "已发送 SIGTERM"; '
f"sleep 2; "
f"if ps -p {pid} > /dev/null 2>&1; then "
f' kill -9 {pid} 2>/dev/null && echo "已强制杀死"; '
f"fi; "
f'echo "进程 {pid} 已停止"'
)
out, err, code = self.vm.exec_command(stop_cmd)
return f"{out}\n{err}" if err else out or "无输出"
elif func_name == "upload_file":
success = self.vm.upload_file(args["local_path"], args["remote_path"])
return "上传成功" if success else "上传失败"
elif func_name == "download_file":
success = self.vm.download_file(args["remote_path"], args["local_path"])
return "下载成功" if success else "下载失败"
elif func_name == "finish_task":
return args.get("summary", "任务完成")
else:
return f"未知工具: {func_name}"
Generated
+664
View File
@@ -0,0 +1,664 @@
version = 1
revision = 3
requires-python = ">=3.14"
[[package]]
name = "altgraph"
version = "0.17.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
]
[[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.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
]
[[package]]
name = "bcrypt"
version = "5.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" },
{ url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" },
{ url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" },
{ url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" },
{ url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" },
{ url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" },
{ url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" },
{ url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" },
{ url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" },
{ url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" },
{ url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" },
{ url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" },
{ url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" },
{ url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" },
{ url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" },
{ url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" },
{ url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" },
{ url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" },
{ url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" },
{ url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" },
{ url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" },
{ url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" },
{ url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" },
{ url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" },
{ url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" },
{ url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" },
{ url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" },
{ url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" },
{ url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" },
{ url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" },
{ url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" },
{ url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" },
{ url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" },
{ url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" },
{ url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" },
{ url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" },
{ url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" },
{ url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" },
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
]
[[package]]
name = "certifi"
version = "2026.4.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[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 = "48.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
]
[[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 = "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 = "idna"
version = "3.13"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
]
[[package]]
name = "invoke"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/33/f6/227c48c5fe47fa178ccf1fda8f047d16c97ba926567b661e9ce2045c600c/invoke-3.0.3.tar.gz", hash = "sha256:437b6a622223824380bfb4e64f612711a6b648c795f565efc8625af66fb57f0c", size = 343419, upload-time = "2026-04-07T15:17:48.307Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/de/bbc12563bbf979618d17625a4e753ff7a078523e28d870d3626daa97261a/invoke-3.0.3-py3-none-any.whl", hash = "sha256:f11327165e5cbb89b2ad1d88d3292b5113332c43b8553b494da435d6ec6f5053", size = 160958, upload-time = "2026-04-07T15:17:46.875Z" },
]
[[package]]
name = "jiter"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" },
{ url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" },
{ url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" },
{ url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" },
{ url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" },
{ url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" },
{ url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" },
{ url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" },
{ url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" },
{ url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" },
{ url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" },
{ url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" },
{ url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" },
{ url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" },
{ url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" },
{ url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" },
{ url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" },
{ url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" },
{ url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" },
{ url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" },
{ url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" },
{ url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" },
{ url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" },
{ url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" },
{ url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" },
{ url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" },
]
[[package]]
name = "macholib"
version = "1.16.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
]
[[package]]
name = "openai"
version = "2.36.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/f4/a1/4d5e84cf51720fc1526cc49e10ac1961abcccb55b0efb3d970db1e9a2728/openai-2.36.0.tar.gz", hash = "sha256:139dea0edd2f1b30c33d46ae1a6929e03906254140318e4608e98fe8c566f2e7", size = 753003, upload-time = "2026-05-07T17:33:17.075Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/1c/5d43735b2553baae2a5e899dcbcd0670a86930d993184d72ca909bf11c9b/openai-2.36.0-py3-none-any.whl", hash = "sha256:143f6194b548dbc2c921af1f1b03b9f14c85fed8a75b5b516f5bcc11a2a50c63", size = 1302361, upload-time = "2026-05-07T17:33:15.063Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "paramiko"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "invoke" },
{ name = "pynacl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/e7/81fdcbc7f190cdb058cffc9431587eb289833bdd633e2002455ca9bb13d4/paramiko-4.0.0.tar.gz", hash = "sha256:6a25f07b380cc9c9a88d2b920ad37167ac4667f8d9886ccebd8f90f654b5d69f", size = 1630743, upload-time = "2025-08-04T01:02:03.711Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/90/a744336f5af32c433bd09af7854599682a383b37cfd78f7de263de6ad6cb/paramiko-4.0.0-py3-none-any.whl", hash = "sha256:0e20e00ac666503bf0b4eda3b6d833465a2b7aff2e2b3d79a8bba5ef144ee3b9", size = 223932, upload-time = "2025-08-04T01:02:02.029Z" },
]
[[package]]
name = "pefile"
version = "2024.8.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.13.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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
]
[[package]]
name = "pydantic-core"
version = "2.46.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
]
[[package]]
name = "pyinstaller"
version = "6.20.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "altgraph" },
{ name = "macholib", marker = "sys_platform == 'darwin'" },
{ name = "packaging" },
{ name = "pefile", marker = "sys_platform == 'win32'" },
{ name = "pyinstaller-hooks-contrib" },
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2026.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
]
[[package]]
name = "pynacl"
version = "1.6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/79/0e3c34dc3c4671f67d251c07aa8eb100916f250ee470df230b0ab89551b4/pynacl-1.6.2-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:622d7b07cc5c02c666795792931b50c91f3ce3c2649762efb1ef0d5684c81594", size = 390064, upload-time = "2026-01-01T17:31:57.264Z" },
{ url = "https://files.pythonhosted.org/packages/eb/1c/23a26e931736e13b16483795c8a6b2f641bf6a3d5238c22b070a5112722c/pynacl-1.6.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d071c6a9a4c94d79eb665db4ce5cedc537faf74f2355e4d502591d850d3913c0", size = 809370, upload-time = "2026-01-01T17:31:59.198Z" },
{ url = "https://files.pythonhosted.org/packages/87/74/8d4b718f8a22aea9e8dcc8b95deb76d4aae380e2f5b570cc70b5fd0a852d/pynacl-1.6.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe9847ca47d287af41e82be1dd5e23023d3c31a951da134121ab02e42ac218c9", size = 1408304, upload-time = "2026-01-01T17:32:01.162Z" },
{ url = "https://files.pythonhosted.org/packages/fd/73/be4fdd3a6a87fe8a4553380c2b47fbd1f7f58292eb820902f5c8ac7de7b0/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04316d1fc625d860b6c162fff704eb8426b1a8bcd3abacea11142cbd99a6b574", size = 844871, upload-time = "2026-01-01T17:32:02.824Z" },
{ url = "https://files.pythonhosted.org/packages/55/ad/6efc57ab75ee4422e96b5f2697d51bbcf6cdcc091e66310df91fbdc144a8/pynacl-1.6.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44081faff368d6c5553ccf55322ef2819abb40e25afaec7e740f159f74813634", size = 1446356, upload-time = "2026-01-01T17:32:04.452Z" },
{ url = "https://files.pythonhosted.org/packages/78/b7/928ee9c4779caa0a915844311ab9fb5f99585621c5d6e4574538a17dca07/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:a9f9932d8d2811ce1a8ffa79dcbdf3970e7355b5c8eb0c1a881a57e7f7d96e88", size = 826814, upload-time = "2026-01-01T17:32:06.078Z" },
{ url = "https://files.pythonhosted.org/packages/f7/a9/1bdba746a2be20f8809fee75c10e3159d75864ef69c6b0dd168fc60e485d/pynacl-1.6.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:bc4a36b28dd72fb4845e5d8f9760610588a96d5a51f01d84d8c6ff9849968c14", size = 1411742, upload-time = "2026-01-01T17:32:07.651Z" },
{ url = "https://files.pythonhosted.org/packages/f3/2f/5e7ea8d85f9f3ea5b6b87db1d8388daa3587eed181bdeb0306816fdbbe79/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bffb6d0f6becacb6526f8f42adfb5efb26337056ee0831fb9a7044d1a964444", size = 801714, upload-time = "2026-01-01T17:32:09.558Z" },
{ url = "https://files.pythonhosted.org/packages/06/ea/43fe2f7eab5f200e40fb10d305bf6f87ea31b3bbc83443eac37cd34a9e1e/pynacl-1.6.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fef529ef3ee487ad8113d287a593fa26f48ee3620d92ecc6f1d09ea38e0709b", size = 1372257, upload-time = "2026-01-01T17:32:11.026Z" },
{ url = "https://files.pythonhosted.org/packages/4d/54/c9ea116412788629b1347e415f72195c25eb2f3809b2d3e7b25f5c79f13a/pynacl-1.6.2-cp314-cp314t-win32.whl", hash = "sha256:a84bf1c20339d06dc0c85d9aea9637a24f718f375d861b2668b2f9f96fa51145", size = 231319, upload-time = "2026-01-01T17:32:12.46Z" },
{ url = "https://files.pythonhosted.org/packages/ce/04/64e9d76646abac2dccf904fccba352a86e7d172647557f35b9fe2a5ee4a1/pynacl-1.6.2-cp314-cp314t-win_amd64.whl", hash = "sha256:320ef68a41c87547c91a8b58903c9caa641ab01e8512ce291085b5fe2fcb7590", size = 244044, upload-time = "2026-01-01T17:32:13.781Z" },
{ url = "https://files.pythonhosted.org/packages/33/33/7873dc161c6a06f43cda13dec67b6fe152cb2f982581151956fa5e5cdb47/pynacl-1.6.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d29bfe37e20e015a7d8b23cfc8bd6aa7909c92a1b8f41ee416bbb3e79ef182b2", size = 188740, upload-time = "2026-01-01T17:32:15.083Z" },
{ url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" },
{ url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" },
{ url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" },
{ url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" },
{ url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" },
{ url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" },
{ url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" },
{ url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" },
{ url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" },
{ url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" },
]
[[package]]
name = "pyside6"
version = "6.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-addons" },
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/95/f3f5a2799163b6658126d78a85bc1dec9eda88c75c26780556b26071a1d8/pyside6-6.11.0-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1f2735dc4f2bd4ec452ae50502c8a22128bba0aced35358a2bbc58384b820c6f", size = 571544, upload-time = "2026-03-23T12:47:20.263Z" },
{ url = "https://files.pythonhosted.org/packages/da/89/9a1f521051714e6694ebbe2b979ded279845ec8e25cb309ca3960158d74f/pyside6-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c642e2d25704ca746fd37f56feacf25c5aecc4cd40bef23d18eec81f87d9dc00", size = 571725, upload-time = "2026-03-23T12:47:21.727Z" },
{ url = "https://files.pythonhosted.org/packages/c2/3d/f779d8bba00fcde31a7d7fb6b59347a70773c9cc8135592dea9972579877/pyside6-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:267b344c73580ac938ca63c611881fb42a3922ebfe043e271005f4f06c372c4e", size = 571722, upload-time = "2026-03-23T12:47:22.761Z" },
{ url = "https://files.pythonhosted.org/packages/ac/98/150e01a026df3e9697310236821fa825319bb4b9d6137539cb25a3032968/pyside6-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:9092cb002ca43c64006afb2e0d0f6f51aef17aa737c33a45e502326a081ddcbc", size = 577988, upload-time = "2026-03-23T12:47:23.795Z" },
{ url = "https://files.pythonhosted.org/packages/50/e7/55960f7c6b41d058e95cb4af02652c46c48702c506c8bbf12e99550e1fb3/pyside6-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:b15f39acc2b8f46251a630acad0d97f9a0a0461f2baffcd66d7adfada8eb641e", size = 561372, upload-time = "2026-03-23T12:47:25.073Z" },
]
[[package]]
name = "pyside6-addons"
version = "6.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyside6-essentials" },
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/df/241f311c61a46b7b1195927da77b2537692ee3442aa9ccd87981164ff78d/pyside6_addons-6.11.0-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:d5eaa4643302e3a0fa94c5766234bee4073d7d5ab9c2b7fd222692a176faf182", size = 331554157, upload-time = "2026-03-23T12:40:40.497Z" },
{ url = "https://files.pythonhosted.org/packages/31/b9/e81172835ccc9d8b9792cc6bf7524a252a0db9a76ddd693de230402697f9/pyside6_addons-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ac6fe3d4ef4497dde3efc5e896b0acd53ff6c93be4bf485f045690f919419f35", size = 174948482, upload-time = "2026-03-23T12:41:05.379Z" },
{ url = "https://files.pythonhosted.org/packages/a8/a4/426d9333782bf65ab2a20257d6b4b3af9b8d5d7a710da719865fab49d492/pyside6_addons-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:8ffb40222456078930816ebcac2f2511716d2acbc11716dd5acc5c365179a753", size = 170430798, upload-time = "2026-03-23T12:41:38.134Z" },
{ url = "https://files.pythonhosted.org/packages/35/9a/46d271fedfabad8c6dce2ebb69bb593745487ed33753a56a47c3ba4fdb1c/pyside6_addons-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:413e6121c24f5ffdce376298059eddecff74aa6d638e94e0f6015b33d29b889e", size = 168723088, upload-time = "2026-03-23T12:42:00.668Z" },
{ url = "https://files.pythonhosted.org/packages/16/cd/1b28264f7dc9a642da2e4e7c02f67418d0949eb7ce329ae20869703c2630/pyside6_addons-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:aaaee83385977a0fe134b2f4fbfb92b45a880d5b656e4d90a708eef10b1b6de8", size = 35698324, upload-time = "2026-03-23T12:42:13.748Z" },
]
[[package]]
name = "pyside6-essentials"
version = "6.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "shiboken6" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/00/8a8583d3429c737cc20e61a43eba8ab1ec13ddb101e99802c2ffeedf3b41/pyside6_essentials-6.11.0-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:85d6ca87ef35fa6565d385ede72ae48420dd3f63113929d10fc800f6b0360e01", size = 108085251, upload-time = "2026-03-23T12:42:52.872Z" },
{ url = "https://files.pythonhosted.org/packages/f3/a9/07c9e5c014b871c1b19caf8f994bcd50b345559b81f81671217b49559b67/pyside6_essentials-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:dc20e7afd5fc6fe51297db91cef997ce60844be578f7a49fc61b7ab9657a8849", size = 78316055, upload-time = "2026-03-23T12:43:04.19Z" },
{ url = "https://files.pythonhosted.org/packages/7c/35/f06b1b641d7600ec46374c16cd37c66fa4a22870326b4eb073a95471035f/pyside6_essentials-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:4854cb0a1b061e7a576d8fb7bb7cf9f49540d558b1acb7df0742a7afefe61e4e", size = 77380821, upload-time = "2026-03-23T12:43:24.649Z" },
{ url = "https://files.pythonhosted.org/packages/ff/37/ba95c6262836d2b286b4e05a9d16a5e870995d5d2503ac6adc6312208049/pyside6_essentials-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:3b3362882ad9389357a80504e600180006a957731fec05786fced7b038461fdf", size = 75793322, upload-time = "2026-03-23T12:43:35.575Z" },
{ url = "https://files.pythonhosted.org/packages/53/27/d17f25e45820e633a70e6109b35991eda09a5e8000c2a306f0ab7538d48c/pyside6_essentials-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:81ca603dbf21bc39f89bb42db215c25ebe0c879a1a4c387625c321d2730ec187", size = 56337457, upload-time = "2026-03-23T12:43:43.573Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
name = "pywin32"
version = "311"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
]
[[package]]
name = "pywin32-ctypes"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
]
[[package]]
name = "setuptools"
version = "82.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
]
[[package]]
name = "shiboken6"
version = "6.11.0"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/1d/b56b7b694fbc871496435488d1f41c5068de546334850d722756511cef65/shiboken6-6.11.0-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:d88e8a1eb705f2b9ad21db08a61ae1dc0c773e5cd86a069de0754c4cf1f9b43b", size = 476085, upload-time = "2026-03-23T12:47:05.724Z" },
{ url = "https://files.pythonhosted.org/packages/65/cb/4bb0c76011166230daa7c0074aeb3fdb3935c83ac1fef3789b85fcd1a8fc/shiboken6-6.11.0-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad54e64f8192ddbdff0c54ac82b89edcd62ed623f502ea21c960541d19514053", size = 271055, upload-time = "2026-03-23T12:47:07.349Z" },
{ url = "https://files.pythonhosted.org/packages/f5/96/771a6e2b530f725303d16d78a321fa4876b98b4f3615c9851880df8c1a43/shiboken6-6.11.0-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a10dc7718104ea2dc15d5b0b96909b77162ce1c76fcc6968e6df692b947a00e9", size = 267456, upload-time = "2026-03-23T12:47:08.689Z" },
{ url = "https://files.pythonhosted.org/packages/72/f7/44c0c42c3f5f29dec457fd46ea0552174bcb8aa75becf03bbd90308ba07b/shiboken6-6.11.0-cp310-abi3-win_amd64.whl", hash = "sha256:483ff78a73c7b3189ca924abc694318084f078bcfeaffa68e32024ff2d025ee1", size = 1222132, upload-time = "2026-03-23T12:47:10.143Z" },
{ url = "https://files.pythonhosted.org/packages/fb/99/6e5ee21db2d6af84bbbd7d871d441dafeb069c6de5667b1aa49891a77c66/shiboken6-6.11.0-cp310-abi3-win_arm64.whl", hash = "sha256:3bd76cf56105ab2d62ecaff630366f11264f69b88d488f10f048da9a065781f4", size = 1783186, upload-time = "2026-03-23T12:47:11.832Z" },
]
[[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 = "tqdm"
version = "4.67.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "virtualbox-client"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "openai" },
{ name = "paramiko" },
{ name = "pyside6" },
{ name = "python-dotenv" },
{ name = "pywin32" },
]
[package.dev-dependencies]
dev = [
{ name = "pyinstaller" },
{ name = "setuptools" },
]
[package.metadata]
requires-dist = [
{ name = "openai", specifier = ">=2.35.1" },
{ name = "paramiko", specifier = ">=4.0.0" },
{ name = "pyside6", specifier = ">=6.8.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "pywin32", specifier = ">=311" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pyinstaller", specifier = ">=6.20.0" },
{ name = "setuptools", specifier = ">=82.0.1" },
]