Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
403c6cfb2c | ||
|
|
8d0ca11c94 | ||
|
|
f1ff798f46 | ||
|
|
8548ce50e6 | ||
|
|
852a7eef34 |
@@ -1,127 +0,0 @@
|
||||
name: 🏗️ Enterprise CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
env:
|
||||
REGISTRY: 192.168.1.83:8443/library
|
||||
IMAGE_NAME: ai-stock-service
|
||||
HARBOR_URL: 192.168.1.83:8443
|
||||
|
||||
jobs:
|
||||
# 🔍 质量门禁
|
||||
quality-gates:
|
||||
name: 🔍 质量门禁
|
||||
runs-on: macos
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 🎨 前端质量检查
|
||||
uses: ./.gitea/actions/frontend-verify
|
||||
- name: 🐍 后端质量检查
|
||||
uses: ./.gitea/actions/backend-verify
|
||||
- name: 📋 API 契约检查
|
||||
uses: ./.gitea/actions/contract-verify
|
||||
|
||||
# 🧪 测试阶段
|
||||
test:
|
||||
name: 🧪 测试执行
|
||||
runs-on: [macos, docker]
|
||||
needs: quality-gates
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 🔧 设置环境
|
||||
run: |
|
||||
npm ci
|
||||
python -m pip install -r requirements-dev.txt
|
||||
- name: 🧪 执行测试
|
||||
run: |
|
||||
npm run test:coverage
|
||||
pytest --cov=src --cov-report=xml
|
||||
- name: 📊 上传测试报告
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-reports
|
||||
path: coverage/ test-results/
|
||||
|
||||
# 🏗️ 多架构构建
|
||||
build:
|
||||
name: 🏗️ 多架构构建
|
||||
runs-on: [macos, docker]
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 🐳 设置 Docker Buildx
|
||||
run: |
|
||||
docker buildx version
|
||||
docker buildx inspect
|
||||
- name: 🔧 设置 QEMU (多架构)
|
||||
run: |
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
- name: 📝 生成元数据
|
||||
id: meta
|
||||
run: |
|
||||
echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
echo "VCS_REF=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||
echo "IMAGE_TAG=${{ github.ref_name }}-${{ github.sha }}" >> $GITHUB_ENV
|
||||
- name: 🔐 登录 Harbor 镜像仓库
|
||||
run: |
|
||||
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login https://192.168.1.83:8443 --username "${{ secrets.HARBOR_USERNAME }}" --password-stdin
|
||||
- name: 🏗️ 构建并推送多架构镜像
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--build-arg BUILD_DATE=${BUILD_DATE} \
|
||||
--build-arg VCS_REF=${VCS_REF} \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
|
||||
-f Dockerfile.optimized \
|
||||
--push \
|
||||
.
|
||||
- name: 📊 镜像信息
|
||||
run: |
|
||||
echo "✅ 镜像已推送到 Harbor:"
|
||||
echo "📦 Registry: ${{ env.REGISTRY }}"
|
||||
echo "🏷️ Tags: ${{ env.IMAGE_TAG }}, latest, ${{ github.ref_name }}"
|
||||
echo "🏗️ Platforms: linux/amd64, linux/arm64"
|
||||
|
||||
# 📋 构建报告
|
||||
build-report:
|
||||
name: 📋 构建报告
|
||||
runs-on: macos
|
||||
needs: [quality-gates, test, build]
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 📊 生成报告
|
||||
run: |
|
||||
cat > build-report.md << EOF
|
||||
# 🚀 构建报告
|
||||
|
||||
## 📋 构建信息
|
||||
- **时间**: $(date)
|
||||
- **分支**: ${{ github.ref_name }}
|
||||
- **提交**: ${{ github.sha }}
|
||||
|
||||
## 🎯 质量检查结果
|
||||
- **前端质量**: ${{ needs.quality-gates.result }}
|
||||
- **测试结果**: ${{ needs.test.result }}
|
||||
- **构建结果**: ${{ needs.build.result }}
|
||||
|
||||
## 📊 后续计划
|
||||
- [x] 配置 Harbor 私有镜像仓库
|
||||
- [ ] 部署到 Kubernetes
|
||||
- [ ] 配置监控告警
|
||||
- [ ] 配置镜像签名策略
|
||||
EOF
|
||||
- name: 📦 上传报告
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-report
|
||||
path: build-report.md
|
||||
@@ -0,0 +1,29 @@
|
||||
name: 简单测试工作流
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ test-workflow-trigger ]
|
||||
pull_request:
|
||||
branches: [ test-workflow-trigger ]
|
||||
|
||||
jobs:
|
||||
hello-world:
|
||||
name: 👋 Hello World 测试
|
||||
runs-on: macos
|
||||
|
||||
steps:
|
||||
- name: 🎯 输出 Hello World
|
||||
run: |
|
||||
echo "🎉 Hello from Gitea Actions!"
|
||||
echo "🏃 Runner: ${{ runner.name }}"
|
||||
echo "🖥️ 系统: $(uname -s)"
|
||||
echo "📅 时间: $(date)"
|
||||
|
||||
- name: 🔍 检查环境
|
||||
run: |
|
||||
echo "Node.js 版本: $(node --version 2>/dev/null || echo '未安装')"
|
||||
echo "Python 版本: $(python --version 2>/dev/null || echo '未安装')"
|
||||
echo "Docker 版本: $(docker --version 2>/dev/null || echo '未安装')"
|
||||
|
||||
- name: ✅ 成功标记
|
||||
run: echo "🎯 简单测试工作流执行成功!"
|
||||
@@ -1,58 +0,0 @@
|
||||
# 多阶段优化 Dockerfile
|
||||
# 支持多架构构建
|
||||
|
||||
# 构建阶段
|
||||
FROM --platform=$BUILDPLATFORM python:3.11-slim as builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装构建依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装 Python 依赖(使用缓存)
|
||||
RUN pip install --no-cache-dir --user -r requirements.txt
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 安装应用
|
||||
RUN pip install --no-cache-dir --user -e .
|
||||
|
||||
# 生产阶段
|
||||
FROM --platform=$BUILDPLATFORM python:3.11-slim
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN adduser --disabled-password --gecos '' appuser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制
|
||||
COPY --from=builder /root/.local /home/appuser/.local
|
||||
|
||||
# 复制应用代码
|
||||
COPY --from=builder /app /app
|
||||
|
||||
# 设置用户和权限
|
||||
USER appuser
|
||||
|
||||
# 设置环境变量
|
||||
ENV PATH="/home/appuser/.local/bin:$PATH"
|
||||
ENV PYTHONPATH="/app"
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "app.py"]
|
||||
@@ -1,16 +0,0 @@
|
||||
# 🚀 企业级 CI/CD 优化测试
|
||||
|
||||
## 优化内容
|
||||
- [x] Runner 配置优化
|
||||
- [x] Docker Buildx 多架构支持
|
||||
- [x] 企业级工作流创建
|
||||
- [x] 优化的 Dockerfile
|
||||
|
||||
## 测试时间
|
||||
Tue Oct 28 19:27:07 CST 2025
|
||||
|
||||
## 预期效果
|
||||
- 构建速度提升 30-50%
|
||||
- 支持 AMD64 + ARM64 架构
|
||||
- 质量门禁自动化执行
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
# 🚀 Gitea Actions 工作流演示指南
|
||||
|
||||
> **目标**: 演示如何通过创建 Pull Request 自动触发质量门禁检查
|
||||
> **Runner 状态**: ✅ 运行中 (PID: 91128)
|
||||
> **测试分支**: `test-workflow-trigger`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 演示概览
|
||||
|
||||
### 📋 工作流配置
|
||||
|
||||
我们已经配置了完整的质量门禁工作流,位于 `.gitea/workflows/quality-gate.yml`:
|
||||
|
||||
```yaml
|
||||
name: 质量门禁 CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
frontend-quality: # 🎨 前端质量检查 (runs-on: [macos, docker])
|
||||
backend-quality: # 🐍 后端质量检查 (runs-on: macos)
|
||||
contract-quality: # 📋 API契约检查 (runs-on: macos)
|
||||
data-quality: # 📊 数据质量检查 (runs-on: macos)
|
||||
prompt-security: # 🔐 提示词安全检查 (runs-on: macos)
|
||||
quality-gate: # 🚪 最终质量门禁 (runs-on: macos)
|
||||
performance-benchmark: # ⚡ 性能基准测试 (runs-on: [macos, docker])
|
||||
```
|
||||
|
||||
### 🏷️ Runner 标签匹配
|
||||
|
||||
我们的本地 Runner 标签:`macos, amd64, docker, local`
|
||||
|
||||
```yaml
|
||||
# 匹配的工作流配置
|
||||
runs-on: macos ✅ 匹配
|
||||
runs-on: [macos, docker] ✅ 匹配
|
||||
runs-on: docker ✅ 匹配
|
||||
runs-on: local ✅ 匹配
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 演示步骤
|
||||
|
||||
### 第一步:创建测试分支 ✅
|
||||
|
||||
```bash
|
||||
# 我们已经创建了测试分支
|
||||
git checkout -b test-workflow-trigger
|
||||
git push origin test-workflow-trigger
|
||||
```
|
||||
|
||||
### 第二步:在 Gitea 中创建 Pull Request
|
||||
|
||||
1. **访问 Gitea**: http://192.168.1.83:33000
|
||||
2. **进入仓库**: gao/ai-stock
|
||||
3. **创建 PR**:
|
||||
- 点击 "Pull Requests" → "New Pull Request"
|
||||
- 选择源分支: `test-workflow-trigger`
|
||||
- 选择目标分支: `master`
|
||||
- 填写标题: "测试 Gitea Actions 工作流触发"
|
||||
- 填写描述: 参考 `TEST_WORKFLOW.md` 内容
|
||||
|
||||
### 第三步:观察工作流自动触发
|
||||
|
||||
创建 PR 后,工作流会自动触发,执行以下检查:
|
||||
|
||||
#### 🎨 前端质量检查 (frontend-quality)
|
||||
```bash
|
||||
- TypeScript 类型检查
|
||||
- ESLint 代码规范检查
|
||||
- 单元测试执行
|
||||
- 构建验证
|
||||
- 开发标准合规检查
|
||||
```
|
||||
|
||||
#### 🐍 后端质量检查 (backend-quality)
|
||||
```bash
|
||||
- Python 单元测试
|
||||
- 代码质量检查
|
||||
- 安全漏洞扫描
|
||||
- 数据库迁移验证
|
||||
```
|
||||
|
||||
#### 📋 API契约检查 (contract-quality)
|
||||
```bash
|
||||
- OpenAPI 规范验证
|
||||
- API 合同测试
|
||||
- 接口兼容性检查
|
||||
```
|
||||
|
||||
#### 📊 数据质量检查 (data-quality)
|
||||
```bash
|
||||
- 数据完整性验证
|
||||
- 数据格式规范检查
|
||||
- 数据质量指标验证
|
||||
```
|
||||
|
||||
#### 🔐 提示词安全检查 (prompt-security)
|
||||
```bash
|
||||
- 提示词内容安全验证
|
||||
- 注入攻击防护检查
|
||||
- 敏感信息泄露检查
|
||||
```
|
||||
|
||||
#### 🚪 最终质量门禁 (quality-gate)
|
||||
```bash
|
||||
- 汇总所有检查结果
|
||||
- 如果任何检查失败,阻止合并
|
||||
- 生成质量检查报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 监控工作流执行
|
||||
|
||||
### 查看 Runner 状态
|
||||
|
||||
```bash
|
||||
# 进入 Runner 目录
|
||||
cd /Users/brunogao/work/codes/tools/macos/gitea/runners
|
||||
|
||||
# 查看实时日志
|
||||
tail -f runner.log
|
||||
|
||||
# 查看运行状态
|
||||
./manage-runner.sh status
|
||||
|
||||
# 健康检查
|
||||
./manage-runner.sh health
|
||||
```
|
||||
|
||||
### 在 Gitea 界面查看
|
||||
|
||||
1. **Actions 页面**: 仓库 → Actions
|
||||
2. **工作流详情**: 点击具体的工作流执行
|
||||
3. **日志查看**: 点击每个 job 查看详细执行日志
|
||||
4. **结果状态**: 查看每个检查项的通过/失败状态
|
||||
|
||||
---
|
||||
|
||||
## 📊 预期结果
|
||||
|
||||
### ✅ 成功场景
|
||||
|
||||
如果所有检查都通过,你将看到:
|
||||
|
||||
```yaml
|
||||
工作流状态: ✅ Success
|
||||
检查项目:
|
||||
🎨 frontend-quality: ✅ 通过
|
||||
🐍 backend-quality: ✅ 通过
|
||||
📋 contract-quality: ✅ 通过
|
||||
📊 data-quality: ✅ 通过
|
||||
🔐 prompt-security: ✅ 通过
|
||||
🚪 quality-gate: ✅ 通过
|
||||
|
||||
PR 状态: 可以合并
|
||||
合并按钮: ✅ 可用
|
||||
```
|
||||
|
||||
### ❌ 失败场景
|
||||
|
||||
如果任何检查失败,你将看到:
|
||||
|
||||
```yaml
|
||||
工作流状态: ❌ Failed
|
||||
失败的检查项: 🔴 显示具体哪个检查失败
|
||||
详细错误: 在日志中查看错误信息
|
||||
|
||||
PR 状态: 阻止合并
|
||||
合并按钮: ❌ 禁用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 工作流未触发
|
||||
|
||||
**可能原因**:
|
||||
- Gitea Actions 未启用
|
||||
- 分支保护规则未配置
|
||||
- Runner 标签不匹配
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 1. 检查 Gitea Actions 设置
|
||||
管理后台 → Actions → Enable Actions ✅
|
||||
|
||||
# 2. 检查分支保护
|
||||
仓库设置 → 分支保护 → 启用质量门禁 ✅
|
||||
|
||||
# 3. 检查 Runner 状态
|
||||
./manage-runner.sh status
|
||||
```
|
||||
|
||||
### 工作流执行失败
|
||||
|
||||
**可能原因**:
|
||||
- Docker 未运行
|
||||
- 依赖包未安装
|
||||
- 配置文件错误
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 1. 检查 Docker
|
||||
docker info
|
||||
|
||||
# 2. 查看详细日志
|
||||
./manage-runner.sh logs
|
||||
|
||||
# 3. 检查工作流语法
|
||||
cat .gitea/workflows/quality-gate.yml
|
||||
```
|
||||
|
||||
### Runner 连接问题
|
||||
|
||||
**可能原因**:
|
||||
- Gitea 服务未运行
|
||||
- 网络连接问题
|
||||
- Token 过期
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 1. 检查 Gitea 服务
|
||||
curl -s http://localhost:33000/api/v1/version
|
||||
|
||||
# 2. 重启 Runner
|
||||
./manage-runner.sh restart
|
||||
|
||||
# 3. 重新注册 Runner
|
||||
echo -e "http://localhost:33000\ne9udfPmR6PsKbTXNRY61fMiGpmVBXv1PRKhbTfPD\nmacos-local-runner\nmacos,amd64,docker,local" | /Users/brunogao/bin/act_runner register
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 📝 PR 创建规范
|
||||
|
||||
1. **清晰的标题**: 描述变更内容
|
||||
2. **详细的描述**: 说明变更原因和影响
|
||||
3. **关联 Issue**: 引用相关的 Issue 编号
|
||||
4. **测试计划**: 说明如何测试变更
|
||||
|
||||
### 🔄 分支管理
|
||||
|
||||
```bash
|
||||
# 功能开发流程
|
||||
git checkout -b feature/新功能名称
|
||||
# 开发代码...
|
||||
git add .
|
||||
git commit -m "feat: 添加新功能"
|
||||
git push origin feature/新功能名称
|
||||
# 在 Gitea 创建 PR
|
||||
|
||||
# 修复 Bug 流程
|
||||
git checkout -b fix/问题描述
|
||||
# 修复代码...
|
||||
git add .
|
||||
git commit -m "fix: 修复具体问题"
|
||||
git push origin fix/问题描述
|
||||
# 在 Gitea 创建 PR
|
||||
```
|
||||
|
||||
### 📊 质量标准
|
||||
|
||||
- **前端**: TypeScript 0 错误,ESLint ≤ 10 警告,测试覆盖率 ≥ 90%
|
||||
- **后端**: Python 测试覆盖率 ≥ 80%,无高危安全漏洞
|
||||
- **API**: OpenAPI 规范有效,无 Breaking Changes
|
||||
- **文档**: 及时更新相关文档
|
||||
|
||||
---
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
### 🛠️ 管理命令
|
||||
|
||||
```bash
|
||||
# Runner 管理
|
||||
cd /Users/brunogao/work/codes/tools/macos/gitea/runners
|
||||
|
||||
# 查看帮助
|
||||
./manage-runner.sh help
|
||||
|
||||
# 查看状态
|
||||
./manage-runner.sh status
|
||||
|
||||
# 查看日志
|
||||
./manage-runner.sh logs
|
||||
|
||||
# 健康检查
|
||||
./manage-runner.sh health
|
||||
```
|
||||
|
||||
### 📚 相关文档
|
||||
|
||||
- **Gitea Actions 文档**: https://docs.gitea.com/next/usage/actions/overview
|
||||
- **工作流语法**: https://docs.gitea.com/next/usage/actions/workflow-syntax
|
||||
- **Runner 配置**: `/Users/brunogao/work/codes/tools/macos/gitea/GITEA_RUNNER_DEPLOYMENT.md`
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
✅ **工作流配置完成**: 已适配本地 Runner 标签
|
||||
✅ **自动触发机制**: PR 创建时自动执行 CI 检查
|
||||
✅ **质量门禁体系**: 5 维度全面质量检查
|
||||
✅ **本地 Runner**: macOS 优化的高性能执行环境
|
||||
✅ **监控工具**: 完整的管理和诊断工具
|
||||
|
||||
**🚀 现在你可以在 Gitea 中创建 PR,自动触发完整的质量门禁检查!**
|
||||
|
||||
---
|
||||
|
||||
**下一步**: 在 Gitea 中访问 http://192.168.1.83:33000/gao/ai-stock 创建 Pull Request,观察 CI 自动执行!
|
||||
@@ -1,145 +1,37 @@
|
||||
# LJWX-Stock 项目 Makefile
|
||||
# 专业任务管理和Claude协作系统
|
||||
|
||||
.PHONY: help dev-up dev-down task-management task-management-test claude-auto claude-monitor web api bigscreen test lint build clean
|
||||
# 顶层Makefile - 统一命令入口(Claude Code优化版)
|
||||
.PHONY: help dev-up dev-down web api bigscreen test lint build clean claude-auto claude-monitor claude-quality claude-assign dev-claude
|
||||
|
||||
# 默认目标
|
||||
help:
|
||||
@echo "🚀 LJWX-Stock 项目命令汇总"
|
||||
@echo "🚀 LJWX-Stock开发命令汇总 (Claude Code增强版)"
|
||||
@echo ""
|
||||
@echo "任务管理系统:"
|
||||
@echo " task-management 启动任务管理Web服务"
|
||||
@echo " task-management-test 测试任务管理API"
|
||||
@echo "环境管理:"
|
||||
@echo " dev-up 启动开发环境"
|
||||
@echo " dev-down 停止开发环境"
|
||||
@echo ""
|
||||
@echo "Claude协作系统:"
|
||||
@echo " claude-auto 启动Claude自动化任务执行"
|
||||
@echo " claude-monitor 启动Claude任务监控面板"
|
||||
@echo " claude-quality 执行Claude质量检查"
|
||||
@echo " claude-assign 智能分配Claude任务"
|
||||
@echo " dev-claude 智能开发模式(推荐)"
|
||||
@echo "服务启动:"
|
||||
@echo " web 启动前端开发服务器"
|
||||
@echo " api 启动后端API服务"
|
||||
@echo " bigscreen 启动大屏服务"
|
||||
@echo ""
|
||||
@echo "环境管理:"
|
||||
@echo " dev-up 启动开发环境"
|
||||
@echo " dev-down 停止开发环境"
|
||||
@echo "Claude Code智能功能:"
|
||||
@echo " claude-auto 启动Claude自动化任务执行"
|
||||
@echo " claude-monitor 启动Claude任务监控面板"
|
||||
@echo " claude-quality 执行Claude质量检查"
|
||||
@echo " claude-assign 智能分配Claude任务"
|
||||
@echo " dev-claude 智能开发模式(推荐)"
|
||||
@echo ""
|
||||
@echo "服务启动:"
|
||||
@echo " web 启动前端开发服务器"
|
||||
@echo " api 启动后端API服务"
|
||||
@echo " bigscreen 启动大屏服务"
|
||||
@echo "代码质量:"
|
||||
@echo " lint 代码检查和格式化"
|
||||
@echo " test 运行测试"
|
||||
@echo ""
|
||||
@echo "代码质量:"
|
||||
@echo " lint 代码检查和格式化"
|
||||
@echo " test 运行测试"
|
||||
@echo "构建部署:"
|
||||
@echo " build 构建所有服务"
|
||||
@echo " clean 清理临时文件"
|
||||
@echo ""
|
||||
@echo "构建部署:"
|
||||
@echo " build 构建所有服务"
|
||||
@echo " clean 清理临时文件"
|
||||
|
||||
# 任务管理系统
|
||||
task-management:
|
||||
@echo "🎯 启动任务管理Web系统..."
|
||||
@if [ -f "apps/task-management/start.sh" ]; then \
|
||||
./apps/task-management/start.sh; \
|
||||
else \
|
||||
echo "❌ 任务管理系统启动脚本不存在"; \
|
||||
fi
|
||||
|
||||
task-management-test:
|
||||
@echo "🧪 测试任务管理API..."
|
||||
@if [ -f "apps/task-management/test_api.py" ]; then \
|
||||
cd apps/task-management && python3 test_api.py; \
|
||||
else \
|
||||
echo "❌ API测试脚本不存在"; \
|
||||
fi
|
||||
|
||||
task-management-comprehensive:
|
||||
@echo "🔬 综合测试任务管理系统..."
|
||||
@if [ -f "apps/task-management/comprehensive_test.py" ]; then \
|
||||
cd apps/task-management && python3 comprehensive_test.py; \
|
||||
else \
|
||||
echo "❌ 综合测试脚本不存在"; \
|
||||
fi
|
||||
|
||||
task-management-dev:
|
||||
@echo "🔧 开发模式启动任务管理..."
|
||||
@if [ -d "apps/task-management/backend" ]; then \
|
||||
cd apps/task-management/backend && \
|
||||
if [ ! -d "venv" ]; then python3 -m venv venv; fi && \
|
||||
source venv/bin/activate && \
|
||||
pip install -q -r requirements.txt && \
|
||||
python3 main.py; \
|
||||
else \
|
||||
echo "❌ 任务管理后端目录不存在"; \
|
||||
fi
|
||||
|
||||
# Claude协作系统
|
||||
claude-auto:
|
||||
@echo "🤖 启动Claude自动化任务执行..."
|
||||
@if [ -f "claude/task_manager/claude_task_manager.py" ]; then \
|
||||
python3 claude/task_manager/claude_task_manager.py; \
|
||||
else \
|
||||
echo "❌ Claude任务管理器不存在"; \
|
||||
fi
|
||||
|
||||
claude-monitor:
|
||||
@echo "📊 启动Claude任务监控面板..."
|
||||
@if [ -f "claude/task_manager/simple_dashboard.py" ]; then \
|
||||
python3 claude/task_manager/simple_dashboard.py; \
|
||||
else \
|
||||
echo "❌ Claude监控面板不存在"; \
|
||||
fi
|
||||
|
||||
claude-enhanced:
|
||||
@echo "🚀 启动增强版Claude系统..."
|
||||
@if [ -f "claude/task_manager/enhanced_multi_claude_system.py" ]; then \
|
||||
python3 claude/task_manager/enhanced_multi_claude_system.py --mode hybrid --claude-instances 7 --tasks 10; \
|
||||
else \
|
||||
echo "❌ 增强版Claude系统不存在"; \
|
||||
fi
|
||||
|
||||
claude-quality:
|
||||
@echo "🔍 执行Claude质量检查..."
|
||||
@if [ -f "claude/automation/quality_checker.py" ]; then \
|
||||
python3 claude/automation/quality_checker.py; \
|
||||
else \
|
||||
echo "❌ Claude质量检查器不存在"; \
|
||||
fi
|
||||
|
||||
claude-assign:
|
||||
@echo "🎯 智能分配Claude任务..."
|
||||
@if [ -f "claude/automation/task_executor.py" ]; then \
|
||||
python3 claude/automation/task_executor.py --mode=assign; \
|
||||
else \
|
||||
echo "❌ Claude任务执行器不存在"; \
|
||||
fi
|
||||
|
||||
# 智能开发模式
|
||||
dev-claude:
|
||||
@echo "🚀 启动Claude智能开发模式..."
|
||||
@echo "这将依次启动:开发环境 + Claude任务管理 + Claude监控面板"
|
||||
@read -p "是否继续?(y/N): " confirm && [ "$$confirm" = "y" ] || exit 1
|
||||
@make dev-up
|
||||
@echo "等待5秒后启动Claude服务..."
|
||||
@sleep 5
|
||||
@make claude-auto &
|
||||
@sleep 2
|
||||
@make claude-monitor
|
||||
|
||||
# 完整开发环境
|
||||
full-dev:
|
||||
@echo "🌟 启动完整开发环境..."
|
||||
@echo "启动服务:任务管理 + Claude协作 + 监控面板"
|
||||
@read -p "是否继续?(y/N): " confirm && [ "$$confirm" = "y" ] || exit 1
|
||||
@make task-management &
|
||||
@sleep 3
|
||||
@make claude-enhanced &
|
||||
@sleep 2
|
||||
@make claude-monitor &
|
||||
@echo "✅ 所有服务已启动"
|
||||
@echo "访问地址:"
|
||||
@echo " - 任务管理: http://localhost:8000/docs"
|
||||
@echo " - Claude监控: 运行中"
|
||||
@wait
|
||||
@echo "AI训练:"
|
||||
@echo " ai-train 训练AI模型"
|
||||
@echo " ai-eval 评估模型性能"
|
||||
|
||||
# 环境管理
|
||||
dev-up:
|
||||
@@ -183,6 +75,51 @@ bigscreen:
|
||||
echo "⚠️ 大屏应用目录不存在,请先初始化 apps/bigscreen"; \
|
||||
fi
|
||||
|
||||
# Claude Code智能功能
|
||||
claude-auto:
|
||||
@echo "🤖 启动Claude自动化任务执行..."
|
||||
@if [ -f "claude/task_manager/claude_task_manager.py" ]; then \
|
||||
python claude/task_manager/claude_task_manager.py; \
|
||||
else \
|
||||
echo "⚠️ Claude任务管理器不存在,请先初始化 claude/task_manager/"; \
|
||||
fi
|
||||
|
||||
claude-monitor:
|
||||
@echo "📊 启动Claude任务监控面板..."
|
||||
@if [ -f "claude/task_manager/monitor_dashboard.py" ]; then \
|
||||
python claude/task_manager/monitor_dashboard.py; \
|
||||
else \
|
||||
echo "⚠️ Claude监控面板不存在,请先初始化 claude/task_manager/"; \
|
||||
fi
|
||||
|
||||
claude-quality:
|
||||
@echo "🔍 执行Claude质量检查..."
|
||||
@if [ -f "claude/automation/quality_checker.py" ]; then \
|
||||
python claude/automation/quality_checker.py; \
|
||||
else \
|
||||
echo "⚠️ Claude质量检查器不存在,请先初始化 claude/automation/"; \
|
||||
fi
|
||||
|
||||
claude-assign:
|
||||
@echo "🎯 智能分配Claude任务..."
|
||||
@if [ -f "claude/automation/task_executor.py" ]; then \
|
||||
python claude/automation/task_executor.py --mode=assign; \
|
||||
else \
|
||||
echo "⚠️ Claude任务执行器不存在,请先初始化 claude/automation/"; \
|
||||
fi
|
||||
|
||||
# 智能开发模式
|
||||
dev-claude:
|
||||
@echo "🚀 启动Claude智能开发模式..."
|
||||
@echo "这将依次启动:开发环境 + Claude任务管理 + Claude监控面板"
|
||||
@read -p "是否继续?(y/N): " confirm && [ "$$confirm" = "y" ] || exit 1
|
||||
@make dev-up
|
||||
@echo "等待5秒后启动Claude服务..."
|
||||
@sleep 5
|
||||
@make claude-auto &
|
||||
@sleep 2
|
||||
@make claude-monitor
|
||||
|
||||
# 代码质量
|
||||
lint:
|
||||
@echo "🔍 代码检查和格式化..."
|
||||
@@ -192,11 +129,6 @@ lint:
|
||||
@if [ -d "apps/api" ]; then \
|
||||
echo "检查后端代码..." && cd apps/api && uv run ruff check . && uv run ruff format .; \
|
||||
fi
|
||||
@if [ -d "apps/task-management/backend" ]; then \
|
||||
echo "检查任务管理后端代码..." && cd apps/task-management/backend && \
|
||||
if [ -d "venv" ]; then source venv/bin/activate; fi && \
|
||||
ruff check . --fix && ruff format .; \
|
||||
fi
|
||||
|
||||
test:
|
||||
@echo "🧪 运行测试..."
|
||||
@@ -206,11 +138,6 @@ test:
|
||||
@if [ -d "apps/web" ]; then \
|
||||
echo "运行前端测试..." && cd apps/web && npm run test; \
|
||||
fi
|
||||
@if [ -d "apps/task-management/backend" ]; then \
|
||||
echo "运行任务管理测试..." && cd apps/task-management/backend && \
|
||||
if [ -d "venv" ]; then source venv/bin/activate; fi && \
|
||||
python -m pytest tests/ -v || echo "测试目录不存在,跳过"; \
|
||||
fi
|
||||
|
||||
# 构建部署
|
||||
build:
|
||||
@@ -233,69 +160,64 @@ clean:
|
||||
@if [ -d "apps/web" ]; then \
|
||||
cd apps/web && rm -rf dist 2>/dev/null || true; \
|
||||
fi
|
||||
@if [ -d "apps/task-management/backend" ]; then \
|
||||
cd apps/task-management/backend && rm -rf logs 2>/dev/null || true; \
|
||||
|
||||
# AI训练
|
||||
ai-train:
|
||||
@echo "🤖 训练AI模型..."
|
||||
@if [ -f "ai/pipelines/Makefile" ]; then \
|
||||
cd ai/pipelines && make train-xgb; \
|
||||
else \
|
||||
echo "⚠️ AI训练流水线不存在,请先初始化 ai/pipelines/"; \
|
||||
fi
|
||||
|
||||
# 快速启动命令
|
||||
dev: full-dev
|
||||
@echo "✅ 完整开发环境已启动"
|
||||
ai-eval:
|
||||
@echo "📊 评估模型性能..."
|
||||
@if [ -f "ai/pipelines/Makefile" ]; then \
|
||||
cd ai/pipelines && make eval; \
|
||||
else \
|
||||
echo "⚠️ AI评估流水线不存在,请先初始化 ai/pipelines/"; \
|
||||
fi
|
||||
|
||||
# 项目状态
|
||||
status:
|
||||
@echo "📊 项目状态检查..."
|
||||
@echo ""
|
||||
@echo "📁 项目结构:"
|
||||
@find . -maxdepth 2 -type d -name "apps" -o -name "claude" -o -name "docs" -o -name "infra" | sort
|
||||
@echo ""
|
||||
@echo "🔧 运行中的服务:"
|
||||
@ps aux | grep -E "(python.*claude|uvicorn|flask)" | grep -v grep | wc -l | xargs -I {} echo " - {} 个服务正在运行"
|
||||
@echo ""
|
||||
@echo "📝 可用命令:"
|
||||
@echo " make task-management - 启动任务管理Web系统"
|
||||
@echo " make claude-enhanced - 启动增强版Claude系统"
|
||||
@echo " make full-dev - 启动完整开发环境"
|
||||
@echo " make status - 查看项目状态"
|
||||
# 数据库操作
|
||||
db-migrate:
|
||||
@echo "🗄️ 运行数据库迁移..."
|
||||
@if [ -d "apps/api" ]; then \
|
||||
cd apps/api && uv run alembic upgrade head; \
|
||||
else \
|
||||
echo "⚠️ API应用不存在,无法执行数据库迁移"; \
|
||||
fi
|
||||
|
||||
db-seed:
|
||||
@echo "🌱 初始化基础数据..."
|
||||
@if [ -f "infra/db/seed/seed.sh" ]; then \
|
||||
bash infra/db/seed/seed.sh; \
|
||||
else \
|
||||
echo "⚠️ 数据初始化脚本不存在,请先创建 infra/db/seed/seed.sh"; \
|
||||
fi
|
||||
|
||||
# 快速启动命令(开发时常用)
|
||||
dev: dev-up
|
||||
@echo "✅ 开发环境已启动"
|
||||
@echo "在新的终端中运行:"
|
||||
@echo " make api # 启动后端"
|
||||
@echo " make web # 启动前端"
|
||||
@echo " make claude-auto # 启动Claude自动化"
|
||||
|
||||
# Claude报告生成
|
||||
claude-report:
|
||||
@echo "📋 生成Claude执行报告..."
|
||||
@if [ -f "claude_execution_report.json" ]; then \
|
||||
echo "Claude执行报告已存在:claude_execution_report.json"; \
|
||||
python3 -m json.tool claude_execution_report.json | head -30; \
|
||||
elif [ -f "enhanced_claude_execution_report.json" ]; then \
|
||||
echo "增强版Claude执行报告已存在:enhanced_claude_execution_report.json"; \
|
||||
python3 -m json.tool enhanced_claude_execution_report.json | head -30; \
|
||||
cat claude_execution_report.json | python -m json.tool | head -50; \
|
||||
else \
|
||||
echo "⚠️ Claude执行报告不存在,请先运行 make claude-auto 或 make claude-enhanced"; \
|
||||
echo "⚠️ Claude执行报告不存在,请先运行 make claude-auto"; \
|
||||
fi
|
||||
|
||||
# 安装项目依赖
|
||||
setup:
|
||||
@echo "📦 安装项目依赖..."
|
||||
@if [ -f "apps/task-management/backend/requirements.txt" ]; then \
|
||||
echo "安装任务管理后端依赖..." && \
|
||||
cd apps/task-management/backend && \
|
||||
if [ ! -d "venv" ]; then python3 -m venv venv; fi && \
|
||||
source venv/bin/activate && \
|
||||
pip install -r requirements.txt; \
|
||||
fi
|
||||
@echo "✅ 依赖安装完成"
|
||||
# 生产环境部署
|
||||
deploy-staging:
|
||||
@echo "🚀 部署到预发布环境..."
|
||||
@echo "⚠️ 生产部署功能待实现"
|
||||
|
||||
# 初始化项目
|
||||
init:
|
||||
@echo "🎯 初始化LJWX-Stock项目..."
|
||||
@make setup
|
||||
@echo ""
|
||||
@echo "📚 快速开始:"
|
||||
@echo " 1. make task-management # 启动任务管理系统"
|
||||
@echo " 2. make claude-enhanced # 启动Claude协作系统"
|
||||
@echo " 3. make full-dev # 启动完整开发环境"
|
||||
@echo ""
|
||||
@echo "📖 文档:"
|
||||
@echo " - apps/task-management/README.md"
|
||||
@echo " - docs/architecture/"
|
||||
@echo ""
|
||||
@echo "🌐 访问地址:"
|
||||
@echo " - 任务管理API: http://localhost:8000/docs"
|
||||
@echo " - Claude监控: 启动后查看终端输出"
|
||||
deploy-prod:
|
||||
@echo "🚀 部署到生产环境..."
|
||||
@echo "⚠️ 生产部署功能待实现"
|
||||
@@ -0,0 +1,28 @@
|
||||
# 测试 Gitea Actions 工作流触发
|
||||
|
||||
这个 PR 用于验证 Gitea Actions 工作流是否能正确触发和执行。
|
||||
|
||||
## 测试内容
|
||||
|
||||
- [ ] 前端质量检查 (TypeScript + ESLint + 测试)
|
||||
- [ ] 后端质量检查 (Python 测试 + 安全检查)
|
||||
- [ ] API 契约检查
|
||||
- [ ] 数据质量检查
|
||||
- [ ] 提示词安全检查
|
||||
- [ ] 最终质量门禁
|
||||
|
||||
## 预期结果
|
||||
|
||||
所有检查项都应该通过,因为我们的代码符合质量标准。
|
||||
|
||||
|
||||
|
||||
|
||||
## Actions 启用后测试更新
|
||||
|
||||
更新时间: Tue Oct 28 15:44:19 CST 2025
|
||||
- Actions 功能已启用: ✅ confirmed
|
||||
- 准备触发工作流执行
|
||||
- 测试质量门禁检查
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# 应用配置
|
||||
APP_NAME=LJWX-Stock AI执行平台
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=true
|
||||
ENVIRONMENT=development
|
||||
|
||||
# 服务器配置
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# 数据库配置
|
||||
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/ai_platform
|
||||
DATABASE_POOL_SIZE=20
|
||||
DATABASE_MAX_OVERFLOW=0
|
||||
|
||||
# Redis配置
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
REDIS_MAX_CONNECTIONS=20
|
||||
|
||||
# JWT配置
|
||||
SECRET_KEY=your-secret-key-change-in-production-please
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||
|
||||
# Claude API配置
|
||||
CLAUDE_API_KEY=your-claude-api-key
|
||||
CLAUDE_API_BASE_URL=https://api.anthropic.com
|
||||
CLAUDE_MODEL=claude-3-sonnet-20240229
|
||||
CLAUDE_MAX_TOKENS=4000
|
||||
CLAUDE_TEMPERATURE=0.7
|
||||
|
||||
# MinIO配置
|
||||
MINIO_ENDPOINT=localhost:9000
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_SECURE=false
|
||||
MINIO_BUCKET_NAME=ai-platform
|
||||
|
||||
# Gitea配置
|
||||
GITEA_BASE_URL=http://localhost:3000
|
||||
GITEA_TOKEN=your-gitea-token
|
||||
GITEA_WEBHOOK_SECRET=your-webhook-secret
|
||||
|
||||
# 邮件配置
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=[email protected]
|
||||
SMTP_PASSWORD=your-email-password
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FORMAT=json
|
||||
|
||||
# 安全配置
|
||||
CORS_ORIGINS=*
|
||||
CORS_ALLOW_CREDENTIALS=true
|
||||
CORS_ALLOW_METHODS=*
|
||||
CORS_ALLOW_HEADERS=*
|
||||
|
||||
# 限流配置
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_WINDOW=60
|
||||
|
||||
# 监控配置
|
||||
ENABLE_METRICS=true
|
||||
METRICS_PATH=/metrics
|
||||
|
||||
# 文件上传配置
|
||||
MAX_FILE_SIZE=10485760
|
||||
ALLOWED_FILE_TYPES=.txt,.md,.json,.yaml,.yml,.py,.js,.ts,.vue,.html,.css
|
||||
@@ -1,39 +0,0 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
libpq-dev \
|
||||
curl \
|
||||
vim \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建非root用户
|
||||
RUN useradd --create-home --shell /bin/bash app \
|
||||
&& chown -R app:app /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER app
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 开发模式启动命令
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload", "--log-level", "info"]
|
||||
@@ -1,60 +0,0 @@
|
||||
# 多阶段构建 - 生产环境
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装构建依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
libpq-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖到临时目录
|
||||
RUN pip install --user --no-cache-dir -r requirements.txt
|
||||
|
||||
# 生产阶段
|
||||
FROM python:3.11-slim as production
|
||||
|
||||
# 安装运行时依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq5 \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
# 创建应用用户
|
||||
RUN useradd --create-home --shell /bin/bash app
|
||||
|
||||
# 从构建阶段复制依赖
|
||||
COPY --from=builder /root/.local /home/app/.local
|
||||
|
||||
# 设置PATH
|
||||
ENV PATH=/home/app/.local/bin:$PATH
|
||||
|
||||
# 复制应用代码
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# 更改文件所有者
|
||||
RUN chown -R app:app /app
|
||||
|
||||
# 切换到应用用户
|
||||
USER app
|
||||
|
||||
# 创建非root用户可写的目录
|
||||
RUN mkdir -p /app/logs /app/temp && \
|
||||
chmod 755 /app/logs /app/temp
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
||||
CMD curl -f http://localhost:8000/health || exit 1
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 生产模式启动命令
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4", "--access-log", "--log-level", "info"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,700 +0,0 @@
|
||||
"""
|
||||
LJWX-Stock AI执行平台 - 实际可用的管理平台
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import sqlite3
|
||||
import asyncio
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="LJWX-Stock AI执行平台",
|
||||
version="1.0.0",
|
||||
description="""
|
||||
## 核心功能
|
||||
- 🤖 **AI任务执行**: 基于Prompt契约的智能化任务执行
|
||||
- 👥 **人工审核**: 专业的工作流程和质量管理
|
||||
- 📝 **需求管理**: 从原始需求到任务分解的完整流程
|
||||
- 🔧 **任务管理**: 智能任务分配和状态跟踪
|
||||
- 📊 **实时监控**: 全方位的执行状态可视化
|
||||
- 🔄 **结果回溯**: 完整的版本控制和回滚机制
|
||||
- 🚪 **质量门禁**: 多层次的质量检查和评分机制
|
||||
"""
|
||||
)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 数据库初始化
|
||||
def init_database():
|
||||
"""初始化SQLite数据库"""
|
||||
conn = sqlite3.connect('ai_platform.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建用户表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_superuser BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建需求表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS requirements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 3,
|
||||
status VARCHAR(20) DEFAULT 'draft',
|
||||
creator_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (creator_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建任务表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
requirement_id INTEGER,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
assignee_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (requirement_id) REFERENCES requirements (id),
|
||||
FOREIGN KEY (assignee_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建AI执行记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS ai_executions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER,
|
||||
agent VARCHAR(50) DEFAULT 'claude',
|
||||
prompt TEXT NOT NULL,
|
||||
result TEXT,
|
||||
status VARCHAR(20) DEFAULT 'running',
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建审核记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS reviews (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
execution_id INTEGER,
|
||||
reviewer_id INTEGER,
|
||||
decision VARCHAR(20) NOT NULL,
|
||||
comments TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (execution_id) REFERENCES ai_executions (id),
|
||||
FOREIGN KEY (reviewer_id) REFERENCES users (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 插入默认管理员用户
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO users (username, email, full_name, is_superuser)
|
||||
VALUES ('admin', '[email protected]', '系统管理员', TRUE)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Pydantic模型
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
full_name: Optional[str] = None
|
||||
is_active: bool = True
|
||||
is_superuser: bool = False
|
||||
|
||||
class Requirement(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
priority: int
|
||||
status: str
|
||||
creator_id: int
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
|
||||
class RequirementCreate(BaseModel):
|
||||
title: str
|
||||
description: str
|
||||
priority: int = 3
|
||||
|
||||
class Task(BaseModel):
|
||||
id: int
|
||||
requirement_id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
status: str
|
||||
assignee_id: Optional[int] = None
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
|
||||
class TaskCreate(BaseModel):
|
||||
requirement_id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class AIExecution(BaseModel):
|
||||
id: int
|
||||
task_id: int
|
||||
agent: str
|
||||
prompt: str
|
||||
result: Optional[str] = None
|
||||
status: str
|
||||
error_message: Optional[str] = None
|
||||
created_at: datetime.datetime
|
||||
completed_at: Optional[datetime.datetime] = None
|
||||
|
||||
class AIExecutionCreate(BaseModel):
|
||||
task_id: int
|
||||
prompt: str
|
||||
|
||||
class Review(BaseModel):
|
||||
id: int
|
||||
execution_id: int
|
||||
reviewer_id: int
|
||||
decision: str
|
||||
comments: Optional[str] = None
|
||||
created_at: datetime.datetime
|
||||
|
||||
class ReviewCreate(BaseModel):
|
||||
execution_id: int
|
||||
decision: str
|
||||
comments: Optional[str] = None
|
||||
|
||||
# 数据库操作函数
|
||||
def get_db_connection():
|
||||
"""获取数据库连接"""
|
||||
return sqlite3.connect('ai_platform.db')
|
||||
|
||||
# API路由
|
||||
@app.get("/", tags=["系统"])
|
||||
async def root():
|
||||
"""根路径 - 系统信息"""
|
||||
return {
|
||||
"name": "LJWX-Stock AI执行平台",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"message": "欢迎使用LJWX-Stock AI执行平台!",
|
||||
"features": {
|
||||
"用户管理": "✅ 已实现",
|
||||
"需求管理": "✅ 已实现",
|
||||
"任务管理": "✅ 已实现",
|
||||
"AI执行": "✅ 已实现",
|
||||
"审核工作流": "✅ 已实现",
|
||||
"质量门禁": "✅ 已实现"
|
||||
}
|
||||
}
|
||||
|
||||
@app.get("/health", tags=["系统"])
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
"version": "1.0.0",
|
||||
"database": "connected",
|
||||
"services": {
|
||||
"api": "✅ 运行中",
|
||||
"database": "✅ 已连接",
|
||||
"ai_execution": "✅ 可用"
|
||||
}
|
||||
}
|
||||
|
||||
# 用户管理
|
||||
@app.get("/api/v1/users/", response_model=List[User], tags=["用户管理"])
|
||||
async def list_users():
|
||||
"""获取用户列表"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE is_active = TRUE")
|
||||
users = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
User(
|
||||
id=row[0],
|
||||
username=row[1],
|
||||
email=row[2],
|
||||
full_name=row[3],
|
||||
is_active=bool(row[4]),
|
||||
is_superuser=bool(row[5])
|
||||
)
|
||||
for row in users
|
||||
]
|
||||
|
||||
# 需求管理
|
||||
@app.get("/api/v1/requirements/", response_model=List[Requirement], tags=["需求管理"])
|
||||
async def list_requirements():
|
||||
"""获取需求列表"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT r.*, u.username as creator_username
|
||||
FROM requirements r
|
||||
LEFT JOIN users u ON r.creator_id = u.id
|
||||
ORDER BY r.created_at DESC
|
||||
""")
|
||||
requirements = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
Requirement(
|
||||
id=row[0],
|
||||
title=row[1],
|
||||
description=row[2],
|
||||
priority=row[3],
|
||||
status=row[4],
|
||||
creator_id=row[5],
|
||||
created_at=datetime.datetime.fromisoformat(row[7]),
|
||||
updated_at=datetime.datetime.fromisoformat(row[8])
|
||||
)
|
||||
for row in requirements
|
||||
]
|
||||
|
||||
@app.post("/api/v1/requirements/", response_model=Requirement, tags=["需求管理"])
|
||||
async def create_requirement(requirement: RequirementCreate):
|
||||
"""创建新需求"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO requirements (title, description, priority, creator_id)
|
||||
VALUES (?, ?, ?, 1)
|
||||
''', (requirement.title, requirement.description, requirement.priority))
|
||||
requirement_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
# 获取创建的需求
|
||||
cursor.execute("SELECT * FROM requirements WHERE id = ?", (requirement_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return Requirement(
|
||||
id=row[0],
|
||||
title=row[1],
|
||||
description=row[2],
|
||||
priority=row[3],
|
||||
status=row[4],
|
||||
creator_id=row[5],
|
||||
created_at=datetime.datetime.fromisoformat(row[7]),
|
||||
updated_at=datetime.datetime.fromisoformat(row[8])
|
||||
)
|
||||
|
||||
# 任务管理
|
||||
@app.get("/api/v1/tasks/", response_model=List[Task], tags=["任务管理"])
|
||||
async def list_tasks():
|
||||
"""获取任务列表"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT t.*, r.title as requirement_title
|
||||
FROM tasks t
|
||||
LEFT JOIN requirements r ON t.requirement_id = r.id
|
||||
ORDER BY t.created_at DESC
|
||||
""")
|
||||
tasks = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
Task(
|
||||
id=row[0],
|
||||
requirement_id=row[1],
|
||||
title=row[2],
|
||||
description=row[3],
|
||||
status=row[4],
|
||||
assignee_id=row[5],
|
||||
created_at=datetime.datetime.fromisoformat(row[7]),
|
||||
updated_at=datetime.datetime.fromisoformat(row[8])
|
||||
)
|
||||
for row in tasks
|
||||
]
|
||||
|
||||
@app.post("/api/v1/tasks/", response_model=Task, tags=["任务管理"])
|
||||
async def create_task(task: TaskCreate):
|
||||
"""创建新任务"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO tasks (requirement_id, title, description)
|
||||
VALUES (?, ?, ?)
|
||||
''', (task.requirement_id, task.title, task.description))
|
||||
task_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
# 获取创建的任务
|
||||
cursor.execute("SELECT * FROM tasks WHERE id = ?", (task_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return Task(
|
||||
id=row[0],
|
||||
requirement_id=row[1],
|
||||
title=row[2],
|
||||
description=row[3],
|
||||
status=row[4],
|
||||
assignee_id=row[5],
|
||||
created_at=datetime.datetime.fromisoformat(row[7]),
|
||||
updated_at=datetime.datetime.fromisoformat(row[8])
|
||||
)
|
||||
|
||||
# AI执行
|
||||
@app.get("/api/v1/executions/", response_model=List[AIExecution], tags=["AI执行"])
|
||||
async def list_executions():
|
||||
"""获取AI执行记录"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT e.*, t.title as task_title
|
||||
FROM ai_executions e
|
||||
LEFT JOIN tasks t ON e.task_id = t.id
|
||||
ORDER BY e.created_at DESC
|
||||
''')
|
||||
executions = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
AIExecution(
|
||||
id=row[0],
|
||||
task_id=row[1],
|
||||
agent=row[2],
|
||||
prompt=row[3],
|
||||
result=row[4],
|
||||
status=row[5],
|
||||
error_message=row[6],
|
||||
created_at=datetime.datetime.fromisoformat(row[8]),
|
||||
completed_at=datetime.datetime.fromisoformat(row[9]) if row[9] else None
|
||||
)
|
||||
for row in executions
|
||||
]
|
||||
|
||||
@app.post("/api/v1/executions/", response_model=AIExecution, tags=["AI执行"])
|
||||
async def create_execution(execution: AIExecutionCreate):
|
||||
"""创建AI执行"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 更新任务状态为进行中
|
||||
cursor.execute("UPDATE tasks SET status = 'in_progress' WHERE id = ?", (execution.task_id,))
|
||||
|
||||
# 创建执行记录
|
||||
cursor.execute('''
|
||||
INSERT INTO ai_executions (task_id, prompt, status)
|
||||
VALUES (?, ?, 'running')
|
||||
''', (execution.task_id, execution.prompt))
|
||||
execution_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
# 模拟AI执行(实际应用中这里会调用Claude API)
|
||||
await asyncio.sleep(1) # 模拟处理时间
|
||||
|
||||
# 模拟AI结果
|
||||
ai_result = f"""基于任务需求,我已完成以下工作:
|
||||
|
||||
## 代码实现
|
||||
def task_{execution.task_id}():
|
||||
'''{execution.prompt}'''
|
||||
# 实现代码...
|
||||
return "执行完成"
|
||||
|
||||
## 测试用例
|
||||
def test_task_{execution.task_id}():
|
||||
assert task_{execution.task_id}() is not None
|
||||
print("测试通过")
|
||||
|
||||
## 文档
|
||||
- 实现了{execution.prompt}的完整功能
|
||||
- 添加了单元测试
|
||||
- 编写了详细的技术文档
|
||||
|
||||
## 质量检查
|
||||
- 代码规范检查通过
|
||||
- 单元测试通过
|
||||
- 性能测试通过
|
||||
- 安全检查通过
|
||||
"""
|
||||
|
||||
# 更新执行结果
|
||||
cursor.execute('''
|
||||
UPDATE ai_executions
|
||||
SET result = ?, status = 'completed', completed_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (ai_result, execution_id))
|
||||
|
||||
# 更新任务状态为已完成
|
||||
cursor.execute("UPDATE tasks SET status = 'completed' WHERE id = ?", (execution.task_id,))
|
||||
conn.commit()
|
||||
|
||||
# 获取创建的执行记录
|
||||
cursor.execute("SELECT * FROM ai_executions WHERE id = ?", (execution_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return AIExecution(
|
||||
id=row[0],
|
||||
task_id=row[1],
|
||||
agent=row[2],
|
||||
prompt=row[3],
|
||||
result=row[4],
|
||||
status=row[5],
|
||||
error_message=row[6],
|
||||
created_at=datetime.datetime.fromisoformat(row[8]),
|
||||
completed_at=datetime.datetime.fromisoformat(row[9])
|
||||
)
|
||||
|
||||
# 审核管理
|
||||
@app.get("/api/v1/reviews/", response_model=List[Review], tags=["审核管理"])
|
||||
async def list_reviews():
|
||||
"""获取审核记录"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT r.*, u.username as reviewer_username, e.result as execution_result
|
||||
FROM reviews r
|
||||
LEFT JOIN users u ON r.reviewer_id = u.id
|
||||
LEFT JOIN ai_executions e ON r.execution_id = e.id
|
||||
ORDER BY r.created_at DESC
|
||||
''')
|
||||
reviews = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
Review(
|
||||
id=row[0],
|
||||
execution_id=row[1],
|
||||
reviewer_id=row[2],
|
||||
decision=row[3],
|
||||
comments=row[4],
|
||||
created_at=datetime.datetime.fromisoformat(row[5])
|
||||
)
|
||||
for row in reviews
|
||||
]
|
||||
|
||||
@app.post("/api/v1/reviews/", response_model=Review, tags=["审核管理"])
|
||||
async def create_review(review: ReviewCreate):
|
||||
"""创建审核记录"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO reviews (execution_id, reviewer_id, decision, comments)
|
||||
VALUES (?, 1, ?, ?)
|
||||
''', (review.execution_id, review.decision, review.comments))
|
||||
review_id = cursor.lastrowid
|
||||
|
||||
# 如果审核通过,更新任务状态
|
||||
if review.decision == 'approved':
|
||||
cursor.execute('''
|
||||
UPDATE tasks SET status = 'approved'
|
||||
WHERE id = (
|
||||
SELECT task_id FROM ai_executions
|
||||
WHERE id = ?
|
||||
)
|
||||
''', (review.execution_id,))
|
||||
elif review.decision == 'rejected':
|
||||
cursor.execute('''
|
||||
UPDATE tasks SET status = 'rejected'
|
||||
WHERE id = (
|
||||
SELECT task_id FROM ai_executions
|
||||
WHERE id = ?
|
||||
)
|
||||
''', (review.execution_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# 获取创建的审核记录
|
||||
cursor.execute("SELECT * FROM reviews WHERE id = ?", (review_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return Review(
|
||||
id=row[0],
|
||||
execution_id=row[1],
|
||||
reviewer_id=row[2],
|
||||
decision=row[3],
|
||||
comments=row[4],
|
||||
created_at=datetime.datetime.fromisoformat(row[5])
|
||||
)
|
||||
|
||||
# 统计信息
|
||||
@app.get("/api/v1/stats", tags=["统计"])
|
||||
async def get_stats():
|
||||
"""获取平台统计信息"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取各种统计
|
||||
cursor.execute("SELECT COUNT(*) FROM requirements")
|
||||
total_requirements = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks")
|
||||
total_tasks = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks WHERE status = 'completed'")
|
||||
completed_tasks = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM ai_executions")
|
||||
total_executions = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM ai_executions WHERE status = 'completed'")
|
||||
completed_executions = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM reviews")
|
||||
total_reviews = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM reviews WHERE decision = 'approved'")
|
||||
approved_reviews = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
||||
success_rate = (completed_executions / total_executions * 100) if total_executions > 0 else 0
|
||||
approval_rate = (approved_reviews / total_reviews * 100) if total_reviews > 0 else 0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"requirements": {
|
||||
"total": total_requirements,
|
||||
"growth": "+12.5%"
|
||||
},
|
||||
"tasks": {
|
||||
"total": total_tasks,
|
||||
"completed": completed_tasks,
|
||||
"in_progress": total_tasks - completed_tasks,
|
||||
"completion_rate": round(completion_rate, 2)
|
||||
},
|
||||
"ai_executions": {
|
||||
"total": total_executions,
|
||||
"completed": completed_executions,
|
||||
"success_rate": round(success_rate, 2),
|
||||
"avg_time": "2.3分钟"
|
||||
},
|
||||
"reviews": {
|
||||
"total": total_reviews,
|
||||
"approved": approved_reviews,
|
||||
"approval_rate": round(approval_rate, 2)
|
||||
},
|
||||
"system_health": {
|
||||
"status": "excellent",
|
||||
"uptime": "99.9%",
|
||||
"response_time": "120ms"
|
||||
}
|
||||
},
|
||||
"timestamp": datetime.datetime.now().isoformat()
|
||||
}
|
||||
|
||||
# Claude实例API
|
||||
@app.get("/api/v1/claude/instances", tags=["Claude管理"])
|
||||
async def list_claude_instances():
|
||||
"""获取Claude实例列表"""
|
||||
instances = [
|
||||
{
|
||||
"id": "claude_architect_001",
|
||||
"name": "系统架构师",
|
||||
"specialization": "系统设计、架构规划、技术选型",
|
||||
"status": "online",
|
||||
"utilization": 0.85,
|
||||
"tasks_completed": 5,
|
||||
"avg_quality_score": 9.1,
|
||||
"capabilities": ["需求分析", "系统设计", "技术方案", "架构评审"]
|
||||
},
|
||||
{
|
||||
"id": "claude_backend_001",
|
||||
"name": "后端开发工程师",
|
||||
"specialization": "API开发、数据库设计、微服务架构",
|
||||
"status": "online",
|
||||
"utilization": 0.70,
|
||||
"tasks_completed": 3,
|
||||
"avg_quality_score": 8.9,
|
||||
"capabilities": ["API开发", "数据库设计", "服务集成", "代码优化"]
|
||||
},
|
||||
{
|
||||
"id": "claude_frontend_001",
|
||||
"name": "前端开发工程师",
|
||||
"specialization": "UI/UX设计、前端框架、交互开发",
|
||||
"status": "idle",
|
||||
"utilization": 0.0,
|
||||
"tasks_completed": 0,
|
||||
"avg_quality_score": 0.0,
|
||||
"capabilities": ["页面开发", "组件设计", "交互实现", "性能优化"]
|
||||
},
|
||||
{
|
||||
"id": "claude_devops_001",
|
||||
"name": "DevOps工程师",
|
||||
"specialization": "CI/CD、容器化、自动化部署、运维监控",
|
||||
"status": "online",
|
||||
"utilization": 0.60,
|
||||
"tasks_completed": 2,
|
||||
"avg_quality_score": 9.3,
|
||||
"capabilities": ["Docker化", "CI/CD流水线", "自动化部署", "监控告警"]
|
||||
}
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": instances,
|
||||
"total": len(instances),
|
||||
"active": len([i for i in instances if i["status"] == "online"])
|
||||
}
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时执行
|
||||
print("🚀 LJWX-Stock AI执行平台启动中...")
|
||||
print("📊 正在初始化数据库...")
|
||||
init_database()
|
||||
print("✅ 数据库初始化完成")
|
||||
print("🎉 系统启动完成,开始提供服务")
|
||||
yield
|
||||
# 关闭时执行
|
||||
print("🛑 系统正在关闭...")
|
||||
|
||||
app.router.lifespan = lifespan
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("📍 API服务地址: http://localhost:8000")
|
||||
print("📖 API文档地址: http://localhost:8000/docs")
|
||||
print("🏥 健康检查地址: http://localhost:8000/health")
|
||||
print("\n✅ LJWX-Stock AI执行平台启动成功!")
|
||||
|
||||
uvicorn.run(
|
||||
"ai_platform_app:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=False,
|
||||
log_level="info"
|
||||
)
|
||||
@@ -1,113 +0,0 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version number format. This value should be passed
|
||||
# directly to the "alembic revision" command via the
|
||||
# --version-path or --rev-id options
|
||||
# version_num_format = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses
|
||||
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os
|
||||
|
||||
# set to 'true' to search source files recursively
|
||||
# in each "version_locations" directory
|
||||
# new in Alembic version 1.10
|
||||
# recursive_version_locations = false
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/ai_platform
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -1,3 +0,0 @@
|
||||
"""
|
||||
LJWX-Stock AI执行平台后端应用
|
||||
"""
|
||||
@@ -1,45 +0,0 @@
|
||||
"""
|
||||
简化的应用配置
|
||||
"""
|
||||
import os
|
||||
|
||||
|
||||
class SimpleSettings:
|
||||
"""简化的配置类"""
|
||||
|
||||
# 数据库配置
|
||||
database_url = os.getenv("DATABASE_URL", "postgresql+asyncpg://postgres:postgres@postgres:5432/ai_platform")
|
||||
|
||||
# Redis配置
|
||||
redis_url = os.getenv("REDIS_URL", "redis://:redis123@redis:6379/0")
|
||||
|
||||
# JWT配置
|
||||
secret_key = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
||||
algorithm = "HS256"
|
||||
access_token_expire_minutes = 30
|
||||
|
||||
# 应用配置
|
||||
app_name = "LJWX-Stock AI执行平台"
|
||||
app_version = "1.0.0"
|
||||
debug = True
|
||||
|
||||
# Claude API配置
|
||||
claude_api_key = os.getenv("CLAUDE_API_KEY")
|
||||
claude_model = "claude-3-sonnet-20240229"
|
||||
|
||||
# MinIO配置
|
||||
minio_endpoint = os.getenv("MINIO_ENDPOINT", "minio:9000")
|
||||
minio_access_key = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
|
||||
minio_secret_key = os.getenv("MINIO_SECRET_KEY", "minioadmin123")
|
||||
minio_secure = False
|
||||
|
||||
# Gitea配置
|
||||
gitea_base_url = os.getenv("GITEA_BASE_URL", "http://localhost:3000")
|
||||
gitea_token = os.getenv("GITEA_TOKEN")
|
||||
|
||||
# CORS配置
|
||||
cors_origins = ["*"]
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
settings = SimpleSettings()
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
应用配置管理
|
||||
"""
|
||||
import os
|
||||
from typing import Optional, List
|
||||
from pydantic import validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
|
||||
# 应用基本信息
|
||||
app_name: str = "LJWX-Stock AI执行平台"
|
||||
app_version: str = "1.0.0"
|
||||
debug: bool = False
|
||||
environment: str = "development"
|
||||
|
||||
# 服务器配置
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
|
||||
# 数据库配置
|
||||
database_url: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/ai_platform"
|
||||
database_pool_size: int = 20
|
||||
database_max_overflow: int = 0
|
||||
|
||||
# Redis配置
|
||||
redis_url: str = "redis://:123456@localhost:6379/0"
|
||||
redis_max_connections: int = 20
|
||||
|
||||
# JWT配置
|
||||
secret_key: str = "your-secret-key-change-in-production"
|
||||
algorithm: str = "HS256"
|
||||
access_token_expire_minutes: int = 30
|
||||
refresh_token_expire_days: int = 7
|
||||
|
||||
# Claude API配置
|
||||
claude_api_key: Optional[str] = None
|
||||
claude_api_base_url: str = "https://api.anthropic.com"
|
||||
claude_model: str = "claude-3-sonnet-20240229"
|
||||
claude_max_tokens: int = 4000
|
||||
claude_temperature: float = 0.7
|
||||
|
||||
# MinIO配置
|
||||
minio_endpoint: str = "localhost:9000"
|
||||
minio_access_key: str = "minioadmin"
|
||||
minio_secret_key: str = "minioadmin"
|
||||
minio_secure: bool = False
|
||||
minio_bucket_name: str = "ai-platform"
|
||||
|
||||
# Gitea配置
|
||||
gitea_base_url: str = "http://localhost:3000"
|
||||
gitea_token: Optional[str] = None
|
||||
gitea_webhook_secret: Optional[str] = None
|
||||
|
||||
# 邮件配置
|
||||
smtp_host: Optional[str] = None
|
||||
smtp_port: int = 587
|
||||
smtp_username: Optional[str] = None
|
||||
smtp_password: Optional[str] = None
|
||||
smtp_use_tls: bool = True
|
||||
|
||||
# 日志配置
|
||||
log_level: str = "INFO"
|
||||
log_format: str = "json"
|
||||
|
||||
# 安全配置
|
||||
cors_origins: List[str] = ["*"]
|
||||
cors_allow_credentials: bool = True
|
||||
cors_allow_methods: List[str] = ["*"]
|
||||
cors_allow_headers: List[str] = ["*"]
|
||||
|
||||
# 限流配置
|
||||
rate_limit_requests: int = 100
|
||||
rate_limit_window: int = 60 # 秒
|
||||
|
||||
# 监控配置
|
||||
enable_metrics: bool = True
|
||||
metrics_path: str = "/metrics"
|
||||
|
||||
# 文件上传配置
|
||||
max_file_size: int = 10 * 1024 * 1024 # 10MB
|
||||
allowed_file_types: List[str] = [
|
||||
".txt", ".md", ".json", ".yaml", ".yml",
|
||||
".py", ".js", ".ts", ".vue", ".html", ".css"
|
||||
]
|
||||
|
||||
@validator("cors_origins", pre=True)
|
||||
def assemble_cors_origins(cls, v):
|
||||
if isinstance(v, str):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
return v
|
||||
|
||||
@validator("allowed_file_types", pre=True)
|
||||
def assemble_allowed_file_types(cls, v):
|
||||
if isinstance(v, str):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
return v
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
# 创建全局配置实例
|
||||
settings = Settings()
|
||||
@@ -1,74 +0,0 @@
|
||||
"""
|
||||
数据库连接和会话管理
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.pool import NullPool
|
||||
import structlog
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# 数据库引擎
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
echo=settings.debug,
|
||||
pool_size=settings.database_pool_size,
|
||||
max_overflow=settings.database_max_overflow,
|
||||
pool_pre_ping=True,
|
||||
poolclass=NullPool if settings.environment == "testing" else None,
|
||||
)
|
||||
|
||||
# 会话工厂
|
||||
AsyncSessionLocal = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""数据库模型基类"""
|
||||
pass
|
||||
|
||||
|
||||
async def get_db() -> AsyncSession:
|
||||
"""
|
||||
获取数据库会话
|
||||
|
||||
Yields:
|
||||
AsyncSession: 数据库会话
|
||||
"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
except Exception as e:
|
||||
logger.error("Database session error", error=str(e), exc_info=True)
|
||||
await session.rollback()
|
||||
raise
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
async def init_db():
|
||||
"""初始化数据库"""
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
# 创建所有表
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
logger.info("Database initialized successfully")
|
||||
except Exception as e:
|
||||
logger.error("Failed to initialize database", error=str(e), exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
async def close_db():
|
||||
"""关闭数据库连接"""
|
||||
try:
|
||||
await engine.dispose()
|
||||
logger.info("Database connections closed")
|
||||
except Exception as e:
|
||||
logger.error("Failed to close database connections", error=str(e), exc_info=True)
|
||||
@@ -1,72 +0,0 @@
|
||||
"""
|
||||
日志配置
|
||||
"""
|
||||
import logging
|
||||
import structlog
|
||||
from typing import Any, Dict
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""设置结构化日志"""
|
||||
|
||||
# 配置标准库logging
|
||||
logging.basicConfig(
|
||||
format="%(message)s",
|
||||
stream=sys.stdout,
|
||||
level=logging.INFO,
|
||||
)
|
||||
|
||||
# 配置structlog处理器
|
||||
processors = [
|
||||
structlog.stdlib.filter_by_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
structlog.processors.UnicodeDecoder(),
|
||||
structlog.dev.ConsoleRenderer(colors=True)
|
||||
]
|
||||
|
||||
structlog.configure(
|
||||
processors=processors,
|
||||
context_class=dict,
|
||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||
wrapper_class=structlog.stdlib.BoundLogger,
|
||||
cache_logger_on_first_use=True,
|
||||
)
|
||||
|
||||
|
||||
class CustomJSONEncoder(json.JSONEncoder):
|
||||
"""自定义JSON编码器"""
|
||||
|
||||
def default(self, obj: Any) -> Any:
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat()
|
||||
elif isinstance(obj, Decimal):
|
||||
return float(obj)
|
||||
elif hasattr(obj, "dict"):
|
||||
return obj.dict()
|
||||
elif hasattr(obj, "__dict__"):
|
||||
return obj.__dict__
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
def get_logger(name: str = None) -> structlog.stdlib.BoundLogger:
|
||||
"""
|
||||
获取日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
|
||||
Returns:
|
||||
structlog.stdlib.BoundLogger: 日志记录器
|
||||
"""
|
||||
return structlog.get_logger(name)
|
||||
|
||||
|
||||
# 初始化日志配置
|
||||
setup_logging()
|
||||
@@ -1,156 +0,0 @@
|
||||
"""
|
||||
安全相关功能
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Union
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import HTTPException, status
|
||||
import structlog
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# 密码加密上下文
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""
|
||||
创建访问令牌
|
||||
|
||||
Args:
|
||||
data: 要编码的数据
|
||||
expires_delta: 过期时间增量
|
||||
|
||||
Returns:
|
||||
str: JWT令牌
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.access_token_expire_minutes)
|
||||
|
||||
to_encode.update({"exp": expire, "type": "access"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def create_refresh_token(data: dict) -> str:
|
||||
"""
|
||||
创建刷新令牌
|
||||
|
||||
Args:
|
||||
data: 要编码的数据
|
||||
|
||||
Returns:
|
||||
str: JWT刷新令牌
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expire_days)
|
||||
to_encode.update({"exp": expire, "type": "refresh"})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.algorithm)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_token(token: str, token_type: str = "access") -> dict:
|
||||
"""
|
||||
验证令牌
|
||||
|
||||
Args:
|
||||
token: JWT令牌
|
||||
token_type: 令牌类型 (access/refresh)
|
||||
|
||||
Returns:
|
||||
dict: 解码后的令牌数据
|
||||
|
||||
Raises:
|
||||
HTTPException: 令牌无效时抛出异常
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||
|
||||
# 检查令牌类型
|
||||
if payload.get("type") != token_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type"
|
||||
)
|
||||
|
||||
return payload
|
||||
except JWTError as e:
|
||||
logger.warning("Token verification failed", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials"
|
||||
)
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""
|
||||
哈希密码
|
||||
|
||||
Args:
|
||||
password: 明文密码
|
||||
|
||||
Returns:
|
||||
str: 哈希后的密码
|
||||
"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""
|
||||
验证密码
|
||||
|
||||
Args:
|
||||
plain_password: 明文密码
|
||||
hashed_password: 哈希密码
|
||||
|
||||
Returns:
|
||||
bool: 密码是否正确
|
||||
"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def create_password_reset_token(email: str) -> str:
|
||||
"""
|
||||
创建密码重置令牌
|
||||
|
||||
Args:
|
||||
email: 用户邮箱
|
||||
|
||||
Returns:
|
||||
str: 密码重置令牌
|
||||
"""
|
||||
delta = timedelta(hours=1) # 1小时有效期
|
||||
now = datetime.utcnow()
|
||||
expires = now + delta
|
||||
exp = expires.timestamp()
|
||||
encoded_jwt = jwt.encode(
|
||||
{"exp": exp, "nbf": now, "sub": email, "type": "password_reset"},
|
||||
settings.secret_key,
|
||||
algorithm=settings.algorithm,
|
||||
)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_password_reset_token(token: str) -> Optional[str]:
|
||||
"""
|
||||
验证密码重置令牌
|
||||
|
||||
Args:
|
||||
token: 密码重置令牌
|
||||
|
||||
Returns:
|
||||
Optional[str]: 用户邮箱,验证失败返回None
|
||||
"""
|
||||
try:
|
||||
decoded_token = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm])
|
||||
if decoded_token.get("type") != "password_reset":
|
||||
return None
|
||||
return decoded_token["sub"]
|
||||
except JWTError:
|
||||
return None
|
||||
@@ -1,268 +0,0 @@
|
||||
"""
|
||||
LJWX-Stock AI执行平台主应用入口
|
||||
"""
|
||||
from contextlib import asynccontextmanager
|
||||
import time
|
||||
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
import structlog
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import init_db, close_db
|
||||
from app.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
# 启动时执行
|
||||
logger.info(
|
||||
"AI执行平台启动中...",
|
||||
app_name=settings.app_name,
|
||||
version=settings.app_version,
|
||||
environment=settings.environment
|
||||
)
|
||||
|
||||
# 初始化数据库
|
||||
await init_db()
|
||||
|
||||
logger.info("AI执行平台启动完成")
|
||||
yield
|
||||
|
||||
# 关闭时执行
|
||||
logger.info("AI执行平台正在关闭...")
|
||||
await close_db()
|
||||
logger.info("AI执行平台已安全关闭")
|
||||
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.app_name,
|
||||
version=settings.app_version,
|
||||
description="""
|
||||
## LJWX-Stock AI执行平台
|
||||
|
||||
### 核心功能
|
||||
- 🤖 **AI任务执行**: 基于Prompt契约的智能化任务执行
|
||||
- 👥 **人工审核**: 专业的工作流程和质量管理
|
||||
- 📝 **需求管理**: 从原始需求到任务分解的完整流程
|
||||
- 🔧 **任务管理**: 智能任务分配和状态跟踪
|
||||
- 📊 **实时监控**: 全方位的执行状态可视化
|
||||
- 🔄 **结果回溯**: 完整的版本控制和回滚机制
|
||||
- 🚪 **质量门禁**: 多层次的质量检查和评分机制
|
||||
|
||||
### 技术特色
|
||||
- 企业级微服务架构
|
||||
- 与Gitea深度集成
|
||||
- Claude AI驱动
|
||||
- 实时质量监控
|
||||
""",
|
||||
lifespan=lifespan,
|
||||
docs_url="/docs" if settings.debug else None,
|
||||
redoc_url="/redoc" if settings.debug else None,
|
||||
openapi_url="/openapi.json" if settings.debug else None,
|
||||
)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins,
|
||||
allow_credentials=settings.cors_allow_credentials,
|
||||
allow_methods=settings.cors_allow_methods,
|
||||
allow_headers=settings.cors_allow_headers,
|
||||
)
|
||||
|
||||
# 添加受信任主机中间件
|
||||
if not settings.debug:
|
||||
app.add_middleware(
|
||||
TrustedHostMiddleware,
|
||||
allowed_hosts=["localhost", "127.0.0.1", "*.yourdomain.com"]
|
||||
)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def logging_middleware(request: Request, call_next):
|
||||
"""请求日志中间件"""
|
||||
start_time = time.time()
|
||||
|
||||
# 记录请求开始
|
||||
logger.info(
|
||||
"Request started",
|
||||
method=request.method,
|
||||
url=str(request.url),
|
||||
client_ip=request.client.host,
|
||||
user_agent=request.headers.get("user-agent")
|
||||
)
|
||||
|
||||
try:
|
||||
response = await call_next(request)
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# 记录请求完成
|
||||
logger.info(
|
||||
"Request completed",
|
||||
method=request.method,
|
||||
url=str(request.url),
|
||||
status_code=response.status_code,
|
||||
process_time=round(process_time, 4)
|
||||
)
|
||||
|
||||
# 添加响应头
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# 记录请求错误
|
||||
logger.error(
|
||||
"Request failed",
|
||||
method=request.method,
|
||||
url=str(request.url),
|
||||
error=str(e),
|
||||
process_time=round(process_time, 4),
|
||||
exc_info=True
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""全局异常处理器"""
|
||||
logger.error(
|
||||
"Unhandled exception",
|
||||
method=request.method,
|
||||
url=str(request.url),
|
||||
error=str(exc),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"success": False,
|
||||
"code": 500,
|
||||
"message": "内部服务器错误" if not settings.debug else str(exc),
|
||||
"data": None,
|
||||
"meta": {
|
||||
"timestamp": time.time(),
|
||||
"request_id": getattr(request.state, "request_id", None)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# 根路径
|
||||
@app.get("/", tags=["系统"])
|
||||
async def root():
|
||||
"""根路径 - 系统信息"""
|
||||
return {
|
||||
"name": settings.app_name,
|
||||
"version": settings.app_version,
|
||||
"status": "running",
|
||||
"environment": settings.environment,
|
||||
"debug": settings.debug,
|
||||
"docs_url": "/docs" if settings.debug else None,
|
||||
"message": "欢迎使用LJWX-Stock AI执行平台!"
|
||||
}
|
||||
|
||||
|
||||
# 健康检查
|
||||
@app.get("/health", tags=["系统"])
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": time.time(),
|
||||
"version": settings.app_version,
|
||||
"environment": settings.environment,
|
||||
"uptime": "系统正常运行"
|
||||
}
|
||||
|
||||
|
||||
# 系统信息
|
||||
@app.get("/info", tags=["系统"])
|
||||
async def system_info():
|
||||
"""系统信息"""
|
||||
return {
|
||||
"app_name": settings.app_name,
|
||||
"version": settings.app_version,
|
||||
"environment": settings.environment,
|
||||
"debug": settings.debug,
|
||||
"features": {
|
||||
"用户管理": "✅ 已实现",
|
||||
"需求管理": "✅ 已实现",
|
||||
"任务管理": "✅ 已实现",
|
||||
"AI执行": "✅ 已实现",
|
||||
"审核工作流": "✅ 已实现",
|
||||
"质量门禁": "✅ 已实现",
|
||||
"Gitea集成": "✅ 已实现"
|
||||
},
|
||||
"integrations": {
|
||||
"Claude API": "✅ 已配置",
|
||||
"Gitea": "✅ 已配置",
|
||||
"Redis": "✅ 已配置",
|
||||
"PostgreSQL": "✅ 已配置",
|
||||
"MinIO": "✅ 已配置"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 模拟API用于开发测试
|
||||
@app.get("/api/v1/requirements/", tags=["需求管理"])
|
||||
async def list_requirements():
|
||||
"""获取需求列表"""
|
||||
return {
|
||||
"success": True,
|
||||
"code": 200,
|
||||
"message": "获取需求列表成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "req-001",
|
||||
"title": "用户权限管理系统",
|
||||
"description": "企业级权限管理,支持RBAC模型",
|
||||
"priority": 1,
|
||||
"status": "approved",
|
||||
"tags": ["权限", "RBAC", "安全"],
|
||||
"created_at": "2025-10-28T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "req-002",
|
||||
"title": "数据分析平台",
|
||||
"description": "股票数据分析和可视化平台",
|
||||
"priority": 2,
|
||||
"status": "reviewing",
|
||||
"tags": ["数据分析", "可视化", "股票"],
|
||||
"created_at": "2025-10-28T01:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"total": 2,
|
||||
"pages": 1
|
||||
},
|
||||
"meta": {
|
||||
"timestamp": time.time(),
|
||||
"request_id": "req_123456"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=settings.debug,
|
||||
log_level=settings.log_level.lower(),
|
||||
access_log=True,
|
||||
)
|
||||
@@ -1,47 +0,0 @@
|
||||
"""
|
||||
数据库模型
|
||||
"""
|
||||
from app.models.user import User, Role, UserRole
|
||||
from app.models.requirement import Requirement, RequirementComment
|
||||
from app.models.spec import Spec, SpecReview
|
||||
from app.models.work_item import WorkItem, WorkItemDependency
|
||||
from app.models.prompt_contract import PromptContract, PromptContractHistory
|
||||
from app.models.execution import ExecutionRun, ExecutionLog
|
||||
from app.models.review import ReviewDecision, ReviewHistory
|
||||
from app.models.system import SystemConfig, NotificationTemplate, AuditLog
|
||||
|
||||
__all__ = [
|
||||
# 用户和权限
|
||||
"User",
|
||||
"Role",
|
||||
"UserRole",
|
||||
|
||||
# 需求管理
|
||||
"Requirement",
|
||||
"RequirementComment",
|
||||
|
||||
# 设计方案
|
||||
"Spec",
|
||||
"SpecReview",
|
||||
|
||||
# 任务管理
|
||||
"WorkItem",
|
||||
"WorkItemDependency",
|
||||
|
||||
# AI Prompt契约
|
||||
"PromptContract",
|
||||
"PromptContractHistory",
|
||||
|
||||
# 执行记录
|
||||
"ExecutionRun",
|
||||
"ExecutionLog",
|
||||
|
||||
# 审核和决策
|
||||
"ReviewDecision",
|
||||
"ReviewHistory",
|
||||
|
||||
# 系统管理
|
||||
"SystemConfig",
|
||||
"NotificationTemplate",
|
||||
"AuditLog",
|
||||
]
|
||||
@@ -1,107 +0,0 @@
|
||||
"""
|
||||
数据库模型基类
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Column, Integer, DateTime, String, SmallInteger, Boolean
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
|
||||
class BaseModel(declarative_base()):
|
||||
"""数据库模型基类"""
|
||||
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, comment="主键ID")
|
||||
create_time = Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
nullable=False,
|
||||
comment="创建时间"
|
||||
)
|
||||
update_time = Column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=True,
|
||||
comment="更新时间"
|
||||
)
|
||||
create_user = Column(String(40), nullable=True, comment="创建用户")
|
||||
update_user = Column(String(40), nullable=True, comment="更新用户")
|
||||
|
||||
|
||||
class SoftDeleteMixin:
|
||||
"""软删除混入类"""
|
||||
|
||||
is_deleted = Column(
|
||||
SmallInteger,
|
||||
default=0,
|
||||
nullable=False,
|
||||
comment="是否删除 0:未删除 1:已删除"
|
||||
)
|
||||
|
||||
def soft_delete(self):
|
||||
"""软删除"""
|
||||
self.is_deleted = 1
|
||||
|
||||
def restore(self):
|
||||
"""恢复"""
|
||||
self.is_deleted = 0
|
||||
|
||||
@property
|
||||
def deleted(self) -> bool:
|
||||
"""是否已删除"""
|
||||
return self.is_deleted == 1
|
||||
|
||||
|
||||
class AuditMixin:
|
||||
"""审计混入类"""
|
||||
|
||||
creator_id = Column(Integer, nullable=True, comment="创建者ID")
|
||||
updater_id = Column(Integer, nullable=True, comment="更新者ID")
|
||||
|
||||
|
||||
class StatusMixin:
|
||||
"""状态混入类"""
|
||||
|
||||
status = Column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
default="active",
|
||||
comment="状态"
|
||||
)
|
||||
is_active = Column(
|
||||
Boolean,
|
||||
default=True,
|
||||
nullable=False,
|
||||
comment="是否激活"
|
||||
)
|
||||
|
||||
@property
|
||||
def active(self) -> bool:
|
||||
"""是否激活"""
|
||||
return self.is_active
|
||||
|
||||
|
||||
class PriorityMixin:
|
||||
"""优先级混入类"""
|
||||
|
||||
priority = Column(
|
||||
SmallInteger,
|
||||
default=3,
|
||||
nullable=False,
|
||||
comment="优先级 1:最高 5:最低"
|
||||
)
|
||||
|
||||
|
||||
class VersionMixin:
|
||||
"""版本混入类"""
|
||||
|
||||
version = Column(
|
||||
String(32),
|
||||
nullable=False,
|
||||
default="v1.0",
|
||||
comment="版本号"
|
||||
)
|
||||
@@ -1,177 +0,0 @@
|
||||
"""
|
||||
AI执行记录模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, DateTime, ForeignKey, JSON,
|
||||
SmallInt, Numeric
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, AuditMixin
|
||||
|
||||
|
||||
class ExecutionRun(BaseModel, AuditMixin):
|
||||
"""AI执行记录模型"""
|
||||
|
||||
__tablename__ = "exec_run"
|
||||
|
||||
work_item_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('dev_work_item.id', ondelete='CASCADE'), nullable=False, comment="工作项ID"
|
||||
)
|
||||
prompt_contract_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('ai_prompt_contract.id'), nullable=False, comment="Prompt契约ID"
|
||||
)
|
||||
agent: Mapped[str] = mapped_column(String(32), default="claude", nullable=False, comment="执行代理")
|
||||
trigger_type: Mapped[str] = mapped_column(
|
||||
String(32), default="manual", nullable=False,
|
||||
comment="触发类型: manual,auto,scheduled,webhook"
|
||||
)
|
||||
input_prompt: Mapped[str] = mapped_column(Text, nullable=False, comment="输入提示")
|
||||
input_context: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="输入上下文")
|
||||
output_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="输出摘要")
|
||||
output_content: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="输出内容")
|
||||
output_artifacts: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="输出产物")
|
||||
gitea_branch: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, comment="Gitea分支")
|
||||
gitea_pr_url: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment="Gitea PR URL")
|
||||
gitea_commit_sha: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, comment="Gitea提交SHA")
|
||||
ci_pipeline_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="CI流水线ID")
|
||||
ci_status: Mapped[Optional[str]] = mapped_column(String(32), nullable=True, comment="CI状态")
|
||||
quality_score: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(5, 2), nullable=True, comment="质量分数"
|
||||
)
|
||||
quality_details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="质量详情")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), default="running", nullable=False,
|
||||
comment="状态: running,succeeded,failed,cancelled,needs_review,timeout"
|
||||
)
|
||||
error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="错误信息")
|
||||
error_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="错误类型")
|
||||
retry_count: Mapped[int] = mapped_column(default=0, nullable=False, comment="重试次数")
|
||||
max_retries: Mapped[int] = mapped_column(default=3, nullable=False, comment="最大重试次数")
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False, comment="开始时间"
|
||||
)
|
||||
finished_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="结束时间"
|
||||
)
|
||||
|
||||
# 关联用户
|
||||
creator_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="创建者ID"
|
||||
)
|
||||
|
||||
# 关系
|
||||
work_item: Mapped["WorkItem"] = relationship("WorkItem", back_populates="execution_runs")
|
||||
prompt_contract: Mapped["PromptContract"] = relationship("PromptContract", back_populates="execution_runs")
|
||||
creator: Mapped["User"] = relationship("User", foreign_keys=[creator_id])
|
||||
|
||||
# 执行日志
|
||||
logs: Mapped[List["ExecutionLog"]] = relationship(
|
||||
"ExecutionLog", back_populates="run", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# 审核决策
|
||||
review_decisions: Mapped[List["ReviewDecision"]] = relationship(
|
||||
"ReviewDecision", back_populates="run", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ExecutionRun(id={self.id}, status='{self.status}', agent='{self.agent}')>"
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""是否正在运行"""
|
||||
return self.status == "running"
|
||||
|
||||
@property
|
||||
def is_completed(self) -> bool:
|
||||
"""是否已完成"""
|
||||
return self.status in ["succeeded", "failed", "cancelled", "timeout"]
|
||||
|
||||
@property
|
||||
def is_successful(self) -> bool:
|
||||
"""是否成功"""
|
||||
return self.status == "succeeded"
|
||||
|
||||
@property
|
||||
def needs_review(self) -> bool:
|
||||
"""是否需要审核"""
|
||||
return self.status == "needs_review"
|
||||
|
||||
@property
|
||||
def can_retry(self) -> bool:
|
||||
"""是否可以重试"""
|
||||
return self.retry_count < self.max_retries and self.status in ["failed", "timeout"]
|
||||
|
||||
@property
|
||||
def execution_duration(self) -> Optional[float]:
|
||||
"""执行时长(秒)"""
|
||||
if not self.finished_at:
|
||||
return None
|
||||
return (self.finished_at - self.started_at).total_seconds()
|
||||
|
||||
def succeed(self, output_summary: str = None, output_content: str = None,
|
||||
output_artifacts: list = None, quality_score: float = None):
|
||||
"""标记执行成功"""
|
||||
self.status = "succeeded"
|
||||
self.finished_at = datetime.utcnow()
|
||||
if output_summary:
|
||||
self.output_summary = output_summary
|
||||
if output_content:
|
||||
self.output_content = output_content
|
||||
if output_artifacts:
|
||||
self.output_artifacts = output_artifacts
|
||||
if quality_score is not None:
|
||||
self.quality_score = Decimal(str(quality_score))
|
||||
|
||||
def fail(self, error_message: str, error_type: str = None):
|
||||
"""标记执行失败"""
|
||||
self.status = "failed"
|
||||
self.finished_at = datetime.utcnow()
|
||||
self.error_message = error_message
|
||||
if error_type:
|
||||
self.error_type = error_type
|
||||
|
||||
def cancel(self):
|
||||
"""取消执行"""
|
||||
self.status = "cancelled"
|
||||
self.finished_at = datetime.utcnow()
|
||||
|
||||
def request_review(self):
|
||||
"""请求审核"""
|
||||
self.status = "needs_review"
|
||||
|
||||
def retry(self):
|
||||
"""重试执行"""
|
||||
self.retry_count += 1
|
||||
self.status = "running"
|
||||
self.error_message = None
|
||||
self.error_type = None
|
||||
self.finished_at = None
|
||||
|
||||
|
||||
class ExecutionLog(BaseModel):
|
||||
"""执行日志模型"""
|
||||
|
||||
__tablename__ = "exec_run_log"
|
||||
|
||||
run_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('exec_run.id', ondelete='CASCADE'), nullable=False, comment="执行记录ID"
|
||||
)
|
||||
level: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False,
|
||||
comment="日志级别: DEBUG,INFO,WARNING,ERROR,CRITICAL"
|
||||
)
|
||||
message: Mapped[str] = mapped_column(Text, nullable=False, comment="日志消息")
|
||||
details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="详细信息")
|
||||
|
||||
# 关系
|
||||
run: Mapped["ExecutionRun"] = relationship("ExecutionRun", back_populates="logs")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ExecutionLog(run_id={self.run_id}, level='{self.level}')>"
|
||||
@@ -1,109 +0,0 @@
|
||||
"""
|
||||
AI Prompt契约模型
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, DateTime, ForeignKey, JSON,
|
||||
Boolean, Numeric, UniqueConstraint
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin, VersionMixin
|
||||
|
||||
|
||||
class PromptContract(BaseModel, SoftDeleteMixin, AuditMixin, VersionMixin):
|
||||
"""AI Prompt契约模型"""
|
||||
|
||||
__tablename__ = "ai_prompt_contract"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(128), unique=True, nullable=False, comment="契约名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="契约描述")
|
||||
category: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="契约分类")
|
||||
context_yaml: Mapped[str] = mapped_column(Text, nullable=False, comment="上下文YAML")
|
||||
guardrails_yaml: Mapped[str] = mapped_column(Text, nullable=False, comment="约束YAML")
|
||||
output_schema: Mapped[dict] = mapped_column(JSON, nullable=False, comment="输出Schema")
|
||||
quality_checklist: Mapped[dict] = mapped_column(JSON, nullable=False, comment="质量检查清单")
|
||||
examples: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="示例列表")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment="是否默认")
|
||||
usage_count: Mapped[int] = mapped_column(default=0, nullable=False, comment="使用次数")
|
||||
success_count: Mapped[int] = mapped_column(default=0, nullable=False, comment="成功次数")
|
||||
failure_count: Mapped[int] = mapped_column(default=0, nullable=False, comment="失败次数")
|
||||
avg_quality_score: Mapped[Decimal] = mapped_column(
|
||||
Numeric(5, 2), default=0.00, nullable=False, comment="平均质量分数"
|
||||
)
|
||||
avg_execution_time: Mapped[Decimal] = mapped_column(
|
||||
Numeric(10, 3), default=0.000, nullable=False, comment="平均执行时间(秒)"
|
||||
)
|
||||
|
||||
# 关联用户
|
||||
creator_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="创建者ID"
|
||||
)
|
||||
|
||||
# 关系
|
||||
creator: Mapped["User"] = relationship("User", foreign_keys=[creator_id])
|
||||
|
||||
# 历史版本
|
||||
history: Mapped[List["PromptContractHistory"]] = relationship(
|
||||
"PromptContractHistory", back_populates="contract", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# 执行记录
|
||||
execution_runs: Mapped[List["ExecutionRun"]] = relationship(
|
||||
"ExecutionRun", back_populates="prompt_contract"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PromptContract(id={self.id}, name='{self.name}', version='{self.version}')>"
|
||||
|
||||
@property
|
||||
def success_rate(self) -> float:
|
||||
"""成功率"""
|
||||
if self.usage_count == 0:
|
||||
return 0.0
|
||||
return float(self.success_count) / self.usage_count
|
||||
|
||||
def record_usage(self, success: bool, quality_score: float = None, execution_time: float = None):
|
||||
"""记录使用情况"""
|
||||
self.usage_count += 1
|
||||
if success:
|
||||
self.success_count += 1
|
||||
else:
|
||||
self.failure_count += 1
|
||||
|
||||
# 更新平均质量分数
|
||||
if quality_score is not None:
|
||||
total_score = float(self.avg_quality_score) * (self.usage_count - 1) + quality_score
|
||||
self.avg_quality_score = Decimal(str(total_score / self.usage_count))
|
||||
|
||||
# 更新平均执行时间
|
||||
if execution_time is not None:
|
||||
total_time = float(self.avg_execution_time) * (self.usage_count - 1) + execution_time
|
||||
self.avg_execution_time = Decimal(str(total_time / self.usage_count))
|
||||
|
||||
|
||||
class PromptContractHistory(BaseModel):
|
||||
"""Prompt契约历史版本模型"""
|
||||
|
||||
__tablename__ = "ai_prompt_contract_history"
|
||||
|
||||
contract_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('ai_prompt_contract.id', ondelete='CASCADE'), nullable=False, comment="契约ID"
|
||||
)
|
||||
version: Mapped[str] = mapped_column(String(32), nullable=False, comment="版本号")
|
||||
changes: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="变更内容")
|
||||
change_reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="变更原因")
|
||||
changed_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="变更人ID"
|
||||
)
|
||||
|
||||
# 关系
|
||||
contract: Mapped["PromptContract"] = relationship("PromptContract", back_populates="history")
|
||||
changed_by_user: Mapped[Optional["User"]] = relationship("User", foreign_keys=[changed_by])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<PromptContractHistory(contract_id={self.contract_id}, version='{self.version}')>"
|
||||
@@ -1,200 +0,0 @@
|
||||
"""
|
||||
需求管理模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, SmallInteger, DateTime, ForeignKey,
|
||||
JSON, Boolean
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin, PriorityMixin
|
||||
|
||||
|
||||
class Requirement(BaseModel, SoftDeleteMixin, AuditMixin, PriorityMixin):
|
||||
"""需求模型"""
|
||||
|
||||
__tablename__ = "req_requirement"
|
||||
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="需求标题")
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False, comment="需求描述")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), default="draft", nullable=False,
|
||||
comment="状态: draft,in_review,approved,rejected,closed,cancelled"
|
||||
)
|
||||
tags: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="标签列表")
|
||||
business_value: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="业务价值")
|
||||
acceptance_criteria: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="验收标准")
|
||||
estimated_effort: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="预估工作量")
|
||||
actual_effort: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="实际工作量")
|
||||
complexity_level: Mapped[int] = mapped_column(
|
||||
SmallInteger, default=3, nullable=False, comment="复杂度等级 1-5"
|
||||
)
|
||||
risk_level: Mapped[int] = mapped_column(
|
||||
SmallInteger, default=3, nullable=False, comment="风险等级 1-5"
|
||||
)
|
||||
|
||||
# 关联用户
|
||||
assignee_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="负责人ID"
|
||||
)
|
||||
reviewer_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="审核人ID"
|
||||
)
|
||||
creator_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="创建者ID"
|
||||
)
|
||||
approved_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="批准人ID"
|
||||
)
|
||||
rejected_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="拒绝人ID"
|
||||
)
|
||||
closed_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="关闭人ID"
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
approved_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="批准时间"
|
||||
)
|
||||
rejected_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="拒绝时间"
|
||||
)
|
||||
closed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="关闭时间"
|
||||
)
|
||||
|
||||
# 关系
|
||||
creator: Mapped["User"] = relationship(
|
||||
"User", foreign_keys=[creator_id], back_populates="created_requirements"
|
||||
)
|
||||
assignee: Mapped[Optional["User"]] = relationship(
|
||||
"User", foreign_keys=[assignee_id], back_populates="assigned_requirements"
|
||||
)
|
||||
reviewer: Mapped[Optional["User"]] = relationship(
|
||||
"User", foreign_keys=[reviewer_id], back_populates="reviewed_requirements"
|
||||
)
|
||||
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approved_by])
|
||||
rejecter: Mapped[Optional["User"]] = relationship("User", foreign_keys=[rejected_by])
|
||||
closer: Mapped[Optional["User"]] = relationship("User", foreign_keys=[closed_by])
|
||||
|
||||
# 关联的方案
|
||||
specs: Mapped[List["Spec"]] = relationship(
|
||||
"Spec", back_populates="requirement", cascade="all, delete-orphan", lazy="selectin"
|
||||
)
|
||||
|
||||
# 评论
|
||||
comments: Mapped[List["RequirementComment"]] = relationship(
|
||||
"RequirementComment", back_populates="requirement", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Requirement(id={self.id}, title='{self.title}', status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def is_approved(self) -> bool:
|
||||
"""是否已批准"""
|
||||
return self.status == "approved"
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
"""是否已关闭"""
|
||||
return self.status in ["closed", "cancelled"]
|
||||
|
||||
@property
|
||||
def can_edit(self) -> bool:
|
||||
"""是否可以编辑"""
|
||||
return self.status in ["draft", "rejected"]
|
||||
|
||||
@property
|
||||
def can_approve(self) -> bool:
|
||||
"""是否可以批准"""
|
||||
return self.status == "in_review"
|
||||
|
||||
def approve(self, approver_id: int):
|
||||
"""批准需求"""
|
||||
self.status = "approved"
|
||||
self.approved_by = approver_id
|
||||
self.approved_at = datetime.utcnow()
|
||||
|
||||
def reject(self, rejecter_id: int, reason: str = None):
|
||||
"""拒绝需求"""
|
||||
self.status = "rejected"
|
||||
self.rejected_by = rejecter_id
|
||||
self.rejected_at = datetime.utcnow()
|
||||
|
||||
def close(self, closer_id: int):
|
||||
"""关闭需求"""
|
||||
self.status = "closed"
|
||||
self.closed_by = closer_id
|
||||
self.closed_at = datetime.utcnow()
|
||||
|
||||
@property
|
||||
def effort_variance(self) -> Optional[int]:
|
||||
"""工作量差异"""
|
||||
if self.estimated_effort and self.actual_effort:
|
||||
return self.actual_effort - self.estimated_effort
|
||||
return None
|
||||
|
||||
@property
|
||||
def completion_rate(self) -> Optional[float]:
|
||||
"""完成率"""
|
||||
if not self.estimated_effort:
|
||||
return None
|
||||
if not self.actual_effort:
|
||||
return 0.0
|
||||
return min(self.actual_effort / self.estimated_effort, 1.0)
|
||||
|
||||
|
||||
class RequirementComment(BaseModel):
|
||||
"""需求评论模型"""
|
||||
|
||||
__tablename__ = "req_requirement_comment"
|
||||
|
||||
requirement_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('req_requirement.id', ondelete='CASCADE'), nullable=False, comment="需求ID"
|
||||
)
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="用户ID"
|
||||
)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False, comment="评论内容")
|
||||
comment_type: Mapped[str] = mapped_column(
|
||||
String(20), default="comment", nullable=False,
|
||||
comment="评论类型: comment,suggestion,approval,rejection"
|
||||
)
|
||||
parent_comment_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('req_requirement_comment.id'), nullable=True, comment="父评论ID"
|
||||
)
|
||||
is_internal: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment="是否内部评论")
|
||||
|
||||
# 关系
|
||||
requirement: Mapped["Requirement"] = relationship("Requirement", back_populates="comments")
|
||||
user: Mapped["User"] = relationship("User")
|
||||
parent_comment: Mapped[Optional["RequirementComment"]] = relationship(
|
||||
"RequirementComment", remote_side="RequirementComment.id"
|
||||
)
|
||||
replies: Mapped[List["RequirementComment"]] = relationship(
|
||||
"RequirementComment", back_populates="parent_comment"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<RequirementComment(id={self.id}, requirement_id={self.requirement_id}, user_id={self.user_id})>"
|
||||
|
||||
@property
|
||||
def is_approval(self) -> bool:
|
||||
"""是否是批准评论"""
|
||||
return self.comment_type == "approval"
|
||||
|
||||
@property
|
||||
def is_rejection(self) -> bool:
|
||||
"""是否是拒绝评论"""
|
||||
return self.comment_type == "rejection"
|
||||
|
||||
@property
|
||||
def is_suggestion(self) -> bool:
|
||||
"""是否是建议评论"""
|
||||
return self.comment_type == "suggestion"
|
||||
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
审核和决策模型
|
||||
"""
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, SmallInt, DateTime, ForeignKey, JSON
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin
|
||||
|
||||
|
||||
class ReviewDecision(BaseModel, SoftDeleteMixin, AuditMixin):
|
||||
"""审核决策模型"""
|
||||
|
||||
__tablename__ = "review_decision"
|
||||
|
||||
run_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('exec_run.id', ondelete='CASCADE'), nullable=False, comment="执行记录ID"
|
||||
)
|
||||
reviewer_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="审核人ID"
|
||||
)
|
||||
decision: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, comment="决策: approve,request_changes,reject"
|
||||
)
|
||||
comments: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="审核意见")
|
||||
checklist: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="检查清单")
|
||||
score: Mapped[Optional[int]] = mapped_column(
|
||||
SmallInt, nullable=True, comment="评分 1-5"
|
||||
)
|
||||
review_type: Mapped[str] = mapped_column(
|
||||
String(20), default="manual", nullable=False,
|
||||
comment="审核类型: manual,auto,assisted"
|
||||
)
|
||||
|
||||
# 关系
|
||||
run: Mapped["ExecutionRun"] = relationship("ExecutionRun", back_populates="review_decisions")
|
||||
reviewer: Mapped["User"] = relationship("User", foreign_keys=[reviewer_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ReviewDecision(run_id={self.run_id}, reviewer_id={self.reviewer_id}, decision='{self.decision}')>"
|
||||
|
||||
@property
|
||||
def is_approved(self) -> bool:
|
||||
"""是否批准"""
|
||||
return self.decision == "approve"
|
||||
|
||||
@property
|
||||
def is_rejected(self) -> bool:
|
||||
"""是否拒绝"""
|
||||
return self.decision == "reject"
|
||||
|
||||
@property
|
||||
def needs_changes(self) -> bool:
|
||||
"""是否需要修改"""
|
||||
return self.decision == "request_changes"
|
||||
|
||||
|
||||
class ReviewHistory(BaseModel):
|
||||
"""审核历史模型"""
|
||||
|
||||
__tablename__ = "review_history"
|
||||
|
||||
target_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="目标类型")
|
||||
target_id: Mapped[int] = mapped_column(Integer, nullable=False, comment="目标ID")
|
||||
reviewer_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="审核人ID"
|
||||
)
|
||||
action: Mapped[str] = mapped_column(String(32), nullable=False, comment="操作")
|
||||
details: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="详细信息")
|
||||
|
||||
# 关系
|
||||
reviewer: Mapped["User"] = relationship("User", foreign_keys=[reviewer_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ReviewHistory(target_type='{self.target_type}', target_id={self.target_id}, action='{self.action}')>"
|
||||
@@ -1,135 +0,0 @@
|
||||
"""
|
||||
设计方案模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, DateTime, ForeignKey, JSON, SmallInt
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin, VersionMixin
|
||||
|
||||
|
||||
class Spec(BaseModel, SoftDeleteMixin, AuditMixin, VersionMixin):
|
||||
"""设计方案模型"""
|
||||
|
||||
__tablename__ = "des_spec"
|
||||
|
||||
requirement_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('req_requirement.id', ondelete='CASCADE'), nullable=False, comment="需求ID"
|
||||
)
|
||||
spec_type: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False,
|
||||
comment="方案类型: arch,detailed,api,ui,ops,security"
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="方案标题")
|
||||
content_md: Mapped[str] = mapped_column(Text, nullable=False, comment="Markdown内容")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), default="draft", nullable=False,
|
||||
comment="状态: draft,in_review,approved,rejected,archived"
|
||||
)
|
||||
attachments: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="附件列表")
|
||||
tech_stack: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="技术栈")
|
||||
risks: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="风险点")
|
||||
assumptions: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="假设条件")
|
||||
constraints: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="约束条件")
|
||||
|
||||
# 关联用户
|
||||
creator_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="创建者ID"
|
||||
)
|
||||
reviewer_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="审核人ID"
|
||||
)
|
||||
approved_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="批准人ID"
|
||||
)
|
||||
rejected_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="拒绝人ID"
|
||||
)
|
||||
|
||||
# 时间戳
|
||||
approved_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="批准时间"
|
||||
)
|
||||
rejected_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="拒绝时间"
|
||||
)
|
||||
|
||||
# 关系
|
||||
requirement: Mapped["Requirement"] = relationship("Requirement", back_populates="specs")
|
||||
creator: Mapped["User"] = relationship("User", foreign_keys=[creator_id])
|
||||
reviewer: Mapped[Optional["User"]] = relationship("User", foreign_keys=[reviewer_id])
|
||||
approver: Mapped[Optional["User"]] = relationship("User", foreign_keys=[approved_by])
|
||||
rejecter: Mapped[Optional["User"]] = relationship("User", foreign_keys=[rejected_by])
|
||||
|
||||
# 评审记录
|
||||
reviews: Mapped[List["SpecReview"]] = relationship(
|
||||
"SpecReview", back_populates="spec", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# 关联任务
|
||||
work_items: Mapped[List["WorkItem"]] = relationship(
|
||||
"WorkItem", back_populates="spec", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Spec(id={self.id}, title='{self.title}', status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def is_approved(self) -> bool:
|
||||
"""是否已批准"""
|
||||
return self.status == "approved"
|
||||
|
||||
def approve(self, approver_id: int):
|
||||
"""批准方案"""
|
||||
self.status = "approved"
|
||||
self.approved_by = approver_id
|
||||
self.approved_at = datetime.utcnow()
|
||||
|
||||
def reject(self, rejecter_id: int):
|
||||
"""拒绝方案"""
|
||||
self.status = "rejected"
|
||||
self.rejected_by = rejecter_id
|
||||
self.rejected_at = datetime.utcnow()
|
||||
|
||||
|
||||
class SpecReview(BaseModel):
|
||||
"""方案评审模型"""
|
||||
|
||||
__tablename__ = "des_spec_review"
|
||||
|
||||
spec_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('des_spec.id', ondelete='CASCADE'), nullable=False, comment="方案ID"
|
||||
)
|
||||
reviewer_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="审核人ID"
|
||||
)
|
||||
decision: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, comment="决策: approve,request_changes,reject"
|
||||
)
|
||||
comments: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="评审意见")
|
||||
checklist: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="检查清单")
|
||||
score: Mapped[Optional[int]] = mapped_column(
|
||||
SmallInt, nullable=True, comment="评分 1-5"
|
||||
)
|
||||
|
||||
# 关系
|
||||
spec: Mapped["Spec"] = relationship("Spec", back_populates="reviews")
|
||||
reviewer: Mapped["User"] = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SpecReview(spec_id={self.spec_id}, reviewer_id={self.reviewer_id}, decision='{self.decision}')>"
|
||||
|
||||
@property
|
||||
def is_approved(self) -> bool:
|
||||
"""是否批准"""
|
||||
return self.decision == "approve"
|
||||
|
||||
@property
|
||||
def is_rejected(self) -> bool:
|
||||
"""是否拒绝"""
|
||||
return self.decision == "reject"
|
||||
@@ -1,70 +0,0 @@
|
||||
"""
|
||||
系统管理模型
|
||||
"""
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, Boolean, DateTime, ForeignKey, JSON
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, AuditMixin
|
||||
|
||||
|
||||
class SystemConfig(BaseModel, AuditMixin):
|
||||
"""系统配置模型"""
|
||||
|
||||
__tablename__ = "sys_config"
|
||||
|
||||
category: Mapped[str] = mapped_column(String(50), nullable=False, comment="配置分类")
|
||||
key: Mapped[str] = mapped_column(String(100), nullable=False, comment="配置键")
|
||||
value: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="配置值")
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="配置描述")
|
||||
data_type: Mapped[str] = mapped_column(
|
||||
String(20), default="string", nullable=False,
|
||||
comment="数据类型: string,integer,float,boolean,json"
|
||||
)
|
||||
is_encrypted: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment="是否加密")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<SystemConfig(category='{self.category}', key='{self.key}')>"
|
||||
|
||||
|
||||
class NotificationTemplate(BaseModel, AuditMixin):
|
||||
"""通知模板模型"""
|
||||
|
||||
__tablename__ = "notification_template"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, comment="模板名称")
|
||||
type: Mapped[str] = mapped_column(String(50), nullable=False, comment="通知类型")
|
||||
subject_template: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="主题模板")
|
||||
body_template: Mapped[str] = mapped_column(Text, nullable=False, comment="内容模板")
|
||||
variables: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="变量列表")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NotificationTemplate(name='{self.name}', type='{self.type}')>"
|
||||
|
||||
|
||||
class AuditLog(BaseModel):
|
||||
"""操作审计模型"""
|
||||
|
||||
__tablename__ = "audit_log"
|
||||
|
||||
user_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="用户ID"
|
||||
)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False, comment="操作")
|
||||
resource_type: Mapped[str] = mapped_column(String(50), nullable=False, comment="资源类型")
|
||||
resource_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="资源ID")
|
||||
old_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="旧值")
|
||||
new_values: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, comment="新值")
|
||||
ip_address: Mapped[Optional[str]] = mapped_column(String(45), nullable=True, comment="IP地址")
|
||||
user_agent: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="用户代理")
|
||||
request_id: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="请求ID")
|
||||
|
||||
# 关系
|
||||
user: Mapped[Optional["User"]] = relationship("User", foreign_keys=[user_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AuditLog(user_id={self.user_id}, action='{self.action}', resource_type='{self.resource_type}')>"
|
||||
@@ -1,207 +0,0 @@
|
||||
"""
|
||||
用户和权限模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Boolean, DateTime, Text, SmallInteger,
|
||||
ForeignKey, Table, JSON
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, AuditMixin, StatusMixin
|
||||
|
||||
|
||||
# 用户角色关联表
|
||||
user_role_table = Table(
|
||||
'sys_user_role',
|
||||
BaseModel.metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), nullable=False),
|
||||
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), nullable=False),
|
||||
Column('assigned_at', DateTime(timezone=True), server_default=func.now(), nullable=False),
|
||||
Column('assigned_by', Integer, ForeignKey('sys_user.id'), nullable=True),
|
||||
Column('expires_at', DateTime(timezone=True), nullable=True),
|
||||
Column('is_active', Boolean, default=True, nullable=False),
|
||||
schema=None
|
||||
)
|
||||
|
||||
|
||||
class Role(BaseModel, StatusMixin):
|
||||
"""角色模型"""
|
||||
|
||||
__tablename__ = "sys_role"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="角色名称")
|
||||
display_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="显示名称")
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="角色描述")
|
||||
permissions: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="权限列表")
|
||||
|
||||
# 关系
|
||||
users: Mapped[List["User"]] = relationship(
|
||||
"User",
|
||||
secondary=user_role_table,
|
||||
back_populates="roles",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Role(name='{self.name}', display_name='{self.display_name}')>"
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""检查是否有指定权限"""
|
||||
if not self.permissions:
|
||||
return False
|
||||
|
||||
# 支持通配符权限
|
||||
for perm in self.permissions:
|
||||
if perm == "*": # 超级权限
|
||||
return True
|
||||
if permission.startswith(perm.rstrip("*")):
|
||||
return True
|
||||
return permission in self.permissions
|
||||
|
||||
|
||||
class User(BaseModel, AuditMixin):
|
||||
"""用户模型"""
|
||||
|
||||
__tablename__ = "sys_user"
|
||||
|
||||
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, comment="用户名")
|
||||
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False, comment="邮箱")
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False, comment="密码哈希")
|
||||
full_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="全名")
|
||||
avatar_url: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, comment="头像URL")
|
||||
phone: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="手机号")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False, comment="是否超级用户")
|
||||
last_login_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="最后登录时间"
|
||||
)
|
||||
email_verified_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="邮箱验证时间"
|
||||
)
|
||||
phone_verified_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="手机验证时间"
|
||||
)
|
||||
|
||||
# 关系
|
||||
roles: Mapped[List[Role]] = relationship(
|
||||
"Role",
|
||||
secondary=user_role_table,
|
||||
back_populates="users",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
# 创建的需求
|
||||
created_requirements: Mapped[List["Requirement"]] = relationship(
|
||||
"Requirement",
|
||||
foreign_keys="Requirement.creator_id",
|
||||
back_populates="creator",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
# 分配的需求
|
||||
assigned_requirements: Mapped[List["Requirement"]] = relationship(
|
||||
"Requirement",
|
||||
foreign_keys="Requirement.assignee_id",
|
||||
back_populates="assignee",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
# 审核的需求
|
||||
reviewed_requirements: Mapped[List["Requirement"]] = relationship(
|
||||
"Requirement",
|
||||
foreign_keys="Requirement.reviewer_id",
|
||||
back_populates="reviewer",
|
||||
lazy="selectin"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(username='{self.username}', email='{self.email}')>"
|
||||
|
||||
def has_role(self, role_name: str) -> bool:
|
||||
"""检查是否有指定角色"""
|
||||
return any(role.name == role_name for role in self.roles)
|
||||
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
"""检查是否有指定权限"""
|
||||
# 超级用户拥有所有权限
|
||||
if self.is_superuser:
|
||||
return True
|
||||
|
||||
# 检查所有角色的权限
|
||||
for role in self.roles:
|
||||
if role.is_active and role.has_permission(permission):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_permissions(self) -> List[str]:
|
||||
"""获取用户所有权限"""
|
||||
if self.is_superuser:
|
||||
return ["*"]
|
||||
|
||||
permissions = set()
|
||||
for role in self.roles:
|
||||
if role.is_active and role.permissions:
|
||||
permissions.update(role.permissions)
|
||||
return list(permissions)
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""是否是管理员"""
|
||||
return self.is_superuser or self.has_role("admin")
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
"""显示名称"""
|
||||
return self.full_name or self.username
|
||||
|
||||
def update_last_login(self):
|
||||
"""更新最后登录时间"""
|
||||
self.last_login_at = datetime.utcnow()
|
||||
|
||||
|
||||
class UserRole(BaseModel):
|
||||
"""用户角色关联模型"""
|
||||
|
||||
__tablename__ = "sys_user_role"
|
||||
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), nullable=False, comment="用户ID"
|
||||
)
|
||||
role_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), nullable=False, comment="角色ID"
|
||||
)
|
||||
assigned_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), nullable=False, comment="分配时间"
|
||||
)
|
||||
assigned_by: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="分配者ID"
|
||||
)
|
||||
expires_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="过期时间"
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False, comment="是否激活")
|
||||
|
||||
# 关系
|
||||
user: Mapped[User] = relationship("User", foreign_keys=[user_id])
|
||||
role: Mapped[Role] = relationship("Role", foreign_keys=[role_id])
|
||||
assigned_by_user: Mapped[Optional[User]] = relationship("User", foreign_keys=[assigned_by])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<UserRole(user_id={self.user_id}, role_id={self.role_id})>"
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""是否已过期"""
|
||||
if not self.expires_at:
|
||||
return False
|
||||
return datetime.utcnow() > self.expires_at
|
||||
|
||||
@property
|
||||
def is_valid(self) -> bool:
|
||||
"""是否有效"""
|
||||
return self.is_active and not self.is_expired
|
||||
@@ -1,175 +0,0 @@
|
||||
"""
|
||||
任务管理模型
|
||||
"""
|
||||
from datetime import datetime, date
|
||||
from typing import Optional, List
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Text, SmallInt, DateTime, ForeignKey,
|
||||
JSON, Boolean, Date, UniqueConstraint, CheckConstraint
|
||||
)
|
||||
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel, SoftDeleteMixin, AuditMixin, PriorityMixin
|
||||
|
||||
|
||||
class WorkItem(BaseModel, SoftDeleteMixin, AuditMixin, PriorityMixin):
|
||||
"""工作项模型"""
|
||||
|
||||
__tablename__ = "dev_work_item"
|
||||
|
||||
spec_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('des_spec.id', ondelete='CASCADE'), nullable=False, comment="方案ID"
|
||||
)
|
||||
parent_task_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('dev_work_item.id'), nullable=True, comment="父任务ID"
|
||||
)
|
||||
type: Mapped[str] = mapped_column(
|
||||
String(32), nullable=False,
|
||||
comment="任务类型: feature,bugfix,refactor,test,doc,ops,research"
|
||||
)
|
||||
title: Mapped[str] = mapped_column(String(200), nullable=False, comment="任务标题")
|
||||
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True, comment="任务描述")
|
||||
acceptance_criteria: Mapped[str] = mapped_column(Text, nullable=False, comment="验收标准")
|
||||
effort_estimate: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="预估工作量")
|
||||
effort_actual: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment="实际工作量")
|
||||
story_points: Mapped[Optional[int]] = mapped_column(
|
||||
SmallInt, nullable=True, comment="故事点数 1-13"
|
||||
)
|
||||
owner_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="负责人ID"
|
||||
)
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(32), default="pending", nullable=False,
|
||||
comment="状态: pending,in_progress,in_review,blocked,done,cancelled"
|
||||
)
|
||||
labels: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="标签列表")
|
||||
dependencies: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="依赖关系")
|
||||
related_repos: Mapped[dict] = mapped_column(JSON, default=list, nullable=False, comment="相关仓库")
|
||||
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True, comment="截止日期")
|
||||
started_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="开始时间"
|
||||
)
|
||||
completed_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True, comment="完成时间"
|
||||
)
|
||||
|
||||
# 关联用户
|
||||
creator_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=False, comment="创建者ID"
|
||||
)
|
||||
assignee_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey('sys_user.id'), nullable=True, comment="分配者ID"
|
||||
)
|
||||
|
||||
# 关系
|
||||
spec: Mapped["Spec"] = relationship("Spec", back_populates="work_items")
|
||||
parent_task: Mapped[Optional["WorkItem"]] = relationship(
|
||||
"WorkItem", remote_side="WorkItem.id", back_populates="subtasks"
|
||||
)
|
||||
subtasks: Mapped[List["WorkItem"]] = relationship(
|
||||
"WorkItem", back_populates="parent_task", cascade="all, delete-orphan"
|
||||
)
|
||||
creator: Mapped["User"] = relationship("User", foreign_keys=[creator_id])
|
||||
owner: Mapped[Optional["User"]] = relationship("User", foreign_keys=[owner_id])
|
||||
assignee: Mapped[Optional["User"]] = relationship("User", foreign_keys=[assignee_id])
|
||||
|
||||
# 执行记录
|
||||
execution_runs: Mapped[List["ExecutionRun"]] = relationship(
|
||||
"ExecutionRun", back_populates="work_item", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
# 依赖关系
|
||||
dependencies_out: Mapped[List["WorkItemDependency"]] = relationship(
|
||||
"WorkItemDependency", foreign_keys="WorkItemDependency.predecessor_id",
|
||||
back_populates="predecessor", cascade="all, delete-orphan"
|
||||
)
|
||||
dependencies_in: Mapped[List["WorkItemDependency"]] = relationship(
|
||||
"WorkItemDependency", foreign_keys="WorkItemDependency.successor_id",
|
||||
back_populates="successor", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkItem(id={self.id}, title='{self.title}', status='{self.status}')>"
|
||||
|
||||
@property
|
||||
def is_done(self) -> bool:
|
||||
"""是否已完成"""
|
||||
return self.status == "done"
|
||||
|
||||
@property
|
||||
def is_blocked(self) -> bool:
|
||||
"""是否被阻塞"""
|
||||
return self.status == "blocked"
|
||||
|
||||
@property
|
||||
def is_active(self) -> bool:
|
||||
"""是否进行中"""
|
||||
return self.status in ["pending", "in_progress", "in_review"]
|
||||
|
||||
@property
|
||||
def effort_variance(self) -> Optional[int]:
|
||||
"""工作量差异"""
|
||||
if self.effort_estimate and self.effort_actual:
|
||||
return self.effort_actual - self.effort_estimate
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_overdue(self) -> bool:
|
||||
"""是否逾期"""
|
||||
if not self.due_date or self.is_done:
|
||||
return False
|
||||
return datetime.utcnow().date() > self.due_date
|
||||
|
||||
def start(self):
|
||||
"""开始任务"""
|
||||
self.status = "in_progress"
|
||||
self.started_at = datetime.utcnow()
|
||||
|
||||
def complete(self):
|
||||
"""完成任务"""
|
||||
self.status = "done"
|
||||
self.completed_at = datetime.utcnow()
|
||||
|
||||
def block(self):
|
||||
"""阻塞任务"""
|
||||
self.status = "blocked"
|
||||
|
||||
def unblock(self):
|
||||
"""解除阻塞"""
|
||||
if self.status == "blocked":
|
||||
self.status = "in_progress"
|
||||
|
||||
|
||||
class WorkItemDependency(BaseModel):
|
||||
"""工作项依赖关系模型"""
|
||||
|
||||
__tablename__ = "dev_work_item_dependency"
|
||||
|
||||
predecessor_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('dev_work_item.id', ondelete='CASCADE'), nullable=False, comment="前置任务ID"
|
||||
)
|
||||
successor_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey('dev_work_item.id', ondelete='CASCADE'), nullable=False, comment="后置任务ID"
|
||||
)
|
||||
dependency_type: Mapped[str] = mapped_column(
|
||||
String(20), default="finish_to_start", nullable=False,
|
||||
comment="依赖类型: finish_to_start,start_to_start,finish_to_finish,start_to_finish"
|
||||
)
|
||||
|
||||
# 关系
|
||||
predecessor: Mapped["WorkItem"] = relationship(
|
||||
"WorkItem", foreign_keys=[predecessor_id], back_populates="dependencies_out"
|
||||
)
|
||||
successor: Mapped["WorkItem"] = relationship(
|
||||
"WorkItem", foreign_keys=[successor_id], back_populates="dependencies_in"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkItemDependency(predecessor={self.predecessor_id}, successor={self.successor_id})>"
|
||||
|
||||
@property
|
||||
def is_finish_to_start(self) -> bool:
|
||||
"""是否是完成-开始依赖"""
|
||||
return self.dependency_type == "finish_to_start"
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
Alembic环境配置
|
||||
"""
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
from alembic import context
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目路径到sys.path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
|
||||
from app.core.database import Base
|
||||
from app.core.config import settings
|
||||
from app.models import * # 导入所有模型
|
||||
|
||||
# 获取Alembic配置对象
|
||||
config = context.config
|
||||
|
||||
# 设置数据库URL
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
# 设置日志配置
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 添加模型的MetaData对象
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""在离线模式下运行迁移"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在在线模式下运行迁移"""
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -1,24 +0,0 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -1,134 +0,0 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 001_initial
|
||||
Revises:
|
||||
Create Date: 2025-10-28 21:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '001_initial'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 创建扩展
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "pg_trgm"')
|
||||
|
||||
# 创建用户和权限表
|
||||
op.create_table('sys_user',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('username', sa.String(length=50), nullable=False),
|
||||
sa.Column('email', sa.String(length=100), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=255), nullable=False),
|
||||
sa.Column('full_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('avatar_url', sa.String(length=255), nullable=True),
|
||||
sa.Column('phone', sa.String(length=20), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('is_superuser', sa.Boolean(), nullable=False),
|
||||
sa.Column('last_login_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('email_verified_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('phone_verified_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('create_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('update_time', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('create_user', sa.String(length=40), nullable=True),
|
||||
sa.Column('update_user', sa.String(length=40), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_index('idx_sys_user_username', 'sys_user', ['username'], unique=False)
|
||||
op.create_index('idx_sys_user_email', 'sys_user', ['email'], unique=False)
|
||||
op.create_index('idx_sys_user_active', 'sys_user', ['is_active'], unique=False)
|
||||
|
||||
op.create_table('sys_role',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=False),
|
||||
sa.Column('display_name', sa.String(length=100), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('permissions', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('create_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('update_time', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('create_user', sa.String(length=40), nullable=True),
|
||||
sa.Column('update_user', sa.String(length=40), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_index('idx_sys_role_name', 'sys_role', ['name'], unique=False)
|
||||
op.create_index('idx_sys_role_active', 'sys_role', ['is_active'], unique=False)
|
||||
|
||||
# 创建需求表
|
||||
op.create_table('req_requirement',
|
||||
sa.Column('id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('title', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=False),
|
||||
sa.Column('priority', sa.SmallInteger(), nullable=False),
|
||||
sa.Column('status', sa.String(length=32), nullable=False),
|
||||
sa.Column('tags', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column('business_value', sa.Text(), nullable=True),
|
||||
sa.Column('acceptance_criteria', sa.Text(), nullable=True),
|
||||
sa.Column('estimated_effort', sa.Integer(), nullable=True),
|
||||
sa.Column('actual_effort', sa.Integer(), nullable=True),
|
||||
sa.Column('complexity_level', sa.SmallInteger(), nullable=False),
|
||||
sa.Column('risk_level', sa.SmallInteger(), nullable=False),
|
||||
sa.Column('assignee_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('reviewer_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('creator_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('approved_by', sa.BigInteger(), nullable=True),
|
||||
sa.Column('approved_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('rejected_by', sa.BigInteger(), nullable=True),
|
||||
sa.Column('rejected_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('closed_by', sa.BigInteger(), nullable=True),
|
||||
sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('create_time', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||
sa.Column('update_time', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('create_user', sa.String(length=40), nullable=True),
|
||||
sa.Column('update_user', sa.String(length=40), nullable=True),
|
||||
sa.Column('is_deleted', sa.SmallInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['assignee_id'], ['sys_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['approved_by'], ['sys_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['closed_by'], ['sys_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['creator_id'], ['sys_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['rejected_by'], ['sys_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['reviewer_id'], ['sys_user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.CheckConstraint('complexity_level BETWEEN 1 AND 5', name='ck_requirement_complexity'),
|
||||
sa.CheckConstraint('is_deleted IN (0, 1)', name='ck_requirement_deleted'),
|
||||
sa.CheckConstraint('priority BETWEEN 1 AND 5', name='ck_requirement_priority'),
|
||||
sa.CheckConstraint('risk_level BETWEEN 1 AND 5', name='ck_requirement_risk'),
|
||||
sa.CheckConstraint("status IN ('draft', 'in_review', 'approved', 'rejected', 'closed', 'cancelled')", name='ck_requirement_status')
|
||||
)
|
||||
|
||||
# 创建更多表...
|
||||
# 这里为了简洁,只创建核心表,其他表的结构类似
|
||||
|
||||
# 插入初始数据
|
||||
op.execute("""
|
||||
INSERT INTO sys_role (name, display_name, description, permissions, create_user) VALUES
|
||||
('super_admin', '超级管理员', '拥有所有权限的系统管理员', '["*"]', 'system'),
|
||||
('admin', '管理员', '平台管理员,负责用户和系统配置', '["user.*", "role.*", "config.*"]', 'system'),
|
||||
('architect', '架构师', '负责技术设计和方案评审', '["requirement.*", "spec.*", "task.read"]', 'system'),
|
||||
('pm', '项目经理', '负责需求管理和项目协调', '["requirement.*", "task.*", "report.*"]', 'system'),
|
||||
('developer', '开发工程师', '负责任务执行和代码开发', '["task.*", "run.*"]', 'system'),
|
||||
('reviewer', '审核员', '负责AI产出审核', '["review.*", "run.read"]', 'system'),
|
||||
('stakeholder', '利益相关者', '只读权限,查看报告和状态', '["dashboard.*", "report.*"]', 'system')
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 删除表(按依赖关系逆序)
|
||||
op.drop_table('req_requirement')
|
||||
op.drop_table('sys_role')
|
||||
op.drop_table('sys_user')
|
||||
|
||||
# 删除扩展
|
||||
op.execute('DROP EXTENSION IF EXISTS "pg_trgm"')
|
||||
op.execute('DROP EXTENSION IF EXISTS "uuid-ossp"')
|
||||
@@ -1,6 +0,0 @@
|
||||
# 最小化依赖 - 用于快速启动
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
python-dotenv==1.0.0
|
||||
structlog==23.2.0
|
||||
@@ -1,49 +0,0 @@
|
||||
# FastAPI核心依赖
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
|
||||
# 数据库相关
|
||||
sqlalchemy==2.0.23
|
||||
asyncpg==0.29.0
|
||||
alembic==1.13.1
|
||||
redis[hiredis]==5.0.1
|
||||
|
||||
# 认证和安全
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-multipart==0.0.6
|
||||
|
||||
# HTTP客户端
|
||||
httpx==0.25.2
|
||||
aiohttp==3.9.1
|
||||
|
||||
# 工具库
|
||||
python-dotenv==1.0.0
|
||||
structlog==23.2.0
|
||||
prometheus-client==0.19.0
|
||||
slowapi==0.1.9
|
||||
|
||||
# 开发工具
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
black==23.11.0
|
||||
isort==5.12.0
|
||||
flake8==6.1.0
|
||||
mypy==1.7.1
|
||||
|
||||
# Claude API相关
|
||||
anthropic==0.7.8
|
||||
tiktoken==0.5.2
|
||||
|
||||
# 文件处理
|
||||
minio==7.2.0
|
||||
python-magic==0.4.27
|
||||
|
||||
# 数据验证和序列化
|
||||
email-validator==2.1.0
|
||||
phonenumbers==8.13.25
|
||||
|
||||
# 监控和日志
|
||||
sentry-sdk==1.38.0
|
||||
@@ -1,403 +0,0 @@
|
||||
"""
|
||||
LJWX-Stock AI执行平台 - 简化但功能完整的版本
|
||||
"""
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import sqlite3
|
||||
import datetime
|
||||
import asyncio
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="LJWX-Stock AI执行平台",
|
||||
version="1.0.0",
|
||||
description="企业级AI任务执行和人工审核管理平台"
|
||||
)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 数据库初始化
|
||||
def init_database():
|
||||
conn = sqlite3.connect('ai_platform.db')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
is_superuser BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS requirements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 3,
|
||||
status VARCHAR(20) DEFAULT 'draft',
|
||||
creator_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
requirement_id INTEGER,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
description TEXT,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
assignee_id INTEGER,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (requirement_id) REFERENCES requirements (id)
|
||||
)
|
||||
''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS ai_executions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id INTEGER,
|
||||
prompt TEXT NOT NULL,
|
||||
result TEXT,
|
||||
status VARCHAR(20) DEFAULT 'running',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# 插入默认管理员
|
||||
cursor.execute('INSERT OR IGNORE INTO users (username, email, full_name, is_superuser) VALUES (?, ?, ?, ?)',
|
||||
('admin', '[email protected]', '系统管理员', True))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Pydantic模型
|
||||
class User(BaseModel):
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
full_name: Optional[str] = None
|
||||
is_superuser: bool = False
|
||||
|
||||
class Requirement(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
description: str
|
||||
priority: int
|
||||
status: str
|
||||
created_at: datetime.datetime
|
||||
|
||||
class Task(BaseModel):
|
||||
id: int
|
||||
requirement_id: int
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
status: str
|
||||
assignee_id: Optional[int] = None
|
||||
created_at: datetime.datetime
|
||||
|
||||
class AIExecution(BaseModel):
|
||||
id: int
|
||||
task_id: int
|
||||
prompt: str
|
||||
result: Optional[str] = None
|
||||
status: str
|
||||
created_at: datetime.datetime
|
||||
completed_at: Optional[datetime.datetime] = None
|
||||
|
||||
# 数据库操作
|
||||
def get_db():
|
||||
return sqlite3.connect('ai_platform.db')
|
||||
|
||||
# API路由
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径"""
|
||||
return {
|
||||
"name": "LJWX-Stock AI执行平台",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"message": "欢迎使用LJWX-Stock AI执行平台!",
|
||||
"features": {
|
||||
"用户管理": "✅ 已实现",
|
||||
"需求管理": "✅ 已实现",
|
||||
"任务管理": "✅ 已实现",
|
||||
"AI执行": "✅ 已实现",
|
||||
"审核工作流": "✅ 已实现",
|
||||
"质量门禁": "✅ 已实现"
|
||||
}
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.datetime.now().isoformat(),
|
||||
"version": "1.0.0",
|
||||
"database": "connected"
|
||||
}
|
||||
|
||||
@app.get("/api/v1/users/", response_model=List[User])
|
||||
async def list_users():
|
||||
"""获取用户列表"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users")
|
||||
users = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
User(id=row[0], username=row[1], email=row[2], full_name=row[3], is_superuser=bool(row[4]))
|
||||
for row in users
|
||||
]
|
||||
|
||||
@app.get("/api/v1/requirements/", response_model=List[Requirement])
|
||||
async def list_requirements():
|
||||
"""获取需求列表"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM requirements ORDER BY created_at DESC")
|
||||
requirements = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
Requirement(
|
||||
id=row[0], title=row[1], description=row[2], priority=row[3],
|
||||
status=row[4], created_at=row[5]
|
||||
)
|
||||
for row in requirements
|
||||
]
|
||||
|
||||
@app.post("/api/v1/requirements/", response_model=Requirement)
|
||||
async def create_requirement(data: dict):
|
||||
"""创建新需求"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO requirements (title, description, priority, creator_id) VALUES (?, ?, ?, 1)",
|
||||
(data['title'], data['description'], data.get('priority', 3))
|
||||
)
|
||||
req_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT * FROM requirements WHERE id = ?", (req_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return Requirement(
|
||||
id=row[0], title=row[1], description=row[2], priority=row[3],
|
||||
status=row[4], created_at=row[5]
|
||||
)
|
||||
|
||||
@app.get("/api/v1/tasks/", response_model=List[Task])
|
||||
async def list_tasks():
|
||||
"""获取任务列表"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM tasks ORDER BY created_at DESC")
|
||||
tasks = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
Task(
|
||||
id=row[0], requirement_id=row[1], title=row[2], description=row[3],
|
||||
status=row[4], assignee_id=row[5], created_at=row[6]
|
||||
)
|
||||
for row in tasks
|
||||
]
|
||||
|
||||
@app.post("/api/v1/tasks/", response_model=Task)
|
||||
async def create_task(data: dict):
|
||||
"""创建新任务"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO tasks (requirement_id, title, description) VALUES (?, ?, ?)",
|
||||
(data['requirement_id'], data['title'], data.get('description'))
|
||||
)
|
||||
task_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT * FROM tasks WHERE id = ?", (task_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return Task(
|
||||
id=row[0], requirement_id=row[1], title=row[2], description=row[3],
|
||||
status=row[4], assignee_id=row[5], created_at=row[6]
|
||||
)
|
||||
|
||||
@app.get("/api/v1/executions/", response_model=List[AIExecution])
|
||||
async def list_executions():
|
||||
"""获取AI执行记录"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM ai_executions ORDER BY created_at DESC")
|
||||
executions = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [
|
||||
AIExecution(
|
||||
id=row[0], task_id=row[1], prompt=row[2], result=row[3],
|
||||
status=row[4], created_at=row[5],
|
||||
completed_at=row[6] if row[6] else None
|
||||
)
|
||||
for row in executions
|
||||
]
|
||||
|
||||
@app.post("/api/v1/executions/", response_model=AIExecution)
|
||||
async def create_execution(data: dict):
|
||||
"""创建AI执行"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 更新任务状态
|
||||
cursor.execute("UPDATE tasks SET status = 'in_progress' WHERE id = ?", (data['task_id'],))
|
||||
|
||||
# 创建执行记录
|
||||
cursor.execute(
|
||||
"INSERT INTO ai_executions (task_id, prompt, status) VALUES (?, ?, 'running')",
|
||||
(data['task_id'], data['prompt'])
|
||||
)
|
||||
exec_id = cursor.lastrowid
|
||||
conn.commit()
|
||||
|
||||
# 模拟AI执行
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 模拟结果
|
||||
result = f"已完成任务: {data['prompt']}\\n状态: 成功\\n质量评分: 9.5/10"
|
||||
|
||||
# 更新执行结果
|
||||
cursor.execute(
|
||||
"UPDATE ai_executions SET result = ?, status = 'completed', completed_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
(result, exec_id)
|
||||
)
|
||||
|
||||
# 更新任务状态
|
||||
cursor.execute("UPDATE tasks SET status = 'completed' WHERE id = ?", (data['task_id'],))
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT * FROM ai_executions WHERE id = ?", (exec_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return AIExecution(
|
||||
id=row[0], task_id=row[1], prompt=row[2], result=row[3],
|
||||
status=row[4], created_at=row[5],
|
||||
completed_at=row[6]
|
||||
)
|
||||
|
||||
@app.get("/api/v1/stats/")
|
||||
async def get_stats():
|
||||
"""获取统计信息"""
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM requirements")
|
||||
total_req = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks")
|
||||
total_tasks = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM tasks WHERE status = 'completed'")
|
||||
completed_tasks = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM ai_executions")
|
||||
total_exec = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM ai_executions WHERE status = 'completed'")
|
||||
completed_exec = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
completion_rate = (completed_tasks / total_tasks * 100) if total_tasks > 0 else 0
|
||||
success_rate = (completed_exec / total_exec * 100) if total_exec > 0 else 0
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"requirements": {"total": total_req},
|
||||
"tasks": {
|
||||
"total": total_tasks,
|
||||
"completed": completed_tasks,
|
||||
"completion_rate": round(completion_rate, 1)
|
||||
},
|
||||
"ai_executions": {
|
||||
"total": total_exec,
|
||||
"completed": completed_exec,
|
||||
"success_rate": round(success_rate, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@app.get("/api/v1/claude/instances")
|
||||
async def list_claude_instances():
|
||||
"""获取Claude实例"""
|
||||
instances = [
|
||||
{
|
||||
"id": "claude_architect_001",
|
||||
"name": "系统架构师",
|
||||
"specialization": "系统设计、架构规划",
|
||||
"status": "online",
|
||||
"utilization": 0.85,
|
||||
"tasks_completed": 5,
|
||||
"avg_quality_score": 9.1
|
||||
},
|
||||
{
|
||||
"id": "claude_backend_001",
|
||||
"name": "后端开发工程师",
|
||||
"specialization": "API开发、数据库设计",
|
||||
"status": "online",
|
||||
"utilization": 0.70,
|
||||
"tasks_completed": 3,
|
||||
"avg_quality_score": 8.9
|
||||
},
|
||||
{
|
||||
"id": "claude_frontend_001",
|
||||
"name": "前端开发工程师",
|
||||
"specialization": "UI/UX设计、前端框架",
|
||||
"status": "idle",
|
||||
"utilization": 0.0,
|
||||
"tasks_completed": 0,
|
||||
"avg_quality_score": 0.0
|
||||
}
|
||||
]
|
||||
|
||||
return {"success": True, "data": instances, "total": len(instances), "active": len([i for i in instances if i["status"] == "online"])}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("🚀 LJWX-Stock AI执行平台启动中...")
|
||||
print("📊 初始化数据库...")
|
||||
init_database()
|
||||
print("✅ 数据库初始化完成")
|
||||
print("🎉 系统启动完成!")
|
||||
print("\n📍 访问地址: http://localhost:8000")
|
||||
print("📖 API文档: http://localhost:8000/docs")
|
||||
|
||||
uvicorn.run(
|
||||
"simple_ai_platform:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=False,
|
||||
log_level="info"
|
||||
)
|
||||
@@ -1,201 +0,0 @@
|
||||
"""
|
||||
AI执行平台简化启动版本
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="LJWX-Stock AI执行平台",
|
||||
version="1.0.0",
|
||||
description="企业级AI任务执行和人工审核管理平台"
|
||||
)
|
||||
|
||||
# 添加CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 根路径
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径 - 系统信息"""
|
||||
return {
|
||||
"name": "LJWX-Stock AI执行平台",
|
||||
"version": "1.0.0",
|
||||
"status": "running",
|
||||
"message": "欢迎使用LJWX-Stock AI执行平台!"
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": "2025-10-28T00:00:00Z",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
# 系统信息
|
||||
@app.get("/info")
|
||||
async def system_info():
|
||||
"""系统信息"""
|
||||
return {
|
||||
"app_name": "LJWX-Stock AI执行平台",
|
||||
"version": "1.0.0",
|
||||
"features": {
|
||||
"用户管理": "✅ 已实现",
|
||||
"需求管理": "✅ 已实现",
|
||||
"任务管理": "✅ 已实现",
|
||||
"AI执行": "✅ 已实现",
|
||||
"审核工作流": "✅ 已实现",
|
||||
"质量门禁": "✅ 已实现",
|
||||
"Gitea集成": "✅ 已实现"
|
||||
}
|
||||
}
|
||||
|
||||
# 模拟需求管理API
|
||||
@app.get("/api/v1/requirements/")
|
||||
async def list_requirements():
|
||||
"""获取需求列表"""
|
||||
return {
|
||||
"success": True,
|
||||
"code": 200,
|
||||
"message": "获取需求列表成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "req-001",
|
||||
"title": "用户权限管理系统",
|
||||
"description": "企业级权限管理,支持RBAC模型",
|
||||
"priority": 1,
|
||||
"status": "approved",
|
||||
"tags": ["权限", "RBAC", "安全"],
|
||||
"created_at": "2025-10-28T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "req-002",
|
||||
"title": "数据分析平台",
|
||||
"description": "股票数据分析和可视化平台",
|
||||
"priority": 2,
|
||||
"status": "reviewing",
|
||||
"tags": ["数据分析", "可视化", "股票"],
|
||||
"created_at": "2025-10-28T01:00:00Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"total": 2,
|
||||
"pages": 1
|
||||
}
|
||||
}
|
||||
|
||||
# 模拟任务管理API
|
||||
@app.get("/api/v1/tasks/")
|
||||
async def list_tasks():
|
||||
"""获取任务列表"""
|
||||
return {
|
||||
"success": True,
|
||||
"code": 200,
|
||||
"message": "获取任务列表成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "task-001",
|
||||
"name": "权限数据库设计",
|
||||
"requirement_id": "req-001",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"assigned_to": "claude_architect_001",
|
||||
"quality_score": 9.2
|
||||
},
|
||||
{
|
||||
"id": "task-002",
|
||||
"name": "JWT认证服务开发",
|
||||
"requirement_id": "req-001",
|
||||
"status": "in_progress",
|
||||
"progress": 75,
|
||||
"assigned_to": "claude_backend_001",
|
||||
"quality_score": None
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟Claude实例API
|
||||
@app.get("/api/v1/claude/instances")
|
||||
async def list_claude_instances():
|
||||
"""获取Claude实例列表"""
|
||||
return {
|
||||
"success": True,
|
||||
"code": 200,
|
||||
"message": "获取Claude实例成功",
|
||||
"data": [
|
||||
{
|
||||
"id": "claude_architect_001",
|
||||
"name": "系统架构师",
|
||||
"specialization": "系统设计、架构规划",
|
||||
"status": "online",
|
||||
"utilization": 0.85,
|
||||
"tasks_completed": 5,
|
||||
"avg_quality_score": 9.1
|
||||
},
|
||||
{
|
||||
"id": "claude_backend_001",
|
||||
"name": "后端开发工程师",
|
||||
"specialization": "API开发、数据库设计",
|
||||
"status": "online",
|
||||
"utilization": 0.70,
|
||||
"tasks_completed": 3,
|
||||
"avg_quality_score": 8.9
|
||||
},
|
||||
{
|
||||
"id": "claude_frontend_001",
|
||||
"name": "前端开发工程师",
|
||||
"specialization": "UI/UX、前端框架",
|
||||
"status": "idle",
|
||||
"utilization": 0.0,
|
||||
"tasks_completed": 0,
|
||||
"avg_quality_score": 0.0
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# 模拟监控API
|
||||
@app.get("/api/v1/monitoring/stats")
|
||||
async def get_monitoring_stats():
|
||||
"""获取监控统计"""
|
||||
return {
|
||||
"success": True,
|
||||
"code": 200,
|
||||
"message": "获取监控统计成功",
|
||||
"data": {
|
||||
"total_requirements": 2,
|
||||
"total_tasks": 20,
|
||||
"completed_tasks": 18,
|
||||
"in_progress_tasks": 2,
|
||||
"active_claude_instances": 3,
|
||||
"system_health": "excellent",
|
||||
"avg_completion_time": "2.5分钟",
|
||||
"overall_quality_score": 9.01
|
||||
}
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 LJWX-Stock AI执行平台启动中...")
|
||||
print("📍 API服务地址: http://localhost:8000")
|
||||
print("📖 API文档地址: http://localhost:8000/docs")
|
||||
print("🏥 健康检查地址: http://localhost:8000/health")
|
||||
print("\n✅ 服务启动成功!")
|
||||
|
||||
uvicorn.run(
|
||||
"simple_app:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=False,
|
||||
log_level="info",
|
||||
)
|
||||
@@ -1,247 +0,0 @@
|
||||
<#
|
||||
.Synopsis
|
||||
Activate a Python virtual environment for the current PowerShell session.
|
||||
|
||||
.Description
|
||||
Pushes the python executable for a virtual environment to the front of the
|
||||
$Env:PATH environment variable and sets the prompt to signify that you are
|
||||
in a Python virtual environment. Makes use of the command line switches as
|
||||
well as the `pyvenv.cfg` file values present in the virtual environment.
|
||||
|
||||
.Parameter VenvDir
|
||||
Path to the directory that contains the virtual environment to activate. The
|
||||
default value for this is the parent of the directory that the Activate.ps1
|
||||
script is located within.
|
||||
|
||||
.Parameter Prompt
|
||||
The prompt prefix to display when this virtual environment is activated. By
|
||||
default, this prompt is the name of the virtual environment folder (VenvDir)
|
||||
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
|
||||
|
||||
.Example
|
||||
Activate.ps1
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Verbose
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and shows extra information about the activation as it executes.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
|
||||
Activates the Python virtual environment located in the specified location.
|
||||
|
||||
.Example
|
||||
Activate.ps1 -Prompt "MyPython"
|
||||
Activates the Python virtual environment that contains the Activate.ps1 script,
|
||||
and prefixes the current prompt with the specified string (surrounded in
|
||||
parentheses) while the virtual environment is active.
|
||||
|
||||
.Notes
|
||||
On Windows, it may be required to enable this Activate.ps1 script by setting the
|
||||
execution policy for the user. You can do this by issuing the following PowerShell
|
||||
command:
|
||||
|
||||
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||
|
||||
For more information on Execution Policies:
|
||||
https://go.microsoft.com/fwlink/?LinkID=135170
|
||||
|
||||
#>
|
||||
Param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$VenvDir,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[String]
|
||||
$Prompt
|
||||
)
|
||||
|
||||
<# Function declarations --------------------------------------------------- #>
|
||||
|
||||
<#
|
||||
.Synopsis
|
||||
Remove all shell session elements added by the Activate script, including the
|
||||
addition of the virtual environment's Python executable from the beginning of
|
||||
the PATH variable.
|
||||
|
||||
.Parameter NonDestructive
|
||||
If present, do not remove this function from the global namespace for the
|
||||
session.
|
||||
|
||||
#>
|
||||
function global:deactivate ([switch]$NonDestructive) {
|
||||
# Revert to original values
|
||||
|
||||
# The prior prompt:
|
||||
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
|
||||
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
|
||||
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
|
||||
# The prior PYTHONHOME:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
}
|
||||
|
||||
# The prior PATH:
|
||||
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
|
||||
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
|
||||
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
|
||||
}
|
||||
|
||||
# Just remove the VIRTUAL_ENV altogether:
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV
|
||||
}
|
||||
|
||||
# Just remove VIRTUAL_ENV_PROMPT altogether.
|
||||
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
|
||||
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
|
||||
}
|
||||
|
||||
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
|
||||
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
|
||||
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
|
||||
}
|
||||
|
||||
# Leave deactivate function in the global namespace if requested:
|
||||
if (-not $NonDestructive) {
|
||||
Remove-Item -Path function:deactivate
|
||||
}
|
||||
}
|
||||
|
||||
<#
|
||||
.Description
|
||||
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
|
||||
given folder, and returns them in a map.
|
||||
|
||||
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
|
||||
two strings separated by `=` (with any amount of whitespace surrounding the =)
|
||||
then it is considered a `key = value` line. The left hand string is the key,
|
||||
the right hand is the value.
|
||||
|
||||
If the value starts with a `'` or a `"` then the first and last character is
|
||||
stripped from the value before being captured.
|
||||
|
||||
.Parameter ConfigDir
|
||||
Path to the directory that contains the `pyvenv.cfg` file.
|
||||
#>
|
||||
function Get-PyVenvConfig(
|
||||
[String]
|
||||
$ConfigDir
|
||||
) {
|
||||
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
|
||||
|
||||
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
|
||||
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
|
||||
|
||||
# An empty map will be returned if no config file is found.
|
||||
$pyvenvConfig = @{ }
|
||||
|
||||
if ($pyvenvConfigPath) {
|
||||
|
||||
Write-Verbose "File exists, parse `key = value` lines"
|
||||
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
|
||||
|
||||
$pyvenvConfigContent | ForEach-Object {
|
||||
$keyval = $PSItem -split "\s*=\s*", 2
|
||||
if ($keyval[0] -and $keyval[1]) {
|
||||
$val = $keyval[1]
|
||||
|
||||
# Remove extraneous quotations around a string value.
|
||||
if ("'""".Contains($val.Substring(0, 1))) {
|
||||
$val = $val.Substring(1, $val.Length - 2)
|
||||
}
|
||||
|
||||
$pyvenvConfig[$keyval[0]] = $val
|
||||
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
|
||||
}
|
||||
}
|
||||
}
|
||||
return $pyvenvConfig
|
||||
}
|
||||
|
||||
|
||||
<# Begin Activate script --------------------------------------------------- #>
|
||||
|
||||
# Determine the containing directory of this script
|
||||
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
$VenvExecDir = Get-Item -Path $VenvExecPath
|
||||
|
||||
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
|
||||
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
|
||||
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
|
||||
|
||||
# Set values required in priority: CmdLine, ConfigFile, Default
|
||||
# First, get the location of the virtual environment, it might not be
|
||||
# VenvExecDir if specified on the command line.
|
||||
if ($VenvDir) {
|
||||
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
|
||||
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
|
||||
Write-Verbose "VenvDir=$VenvDir"
|
||||
}
|
||||
|
||||
# Next, read the `pyvenv.cfg` file to determine any required value such
|
||||
# as `prompt`.
|
||||
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
|
||||
|
||||
# Next, set the prompt from the command line, or the config file, or
|
||||
# just use the name of the virtual environment folder.
|
||||
if ($Prompt) {
|
||||
Write-Verbose "Prompt specified as argument, using '$Prompt'"
|
||||
}
|
||||
else {
|
||||
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
|
||||
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
|
||||
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
|
||||
$Prompt = $pyvenvCfg['prompt'];
|
||||
}
|
||||
else {
|
||||
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
|
||||
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
|
||||
$Prompt = Split-Path -Path $venvDir -Leaf
|
||||
}
|
||||
}
|
||||
|
||||
Write-Verbose "Prompt = '$Prompt'"
|
||||
Write-Verbose "VenvDir='$VenvDir'"
|
||||
|
||||
# Deactivate any currently active virtual environment, but leave the
|
||||
# deactivate function in place.
|
||||
deactivate -nondestructive
|
||||
|
||||
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
|
||||
# that there is an activated venv.
|
||||
$env:VIRTUAL_ENV = $VenvDir
|
||||
|
||||
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||
|
||||
Write-Verbose "Setting prompt to '$Prompt'"
|
||||
|
||||
# Set the prompt to include the env name
|
||||
# Make sure _OLD_VIRTUAL_PROMPT is global
|
||||
function global:_OLD_VIRTUAL_PROMPT { "" }
|
||||
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
|
||||
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
|
||||
|
||||
function global:prompt {
|
||||
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
|
||||
_OLD_VIRTUAL_PROMPT
|
||||
}
|
||||
$env:VIRTUAL_ENV_PROMPT = $Prompt
|
||||
}
|
||||
|
||||
# Clear PYTHONHOME
|
||||
if (Test-Path -Path Env:PYTHONHOME) {
|
||||
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
|
||||
Remove-Item -Path Env:PYTHONHOME
|
||||
}
|
||||
|
||||
# Add the venv to the PATH
|
||||
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
|
||||
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
|
||||
@@ -1,63 +0,0 @@
|
||||
# This file must be used with "source bin/activate" *from bash*
|
||||
# you cannot run it directly
|
||||
|
||||
deactivate () {
|
||||
# reset old environment variables
|
||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||
export PATH
|
||||
unset _OLD_VIRTUAL_PATH
|
||||
fi
|
||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||
export PYTHONHOME
|
||||
unset _OLD_VIRTUAL_PYTHONHOME
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
|
||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||
export PS1
|
||||
unset _OLD_VIRTUAL_PS1
|
||||
fi
|
||||
|
||||
unset VIRTUAL_ENV
|
||||
unset VIRTUAL_ENV_PROMPT
|
||||
if [ ! "${1:-}" = "nondestructive" ] ; then
|
||||
# Self destruct!
|
||||
unset -f deactivate
|
||||
fi
|
||||
}
|
||||
|
||||
# unset irrelevant variables
|
||||
deactivate nondestructive
|
||||
|
||||
VIRTUAL_ENV=/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv
|
||||
export VIRTUAL_ENV
|
||||
|
||||
_OLD_VIRTUAL_PATH="$PATH"
|
||||
PATH="$VIRTUAL_ENV/"bin":$PATH"
|
||||
export PATH
|
||||
|
||||
# unset PYTHONHOME if set
|
||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||
unset PYTHONHOME
|
||||
fi
|
||||
|
||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||
PS1='(venv) '"${PS1:-}"
|
||||
export PS1
|
||||
VIRTUAL_ENV_PROMPT='(venv) '
|
||||
export VIRTUAL_ENV_PROMPT
|
||||
fi
|
||||
|
||||
# Call hash to forget past commands. Without forgetting
|
||||
# past commands the $PATH changes we made may not be respected
|
||||
hash -r 2> /dev/null
|
||||
@@ -1,26 +0,0 @@
|
||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||
# You cannot run it directly.
|
||||
# Created by Davide Di Blasi <[email protected]>.
|
||||
# Ported to Python 3.3 venv by Andrew Svetlov <[email protected]>
|
||||
|
||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
setenv VIRTUAL_ENV /Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv
|
||||
|
||||
set _OLD_VIRTUAL_PATH="$PATH"
|
||||
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
|
||||
|
||||
|
||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||
|
||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||
set prompt = '(venv) '"$prompt"
|
||||
setenv VIRTUAL_ENV_PROMPT '(venv) '
|
||||
endif
|
||||
|
||||
alias pydoc python -m pydoc
|
||||
|
||||
rehash
|
||||
@@ -1,69 +0,0 @@
|
||||
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
|
||||
# (https://fishshell.com/); you cannot run it directly.
|
||||
|
||||
function deactivate -d "Exit virtual environment and return to normal shell environment"
|
||||
# reset old environment variables
|
||||
if test -n "$_OLD_VIRTUAL_PATH"
|
||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||
set -e _OLD_VIRTUAL_PATH
|
||||
end
|
||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||
end
|
||||
|
||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||
# prevents error when using nested fish instances (Issue #93858)
|
||||
if functions -q _old_fish_prompt
|
||||
functions -e fish_prompt
|
||||
functions -c _old_fish_prompt fish_prompt
|
||||
functions -e _old_fish_prompt
|
||||
end
|
||||
end
|
||||
|
||||
set -e VIRTUAL_ENV
|
||||
set -e VIRTUAL_ENV_PROMPT
|
||||
if test "$argv[1]" != "nondestructive"
|
||||
# Self-destruct!
|
||||
functions -e deactivate
|
||||
end
|
||||
end
|
||||
|
||||
# Unset irrelevant variables.
|
||||
deactivate nondestructive
|
||||
|
||||
set -gx VIRTUAL_ENV /Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv
|
||||
|
||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
|
||||
|
||||
# Unset PYTHONHOME if set.
|
||||
if set -q PYTHONHOME
|
||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||
set -e PYTHONHOME
|
||||
end
|
||||
|
||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||
# fish uses a function instead of an env var to generate the prompt.
|
||||
|
||||
# Save the current fish_prompt function as the function _old_fish_prompt.
|
||||
functions -c fish_prompt _old_fish_prompt
|
||||
|
||||
# With the original prompt function renamed, we can override with our own.
|
||||
function fish_prompt
|
||||
# Save the return status of the last command.
|
||||
set -l old_status $status
|
||||
|
||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
|
||||
|
||||
# Restore the return status of the previous command.
|
||||
echo "exit $old_status" | .
|
||||
# Output the original/"old" prompt.
|
||||
_old_fish_prompt
|
||||
end
|
||||
|
||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||
set -gx VIRTUAL_ENV_PROMPT '(venv) '
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from dotenv.__main__ import cli
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(cli())
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from pip._internal.cli.main import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
@@ -1 +0,0 @@
|
||||
python3.11
|
||||
@@ -1 +0,0 @@
|
||||
python3.11
|
||||
@@ -1 +0,0 @@
|
||||
/opt/homebrew/opt/[email protected]/bin/python3.11
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from uvicorn.main import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from watchfiles.cli import cli
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(cli())
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/Users/brunogao/work/codes/stock/ai_stock/apps/ai-execution-platform/backend/venv/bin/python3.11
|
||||
import sys
|
||||
from websockets.cli import main
|
||||
if __name__ == '__main__':
|
||||
if sys.argv[0].endswith('.exe'):
|
||||
sys.argv[0] = sys.argv[0][:-4]
|
||||
sys.exit(main())
|
||||
BIN
Binary file not shown.
-239
@@ -1,239 +0,0 @@
|
||||
# don't import any costly modules
|
||||
import os
|
||||
import sys
|
||||
|
||||
report_url = (
|
||||
"https://github.com/pypa/setuptools/issues/new?template=distutils-deprecation.yml"
|
||||
)
|
||||
|
||||
|
||||
def warn_distutils_present():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Distutils was imported before Setuptools, but importing Setuptools "
|
||||
"also replaces the `distutils` module in `sys.modules`. This may lead "
|
||||
"to undesirable behaviors or errors. To avoid these issues, avoid "
|
||||
"using distutils directly, ensure that setuptools is installed in the "
|
||||
"traditional way (e.g. not an editable install), and/or make sure "
|
||||
"that setuptools is always imported before distutils."
|
||||
)
|
||||
|
||||
|
||||
def clear_distutils():
|
||||
if 'distutils' not in sys.modules:
|
||||
return
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Setuptools is replacing distutils. Support for replacing "
|
||||
"an already imported distutils is deprecated. In the future, "
|
||||
"this condition will fail. "
|
||||
f"Register concerns at {report_url}"
|
||||
)
|
||||
mods = [
|
||||
name
|
||||
for name in sys.modules
|
||||
if name == "distutils" or name.startswith("distutils.")
|
||||
]
|
||||
for name in mods:
|
||||
del sys.modules[name]
|
||||
|
||||
|
||||
def enabled():
|
||||
"""
|
||||
Allow selection of distutils by environment variable.
|
||||
"""
|
||||
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
|
||||
if which == 'stdlib':
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Reliance on distutils from stdlib is deprecated. Users "
|
||||
"must rely on setuptools to provide the distutils module. "
|
||||
"Avoid importing distutils or import setuptools first, "
|
||||
"and avoid setting SETUPTOOLS_USE_DISTUTILS=stdlib. "
|
||||
f"Register concerns at {report_url}"
|
||||
)
|
||||
return which == 'local'
|
||||
|
||||
|
||||
def ensure_local_distutils():
|
||||
import importlib
|
||||
|
||||
clear_distutils()
|
||||
|
||||
# With the DistutilsMetaFinder in place,
|
||||
# perform an import to cause distutils to be
|
||||
# loaded from setuptools._distutils. Ref #2906.
|
||||
with shim():
|
||||
importlib.import_module('distutils')
|
||||
|
||||
# check that submodules load as expected
|
||||
core = importlib.import_module('distutils.core')
|
||||
assert '_distutils' in core.__file__, core.__file__
|
||||
assert 'setuptools._distutils.log' not in sys.modules
|
||||
|
||||
|
||||
def do_override():
|
||||
"""
|
||||
Ensure that the local copy of distutils is preferred over stdlib.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
|
||||
for more motivation.
|
||||
"""
|
||||
if enabled():
|
||||
warn_distutils_present()
|
||||
ensure_local_distutils()
|
||||
|
||||
|
||||
class _TrivialRe:
|
||||
def __init__(self, *patterns) -> None:
|
||||
self._patterns = patterns
|
||||
|
||||
def match(self, string):
|
||||
return all(pat in string for pat in self._patterns)
|
||||
|
||||
|
||||
class DistutilsMetaFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
# optimization: only consider top level modules and those
|
||||
# found in the CPython test suite.
|
||||
if path is not None and not fullname.startswith('test.'):
|
||||
return None
|
||||
|
||||
method_name = 'spec_for_{fullname}'.format(**locals())
|
||||
method = getattr(self, method_name, lambda: None)
|
||||
return method()
|
||||
|
||||
def spec_for_distutils(self):
|
||||
if self.is_cpython():
|
||||
return None
|
||||
|
||||
import importlib
|
||||
import importlib.abc
|
||||
import importlib.util
|
||||
|
||||
try:
|
||||
mod = importlib.import_module('setuptools._distutils')
|
||||
except Exception:
|
||||
# There are a couple of cases where setuptools._distutils
|
||||
# may not be present:
|
||||
# - An older Setuptools without a local distutils is
|
||||
# taking precedence. Ref #2957.
|
||||
# - Path manipulation during sitecustomize removes
|
||||
# setuptools from the path but only after the hook
|
||||
# has been loaded. Ref #2980.
|
||||
# In either case, fall back to stdlib behavior.
|
||||
return None
|
||||
|
||||
class DistutilsLoader(importlib.abc.Loader):
|
||||
def create_module(self, spec):
|
||||
mod.__name__ = 'distutils'
|
||||
return mod
|
||||
|
||||
def exec_module(self, module):
|
||||
pass
|
||||
|
||||
return importlib.util.spec_from_loader(
|
||||
'distutils', DistutilsLoader(), origin=mod.__file__
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_cpython():
|
||||
"""
|
||||
Suppress supplying distutils for CPython (build and tests).
|
||||
Ref #2965 and #3007.
|
||||
"""
|
||||
return os.path.isfile('pybuilddir.txt')
|
||||
|
||||
def spec_for_pip(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running under pip.
|
||||
See pypa/pip#8761 for rationale.
|
||||
"""
|
||||
if sys.version_info >= (3, 12) or self.pip_imported_during_build():
|
||||
return
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
@classmethod
|
||||
def pip_imported_during_build(cls):
|
||||
"""
|
||||
Detect if pip is being imported in a build script. Ref #2355.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
return any(
|
||||
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def frame_file_is_setup(frame):
|
||||
"""
|
||||
Return True if the indicated frame suggests a setup.py file.
|
||||
"""
|
||||
# some frames may not have __file__ (#2940)
|
||||
return frame.f_globals.get('__file__', '').endswith('setup.py')
|
||||
|
||||
def spec_for_sensitive_tests(self):
|
||||
"""
|
||||
Ensure stdlib distutils when running select tests under CPython.
|
||||
|
||||
python/cpython#91169
|
||||
"""
|
||||
clear_distutils()
|
||||
self.spec_for_distutils = lambda: None
|
||||
|
||||
sensitive_tests = (
|
||||
[
|
||||
'test.test_distutils',
|
||||
'test.test_peg_generator',
|
||||
'test.test_importlib',
|
||||
]
|
||||
if sys.version_info < (3, 10)
|
||||
else [
|
||||
'test.test_distutils',
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
for name in DistutilsMetaFinder.sensitive_tests:
|
||||
setattr(
|
||||
DistutilsMetaFinder,
|
||||
f'spec_for_{name}',
|
||||
DistutilsMetaFinder.spec_for_sensitive_tests,
|
||||
)
|
||||
|
||||
|
||||
DISTUTILS_FINDER = DistutilsMetaFinder()
|
||||
|
||||
|
||||
def add_shim():
|
||||
DISTUTILS_FINDER in sys.meta_path or insert_shim()
|
||||
|
||||
|
||||
class shim:
|
||||
def __enter__(self) -> None:
|
||||
insert_shim()
|
||||
|
||||
def __exit__(self, exc: object, value: object, tb: object) -> None:
|
||||
_remove_shim()
|
||||
|
||||
|
||||
def insert_shim():
|
||||
sys.meta_path.insert(0, DISTUTILS_FINDER)
|
||||
|
||||
|
||||
def _remove_shim():
|
||||
try:
|
||||
sys.meta_path.remove(DISTUTILS_FINDER)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
if sys.version_info < (3, 12):
|
||||
# DistutilsMetaFinder can only be disabled in Python < 3.12 (PEP 632)
|
||||
remove_shim = _remove_shim
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
__import__('_distutils_hack').do_override()
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
# This is a stub package designed to roughly emulate the _yaml
|
||||
# extension module, which previously existed as a standalone module
|
||||
# and has been moved into the `yaml` package namespace.
|
||||
# It does not perfectly mimic its old counterpart, but should get
|
||||
# close enough for anyone who's relying on it even when they shouldn't.
|
||||
import yaml
|
||||
|
||||
# in some circumstances, the yaml module we imoprted may be from a different version, so we need
|
||||
# to tread carefully when poking at it here (it may not have the attributes we expect)
|
||||
if not getattr(yaml, '__with_libyaml__', False):
|
||||
from sys import version_info
|
||||
|
||||
exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
|
||||
raise exc("No module named '_yaml'")
|
||||
else:
|
||||
from yaml._yaml import *
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The _yaml extension module is now located at yaml._yaml'
|
||||
' and its location is subject to change. To use the'
|
||||
' LibYAML-based parser and emitter, import from `yaml`:'
|
||||
' `from yaml import CLoader as Loader, CDumper as Dumper`.',
|
||||
DeprecationWarning
|
||||
)
|
||||
del warnings
|
||||
# Don't `del yaml` here because yaml is actually an existing
|
||||
# namespace member of _yaml.
|
||||
|
||||
__name__ = '_yaml'
|
||||
# If the module is top-level (i.e. not a part of any specific package)
|
||||
# then the attribute should be set to ''.
|
||||
# https://docs.python.org/3.8/library/types.html
|
||||
__package__ = ''
|
||||
BIN
Binary file not shown.
-1
@@ -1 +0,0 @@
|
||||
pip
|
||||
-295
@@ -1,295 +0,0 @@
|
||||
Metadata-Version: 2.3
|
||||
Name: annotated-types
|
||||
Version: 0.7.0
|
||||
Summary: Reusable constraint types to use with typing.Annotated
|
||||
Project-URL: Homepage, https://github.com/annotated-types/annotated-types
|
||||
Project-URL: Source, https://github.com/annotated-types/annotated-types
|
||||
Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases
|
||||
Author-email: Adrian Garcia Badaracco <[email protected]>, Samuel Colvin <[email protected]>, Zac Hatfield-Dodds <[email protected]>
|
||||
License-File: LICENSE
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Environment :: Console
|
||||
Classifier: Environment :: MacOS X
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: Intended Audience :: Information Technology
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX :: Linux
|
||||
Classifier: Operating System :: Unix
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
||||
Classifier: Typing :: Typed
|
||||
Requires-Python: >=3.8
|
||||
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9'
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
# annotated-types
|
||||
|
||||
[](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI)
|
||||
[](https://pypi.python.org/pypi/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types)
|
||||
[](https://github.com/annotated-types/annotated-types/blob/main/LICENSE)
|
||||
|
||||
[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of
|
||||
adding context-specific metadata to existing types, and specifies that
|
||||
`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special
|
||||
logic for `x`.
|
||||
|
||||
This package provides metadata objects which can be used to represent common
|
||||
constraints such as upper and lower bounds on scalar values and collection sizes,
|
||||
a `Predicate` marker for runtime checks, and
|
||||
descriptions of how we intend these metadata to be interpreted. In some cases,
|
||||
we also note alternative representations which do not require this package.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install annotated-types
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from annotated_types import Gt, Len, Predicate
|
||||
|
||||
class MyClass:
|
||||
age: Annotated[int, Gt(18)] # Valid: 19, 20, ...
|
||||
# Invalid: 17, 18, "19", 19.0, ...
|
||||
factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ...
|
||||
# Invalid: 4, 8, -2, 5.0, "prime", ...
|
||||
|
||||
my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50]
|
||||
# Invalid: (1, 2), ["abc"], [0] * 20
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
_While `annotated-types` avoids runtime checks for performance, users should not
|
||||
construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`.
|
||||
Downstream implementors may choose to raise an error, emit a warning, silently ignore
|
||||
a metadata item, etc., if the metadata objects described below are used with an
|
||||
incompatible type - or for any other reason!_
|
||||
|
||||
### Gt, Ge, Lt, Le
|
||||
|
||||
Express inclusive and/or exclusive bounds on orderable values - which may be numbers,
|
||||
dates, times, strings, sets, etc. Note that the boundary value need not be of the
|
||||
same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]`
|
||||
is fine, for example, and implies that the value is an integer x such that `x > 1.5`.
|
||||
|
||||
We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)`
|
||||
as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on
|
||||
the `annotated-types` package.
|
||||
|
||||
To be explicit, these types have the following meanings:
|
||||
|
||||
* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum
|
||||
* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum
|
||||
* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum
|
||||
* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum
|
||||
|
||||
### Interval
|
||||
|
||||
`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single
|
||||
metadata object. `None` attributes should be ignored, and non-`None` attributes
|
||||
treated as per the single bounds above.
|
||||
|
||||
### MultipleOf
|
||||
|
||||
`MultipleOf(multiple_of=x)` might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying `value % multiple_of == 0`, or
|
||||
2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1),
|
||||
where `int(value / multiple_of) == value / multiple_of`.
|
||||
|
||||
We encourage users to be aware of these two common interpretations and their
|
||||
distinct behaviours, especially since very large or non-integer numbers make
|
||||
it easy to cause silent data corruption due to floating-point imprecision.
|
||||
|
||||
We encourage libraries to carefully document which interpretation they implement.
|
||||
|
||||
### MinLen, MaxLen, Len
|
||||
|
||||
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.
|
||||
|
||||
As well as `Len()` which can optionally include upper and lower bounds, we also
|
||||
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
|
||||
and `Len(max_length=y)` respectively.
|
||||
|
||||
`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.
|
||||
|
||||
Examples of usage:
|
||||
|
||||
* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
|
||||
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
|
||||
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
|
||||
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
|
||||
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8
|
||||
|
||||
#### Changed in v0.4.0
|
||||
|
||||
* `min_inclusive` has been renamed to `min_length`, no change in meaning
|
||||
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
|
||||
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
|
||||
meaning of the upper bound in slices vs. `Len`
|
||||
|
||||
See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.
|
||||
|
||||
### Timezone
|
||||
|
||||
`Timezone` can be used with a `datetime` or a `time` to express which timezones
|
||||
are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime.
|
||||
`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis))
|
||||
expresses that any timezone-aware datetime is allowed. You may also pass a specific
|
||||
timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects)
|
||||
object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only
|
||||
allow a specific timezone, though we note that this is often a symptom of fragile design.
|
||||
|
||||
#### Changed in v0.x.x
|
||||
|
||||
* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of
|
||||
`timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries.
|
||||
|
||||
### Unit
|
||||
|
||||
`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of
|
||||
a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]`
|
||||
would be a float representing a velocity in meters per second.
|
||||
|
||||
Please note that `annotated_types` itself makes no attempt to parse or validate
|
||||
the unit string in any way. That is left entirely to downstream libraries,
|
||||
such as [`pint`](https://pint.readthedocs.io) or
|
||||
[`astropy.units`](https://docs.astropy.org/en/stable/units/).
|
||||
|
||||
An example of how a library might use this metadata:
|
||||
|
||||
```python
|
||||
from annotated_types import Unit
|
||||
from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args
|
||||
|
||||
# given a type annotated with a unit:
|
||||
Meters = Annotated[float, Unit("m")]
|
||||
|
||||
|
||||
# you can cast the annotation to a specific unit type with any
|
||||
# callable that accepts a string and returns the desired type
|
||||
T = TypeVar("T")
|
||||
def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None:
|
||||
if get_origin(tp) is Annotated:
|
||||
for arg in get_args(tp):
|
||||
if isinstance(arg, Unit):
|
||||
return unit_cls(arg.unit)
|
||||
return None
|
||||
|
||||
|
||||
# using `pint`
|
||||
import pint
|
||||
pint_unit = cast_unit(Meters, pint.Unit)
|
||||
|
||||
|
||||
# using `astropy.units`
|
||||
import astropy.units as u
|
||||
astropy_unit = cast_unit(Meters, u.Unit)
|
||||
```
|
||||
|
||||
### Predicate
|
||||
|
||||
`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values.
|
||||
Users should prefer the statically inspectable metadata above, but if you need
|
||||
the full power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
For some common constraints, we provide generic types:
|
||||
|
||||
* `IsLower = Annotated[T, Predicate(str.islower)]`
|
||||
* `IsUpper = Annotated[T, Predicate(str.isupper)]`
|
||||
* `IsDigit = Annotated[T, Predicate(str.isdigit)]`
|
||||
* `IsFinite = Annotated[T, Predicate(math.isfinite)]`
|
||||
* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]`
|
||||
* `IsNan = Annotated[T, Predicate(math.isnan)]`
|
||||
* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]`
|
||||
* `IsInfinite = Annotated[T, Predicate(math.isinf)]`
|
||||
* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]`
|
||||
|
||||
so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer
|
||||
(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`.
|
||||
|
||||
Some libraries might have special logic to handle known or understandable predicates,
|
||||
for example by checking for `str.isdigit` and using its presence to both call custom
|
||||
logic to enforce digit-only strings, and customise some generated external schema.
|
||||
Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in
|
||||
favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`.
|
||||
|
||||
To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propagate or discard the resulting
|
||||
`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object`
|
||||
exception. We encourage libraries to document the behaviour they choose.
|
||||
|
||||
### Doc
|
||||
|
||||
`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used.
|
||||
|
||||
It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools.
|
||||
|
||||
It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`.
|
||||
|
||||
This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md).
|
||||
|
||||
### Integrating downstream types with `GroupedMetadata`
|
||||
|
||||
Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata.
|
||||
This can help reduce verbosity and cognitive overhead for users.
|
||||
For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterator
|
||||
from annotated_types import GroupedMetadata, Ge
|
||||
|
||||
@dataclass
|
||||
class Field(GroupedMetadata):
|
||||
ge: int | None = None
|
||||
description: str | None = None
|
||||
|
||||
def __iter__(self) -> Iterator[object]:
|
||||
# Iterating over a GroupedMetadata object should yield annotated-types
|
||||
# constraint metadata objects which describe it as fully as possible,
|
||||
# and may include other unknown objects too.
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.description is not None:
|
||||
yield Description(self.description)
|
||||
```
|
||||
|
||||
Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently.
|
||||
|
||||
Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself.
|
||||
|
||||
Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`.
|
||||
|
||||
### Consuming metadata
|
||||
|
||||
We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103).
|
||||
|
||||
It is up to the implementer to determine how this metadata is used.
|
||||
You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases.
|
||||
|
||||
## Design & History
|
||||
|
||||
This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic
|
||||
and Hypothesis, with the goal of making it as easy as possible for end-users to
|
||||
provide more informative annotations for use by runtime libraries.
|
||||
|
||||
It is deliberately minimal, and following PEP-593 allows considerable downstream
|
||||
discretion in what (if anything!) they choose to support. Nonetheless, we expect
|
||||
that staying simple and covering _only_ the most common use-cases will give users
|
||||
and maintainers the best experience we can. If you'd like more constraints for your
|
||||
types - follow our lead, by defining them and documenting them downstream!
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046
|
||||
annotated_types-0.7.0.dist-info/RECORD,,
|
||||
annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
||||
annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083
|
||||
annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819
|
||||
annotated_types/__pycache__/__init__.cpython-311.pyc,,
|
||||
annotated_types/__pycache__/test_cases.cpython-311.pyc,,
|
||||
annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: hatchling 1.24.2
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 the contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-432
@@ -1,432 +0,0 @@
|
||||
import math
|
||||
import sys
|
||||
import types
|
||||
from dataclasses import dataclass
|
||||
from datetime import tzinfo
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
else:
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated, Literal
|
||||
else:
|
||||
from typing import Annotated, Literal
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
EllipsisType = type(Ellipsis)
|
||||
KW_ONLY = {}
|
||||
SLOTS = {}
|
||||
else:
|
||||
from types import EllipsisType
|
||||
|
||||
KW_ONLY = {"kw_only": True}
|
||||
SLOTS = {"slots": True}
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseMetadata',
|
||||
'GroupedMetadata',
|
||||
'Gt',
|
||||
'Ge',
|
||||
'Lt',
|
||||
'Le',
|
||||
'Interval',
|
||||
'MultipleOf',
|
||||
'MinLen',
|
||||
'MaxLen',
|
||||
'Len',
|
||||
'Timezone',
|
||||
'Predicate',
|
||||
'LowerCase',
|
||||
'UpperCase',
|
||||
'IsDigits',
|
||||
'IsFinite',
|
||||
'IsNotFinite',
|
||||
'IsNan',
|
||||
'IsNotNan',
|
||||
'IsInfinite',
|
||||
'IsNotInfinite',
|
||||
'doc',
|
||||
'DocInfo',
|
||||
'__version__',
|
||||
)
|
||||
|
||||
__version__ = '0.7.0'
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# arguments that start with __ are considered
|
||||
# positional only
|
||||
# see https://peps.python.org/pep-0484/#positional-only-arguments
|
||||
|
||||
|
||||
class SupportsGt(Protocol):
|
||||
def __gt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsGe(Protocol):
|
||||
def __ge__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLt(Protocol):
|
||||
def __lt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLe(Protocol):
|
||||
def __le__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsMod(Protocol):
|
||||
def __mod__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class SupportsDiv(Protocol):
|
||||
def __div__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class BaseMetadata:
|
||||
"""Base class for all metadata.
|
||||
|
||||
This exists mainly so that implementers
|
||||
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Gt(BaseMetadata):
|
||||
"""Gt(gt=x) implies that the value must be greater than x.
|
||||
|
||||
It can be used with any type that supports the ``>`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
gt: SupportsGt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Ge(BaseMetadata):
|
||||
"""Ge(ge=x) implies that the value must be greater than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``>=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
ge: SupportsGe
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Lt(BaseMetadata):
|
||||
"""Lt(lt=x) implies that the value must be less than x.
|
||||
|
||||
It can be used with any type that supports the ``<`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
lt: SupportsLt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Le(BaseMetadata):
|
||||
"""Le(le=x) implies that the value must be less than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``<=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
le: SupportsLe
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GroupedMetadata(Protocol):
|
||||
"""A grouping of multiple objects, like typing.Unpack.
|
||||
|
||||
`GroupedMetadata` on its own is not metadata and has no meaning.
|
||||
All of the constraints and metadata should be fully expressable
|
||||
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
|
||||
|
||||
Concrete implementations should override `GroupedMetadata.__iter__()`
|
||||
to add their own metadata.
|
||||
For example:
|
||||
|
||||
>>> @dataclass
|
||||
>>> class Field(GroupedMetadata):
|
||||
>>> gt: float | None = None
|
||||
>>> description: str | None = None
|
||||
...
|
||||
>>> def __iter__(self) -> Iterable[object]:
|
||||
>>> if self.gt is not None:
|
||||
>>> yield Gt(self.gt)
|
||||
>>> if self.description is not None:
|
||||
>>> yield Description(self.gt)
|
||||
|
||||
Also see the implementation of `Interval` below for an example.
|
||||
|
||||
Parsers should recognize this and unpack it so that it can be used
|
||||
both with and without unpacking:
|
||||
|
||||
- `Annotated[int, Field(...)]` (parser must unpack Field)
|
||||
- `Annotated[int, *Field(...)]` (PEP-646)
|
||||
""" # noqa: trailing-whitespace
|
||||
|
||||
@property
|
||||
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __iter__(self) -> Iterator[object]:
|
||||
...
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__slots__ = () # allow subclasses to use slots
|
||||
|
||||
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
|
||||
# Basic ABC like functionality without the complexity of an ABC
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
if cls.__iter__ is GroupedMetadata.__iter__:
|
||||
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
|
||||
|
||||
def __iter__(self) -> Iterator[object]: # noqa: F811
|
||||
raise NotImplementedError # more helpful than "None has no attribute..." type errors
|
||||
|
||||
|
||||
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
|
||||
class Interval(GroupedMetadata):
|
||||
"""Interval can express inclusive or exclusive bounds with a single object.
|
||||
|
||||
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
|
||||
are interpreted the same way as the single-bound constraints.
|
||||
"""
|
||||
|
||||
gt: Union[SupportsGt, None] = None
|
||||
ge: Union[SupportsGe, None] = None
|
||||
lt: Union[SupportsLt, None] = None
|
||||
le: Union[SupportsLe, None] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack an Interval into zero or more single-bounds."""
|
||||
if self.gt is not None:
|
||||
yield Gt(self.gt)
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.lt is not None:
|
||||
yield Lt(self.lt)
|
||||
if self.le is not None:
|
||||
yield Le(self.le)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MultipleOf(BaseMetadata):
|
||||
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying ``value % multiple_of == 0``, or
|
||||
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
|
||||
|
||||
We encourage users to be aware of these two common interpretations,
|
||||
and libraries to carefully document which they implement.
|
||||
"""
|
||||
|
||||
multiple_of: Union[SupportsDiv, SupportsMod]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MinLen(BaseMetadata):
|
||||
"""
|
||||
MinLen() implies minimum inclusive length,
|
||||
e.g. ``len(value) >= min_length``.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MaxLen(BaseMetadata):
|
||||
"""
|
||||
MaxLen() implies maximum inclusive length,
|
||||
e.g. ``len(value) <= max_length``.
|
||||
"""
|
||||
|
||||
max_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Len(GroupedMetadata):
|
||||
"""
|
||||
Len() implies that ``min_length <= len(value) <= max_length``.
|
||||
|
||||
Upper bound may be omitted or ``None`` to indicate no upper length bound.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)] = 0
|
||||
max_length: Optional[Annotated[int, Ge(0)]] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack a Len into zone or more single-bounds."""
|
||||
if self.min_length > 0:
|
||||
yield MinLen(self.min_length)
|
||||
if self.max_length is not None:
|
||||
yield MaxLen(self.max_length)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Timezone(BaseMetadata):
|
||||
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
|
||||
|
||||
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
|
||||
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
|
||||
tz-aware but any timezone is allowed.
|
||||
|
||||
You may also pass a specific timezone string or tzinfo object such as
|
||||
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
|
||||
you only allow a specific timezone, though we note that this is often
|
||||
a symptom of poor design.
|
||||
"""
|
||||
|
||||
tz: Union[str, tzinfo, EllipsisType, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Unit(BaseMetadata):
|
||||
"""Indicates that the value is a physical quantity with the specified unit.
|
||||
|
||||
It is intended for usage with numeric types, where the value represents the
|
||||
magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]``
|
||||
or ``speed: Annotated[float, Unit('m/s')]``.
|
||||
|
||||
Interpretation of the unit string is left to the discretion of the consumer.
|
||||
It is suggested to follow conventions established by python libraries that work
|
||||
with physical quantities, such as
|
||||
|
||||
- ``pint`` : <https://pint.readthedocs.io/en/stable/>
|
||||
- ``astropy.units``: <https://docs.astropy.org/en/stable/units/>
|
||||
|
||||
For indicating a quantity with a certain dimensionality but without a specific unit
|
||||
it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`.
|
||||
Note, however, ``annotated_types`` itself makes no use of the unit string.
|
||||
"""
|
||||
|
||||
unit: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Predicate(BaseMetadata):
|
||||
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
|
||||
|
||||
Users should prefer statically inspectable metadata, but if you need the full
|
||||
power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
We provide a few predefined predicates for common string constraints:
|
||||
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
|
||||
``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which
|
||||
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
|
||||
|
||||
Some libraries might have special logic to handle certain predicates, e.g. by
|
||||
checking for `str.isdigit` and using its presence to both call custom logic to
|
||||
enforce digit-only strings, and customise some generated external schema.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propagate or discard the resulting exception.
|
||||
"""
|
||||
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
if getattr(self.func, "__name__", "<lambda>") == "<lambda>":
|
||||
return f"{self.__class__.__name__}({self.func!r})"
|
||||
if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and (
|
||||
namespace := getattr(self.func.__self__, "__name__", None)
|
||||
):
|
||||
return f"{self.__class__.__name__}({namespace}.{self.func.__name__})"
|
||||
if isinstance(self.func, type(str.isascii)): # method descriptor
|
||||
return f"{self.__class__.__name__}({self.func.__qualname__})"
|
||||
return f"{self.__class__.__name__}({self.func.__name__})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Not:
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __call__(self, __v: Any) -> bool:
|
||||
return not self.func(__v)
|
||||
|
||||
|
||||
_StrType = TypeVar("_StrType", bound=str)
|
||||
|
||||
LowerCase = Annotated[_StrType, Predicate(str.islower)]
|
||||
"""
|
||||
Return True if the string is a lowercase string, False otherwise.
|
||||
|
||||
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
|
||||
"""
|
||||
Return True if the string is an uppercase string, False otherwise.
|
||||
|
||||
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
IsDigit = Annotated[_StrType, Predicate(str.isdigit)]
|
||||
IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63
|
||||
"""
|
||||
Return True if the string is a digit string, False otherwise.
|
||||
|
||||
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
|
||||
""" # noqa: E501
|
||||
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
|
||||
"""
|
||||
Return True if all characters in the string are ASCII, False otherwise.
|
||||
|
||||
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
|
||||
"""
|
||||
|
||||
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
|
||||
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
|
||||
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
|
||||
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
|
||||
"""Return True if x is one of infinity or NaN, and False otherwise"""
|
||||
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
|
||||
"""Return True if x is a NaN (not a number), and False otherwise."""
|
||||
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
|
||||
"""Return True if x is anything but NaN (not a number), and False otherwise."""
|
||||
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
|
||||
"""Return True if x is a positive or negative infinity, and False otherwise."""
|
||||
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
|
||||
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
|
||||
|
||||
try:
|
||||
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
|
||||
except ImportError:
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class DocInfo: # type: ignore [no-redef]
|
||||
""" "
|
||||
The return value of doc(), mainly to be used by tools that want to extract the
|
||||
Annotated documentation at runtime.
|
||||
"""
|
||||
|
||||
documentation: str
|
||||
"""The documentation string passed to doc()."""
|
||||
|
||||
def doc(
|
||||
documentation: str,
|
||||
) -> DocInfo:
|
||||
"""
|
||||
Add documentation to a type annotation inside of Annotated.
|
||||
|
||||
For example:
|
||||
|
||||
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
|
||||
"""
|
||||
return DocInfo(documentation)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
-151
@@ -1,151 +0,0 @@
|
||||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated
|
||||
else:
|
||||
from typing import Annotated
|
||||
|
||||
import annotated_types as at
|
||||
|
||||
|
||||
class Case(NamedTuple):
|
||||
"""
|
||||
A test case for `annotated_types`.
|
||||
"""
|
||||
|
||||
annotation: Any
|
||||
valid_cases: Iterable[Any]
|
||||
invalid_cases: Iterable[Any]
|
||||
|
||||
|
||||
def cases() -> Iterable[Case]:
|
||||
# Gt, Ge, Lt, Le
|
||||
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(date(2000, 1, 1))],
|
||||
[date(2000, 1, 2), date(2000, 1, 3)],
|
||||
[date(2000, 1, 1), date(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(Decimal('1.123'))],
|
||||
[Decimal('1.1231'), Decimal('123')],
|
||||
[Decimal('1.123'), Decimal('0')],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
|
||||
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
|
||||
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
|
||||
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
|
||||
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
# Interval
|
||||
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
|
||||
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
|
||||
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
|
||||
|
||||
# lengths
|
||||
|
||||
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
|
||||
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
|
||||
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
|
||||
|
||||
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
|
||||
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
|
||||
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
|
||||
|
||||
# Timezone
|
||||
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(timezone.utc)],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone('Europe/London')],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
|
||||
# Quantity
|
||||
|
||||
yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m'))
|
||||
|
||||
# predicate types
|
||||
|
||||
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
|
||||
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
|
||||
yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2'])
|
||||
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
|
||||
|
||||
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
|
||||
|
||||
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
|
||||
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
|
||||
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
|
||||
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
|
||||
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
|
||||
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
|
||||
|
||||
# check stacked predicates
|
||||
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
|
||||
|
||||
# doc
|
||||
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
|
||||
|
||||
# custom GroupedMetadata
|
||||
class MyCustomGroupedMetadata(at.GroupedMetadata):
|
||||
def __iter__(self) -> Iterator[at.Predicate]:
|
||||
yield at.Predicate(lambda x: float(x).is_integer())
|
||||
|
||||
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])
|
||||
-1
@@ -1 +0,0 @@
|
||||
pip
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Alex Grönholm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: anyio
|
||||
Version: 3.7.1
|
||||
Summary: High level compatibility layer for multiple asynchronous event loop implementations
|
||||
Author-email: Alex Grönholm <[email protected]>
|
||||
License: MIT
|
||||
Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/
|
||||
Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html
|
||||
Project-URL: Source code, https://github.com/agronholm/anyio
|
||||
Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Framework :: AnyIO
|
||||
Classifier: Typing :: Typed
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Requires-Python: >=3.7
|
||||
Description-Content-Type: text/x-rst
|
||||
License-File: LICENSE
|
||||
Requires-Dist: idna (>=2.8)
|
||||
Requires-Dist: sniffio (>=1.1)
|
||||
Requires-Dist: exceptiongroup ; python_version < "3.11"
|
||||
Requires-Dist: typing-extensions ; python_version < "3.8"
|
||||
Provides-Extra: doc
|
||||
Requires-Dist: packaging ; extra == 'doc'
|
||||
Requires-Dist: Sphinx ; extra == 'doc'
|
||||
Requires-Dist: sphinx-rtd-theme (>=1.2.2) ; extra == 'doc'
|
||||
Requires-Dist: sphinxcontrib-jquery ; extra == 'doc'
|
||||
Requires-Dist: sphinx-autodoc-typehints (>=1.2.0) ; extra == 'doc'
|
||||
Provides-Extra: test
|
||||
Requires-Dist: anyio[trio] ; extra == 'test'
|
||||
Requires-Dist: coverage[toml] (>=4.5) ; extra == 'test'
|
||||
Requires-Dist: hypothesis (>=4.0) ; extra == 'test'
|
||||
Requires-Dist: psutil (>=5.9) ; extra == 'test'
|
||||
Requires-Dist: pytest (>=7.0) ; extra == 'test'
|
||||
Requires-Dist: pytest-mock (>=3.6.1) ; extra == 'test'
|
||||
Requires-Dist: trustme ; extra == 'test'
|
||||
Requires-Dist: uvloop (>=0.17) ; (python_version < "3.12" and platform_python_implementation == "CPython" and platform_system != "Windows") and extra == 'test'
|
||||
Requires-Dist: mock (>=4) ; (python_version < "3.8") and extra == 'test'
|
||||
Provides-Extra: trio
|
||||
Requires-Dist: trio (<0.22) ; extra == 'trio'
|
||||
|
||||
.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg
|
||||
:target: https://github.com/agronholm/anyio/actions/workflows/test.yml
|
||||
:alt: Build Status
|
||||
.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/agronholm/anyio?branch=master
|
||||
:alt: Code Coverage
|
||||
.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest
|
||||
:target: https://anyio.readthedocs.io/en/latest/?badge=latest
|
||||
:alt: Documentation
|
||||
.. image:: https://badges.gitter.im/gitterHQ/gitter.svg
|
||||
:target: https://gitter.im/python-trio/AnyIO
|
||||
:alt: Gitter chat
|
||||
|
||||
AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or
|
||||
trio_. It implements trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony
|
||||
with the native SC of trio itself.
|
||||
|
||||
Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or
|
||||
trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full
|
||||
refactoring necessary. It will blend in with the native libraries of your chosen backend.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
View full documentation at: https://anyio.readthedocs.io/
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
AnyIO offers the following functionality:
|
||||
|
||||
* Task groups (nurseries_ in trio terminology)
|
||||
* High-level networking (TCP, UDP and UNIX sockets)
|
||||
|
||||
* `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python
|
||||
3.8)
|
||||
* async/await style UDP sockets (unlike asyncio where you still have to use Transports and
|
||||
Protocols)
|
||||
|
||||
* A versatile API for byte streams and object streams
|
||||
* Inter-task synchronization and communication (locks, conditions, events, semaphores, object
|
||||
streams)
|
||||
* Worker threads
|
||||
* Subprocesses
|
||||
* Asynchronous file I/O (using worker threads)
|
||||
* Signal handling
|
||||
|
||||
AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures.
|
||||
It even works with the popular Hypothesis_ library.
|
||||
|
||||
.. _asyncio: https://docs.python.org/3/library/asyncio.html
|
||||
.. _trio: https://github.com/python-trio/trio
|
||||
.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency
|
||||
.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning
|
||||
.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs
|
||||
.. _pytest: https://docs.pytest.org/en/latest/
|
||||
.. _Hypothesis: https://hypothesis.works/
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
anyio-3.7.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
anyio-3.7.1.dist-info/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081
|
||||
anyio-3.7.1.dist-info/METADATA,sha256=mOhfXPB7qKVQh3dUtp2NgLysa10jHWeDBNnRg-93A_c,4708
|
||||
anyio-3.7.1.dist-info/RECORD,,
|
||||
anyio-3.7.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
||||
anyio-3.7.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39
|
||||
anyio-3.7.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6
|
||||
anyio/__init__.py,sha256=Pq9lO03Zm5ynIPlhkquaOuIc1dTTeLGNUQ5HT5qwYMI,4073
|
||||
anyio/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/__pycache__/from_thread.cpython-311.pyc,,
|
||||
anyio/__pycache__/lowlevel.cpython-311.pyc,,
|
||||
anyio/__pycache__/pytest_plugin.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_process.cpython-311.pyc,,
|
||||
anyio/__pycache__/to_thread.cpython-311.pyc,,
|
||||
anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_backends/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_asyncio.cpython-311.pyc,,
|
||||
anyio/_backends/__pycache__/_trio.cpython-311.pyc,,
|
||||
anyio/_backends/_asyncio.py,sha256=fgwZmYnGOxT_pX0OZTPPgRdFqKLjnKvQUk7tsfuNmfM,67056
|
||||
anyio/_backends/_trio.py,sha256=EJAj0tNi0JRM2y3QWP7oS4ct7wnjMSYDG8IZUWMta-E,30035
|
||||
anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/_core/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_compat.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_eventloop.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_exceptions.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_fileio.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_signals.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_synchronization.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/_core/__pycache__/_typedattr.cpython-311.pyc,,
|
||||
anyio/_core/_compat.py,sha256=XZfBUInEt7jaiTBI2Qbul7EpJdngbwTtG4Qj26un1YE,5726
|
||||
anyio/_core/_eventloop.py,sha256=xJ8KflV1bJ9GAuQRr4o1ojv8wWya4nt_XARta8uLPwc,4083
|
||||
anyio/_core/_exceptions.py,sha256=uOrN5l98o6UrOU6O3kPf0VCDl_zPP-kgZs4IyaLVgwU,2916
|
||||
anyio/_core/_fileio.py,sha256=DWuIul5izCocmJpgqDDNKc_GhMUwayHKdM5R-sbT_A8,18026
|
||||
anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435
|
||||
anyio/_core/_signals.py,sha256=KKkZAYL08auydjZnK9S4FQsxx555jT4gXAMcTXdNaok,863
|
||||
anyio/_core/_sockets.py,sha256=szcPd7kKBmlHnx8g_KJWZo2k6syouRNF2614ZrtqiV0,20667
|
||||
anyio/_core/_streams.py,sha256=5gryxQiUisED8uFUAHje5O44RL9wyndNMANzzQWUn1U,1518
|
||||
anyio/_core/_subprocesses.py,sha256=OSAcLAsjfCplXlRyTjWonfS1xU8d5MaZblXYqqY-BM4,4977
|
||||
anyio/_core/_synchronization.py,sha256=Uquo_52vZ7iZzDDoaN_j-N7jeyAlefzOZ8Pxt9mU6gY,16747
|
||||
anyio/_core/_tasks.py,sha256=1wZZWlpDkr6w3kMD629vzJDkPselDvx4XVElgTCVwyM,5316
|
||||
anyio/_core/_testing.py,sha256=7Yll-DOI0uIlIF5VHLUpGGyDPWtDEjFZ85-6ZniwIJU,2217
|
||||
anyio/_core/_typedattr.py,sha256=8o0gwQYSl04zlO9uHqcHu1T6hOw7peY9NW1mOX5DKnY,2551
|
||||
anyio/abc/__init__.py,sha256=UkC-KDbyIoKeDUDhJciwANSoyzz_qaFh4Fb7_AvwjZc,2159
|
||||
anyio/abc/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_resources.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_sockets.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_streams.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_subprocesses.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_tasks.cpython-311.pyc,,
|
||||
anyio/abc/__pycache__/_testing.cpython-311.pyc,,
|
||||
anyio/abc/_resources.py,sha256=h1rkzr3E0MFqdXLh9aLLXe-A5W7k_Jc-5XzNr6SJ4w4,763
|
||||
anyio/abc/_sockets.py,sha256=WWYJ6HndKCEuvobAPDkmX0tjwN2FOxf3eTGb1DB7wHE,5243
|
||||
anyio/abc/_streams.py,sha256=yGhOmlVI3W9whmzPuewwYQ2BrKhrUFuWZ4zpVLWOK84,6584
|
||||
anyio/abc/_subprocesses.py,sha256=r-totaRbFX6kKV-4WTeuswz8n01aap8cvkYVQCRKN0M,2067
|
||||
anyio/abc/_tasks.py,sha256=a_5DLyiCbp0K57LJPOyF-PZyXmUcv_p9VRXPFj_K03M,3413
|
||||
anyio/abc/_testing.py,sha256=Eub7gXJ0tVPo_WN5iJAw10FrvC7C1uaL3b2neGr_pfs,1924
|
||||
anyio/from_thread.py,sha256=aUVKXctPgZ5wK3p5VTyrtjDj9tSQSrH6xCjBuo-hv3A,16563
|
||||
anyio/lowlevel.py,sha256=cOTncxRW5KeswqYQQdp0pfAw6OFWXius1SPhCYwHZL4,4647
|
||||
anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/pytest_plugin.py,sha256=_Txgl0-I3kO1rk_KATXmIUV57C34hajcJCGcgV26CU0,5022
|
||||
anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
anyio/streams/__pycache__/__init__.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/buffered.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/file.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/memory.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/stapled.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/text.cpython-311.pyc,,
|
||||
anyio/streams/__pycache__/tls.cpython-311.pyc,,
|
||||
anyio/streams/buffered.py,sha256=2ifplNLwT73d1UKBxrkFdlC9wTAze9LhPL7pt_7cYgY,4473
|
||||
anyio/streams/file.py,sha256=-NP6jMcUd2f1VJwgcxgiRHdEsNnhE0lANl0ov_i7FrE,4356
|
||||
anyio/streams/memory.py,sha256=QZhc5qdomBpGCgrUVWAaqEBxI0oklVxK_62atW6tnNk,9274
|
||||
anyio/streams/stapled.py,sha256=9u2GxpiOPsGtgO1qsj2tVoW4b8bgiwp5rSDs1BFKkLM,4275
|
||||
anyio/streams/text.py,sha256=1K4ZCLKl2b7yywrW6wKEeMu3xyQHE_T0aU5_oC9GPTE,5043
|
||||
anyio/streams/tls.py,sha256=TbdCz1KtfEnp3mxHvkROXRefhE6S1LHiwgWiJX8zYaU,12099
|
||||
anyio/to_process.py,sha256=_RSsG8UME2nGxeFEdg3OEfv9XshSQwrMU7DAbwWGx9U,9242
|
||||
anyio/to_thread.py,sha256=HVpTvBei2sSXgJJeNKdwhJwQaW76LDbb1htQ-Mc6zDs,2146
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.40.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
[pytest11]
|
||||
anyio = anyio.pytest_plugin
|
||||
-1
@@ -1 +0,0 @@
|
||||
anyio
|
||||
-169
@@ -1,169 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = (
|
||||
"maybe_async",
|
||||
"maybe_async_cm",
|
||||
"run",
|
||||
"sleep",
|
||||
"sleep_forever",
|
||||
"sleep_until",
|
||||
"current_time",
|
||||
"get_all_backends",
|
||||
"get_cancelled_exc_class",
|
||||
"BrokenResourceError",
|
||||
"BrokenWorkerProcess",
|
||||
"BusyResourceError",
|
||||
"ClosedResourceError",
|
||||
"DelimiterNotFound",
|
||||
"EndOfStream",
|
||||
"ExceptionGroup",
|
||||
"IncompleteRead",
|
||||
"TypedAttributeLookupError",
|
||||
"WouldBlock",
|
||||
"AsyncFile",
|
||||
"Path",
|
||||
"open_file",
|
||||
"wrap_file",
|
||||
"aclose_forcefully",
|
||||
"open_signal_receiver",
|
||||
"connect_tcp",
|
||||
"connect_unix",
|
||||
"create_tcp_listener",
|
||||
"create_unix_listener",
|
||||
"create_udp_socket",
|
||||
"create_connected_udp_socket",
|
||||
"getaddrinfo",
|
||||
"getnameinfo",
|
||||
"wait_socket_readable",
|
||||
"wait_socket_writable",
|
||||
"create_memory_object_stream",
|
||||
"run_process",
|
||||
"open_process",
|
||||
"create_lock",
|
||||
"CapacityLimiter",
|
||||
"CapacityLimiterStatistics",
|
||||
"Condition",
|
||||
"ConditionStatistics",
|
||||
"Event",
|
||||
"EventStatistics",
|
||||
"Lock",
|
||||
"LockStatistics",
|
||||
"Semaphore",
|
||||
"SemaphoreStatistics",
|
||||
"create_condition",
|
||||
"create_event",
|
||||
"create_semaphore",
|
||||
"create_capacity_limiter",
|
||||
"open_cancel_scope",
|
||||
"fail_after",
|
||||
"move_on_after",
|
||||
"current_effective_deadline",
|
||||
"TASK_STATUS_IGNORED",
|
||||
"CancelScope",
|
||||
"create_task_group",
|
||||
"TaskInfo",
|
||||
"get_current_task",
|
||||
"get_running_tasks",
|
||||
"wait_all_tasks_blocked",
|
||||
"run_sync_in_worker_thread",
|
||||
"run_async_from_thread",
|
||||
"run_sync_from_thread",
|
||||
"current_default_worker_thread_limiter",
|
||||
"create_blocking_portal",
|
||||
"start_blocking_portal",
|
||||
"typed_attribute",
|
||||
"TypedAttributeSet",
|
||||
"TypedAttributeProvider",
|
||||
)
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ._core._compat import maybe_async, maybe_async_cm
|
||||
from ._core._eventloop import (
|
||||
current_time,
|
||||
get_all_backends,
|
||||
get_cancelled_exc_class,
|
||||
run,
|
||||
sleep,
|
||||
sleep_forever,
|
||||
sleep_until,
|
||||
)
|
||||
from ._core._exceptions import (
|
||||
BrokenResourceError,
|
||||
BrokenWorkerProcess,
|
||||
BusyResourceError,
|
||||
ClosedResourceError,
|
||||
DelimiterNotFound,
|
||||
EndOfStream,
|
||||
ExceptionGroup,
|
||||
IncompleteRead,
|
||||
TypedAttributeLookupError,
|
||||
WouldBlock,
|
||||
)
|
||||
from ._core._fileio import AsyncFile, Path, open_file, wrap_file
|
||||
from ._core._resources import aclose_forcefully
|
||||
from ._core._signals import open_signal_receiver
|
||||
from ._core._sockets import (
|
||||
connect_tcp,
|
||||
connect_unix,
|
||||
create_connected_udp_socket,
|
||||
create_tcp_listener,
|
||||
create_udp_socket,
|
||||
create_unix_listener,
|
||||
getaddrinfo,
|
||||
getnameinfo,
|
||||
wait_socket_readable,
|
||||
wait_socket_writable,
|
||||
)
|
||||
from ._core._streams import create_memory_object_stream
|
||||
from ._core._subprocesses import open_process, run_process
|
||||
from ._core._synchronization import (
|
||||
CapacityLimiter,
|
||||
CapacityLimiterStatistics,
|
||||
Condition,
|
||||
ConditionStatistics,
|
||||
Event,
|
||||
EventStatistics,
|
||||
Lock,
|
||||
LockStatistics,
|
||||
Semaphore,
|
||||
SemaphoreStatistics,
|
||||
create_capacity_limiter,
|
||||
create_condition,
|
||||
create_event,
|
||||
create_lock,
|
||||
create_semaphore,
|
||||
)
|
||||
from ._core._tasks import (
|
||||
TASK_STATUS_IGNORED,
|
||||
CancelScope,
|
||||
create_task_group,
|
||||
current_effective_deadline,
|
||||
fail_after,
|
||||
move_on_after,
|
||||
open_cancel_scope,
|
||||
)
|
||||
from ._core._testing import (
|
||||
TaskInfo,
|
||||
get_current_task,
|
||||
get_running_tasks,
|
||||
wait_all_tasks_blocked,
|
||||
)
|
||||
from ._core._typedattr import TypedAttributeProvider, TypedAttributeSet, typed_attribute
|
||||
|
||||
# Re-exported here, for backwards compatibility
|
||||
# isort: off
|
||||
from .to_thread import current_default_worker_thread_limiter, run_sync_in_worker_thread
|
||||
from .from_thread import (
|
||||
create_blocking_portal,
|
||||
run_async_from_thread,
|
||||
run_sync_from_thread,
|
||||
start_blocking_portal,
|
||||
)
|
||||
|
||||
# Re-export imports so they look like they live directly in this package
|
||||
key: str
|
||||
value: Any
|
||||
for key, value in list(locals().items()):
|
||||
if getattr(value, "__module__", "").startswith("anyio."):
|
||||
value.__module__ = __name__
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
-2117
File diff suppressed because it is too large
Load Diff
-996
@@ -1,996 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import array
|
||||
import math
|
||||
import socket
|
||||
from concurrent.futures import Future
|
||||
from contextvars import copy_context
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from io import IOBase
|
||||
from os import PathLike
|
||||
from signal import Signals
|
||||
from types import TracebackType
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
AsyncGenerator,
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Coroutine,
|
||||
Generic,
|
||||
Iterable,
|
||||
Mapping,
|
||||
NoReturn,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
import sniffio
|
||||
import trio.from_thread
|
||||
from outcome import Error, Outcome, Value
|
||||
from trio.socket import SocketType as TrioSocketType
|
||||
from trio.to_thread import run_sync
|
||||
|
||||
from .. import CapacityLimiterStatistics, EventStatistics, TaskInfo, abc
|
||||
from .._core._compat import DeprecatedAsyncContextManager, DeprecatedAwaitable
|
||||
from .._core._eventloop import claim_worker_thread
|
||||
from .._core._exceptions import (
|
||||
BrokenResourceError,
|
||||
BusyResourceError,
|
||||
ClosedResourceError,
|
||||
EndOfStream,
|
||||
)
|
||||
from .._core._exceptions import ExceptionGroup as BaseExceptionGroup
|
||||
from .._core._sockets import convert_ipv6_sockaddr
|
||||
from .._core._synchronization import CapacityLimiter as BaseCapacityLimiter
|
||||
from .._core._synchronization import Event as BaseEvent
|
||||
from .._core._synchronization import ResourceGuard
|
||||
from .._core._tasks import CancelScope as BaseCancelScope
|
||||
from ..abc import IPSockAddrType, UDPPacketType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from trio_typing import TaskStatus
|
||||
|
||||
try:
|
||||
from trio import lowlevel as trio_lowlevel
|
||||
except ImportError:
|
||||
from trio import hazmat as trio_lowlevel # type: ignore[no-redef]
|
||||
from trio.hazmat import wait_readable, wait_writable
|
||||
else:
|
||||
from trio.lowlevel import wait_readable, wait_writable
|
||||
|
||||
try:
|
||||
trio_open_process = trio_lowlevel.open_process
|
||||
except AttributeError:
|
||||
# isort: off
|
||||
from trio import ( # type: ignore[attr-defined, no-redef]
|
||||
open_process as trio_open_process,
|
||||
)
|
||||
|
||||
T_Retval = TypeVar("T_Retval")
|
||||
T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType)
|
||||
|
||||
|
||||
#
|
||||
# Event loop
|
||||
#
|
||||
|
||||
run = trio.run
|
||||
current_token = trio.lowlevel.current_trio_token
|
||||
RunVar = trio.lowlevel.RunVar
|
||||
|
||||
|
||||
#
|
||||
# Miscellaneous
|
||||
#
|
||||
|
||||
sleep = trio.sleep
|
||||
|
||||
|
||||
#
|
||||
# Timeouts and cancellation
|
||||
#
|
||||
|
||||
|
||||
class CancelScope(BaseCancelScope):
|
||||
def __new__(
|
||||
cls, original: trio.CancelScope | None = None, **kwargs: object
|
||||
) -> CancelScope:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None:
|
||||
self.__original = original or trio.CancelScope(**kwargs)
|
||||
|
||||
def __enter__(self) -> CancelScope:
|
||||
self.__original.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
# https://github.com/python-trio/trio-typing/pull/79
|
||||
return self.__original.__exit__( # type: ignore[func-returns-value]
|
||||
exc_type, exc_val, exc_tb
|
||||
)
|
||||
|
||||
def cancel(self) -> DeprecatedAwaitable:
|
||||
self.__original.cancel()
|
||||
return DeprecatedAwaitable(self.cancel)
|
||||
|
||||
@property
|
||||
def deadline(self) -> float:
|
||||
return self.__original.deadline
|
||||
|
||||
@deadline.setter
|
||||
def deadline(self, value: float) -> None:
|
||||
self.__original.deadline = value
|
||||
|
||||
@property
|
||||
def cancel_called(self) -> bool:
|
||||
return self.__original.cancel_called
|
||||
|
||||
@property
|
||||
def shield(self) -> bool:
|
||||
return self.__original.shield
|
||||
|
||||
@shield.setter
|
||||
def shield(self, value: bool) -> None:
|
||||
self.__original.shield = value
|
||||
|
||||
|
||||
CancelledError = trio.Cancelled
|
||||
checkpoint = trio.lowlevel.checkpoint
|
||||
checkpoint_if_cancelled = trio.lowlevel.checkpoint_if_cancelled
|
||||
cancel_shielded_checkpoint = trio.lowlevel.cancel_shielded_checkpoint
|
||||
current_effective_deadline = trio.current_effective_deadline
|
||||
current_time = trio.current_time
|
||||
|
||||
|
||||
#
|
||||
# Task groups
|
||||
#
|
||||
|
||||
|
||||
class ExceptionGroup(BaseExceptionGroup, trio.MultiError):
|
||||
pass
|
||||
|
||||
|
||||
class TaskGroup(abc.TaskGroup):
|
||||
def __init__(self) -> None:
|
||||
self._active = False
|
||||
self._nursery_manager = trio.open_nursery()
|
||||
self.cancel_scope = None # type: ignore[assignment]
|
||||
|
||||
async def __aenter__(self) -> TaskGroup:
|
||||
self._active = True
|
||||
self._nursery = await self._nursery_manager.__aenter__()
|
||||
self.cancel_scope = CancelScope(self._nursery.cancel_scope)
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
try:
|
||||
return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb)
|
||||
except trio.MultiError as exc:
|
||||
raise ExceptionGroup(exc.exceptions) from None
|
||||
finally:
|
||||
self._active = False
|
||||
|
||||
def start_soon(
|
||||
self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
|
||||
) -> None:
|
||||
if not self._active:
|
||||
raise RuntimeError(
|
||||
"This task group is not active; no new tasks can be started."
|
||||
)
|
||||
|
||||
self._nursery.start_soon(func, *args, name=name)
|
||||
|
||||
async def start(
|
||||
self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None
|
||||
) -> object:
|
||||
if not self._active:
|
||||
raise RuntimeError(
|
||||
"This task group is not active; no new tasks can be started."
|
||||
)
|
||||
|
||||
return await self._nursery.start(func, *args, name=name)
|
||||
|
||||
|
||||
#
|
||||
# Threads
|
||||
#
|
||||
|
||||
|
||||
async def run_sync_in_worker_thread(
|
||||
func: Callable[..., T_Retval],
|
||||
*args: object,
|
||||
cancellable: bool = False,
|
||||
limiter: trio.CapacityLimiter | None = None,
|
||||
) -> T_Retval:
|
||||
def wrapper() -> T_Retval:
|
||||
with claim_worker_thread("trio"):
|
||||
return func(*args)
|
||||
|
||||
# TODO: remove explicit context copying when trio 0.20 is the minimum requirement
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, None)
|
||||
return await run_sync(
|
||||
context.run, wrapper, cancellable=cancellable, limiter=limiter
|
||||
)
|
||||
|
||||
|
||||
# TODO: remove this workaround when trio 0.20 is the minimum requirement
|
||||
def run_async_from_thread(
|
||||
fn: Callable[..., Awaitable[T_Retval]], *args: Any
|
||||
) -> T_Retval:
|
||||
async def wrapper() -> T_Retval:
|
||||
retval: T_Retval
|
||||
|
||||
async def inner() -> None:
|
||||
nonlocal retval
|
||||
__tracebackhide__ = True
|
||||
retval = await fn(*args)
|
||||
|
||||
async with trio.open_nursery() as n:
|
||||
context.run(n.start_soon, inner)
|
||||
|
||||
__tracebackhide__ = True
|
||||
return retval # noqa: F821
|
||||
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, "trio")
|
||||
return trio.from_thread.run(wrapper)
|
||||
|
||||
|
||||
def run_sync_from_thread(fn: Callable[..., T_Retval], *args: Any) -> T_Retval:
|
||||
# TODO: remove explicit context copying when trio 0.20 is the minimum requirement
|
||||
retval = trio.from_thread.run_sync(copy_context().run, fn, *args)
|
||||
return cast(T_Retval, retval)
|
||||
|
||||
|
||||
class BlockingPortal(abc.BlockingPortal):
|
||||
def __new__(cls) -> BlockingPortal:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._token = trio.lowlevel.current_trio_token()
|
||||
|
||||
def _spawn_task_from_thread(
|
||||
self,
|
||||
func: Callable,
|
||||
args: tuple,
|
||||
kwargs: dict[str, Any],
|
||||
name: object,
|
||||
future: Future,
|
||||
) -> None:
|
||||
context = copy_context()
|
||||
context.run(sniffio.current_async_library_cvar.set, "trio")
|
||||
trio.from_thread.run_sync(
|
||||
context.run,
|
||||
partial(self._task_group.start_soon, name=name),
|
||||
self._call_func,
|
||||
func,
|
||||
args,
|
||||
kwargs,
|
||||
future,
|
||||
trio_token=self._token,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Subprocesses
|
||||
#
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class ReceiveStreamWrapper(abc.ByteReceiveStream):
|
||||
_stream: trio.abc.ReceiveStream
|
||||
|
||||
async def receive(self, max_bytes: int | None = None) -> bytes:
|
||||
try:
|
||||
data = await self._stream.receive_some(max_bytes)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError from exc.__cause__
|
||||
except trio.BrokenResourceError as exc:
|
||||
raise BrokenResourceError from exc.__cause__
|
||||
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise EndOfStream
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._stream.aclose()
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class SendStreamWrapper(abc.ByteSendStream):
|
||||
_stream: trio.abc.SendStream
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
try:
|
||||
await self._stream.send_all(item)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError from exc.__cause__
|
||||
except trio.BrokenResourceError as exc:
|
||||
raise BrokenResourceError from exc.__cause__
|
||||
|
||||
async def aclose(self) -> None:
|
||||
await self._stream.aclose()
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
class Process(abc.Process):
|
||||
_process: trio.Process
|
||||
_stdin: abc.ByteSendStream | None
|
||||
_stdout: abc.ByteReceiveStream | None
|
||||
_stderr: abc.ByteReceiveStream | None
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._stdin:
|
||||
await self._stdin.aclose()
|
||||
if self._stdout:
|
||||
await self._stdout.aclose()
|
||||
if self._stderr:
|
||||
await self._stderr.aclose()
|
||||
|
||||
await self.wait()
|
||||
|
||||
async def wait(self) -> int:
|
||||
return await self._process.wait()
|
||||
|
||||
def terminate(self) -> None:
|
||||
self._process.terminate()
|
||||
|
||||
def kill(self) -> None:
|
||||
self._process.kill()
|
||||
|
||||
def send_signal(self, signal: Signals) -> None:
|
||||
self._process.send_signal(signal)
|
||||
|
||||
@property
|
||||
def pid(self) -> int:
|
||||
return self._process.pid
|
||||
|
||||
@property
|
||||
def returncode(self) -> int | None:
|
||||
return self._process.returncode
|
||||
|
||||
@property
|
||||
def stdin(self) -> abc.ByteSendStream | None:
|
||||
return self._stdin
|
||||
|
||||
@property
|
||||
def stdout(self) -> abc.ByteReceiveStream | None:
|
||||
return self._stdout
|
||||
|
||||
@property
|
||||
def stderr(self) -> abc.ByteReceiveStream | None:
|
||||
return self._stderr
|
||||
|
||||
|
||||
async def open_process(
|
||||
command: str | bytes | Sequence[str | bytes],
|
||||
*,
|
||||
shell: bool,
|
||||
stdin: int | IO[Any] | None,
|
||||
stdout: int | IO[Any] | None,
|
||||
stderr: int | IO[Any] | None,
|
||||
cwd: str | bytes | PathLike | None = None,
|
||||
env: Mapping[str, str] | None = None,
|
||||
start_new_session: bool = False,
|
||||
) -> Process:
|
||||
process = await trio_open_process( # type: ignore[misc]
|
||||
command, # type: ignore[arg-type]
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=stderr,
|
||||
shell=shell,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
start_new_session=start_new_session,
|
||||
)
|
||||
stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None
|
||||
stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None
|
||||
stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None
|
||||
return Process(process, stdin_stream, stdout_stream, stderr_stream)
|
||||
|
||||
|
||||
class _ProcessPoolShutdownInstrument(trio.abc.Instrument):
|
||||
def after_run(self) -> None:
|
||||
super().after_run()
|
||||
|
||||
|
||||
current_default_worker_process_limiter: RunVar = RunVar(
|
||||
"current_default_worker_process_limiter"
|
||||
)
|
||||
|
||||
|
||||
async def _shutdown_process_pool(workers: set[Process]) -> None:
|
||||
process: Process
|
||||
try:
|
||||
await sleep(math.inf)
|
||||
except trio.Cancelled:
|
||||
for process in workers:
|
||||
if process.returncode is None:
|
||||
process.kill()
|
||||
|
||||
with CancelScope(shield=True):
|
||||
for process in workers:
|
||||
await process.aclose()
|
||||
|
||||
|
||||
def setup_process_pool_exit_at_shutdown(workers: set[Process]) -> None:
|
||||
trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers)
|
||||
|
||||
|
||||
#
|
||||
# Sockets and networking
|
||||
#
|
||||
|
||||
|
||||
class _TrioSocketMixin(Generic[T_SockAddr]):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
self._trio_socket = trio_socket
|
||||
self._closed = False
|
||||
|
||||
def _check_closed(self) -> None:
|
||||
if self._closed:
|
||||
raise ClosedResourceError
|
||||
if self._trio_socket.fileno() < 0:
|
||||
raise BrokenResourceError
|
||||
|
||||
@property
|
||||
def _raw_socket(self) -> socket.socket:
|
||||
return self._trio_socket._sock # type: ignore[attr-defined]
|
||||
|
||||
async def aclose(self) -> None:
|
||||
if self._trio_socket.fileno() >= 0:
|
||||
self._closed = True
|
||||
self._trio_socket.close()
|
||||
|
||||
def _convert_socket_error(self, exc: BaseException) -> NoReturn:
|
||||
if isinstance(exc, trio.ClosedResourceError):
|
||||
raise ClosedResourceError from exc
|
||||
elif self._trio_socket.fileno() < 0 and self._closed:
|
||||
raise ClosedResourceError from None
|
||||
elif isinstance(exc, OSError):
|
||||
raise BrokenResourceError from exc
|
||||
else:
|
||||
raise exc
|
||||
|
||||
|
||||
class SocketStream(_TrioSocketMixin, abc.SocketStream):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self, max_bytes: int = 65536) -> bytes:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
data = await self._trio_socket.recv(max_bytes)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
if data:
|
||||
return data
|
||||
else:
|
||||
raise EndOfStream
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
with self._send_guard:
|
||||
view = memoryview(item)
|
||||
while view:
|
||||
try:
|
||||
bytes_sent = await self._trio_socket.send(view)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
view = view[bytes_sent:]
|
||||
|
||||
async def send_eof(self) -> None:
|
||||
self._trio_socket.shutdown(socket.SHUT_WR)
|
||||
|
||||
|
||||
class UNIXSocketStream(SocketStream, abc.UNIXSocketStream):
|
||||
async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]:
|
||||
if not isinstance(msglen, int) or msglen < 0:
|
||||
raise ValueError("msglen must be a non-negative integer")
|
||||
if not isinstance(maxfds, int) or maxfds < 1:
|
||||
raise ValueError("maxfds must be a positive integer")
|
||||
|
||||
fds = array.array("i")
|
||||
await checkpoint()
|
||||
with self._receive_guard:
|
||||
while True:
|
||||
try:
|
||||
message, ancdata, flags, addr = await self._trio_socket.recvmsg(
|
||||
msglen, socket.CMSG_LEN(maxfds * fds.itemsize)
|
||||
)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
else:
|
||||
if not message and not ancdata:
|
||||
raise EndOfStream
|
||||
|
||||
break
|
||||
|
||||
for cmsg_level, cmsg_type, cmsg_data in ancdata:
|
||||
if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS:
|
||||
raise RuntimeError(
|
||||
f"Received unexpected ancillary data; message = {message!r}, "
|
||||
f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}"
|
||||
)
|
||||
|
||||
fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
|
||||
|
||||
return message, list(fds)
|
||||
|
||||
async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None:
|
||||
if not message:
|
||||
raise ValueError("message must not be empty")
|
||||
if not fds:
|
||||
raise ValueError("fds must not be empty")
|
||||
|
||||
filenos: list[int] = []
|
||||
for fd in fds:
|
||||
if isinstance(fd, int):
|
||||
filenos.append(fd)
|
||||
elif isinstance(fd, IOBase):
|
||||
filenos.append(fd.fileno())
|
||||
|
||||
fdarray = array.array("i", filenos)
|
||||
await checkpoint()
|
||||
with self._send_guard:
|
||||
while True:
|
||||
try:
|
||||
await self._trio_socket.sendmsg(
|
||||
[message],
|
||||
[
|
||||
(
|
||||
socket.SOL_SOCKET,
|
||||
socket.SCM_RIGHTS, # type: ignore[list-item]
|
||||
fdarray,
|
||||
)
|
||||
],
|
||||
)
|
||||
break
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
class TCPSocketListener(_TrioSocketMixin, abc.SocketListener):
|
||||
def __init__(self, raw_socket: socket.socket):
|
||||
super().__init__(trio.socket.from_stdlib_socket(raw_socket))
|
||||
self._accept_guard = ResourceGuard("accepting connections from")
|
||||
|
||||
async def accept(self) -> SocketStream:
|
||||
with self._accept_guard:
|
||||
try:
|
||||
trio_socket, _addr = await self._trio_socket.accept()
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
return SocketStream(trio_socket)
|
||||
|
||||
|
||||
class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener):
|
||||
def __init__(self, raw_socket: socket.socket):
|
||||
super().__init__(trio.socket.from_stdlib_socket(raw_socket))
|
||||
self._accept_guard = ResourceGuard("accepting connections from")
|
||||
|
||||
async def accept(self) -> UNIXSocketStream:
|
||||
with self._accept_guard:
|
||||
try:
|
||||
trio_socket, _addr = await self._trio_socket.accept()
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
return UNIXSocketStream(trio_socket)
|
||||
|
||||
|
||||
class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self) -> tuple[bytes, IPSockAddrType]:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
data, addr = await self._trio_socket.recvfrom(65536)
|
||||
return data, convert_ipv6_sockaddr(addr)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
async def send(self, item: UDPPacketType) -> None:
|
||||
with self._send_guard:
|
||||
try:
|
||||
await self._trio_socket.sendto(*item)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket):
|
||||
def __init__(self, trio_socket: TrioSocketType) -> None:
|
||||
super().__init__(trio_socket)
|
||||
self._receive_guard = ResourceGuard("reading from")
|
||||
self._send_guard = ResourceGuard("writing to")
|
||||
|
||||
async def receive(self) -> bytes:
|
||||
with self._receive_guard:
|
||||
try:
|
||||
return await self._trio_socket.recv(65536)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
async def send(self, item: bytes) -> None:
|
||||
with self._send_guard:
|
||||
try:
|
||||
await self._trio_socket.send(item)
|
||||
except BaseException as exc:
|
||||
self._convert_socket_error(exc)
|
||||
|
||||
|
||||
async def connect_tcp(
|
||||
host: str, port: int, local_address: IPSockAddrType | None = None
|
||||
) -> SocketStream:
|
||||
family = socket.AF_INET6 if ":" in host else socket.AF_INET
|
||||
trio_socket = trio.socket.socket(family)
|
||||
trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
if local_address:
|
||||
await trio_socket.bind(local_address)
|
||||
|
||||
try:
|
||||
await trio_socket.connect((host, port))
|
||||
except BaseException:
|
||||
trio_socket.close()
|
||||
raise
|
||||
|
||||
return SocketStream(trio_socket)
|
||||
|
||||
|
||||
async def connect_unix(path: str) -> UNIXSocketStream:
|
||||
trio_socket = trio.socket.socket(socket.AF_UNIX)
|
||||
try:
|
||||
await trio_socket.connect(path)
|
||||
except BaseException:
|
||||
trio_socket.close()
|
||||
raise
|
||||
|
||||
return UNIXSocketStream(trio_socket)
|
||||
|
||||
|
||||
async def create_udp_socket(
|
||||
family: socket.AddressFamily,
|
||||
local_address: IPSockAddrType | None,
|
||||
remote_address: IPSockAddrType | None,
|
||||
reuse_port: bool,
|
||||
) -> UDPSocket | ConnectedUDPSocket:
|
||||
trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM)
|
||||
|
||||
if reuse_port:
|
||||
trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
||||
|
||||
if local_address:
|
||||
await trio_socket.bind(local_address)
|
||||
|
||||
if remote_address:
|
||||
await trio_socket.connect(remote_address)
|
||||
return ConnectedUDPSocket(trio_socket)
|
||||
else:
|
||||
return UDPSocket(trio_socket)
|
||||
|
||||
|
||||
getaddrinfo = trio.socket.getaddrinfo
|
||||
getnameinfo = trio.socket.getnameinfo
|
||||
|
||||
|
||||
async def wait_socket_readable(sock: socket.socket) -> None:
|
||||
try:
|
||||
await wait_readable(sock)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError().with_traceback(exc.__traceback__) from None
|
||||
except trio.BusyResourceError:
|
||||
raise BusyResourceError("reading from") from None
|
||||
|
||||
|
||||
async def wait_socket_writable(sock: socket.socket) -> None:
|
||||
try:
|
||||
await wait_writable(sock)
|
||||
except trio.ClosedResourceError as exc:
|
||||
raise ClosedResourceError().with_traceback(exc.__traceback__) from None
|
||||
except trio.BusyResourceError:
|
||||
raise BusyResourceError("writing to") from None
|
||||
|
||||
|
||||
#
|
||||
# Synchronization
|
||||
#
|
||||
|
||||
|
||||
class Event(BaseEvent):
|
||||
def __new__(cls) -> Event:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__original = trio.Event()
|
||||
|
||||
def is_set(self) -> bool:
|
||||
return self.__original.is_set()
|
||||
|
||||
async def wait(self) -> None:
|
||||
return await self.__original.wait()
|
||||
|
||||
def statistics(self) -> EventStatistics:
|
||||
orig_statistics = self.__original.statistics()
|
||||
return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting)
|
||||
|
||||
def set(self) -> DeprecatedAwaitable:
|
||||
self.__original.set()
|
||||
return DeprecatedAwaitable(self.set)
|
||||
|
||||
|
||||
class CapacityLimiter(BaseCapacityLimiter):
|
||||
def __new__(cls, *args: object, **kwargs: object) -> CapacityLimiter:
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(
|
||||
self, *args: Any, original: trio.CapacityLimiter | None = None
|
||||
) -> None:
|
||||
self.__original = original or trio.CapacityLimiter(*args)
|
||||
|
||||
async def __aenter__(self) -> None:
|
||||
return await self.__original.__aenter__()
|
||||
|
||||
async def __aexit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> None:
|
||||
await self.__original.__aexit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
@property
|
||||
def total_tokens(self) -> float:
|
||||
return self.__original.total_tokens
|
||||
|
||||
@total_tokens.setter
|
||||
def total_tokens(self, value: float) -> None:
|
||||
self.__original.total_tokens = value
|
||||
|
||||
@property
|
||||
def borrowed_tokens(self) -> int:
|
||||
return self.__original.borrowed_tokens
|
||||
|
||||
@property
|
||||
def available_tokens(self) -> float:
|
||||
return self.__original.available_tokens
|
||||
|
||||
def acquire_nowait(self) -> DeprecatedAwaitable:
|
||||
self.__original.acquire_nowait()
|
||||
return DeprecatedAwaitable(self.acquire_nowait)
|
||||
|
||||
def acquire_on_behalf_of_nowait(self, borrower: object) -> DeprecatedAwaitable:
|
||||
self.__original.acquire_on_behalf_of_nowait(borrower)
|
||||
return DeprecatedAwaitable(self.acquire_on_behalf_of_nowait)
|
||||
|
||||
async def acquire(self) -> None:
|
||||
await self.__original.acquire()
|
||||
|
||||
async def acquire_on_behalf_of(self, borrower: object) -> None:
|
||||
await self.__original.acquire_on_behalf_of(borrower)
|
||||
|
||||
def release(self) -> None:
|
||||
return self.__original.release()
|
||||
|
||||
def release_on_behalf_of(self, borrower: object) -> None:
|
||||
return self.__original.release_on_behalf_of(borrower)
|
||||
|
||||
def statistics(self) -> CapacityLimiterStatistics:
|
||||
orig = self.__original.statistics()
|
||||
return CapacityLimiterStatistics(
|
||||
borrowed_tokens=orig.borrowed_tokens,
|
||||
total_tokens=orig.total_tokens,
|
||||
borrowers=orig.borrowers,
|
||||
tasks_waiting=orig.tasks_waiting,
|
||||
)
|
||||
|
||||
|
||||
_capacity_limiter_wrapper: RunVar = RunVar("_capacity_limiter_wrapper")
|
||||
|
||||
|
||||
def current_default_thread_limiter() -> CapacityLimiter:
|
||||
try:
|
||||
return _capacity_limiter_wrapper.get()
|
||||
except LookupError:
|
||||
limiter = CapacityLimiter(
|
||||
original=trio.to_thread.current_default_thread_limiter()
|
||||
)
|
||||
_capacity_limiter_wrapper.set(limiter)
|
||||
return limiter
|
||||
|
||||
|
||||
#
|
||||
# Signal handling
|
||||
#
|
||||
|
||||
|
||||
class _SignalReceiver(DeprecatedAsyncContextManager["_SignalReceiver"]):
|
||||
_iterator: AsyncIterator[int]
|
||||
|
||||
def __init__(self, signals: tuple[Signals, ...]):
|
||||
self._signals = signals
|
||||
|
||||
def __enter__(self) -> _SignalReceiver:
|
||||
self._cm = trio.open_signal_receiver(*self._signals)
|
||||
self._iterator = self._cm.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> bool | None:
|
||||
return self._cm.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
def __aiter__(self) -> _SignalReceiver:
|
||||
return self
|
||||
|
||||
async def __anext__(self) -> Signals:
|
||||
signum = await self._iterator.__anext__()
|
||||
return Signals(signum)
|
||||
|
||||
|
||||
def open_signal_receiver(*signals: Signals) -> _SignalReceiver:
|
||||
return _SignalReceiver(signals)
|
||||
|
||||
|
||||
#
|
||||
# Testing and debugging
|
||||
#
|
||||
|
||||
|
||||
def get_current_task() -> TaskInfo:
|
||||
task = trio_lowlevel.current_task()
|
||||
|
||||
parent_id = None
|
||||
if task.parent_nursery and task.parent_nursery.parent_task:
|
||||
parent_id = id(task.parent_nursery.parent_task)
|
||||
|
||||
return TaskInfo(id(task), parent_id, task.name, task.coro)
|
||||
|
||||
|
||||
def get_running_tasks() -> list[TaskInfo]:
|
||||
root_task = trio_lowlevel.current_root_task()
|
||||
task_infos = [TaskInfo(id(root_task), None, root_task.name, root_task.coro)]
|
||||
nurseries = root_task.child_nurseries
|
||||
while nurseries:
|
||||
new_nurseries: list[trio.Nursery] = []
|
||||
for nursery in nurseries:
|
||||
for task in nursery.child_tasks:
|
||||
task_infos.append(
|
||||
TaskInfo(id(task), id(nursery.parent_task), task.name, task.coro)
|
||||
)
|
||||
new_nurseries.extend(task.child_nurseries)
|
||||
|
||||
nurseries = new_nurseries
|
||||
|
||||
return task_infos
|
||||
|
||||
|
||||
def wait_all_tasks_blocked() -> Awaitable[None]:
|
||||
import trio.testing
|
||||
|
||||
return trio.testing.wait_all_tasks_blocked()
|
||||
|
||||
|
||||
class TestRunner(abc.TestRunner):
|
||||
def __init__(self, **options: Any) -> None:
|
||||
from collections import deque
|
||||
from queue import Queue
|
||||
|
||||
self._call_queue: Queue[Callable[..., object]] = Queue()
|
||||
self._result_queue: deque[Outcome] = deque()
|
||||
self._stop_event: trio.Event | None = None
|
||||
self._nursery: trio.Nursery | None = None
|
||||
self._options = options
|
||||
|
||||
async def _trio_main(self) -> None:
|
||||
self._stop_event = trio.Event()
|
||||
async with trio.open_nursery() as self._nursery:
|
||||
await self._stop_event.wait()
|
||||
|
||||
async def _call_func(
|
||||
self, func: Callable[..., Awaitable[object]], args: tuple, kwargs: dict
|
||||
) -> None:
|
||||
try:
|
||||
retval = await func(*args, **kwargs)
|
||||
except BaseException as exc:
|
||||
self._result_queue.append(Error(exc))
|
||||
else:
|
||||
self._result_queue.append(Value(retval))
|
||||
|
||||
def _main_task_finished(self, outcome: object) -> None:
|
||||
self._nursery = None
|
||||
|
||||
def _get_nursery(self) -> trio.Nursery:
|
||||
if self._nursery is None:
|
||||
trio.lowlevel.start_guest_run(
|
||||
self._trio_main,
|
||||
run_sync_soon_threadsafe=self._call_queue.put,
|
||||
done_callback=self._main_task_finished,
|
||||
**self._options,
|
||||
)
|
||||
while self._nursery is None:
|
||||
self._call_queue.get()()
|
||||
|
||||
return self._nursery
|
||||
|
||||
def _call(
|
||||
self, func: Callable[..., Awaitable[T_Retval]], *args: object, **kwargs: object
|
||||
) -> T_Retval:
|
||||
self._get_nursery().start_soon(self._call_func, func, args, kwargs)
|
||||
while not self._result_queue:
|
||||
self._call_queue.get()()
|
||||
|
||||
outcome = self._result_queue.pop()
|
||||
return outcome.unwrap()
|
||||
|
||||
def close(self) -> None:
|
||||
if self._stop_event:
|
||||
self._stop_event.set()
|
||||
while self._nursery is not None:
|
||||
self._call_queue.get()()
|
||||
|
||||
def run_asyncgen_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> Iterable[T_Retval]:
|
||||
async def fixture_runner(*, task_status: TaskStatus[T_Retval]) -> None:
|
||||
agen = fixture_func(**kwargs)
|
||||
retval = await agen.asend(None)
|
||||
task_status.started(retval)
|
||||
await teardown_event.wait()
|
||||
try:
|
||||
await agen.asend(None)
|
||||
except StopAsyncIteration:
|
||||
pass
|
||||
else:
|
||||
await agen.aclose()
|
||||
raise RuntimeError("Async generator fixture did not stop")
|
||||
|
||||
teardown_event = trio.Event()
|
||||
fixture_value = self._call(lambda: self._get_nursery().start(fixture_runner))
|
||||
yield fixture_value
|
||||
teardown_event.set()
|
||||
|
||||
def run_fixture(
|
||||
self,
|
||||
fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]],
|
||||
kwargs: dict[str, Any],
|
||||
) -> T_Retval:
|
||||
return self._call(fixture_func, **kwargs)
|
||||
|
||||
def run_test(
|
||||
self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any]
|
||||
) -> None:
|
||||
self._call(test_func, **kwargs)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user