refactor: 重构项目结构,将前端和后端代码分离到独立目录
- 将前端代码移动到 frontend/ 目录 - 将后端代码移动到 backend/ 目录 - 更新 .gitignore 以包含 Python 和前端构建产物 - 修复 LLM JSON 解析问题,增强错误处理 - 修复前端配置默认值,改为从后端获取 - 删除 AdminDashboard 中的数据库信息和统计卡片 - 完善系统配置管理,支持从后端获取默认配置
|
|
@ -96,3 +96,75 @@ jspm_packages/
|
|||
tmp/
|
||||
temp/
|
||||
.vercel
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# Backend specific
|
||||
backend/.venv/
|
||||
backend/venv/
|
||||
backend/__pycache__/
|
||||
backend/*.pyc
|
||||
backend/.pytest_cache/
|
||||
backend/.coverage
|
||||
backend/alembic.ini.bak
|
||||
|
||||
# Frontend specific
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/build/
|
||||
frontend/.vite/
|
||||
frontend/.next/
|
||||
frontend/.nuxt/
|
||||
frontend/.cache/
|
||||
frontend/.parcel-cache/
|
||||
frontend/.turbo/
|
||||
frontend/.svelte-kit/
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Secrets and keys
|
||||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
*.crt
|
||||
secrets/
|
||||
.secrets/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
.venv/
|
||||
venv/
|
||||
|
|
@ -0,0 +1 @@
|
|||
3.13
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Command is overridden by docker-compose for dev
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# 使用 uv 管理 Python 依赖
|
||||
|
||||
本项目已迁移到使用 [uv](https://github.com/astral-sh/uv) 作为 Python 依赖管理器。
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 安装 uv
|
||||
|
||||
```bash
|
||||
# macOS/Linux
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
|
||||
# 或使用 Homebrew
|
||||
brew install uv
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv sync
|
||||
```
|
||||
|
||||
这会自动创建虚拟环境并安装所有依赖。
|
||||
|
||||
### 运行项目
|
||||
|
||||
```bash
|
||||
# 激活虚拟环境(uv 会自动管理)
|
||||
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
# 或使用 uv 直接运行
|
||||
uv run python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
```bash
|
||||
uv run alembic upgrade head
|
||||
```
|
||||
|
||||
### 添加新依赖
|
||||
|
||||
```bash
|
||||
# 添加依赖
|
||||
uv add package-name
|
||||
|
||||
# 添加开发依赖
|
||||
uv add --dev package-name
|
||||
```
|
||||
|
||||
### 更新依赖
|
||||
|
||||
```bash
|
||||
uv sync --upgrade
|
||||
```
|
||||
|
||||
### 其他常用命令
|
||||
|
||||
```bash
|
||||
# 查看已安装的包
|
||||
uv pip list
|
||||
|
||||
# 运行 Python 脚本
|
||||
uv run python script.py
|
||||
|
||||
# 运行 Alembic 命令
|
||||
uv run alembic <command>
|
||||
```
|
||||
|
||||
## 从 pip/venv 迁移
|
||||
|
||||
如果你之前使用 pip 和 venv:
|
||||
|
||||
1. 删除旧的虚拟环境(可选):
|
||||
```bash
|
||||
rm -rf venv
|
||||
```
|
||||
|
||||
2. 使用 uv 同步依赖:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
3. 之后使用 `uv run` 运行命令,或激活 uv 创建的虚拟环境。
|
||||
|
||||
## 优势
|
||||
|
||||
- **速度快**:比 pip 快 10-100 倍
|
||||
- **可复现**:自动生成锁文件
|
||||
- **简单**:一个命令管理所有依赖
|
||||
- **兼容**:完全兼容 pip 和 requirements.txt
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# 迁移到 uv 依赖管理器
|
||||
|
||||
## ✅ 已完成的工作
|
||||
|
||||
1. **初始化 uv 项目**
|
||||
- 创建了 `pyproject.toml` 配置文件
|
||||
- 所有依赖已迁移到 `pyproject.toml`
|
||||
|
||||
2. **依赖管理**
|
||||
- 使用 `uv sync` 安装所有依赖
|
||||
- 创建了 `.venv` 虚拟环境(uv 自动管理)
|
||||
- 生成了 `requirements-lock.txt` 锁定文件
|
||||
|
||||
3. **工具和脚本**
|
||||
- 创建了 `start.sh` 启动脚本
|
||||
- 创建了 `README_UV.md` 使用文档
|
||||
|
||||
## 📝 使用方式
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./start.sh
|
||||
```
|
||||
|
||||
或手动启动:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
### 数据库迁移
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run alembic upgrade head
|
||||
```
|
||||
|
||||
### 添加新依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv add package-name
|
||||
```
|
||||
|
||||
## 🔄 从旧环境迁移
|
||||
|
||||
如果你之前使用 `venv` 和 `pip`:
|
||||
|
||||
1. **删除旧虚拟环境**(可选):
|
||||
```bash
|
||||
rm -rf venv
|
||||
```
|
||||
|
||||
2. **使用 uv 同步**:
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
3. **之后使用 `uv run` 运行所有命令**
|
||||
|
||||
## 📦 依赖文件说明
|
||||
|
||||
- `pyproject.toml` - 项目配置和依赖声明(主要文件)
|
||||
- `requirements.txt` - 保留用于兼容性,但建议使用 `pyproject.toml`
|
||||
- `requirements-lock.txt` - 自动生成的锁定文件(确保可复现)
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- uv 创建的虚拟环境在 `.venv/` 目录(不是 `venv/`)
|
||||
- 使用 `uv run` 运行命令会自动使用正确的虚拟环境
|
||||
- 也可以手动激活:`source .venv/bin/activate`
|
||||
|
||||
## 🎯 优势
|
||||
|
||||
- ⚡ **速度快**:比 pip 快 10-100 倍
|
||||
- 🔒 **可复现**:自动生成锁定文件
|
||||
- 🎯 **简单**:一个命令管理所有依赖
|
||||
- 🔄 **兼容**:完全兼容 pip 和 requirements.txt
|
||||
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# 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 to be installed.
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for local time
|
||||
# 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 location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator"
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||
|
||||
# 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 # Use os.pathsep. Default configuration used for new projects.
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[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
|
||||
|
||||
# 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
|
||||
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from app.db.base import Base
|
||||
from app.models import * # noqa
|
||||
from app.core.config import settings
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
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()
|
||||
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
"""${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"}
|
||||
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
"""Initial migration - create all tables
|
||||
|
||||
Revision ID: 001_initial
|
||||
Revises:
|
||||
Create Date: 2024-01-01 00:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '001_initial'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create users table
|
||||
op.create_table(
|
||||
'users',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('email', sa.String(), nullable=False),
|
||||
sa.Column('hashed_password', sa.String(), nullable=False),
|
||||
sa.Column('full_name', sa.String(), nullable=True),
|
||||
sa.Column('avatar_url', sa.String(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False),
|
||||
sa.Column('is_superuser', sa.Boolean(), server_default='false', nullable=False),
|
||||
sa.Column('role', sa.String(), server_default='user', nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), onupdate=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||
|
||||
# Create projects table
|
||||
op.create_table(
|
||||
'projects',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('name', sa.String(), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('repository_url', sa.String(), nullable=True),
|
||||
sa.Column('repository_type', sa.String(), nullable=True),
|
||||
sa.Column('default_branch', sa.String(), nullable=True),
|
||||
sa.Column('programming_languages', sa.Text(), nullable=True),
|
||||
sa.Column('owner_id', sa.String(), sa.ForeignKey('users.id'), nullable=False),
|
||||
sa.Column('status', sa.String(), server_default='active', nullable=False),
|
||||
sa.Column('is_deleted', sa.Boolean(), server_default='false', nullable=False),
|
||||
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), onupdate=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_projects_owner_id'), 'projects', ['owner_id'], unique=False)
|
||||
|
||||
# Create project_members table
|
||||
op.create_table(
|
||||
'project_members',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('project_id', sa.String(), sa.ForeignKey('projects.id'), nullable=False),
|
||||
sa.Column('user_id', sa.String(), sa.ForeignKey('users.id'), nullable=False),
|
||||
sa.Column('role', sa.String(), server_default='member', nullable=False),
|
||||
sa.Column('permissions', sa.Text(), server_default='{}', nullable=True),
|
||||
sa.Column('joined_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create audit_tasks table
|
||||
op.create_table(
|
||||
'audit_tasks',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('project_id', sa.String(), sa.ForeignKey('projects.id'), nullable=False),
|
||||
sa.Column('created_by', sa.String(), sa.ForeignKey('users.id'), nullable=False),
|
||||
sa.Column('task_type', sa.String(), nullable=False),
|
||||
sa.Column('status', sa.String(), server_default='pending', nullable=False),
|
||||
sa.Column('branch_name', sa.String(), nullable=True),
|
||||
sa.Column('exclude_patterns', sa.Text(), server_default='[]', nullable=True),
|
||||
sa.Column('scan_config', sa.Text(), server_default='{}', nullable=True),
|
||||
sa.Column('total_files', sa.Integer(), server_default='0', nullable=False),
|
||||
sa.Column('scanned_files', sa.Integer(), server_default='0', nullable=False),
|
||||
sa.Column('total_lines', sa.Integer(), server_default='0', nullable=False),
|
||||
sa.Column('issues_count', sa.Integer(), server_default='0', nullable=False),
|
||||
sa.Column('quality_score', sa.Float(), server_default='0.0', nullable=False),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_audit_tasks_status'), 'audit_tasks', ['status'], unique=False)
|
||||
|
||||
# Create audit_issues table
|
||||
op.create_table(
|
||||
'audit_issues',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('task_id', sa.String(), sa.ForeignKey('audit_tasks.id'), nullable=False),
|
||||
sa.Column('file_path', sa.String(), nullable=False),
|
||||
sa.Column('line_number', sa.Integer(), nullable=True),
|
||||
sa.Column('column_number', sa.Integer(), nullable=True),
|
||||
sa.Column('issue_type', sa.String(), nullable=False),
|
||||
sa.Column('severity', sa.String(), nullable=False),
|
||||
sa.Column('title', sa.String(), nullable=True),
|
||||
sa.Column('message', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('suggestion', sa.Text(), nullable=True),
|
||||
sa.Column('code_snippet', sa.Text(), nullable=True),
|
||||
sa.Column('ai_explanation', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.String(), server_default='open', nullable=False),
|
||||
sa.Column('resolved_by', sa.String(), sa.ForeignKey('users.id'), nullable=True),
|
||||
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create instant_analyses table
|
||||
op.create_table(
|
||||
'instant_analyses',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), sa.ForeignKey('users.id'), nullable=True),
|
||||
sa.Column('language', sa.String(), nullable=False),
|
||||
sa.Column('code_content', sa.Text(), server_default='', nullable=True),
|
||||
sa.Column('analysis_result', sa.Text(), server_default='{}', nullable=True),
|
||||
sa.Column('issues_count', sa.Integer(), server_default='0', nullable=False),
|
||||
sa.Column('quality_score', sa.Float(), server_default='0.0', nullable=False),
|
||||
sa.Column('analysis_time', sa.Float(), server_default='0.0', nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create user_configs table
|
||||
op.create_table(
|
||||
'user_configs',
|
||||
sa.Column('id', sa.String(), nullable=False),
|
||||
sa.Column('user_id', sa.String(), sa.ForeignKey('users.id'), nullable=False),
|
||||
sa.Column('llm_config', sa.Text(), server_default='{}', nullable=True),
|
||||
sa.Column('other_config', sa.Text(), server_default='{}', nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), onupdate=sa.func.now(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id')
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('user_configs')
|
||||
op.drop_table('instant_analyses')
|
||||
op.drop_table('audit_issues')
|
||||
op.drop_index(op.f('ix_audit_tasks_status'), table_name='audit_tasks')
|
||||
op.drop_table('audit_tasks')
|
||||
op.drop_table('project_members')
|
||||
op.drop_index(op.f('ix_projects_owner_id'), table_name='projects')
|
||||
op.drop_table('projects')
|
||||
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||
op.drop_table('users')
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
"""add_missing_user_fields
|
||||
|
||||
Revision ID: 5fc1cc05d5d0
|
||||
Revises: 001_initial
|
||||
Create Date: 2025-11-26 20:27:00.645441
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5fc1cc05d5d0'
|
||||
down_revision = '001_initial'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add missing columns to users table
|
||||
op.add_column('users', sa.Column('phone', sa.String(), nullable=True))
|
||||
op.add_column('users', sa.Column('github_username', sa.String(), nullable=True))
|
||||
op.add_column('users', sa.Column('gitlab_username', sa.String(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove added columns
|
||||
op.drop_column('users', 'gitlab_username')
|
||||
op.drop_column('users', 'github_username')
|
||||
op.drop_column('users', 'phone')
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
"""add_is_active_to_projects
|
||||
|
||||
Revision ID: 73889a94a455
|
||||
Revises: 5fc1cc05d5d0
|
||||
Create Date: 2025-11-26 20:40:11.375161
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '73889a94a455'
|
||||
down_revision = '5fc1cc05d5d0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add is_active column to projects table
|
||||
op.add_column('projects', sa.Column('is_active', sa.Boolean(), server_default='true', nullable=False))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove is_active column from projects table
|
||||
op.drop_column('projects', 'is_active')
|
||||
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
from typing import Generator, Optional
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from jose import jwt, JWTError
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from app.core import security
|
||||
from app.core.config import settings
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas import token as token_schema
|
||||
|
||||
reusable_oauth2 = OAuth2PasswordBearer(
|
||||
tokenUrl=f"{settings.API_V1_STR}/auth/login"
|
||||
)
|
||||
|
||||
async def get_current_user(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
token: str = Depends(reusable_oauth2)
|
||||
) -> User:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||
)
|
||||
token_data = token_schema.TokenPayload(**payload)
|
||||
except (JWTError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无法验证凭据",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
result = await db.execute(select(User).where(User.id == token_data.sub))
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||
return user
|
||||
|
||||
async def get_current_active_superuser(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> User:
|
||||
if not current_user.is_superuser:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="权限不足"
|
||||
)
|
||||
return current_user
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from fastapi import APIRouter
|
||||
from app.api.v1.endpoints import auth, users, projects, tasks, scan, members, config, database
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||
api_router.include_router(projects.router, prefix="/projects", tags=["projects"])
|
||||
api_router.include_router(members.router, prefix="/projects", tags=["members"])
|
||||
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
|
||||
api_router.include_router(scan.router, prefix="/scan", tags=["scan"])
|
||||
api_router.include_router(config.router, prefix="/config", tags=["config"])
|
||||
api_router.include_router(database.router, prefix="/database", tags=["database"])
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
from app.api import deps
|
||||
from app.core import security
|
||||
from app.core.config import settings
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.token import Token
|
||||
from app.schemas.user import User as UserSchema, UserCreate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
full_name: str
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
form_data: OAuth2PasswordRequestForm = Depends()
|
||||
) -> Any:
|
||||
"""
|
||||
OAuth2 compatible token login, get an access token for future requests.
|
||||
Username field should contain the email address.
|
||||
"""
|
||||
result = await db.execute(select(User).where(User.email == form_data.username))
|
||||
user = result.scalars().first()
|
||||
|
||||
if not user or not security.verify_password(form_data.password, user.hashed_password):
|
||||
raise HTTPException(status_code=400, detail="邮箱或密码错误")
|
||||
elif not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="用户已被禁用")
|
||||
|
||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
return {
|
||||
"access_token": security.create_access_token(
|
||||
user.id, expires_delta=access_token_expires
|
||||
),
|
||||
"token_type": "bearer",
|
||||
}
|
||||
|
||||
@router.post("/register", response_model=UserSchema)
|
||||
async def register(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_in: RegisterRequest,
|
||||
) -> Any:
|
||||
"""
|
||||
Register a new user.
|
||||
"""
|
||||
# Check if user already exists
|
||||
result = await db.execute(select(User).where(User.email == user_in.email))
|
||||
existing_user = result.scalars().first()
|
||||
if existing_user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="该邮箱已被注册",
|
||||
)
|
||||
|
||||
# Check if this is the first user (make them admin)
|
||||
count_result = await db.execute(select(User))
|
||||
all_users = count_result.scalars().all()
|
||||
is_first_user = len(all_users) == 0
|
||||
|
||||
# Create new user
|
||||
db_user = User(
|
||||
email=user_in.email,
|
||||
hashed_password=security.get_password_hash(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
is_active=True,
|
||||
is_superuser=is_first_user,
|
||||
role="admin" if is_first_user else "member",
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
"""
|
||||
用户配置API端点
|
||||
"""
|
||||
|
||||
from typing import Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db
|
||||
from app.models.user_config import UserConfig
|
||||
from app.models.user import User
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class LLMConfigSchema(BaseModel):
|
||||
"""LLM配置Schema"""
|
||||
llmProvider: Optional[str] = None
|
||||
llmApiKey: Optional[str] = None
|
||||
llmModel: Optional[str] = None
|
||||
llmBaseUrl: Optional[str] = None
|
||||
llmTimeout: Optional[int] = None
|
||||
llmTemperature: Optional[float] = None
|
||||
llmMaxTokens: Optional[int] = None
|
||||
llmCustomHeaders: Optional[str] = None
|
||||
|
||||
# 平台专用配置
|
||||
geminiApiKey: Optional[str] = None
|
||||
openaiApiKey: Optional[str] = None
|
||||
claudeApiKey: Optional[str] = None
|
||||
qwenApiKey: Optional[str] = None
|
||||
deepseekApiKey: Optional[str] = None
|
||||
zhipuApiKey: Optional[str] = None
|
||||
moonshotApiKey: Optional[str] = None
|
||||
baiduApiKey: Optional[str] = None
|
||||
minimaxApiKey: Optional[str] = None
|
||||
doubaoApiKey: Optional[str] = None
|
||||
ollamaBaseUrl: Optional[str] = None
|
||||
|
||||
|
||||
class OtherConfigSchema(BaseModel):
|
||||
"""其他配置Schema"""
|
||||
githubToken: Optional[str] = None
|
||||
gitlabToken: Optional[str] = None
|
||||
maxAnalyzeFiles: Optional[int] = None
|
||||
llmConcurrency: Optional[int] = None
|
||||
llmGapMs: Optional[int] = None
|
||||
outputLanguage: Optional[str] = None
|
||||
|
||||
|
||||
class UserConfigRequest(BaseModel):
|
||||
"""用户配置请求"""
|
||||
llmConfig: Optional[LLMConfigSchema] = None
|
||||
otherConfig: Optional[OtherConfigSchema] = None
|
||||
|
||||
|
||||
class UserConfigResponse(BaseModel):
|
||||
"""用户配置响应"""
|
||||
id: str
|
||||
user_id: str
|
||||
llmConfig: dict
|
||||
otherConfig: dict
|
||||
created_at: str
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def get_default_config() -> dict:
|
||||
"""获取系统默认配置"""
|
||||
return {
|
||||
"llmConfig": {
|
||||
"llmProvider": settings.LLM_PROVIDER,
|
||||
"llmApiKey": "",
|
||||
"llmModel": settings.LLM_MODEL or "",
|
||||
"llmBaseUrl": settings.LLM_BASE_URL or "",
|
||||
"llmTimeout": settings.LLM_TIMEOUT * 1000, # 转换为毫秒
|
||||
"llmTemperature": settings.LLM_TEMPERATURE,
|
||||
"llmMaxTokens": settings.LLM_MAX_TOKENS,
|
||||
"llmCustomHeaders": "",
|
||||
"geminiApiKey": settings.GEMINI_API_KEY or "",
|
||||
"openaiApiKey": settings.OPENAI_API_KEY or "",
|
||||
"claudeApiKey": settings.CLAUDE_API_KEY or "",
|
||||
"qwenApiKey": settings.QWEN_API_KEY or "",
|
||||
"deepseekApiKey": settings.DEEPSEEK_API_KEY or "",
|
||||
"zhipuApiKey": settings.ZHIPU_API_KEY or "",
|
||||
"moonshotApiKey": settings.MOONSHOT_API_KEY or "",
|
||||
"baiduApiKey": settings.BAIDU_API_KEY or "",
|
||||
"minimaxApiKey": settings.MINIMAX_API_KEY or "",
|
||||
"doubaoApiKey": settings.DOUBAO_API_KEY or "",
|
||||
"ollamaBaseUrl": settings.OLLAMA_BASE_URL or "http://localhost:11434/v1",
|
||||
},
|
||||
"otherConfig": {
|
||||
"githubToken": settings.GITHUB_TOKEN or "",
|
||||
"gitlabToken": settings.GITLAB_TOKEN or "",
|
||||
"maxAnalyzeFiles": settings.MAX_ANALYZE_FILES,
|
||||
"llmConcurrency": settings.LLM_CONCURRENCY,
|
||||
"llmGapMs": settings.LLM_GAP_MS,
|
||||
"outputLanguage": settings.OUTPUT_LANGUAGE,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@router.get("/defaults")
|
||||
async def get_default_config_endpoint() -> Any:
|
||||
"""获取系统默认配置(无需认证)"""
|
||||
return get_default_config()
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserConfigResponse)
|
||||
async def get_my_config(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""获取当前用户的配置(合并用户配置和系统默认配置)"""
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
# 获取系统默认配置
|
||||
default_config = get_default_config()
|
||||
|
||||
if not config:
|
||||
# 返回系统默认配置
|
||||
return UserConfigResponse(
|
||||
id="",
|
||||
user_id=current_user.id,
|
||||
llmConfig=default_config["llmConfig"],
|
||||
otherConfig=default_config["otherConfig"],
|
||||
created_at="",
|
||||
)
|
||||
|
||||
# 合并用户配置和默认配置(用户配置优先)
|
||||
user_llm_config = json.loads(config.llm_config) if config.llm_config else {}
|
||||
user_other_config = json.loads(config.other_config) if config.other_config else {}
|
||||
|
||||
merged_llm_config = {**default_config["llmConfig"], **user_llm_config}
|
||||
merged_other_config = {**default_config["otherConfig"], **user_other_config}
|
||||
|
||||
return UserConfigResponse(
|
||||
id=config.id,
|
||||
user_id=config.user_id,
|
||||
llmConfig=merged_llm_config,
|
||||
otherConfig=merged_other_config,
|
||||
created_at=config.created_at.isoformat() if config.created_at else "",
|
||||
updated_at=config.updated_at.isoformat() if config.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserConfigResponse)
|
||||
async def update_my_config(
|
||||
config_in: UserConfigRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""更新当前用户的配置"""
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
# 创建新配置
|
||||
config = UserConfig(
|
||||
user_id=current_user.id,
|
||||
llm_config=json.dumps(config_in.llmConfig.dict(exclude_none=True) if config_in.llmConfig else {}),
|
||||
other_config=json.dumps(config_in.otherConfig.dict(exclude_none=True) if config_in.otherConfig else {}),
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
# 更新现有配置
|
||||
if config_in.llmConfig:
|
||||
existing_llm = json.loads(config.llm_config) if config.llm_config else {}
|
||||
existing_llm.update(config_in.llmConfig.dict(exclude_none=True))
|
||||
config.llm_config = json.dumps(existing_llm)
|
||||
|
||||
if config_in.otherConfig:
|
||||
existing_other = json.loads(config.other_config) if config.other_config else {}
|
||||
existing_other.update(config_in.otherConfig.dict(exclude_none=True))
|
||||
config.other_config = json.dumps(existing_other)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(config)
|
||||
|
||||
return UserConfigResponse(
|
||||
id=config.id,
|
||||
user_id=config.user_id,
|
||||
llmConfig=json.loads(config.llm_config) if config.llm_config else {},
|
||||
otherConfig=json.loads(config.other_config) if config.other_config else {},
|
||||
created_at=config.created_at.isoformat() if config.created_at else "",
|
||||
updated_at=config.updated_at.isoformat() if config.updated_at else None,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/me")
|
||||
async def delete_my_config(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""删除当前用户的配置(恢复为默认)"""
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
if config:
|
||||
await db.delete(config)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "配置已删除"}
|
||||
|
||||
|
|
@ -0,0 +1,692 @@
|
|||
"""
|
||||
数据库管理API端点
|
||||
提供数据导出、导入、清空等功能
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.models.project import Project, ProjectMember
|
||||
from app.models.audit import AuditTask, AuditIssue
|
||||
from app.models.analysis import InstantAnalysis
|
||||
from app.models.user_config import UserConfig
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class DatabaseExportResponse(BaseModel):
|
||||
"""数据库导出响应"""
|
||||
export_date: str
|
||||
user_id: str
|
||||
data: Dict[str, Any]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/export", response_model=DatabaseExportResponse)
|
||||
async def export_database(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
导出当前用户的所有数据
|
||||
包括:项目、任务、问题、即时分析、用户配置
|
||||
"""
|
||||
try:
|
||||
# 1. 获取用户的所有项目
|
||||
projects_result = await db.execute(
|
||||
select(Project)
|
||||
.where(Project.owner_id == current_user.id)
|
||||
.options(selectinload(Project.tasks))
|
||||
)
|
||||
projects = projects_result.scalars().all()
|
||||
|
||||
# 2. 获取用户的所有任务
|
||||
tasks_result = await db.execute(
|
||||
select(AuditTask)
|
||||
.where(AuditTask.created_by == current_user.id)
|
||||
.options(selectinload(AuditTask.issues))
|
||||
)
|
||||
tasks = tasks_result.scalars().all()
|
||||
|
||||
# 3. 获取用户的所有问题(通过任务关联)
|
||||
task_ids = [task.id for task in tasks]
|
||||
issues = []
|
||||
if task_ids:
|
||||
issues_result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id.in_(task_ids))
|
||||
)
|
||||
issues = issues_result.scalars().all()
|
||||
|
||||
# 4. 获取用户的即时分析记录
|
||||
analyses_result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
analyses = analyses_result.scalars().all()
|
||||
|
||||
# 5. 获取用户的配置
|
||||
config_result = await db.execute(
|
||||
select(UserConfig)
|
||||
.where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = config_result.scalar_one_or_none()
|
||||
|
||||
# 6. 获取用户参与的项目(作为成员)
|
||||
members_result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.user_id == current_user.id)
|
||||
.options(selectinload(ProjectMember.project))
|
||||
)
|
||||
members = members_result.scalars().all()
|
||||
|
||||
# 7. 构建导出数据
|
||||
export_data = {
|
||||
"version": "1.0.0",
|
||||
"export_date": datetime.utcnow().isoformat(),
|
||||
"user": {
|
||||
"id": current_user.id,
|
||||
"email": current_user.email,
|
||||
"full_name": current_user.full_name,
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"id": p.id,
|
||||
"name": p.name,
|
||||
"description": p.description,
|
||||
"repository_url": p.repository_url,
|
||||
"repository_type": p.repository_type,
|
||||
"default_branch": p.default_branch,
|
||||
"programming_languages": json.loads(p.programming_languages) if p.programming_languages else [],
|
||||
"is_active": p.is_active,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
for p in projects
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"id": t.id,
|
||||
"project_id": t.project_id,
|
||||
"task_type": t.task_type,
|
||||
"status": t.status,
|
||||
"branch_name": t.branch_name,
|
||||
"exclude_patterns": json.loads(t.exclude_patterns) if t.exclude_patterns else [],
|
||||
"total_files": t.total_files,
|
||||
"scanned_files": t.scanned_files,
|
||||
"total_lines": t.total_lines,
|
||||
"issues_count": t.issues_count,
|
||||
"quality_score": t.quality_score,
|
||||
"started_at": t.started_at.isoformat() if t.started_at else None,
|
||||
"completed_at": t.completed_at.isoformat() if t.completed_at else None,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
"issues": [
|
||||
{
|
||||
"id": i.id,
|
||||
"task_id": i.task_id,
|
||||
"file_path": i.file_path,
|
||||
"line_number": i.line_number,
|
||||
"column_number": i.column_number,
|
||||
"issue_type": i.issue_type,
|
||||
"severity": i.severity,
|
||||
"title": i.title,
|
||||
"message": i.message,
|
||||
"description": i.description,
|
||||
"suggestion": i.suggestion,
|
||||
"code_snippet": i.code_snippet,
|
||||
"ai_explanation": i.ai_explanation,
|
||||
"status": i.status,
|
||||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||||
}
|
||||
for i in issues
|
||||
],
|
||||
"instant_analyses": [
|
||||
{
|
||||
"id": a.id,
|
||||
"language": a.language,
|
||||
"issues_count": a.issues_count,
|
||||
"quality_score": a.quality_score,
|
||||
"analysis_time": a.analysis_time,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
}
|
||||
for a in analyses
|
||||
],
|
||||
"user_config": {
|
||||
"llm_config": json.loads(config.llm_config) if config and config.llm_config else {},
|
||||
"other_config": json.loads(config.other_config) if config and config.other_config else {},
|
||||
} if config else {},
|
||||
"project_members": [
|
||||
{
|
||||
"id": m.id,
|
||||
"project_id": m.project_id,
|
||||
"role": m.role,
|
||||
"permissions": json.loads(m.permissions) if m.permissions else {},
|
||||
"joined_at": m.joined_at.isoformat() if m.joined_at else None,
|
||||
}
|
||||
for m in members
|
||||
],
|
||||
}
|
||||
|
||||
return DatabaseExportResponse(
|
||||
export_date=export_data["export_date"],
|
||||
user_id=current_user.id,
|
||||
data=export_data
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"导出数据失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"导出数据失败: {str(e)}")
|
||||
|
||||
|
||||
class DatabaseImportRequest(BaseModel):
|
||||
"""数据库导入请求"""
|
||||
data: Dict[str, Any]
|
||||
|
||||
|
||||
@router.post("/import")
|
||||
async def import_database(
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
从 JSON 文件导入数据
|
||||
注意:导入会合并数据,不会删除现有数据
|
||||
"""
|
||||
try:
|
||||
# 读取文件内容
|
||||
content = await file.read()
|
||||
import_data = json.loads(content.decode('utf-8'))
|
||||
|
||||
if not isinstance(import_data, dict) or "data" not in import_data:
|
||||
raise HTTPException(status_code=400, detail="无效的导入文件格式")
|
||||
|
||||
data = import_data["data"]
|
||||
|
||||
# 验证用户ID(只能导入自己的数据)
|
||||
if data.get("user", {}).get("id") != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="只能导入自己的数据")
|
||||
|
||||
imported_count = {
|
||||
"projects": 0,
|
||||
"tasks": 0,
|
||||
"issues": 0,
|
||||
"analyses": 0,
|
||||
"config": 0,
|
||||
}
|
||||
|
||||
# 1. 导入项目(跳过已存在的)
|
||||
if "projects" in data:
|
||||
for p_data in data["projects"]:
|
||||
existing = await db.get(Project, p_data.get("id"))
|
||||
if not existing:
|
||||
project = Project(
|
||||
id=p_data.get("id"),
|
||||
name=p_data.get("name"),
|
||||
description=p_data.get("description"),
|
||||
repository_url=p_data.get("repository_url"),
|
||||
repository_type=p_data.get("repository_type"),
|
||||
default_branch=p_data.get("default_branch"),
|
||||
programming_languages=json.dumps(p_data.get("programming_languages", [])),
|
||||
owner_id=current_user.id,
|
||||
is_active=p_data.get("is_active", True) if "is_active" in p_data else (p_data.get("status") != "inactive" if "status" in p_data else True),
|
||||
)
|
||||
db.add(project)
|
||||
imported_count["projects"] += 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 2. 导入任务(需要先有项目)
|
||||
if "tasks" in data:
|
||||
for t_data in data["tasks"]:
|
||||
existing = await db.get(AuditTask, t_data.get("id"))
|
||||
if not existing:
|
||||
# 检查项目是否存在
|
||||
project = await db.get(Project, t_data.get("project_id"))
|
||||
if project:
|
||||
task = AuditTask(
|
||||
id=t_data.get("id"),
|
||||
project_id=t_data.get("project_id"),
|
||||
created_by=current_user.id,
|
||||
task_type=t_data.get("task_type"),
|
||||
status=t_data.get("status", "pending"),
|
||||
branch_name=t_data.get("branch_name"),
|
||||
exclude_patterns=json.dumps(t_data.get("exclude_patterns", [])),
|
||||
scan_config=json.dumps(t_data.get("scan_config", {})),
|
||||
total_files=t_data.get("total_files", 0),
|
||||
scanned_files=t_data.get("scanned_files", 0),
|
||||
total_lines=t_data.get("total_lines", 0),
|
||||
issues_count=t_data.get("issues_count", 0),
|
||||
quality_score=t_data.get("quality_score", 0.0),
|
||||
)
|
||||
db.add(task)
|
||||
imported_count["tasks"] += 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 3. 导入问题(需要先有任务)
|
||||
if "issues" in data:
|
||||
for i_data in data["issues"]:
|
||||
existing = await db.get(AuditIssue, i_data.get("id"))
|
||||
if not existing:
|
||||
# 检查任务是否存在
|
||||
task = await db.get(AuditTask, i_data.get("task_id"))
|
||||
if task:
|
||||
issue = AuditIssue(
|
||||
id=i_data.get("id"),
|
||||
task_id=i_data.get("task_id"),
|
||||
file_path=i_data.get("file_path"),
|
||||
line_number=i_data.get("line_number"),
|
||||
column_number=i_data.get("column_number"),
|
||||
issue_type=i_data.get("issue_type"),
|
||||
severity=i_data.get("severity"),
|
||||
title=i_data.get("title"),
|
||||
message=i_data.get("message"),
|
||||
description=i_data.get("description"),
|
||||
suggestion=i_data.get("suggestion"),
|
||||
code_snippet=i_data.get("code_snippet"),
|
||||
ai_explanation=i_data.get("ai_explanation"),
|
||||
status=i_data.get("status", "open"),
|
||||
)
|
||||
db.add(issue)
|
||||
imported_count["issues"] += 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 4. 导入即时分析
|
||||
if "instant_analyses" in data:
|
||||
for a_data in data["instant_analyses"]:
|
||||
existing = await db.get(InstantAnalysis, a_data.get("id"))
|
||||
if not existing:
|
||||
analysis = InstantAnalysis(
|
||||
id=a_data.get("id"),
|
||||
user_id=current_user.id,
|
||||
language=a_data.get("language"),
|
||||
code_content="",
|
||||
analysis_result=json.dumps(a_data.get("analysis_result", {})),
|
||||
issues_count=a_data.get("issues_count", 0),
|
||||
quality_score=a_data.get("quality_score", 0.0),
|
||||
analysis_time=a_data.get("analysis_time", 0.0),
|
||||
)
|
||||
db.add(analysis)
|
||||
imported_count["analyses"] += 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 5. 导入用户配置(合并)
|
||||
if "user_config" in data and data["user_config"]:
|
||||
config_result = await db.execute(
|
||||
select(UserConfig)
|
||||
.where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = config_result.scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
config = UserConfig(
|
||||
user_id=current_user.id,
|
||||
llm_config=json.dumps(data["user_config"].get("llm_config", {})),
|
||||
other_config=json.dumps(data["user_config"].get("other_config", {})),
|
||||
)
|
||||
db.add(config)
|
||||
else:
|
||||
# 合并配置
|
||||
existing_llm = json.loads(config.llm_config) if config.llm_config else {}
|
||||
existing_other = json.loads(config.other_config) if config.other_config else {}
|
||||
existing_llm.update(data["user_config"].get("llm_config", {}))
|
||||
existing_other.update(data["user_config"].get("other_config", {}))
|
||||
config.llm_config = json.dumps(existing_llm)
|
||||
config.other_config = json.dumps(existing_other)
|
||||
|
||||
imported_count["config"] = 1
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "数据导入成功",
|
||||
"imported": imported_count
|
||||
}
|
||||
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail="无效的 JSON 文件格式")
|
||||
except Exception as e:
|
||||
print(f"导入数据失败: {e}")
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"导入数据失败: {str(e)}")
|
||||
|
||||
|
||||
@router.delete("/clear")
|
||||
async def clear_database(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
清空当前用户的所有数据
|
||||
注意:此操作不可恢复,请谨慎使用
|
||||
"""
|
||||
try:
|
||||
deleted_count = {
|
||||
"projects": 0,
|
||||
"tasks": 0,
|
||||
"issues": 0,
|
||||
"analyses": 0,
|
||||
"config": 0,
|
||||
}
|
||||
|
||||
# 1. 删除用户的所有问题(通过任务)
|
||||
tasks_result = await db.execute(
|
||||
select(AuditTask)
|
||||
.where(AuditTask.created_by == current_user.id)
|
||||
)
|
||||
tasks = tasks_result.scalars().all()
|
||||
task_ids = [task.id for task in tasks]
|
||||
|
||||
if task_ids:
|
||||
issues_result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id.in_(task_ids))
|
||||
)
|
||||
issues = issues_result.scalars().all()
|
||||
for issue in issues:
|
||||
await db.delete(issue)
|
||||
deleted_count["issues"] = len(issues)
|
||||
|
||||
# 2. 删除用户的所有任务
|
||||
for task in tasks:
|
||||
await db.delete(task)
|
||||
deleted_count["tasks"] = len(tasks)
|
||||
|
||||
# 3. 删除用户的所有项目
|
||||
projects_result = await db.execute(
|
||||
select(Project)
|
||||
.where(Project.owner_id == current_user.id)
|
||||
)
|
||||
projects = projects_result.scalars().all()
|
||||
for project in projects:
|
||||
await db.delete(project)
|
||||
deleted_count["projects"] = len(projects)
|
||||
|
||||
# 4. 删除用户的即时分析
|
||||
analyses_result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
analyses = analyses_result.scalars().all()
|
||||
for analysis in analyses:
|
||||
await db.delete(analysis)
|
||||
deleted_count["analyses"] = len(analyses)
|
||||
|
||||
# 5. 删除用户配置
|
||||
config_result = await db.execute(
|
||||
select(UserConfig)
|
||||
.where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = config_result.scalar_one_or_none()
|
||||
if config:
|
||||
await db.delete(config)
|
||||
deleted_count["config"] = 1
|
||||
|
||||
# 6. 删除用户的项目成员关系(作为成员)
|
||||
members_result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.user_id == current_user.id)
|
||||
)
|
||||
members = members_result.scalars().all()
|
||||
for member in members:
|
||||
await db.delete(member)
|
||||
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"message": "数据已清空",
|
||||
"deleted": deleted_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"清空数据失败: {e}")
|
||||
await db.rollback()
|
||||
raise HTTPException(status_code=500, detail=f"清空数据失败: {str(e)}")
|
||||
|
||||
|
||||
class DatabaseStatsResponse(BaseModel):
|
||||
"""数据库统计信息响应"""
|
||||
total_projects: int
|
||||
active_projects: int
|
||||
total_tasks: int
|
||||
completed_tasks: int
|
||||
pending_tasks: int
|
||||
running_tasks: int
|
||||
failed_tasks: int
|
||||
total_issues: int
|
||||
open_issues: int
|
||||
resolved_issues: int
|
||||
critical_issues: int
|
||||
high_issues: int
|
||||
medium_issues: int
|
||||
low_issues: int
|
||||
total_analyses: int
|
||||
total_members: int
|
||||
has_config: bool
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DatabaseStatsResponse)
|
||||
async def get_database_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
获取当前用户的数据库统计信息
|
||||
"""
|
||||
try:
|
||||
# 1. 项目统计
|
||||
projects_result = await db.execute(
|
||||
select(Project)
|
||||
.where(Project.owner_id == current_user.id)
|
||||
)
|
||||
projects = projects_result.scalars().all()
|
||||
total_projects = len(projects)
|
||||
active_projects = len([p for p in projects if p.is_active])
|
||||
|
||||
# 2. 任务统计
|
||||
tasks_result = await db.execute(
|
||||
select(AuditTask)
|
||||
.where(AuditTask.created_by == current_user.id)
|
||||
)
|
||||
tasks = tasks_result.scalars().all()
|
||||
total_tasks = len(tasks)
|
||||
completed_tasks = len([t for t in tasks if t.status == "completed"])
|
||||
pending_tasks = len([t for t in tasks if t.status == "pending"])
|
||||
running_tasks = len([t for t in tasks if t.status == "running"])
|
||||
failed_tasks = len([t for t in tasks if t.status == "failed"])
|
||||
|
||||
# 3. 问题统计
|
||||
task_ids = [task.id for task in tasks]
|
||||
total_issues = 0
|
||||
open_issues = 0
|
||||
resolved_issues = 0
|
||||
critical_issues = 0
|
||||
high_issues = 0
|
||||
medium_issues = 0
|
||||
low_issues = 0
|
||||
|
||||
if task_ids:
|
||||
issues_result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id.in_(task_ids))
|
||||
)
|
||||
issues = issues_result.scalars().all()
|
||||
total_issues = len(issues)
|
||||
open_issues = len([i for i in issues if i.status == "open"])
|
||||
resolved_issues = len([i for i in issues if i.status == "resolved"])
|
||||
critical_issues = len([i for i in issues if i.severity == "critical"])
|
||||
high_issues = len([i for i in issues if i.severity == "high"])
|
||||
medium_issues = len([i for i in issues if i.severity == "medium"])
|
||||
low_issues = len([i for i in issues if i.severity == "low"])
|
||||
|
||||
# 4. 即时分析统计
|
||||
analyses_result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
)
|
||||
analyses = analyses_result.scalars().all()
|
||||
total_analyses = len(analyses)
|
||||
|
||||
# 5. 项目成员统计
|
||||
members_result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.user_id == current_user.id)
|
||||
)
|
||||
members = members_result.scalars().all()
|
||||
total_members = len(members)
|
||||
|
||||
# 6. 配置检查
|
||||
config_result = await db.execute(
|
||||
select(UserConfig)
|
||||
.where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
has_config = config_result.scalar_one_or_none() is not None
|
||||
|
||||
return DatabaseStatsResponse(
|
||||
total_projects=total_projects,
|
||||
active_projects=active_projects,
|
||||
total_tasks=total_tasks,
|
||||
completed_tasks=completed_tasks,
|
||||
pending_tasks=pending_tasks,
|
||||
running_tasks=running_tasks,
|
||||
failed_tasks=failed_tasks,
|
||||
total_issues=total_issues,
|
||||
open_issues=open_issues,
|
||||
resolved_issues=resolved_issues,
|
||||
critical_issues=critical_issues,
|
||||
high_issues=high_issues,
|
||||
medium_issues=medium_issues,
|
||||
low_issues=low_issues,
|
||||
total_analyses=total_analyses,
|
||||
total_members=total_members,
|
||||
has_config=has_config,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取统计信息失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}")
|
||||
|
||||
|
||||
class DatabaseHealthResponse(BaseModel):
|
||||
"""数据库健康检查响应"""
|
||||
status: str # healthy, warning, error
|
||||
database_connected: bool
|
||||
total_records: int
|
||||
last_backup_date: str | None
|
||||
issues: List[str]
|
||||
warnings: List[str]
|
||||
|
||||
|
||||
@router.get("/health", response_model=DatabaseHealthResponse)
|
||||
async def check_database_health(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
检查数据库健康状态
|
||||
"""
|
||||
try:
|
||||
issues = []
|
||||
warnings = []
|
||||
database_connected = True
|
||||
total_records = 0
|
||||
last_backup_date = None
|
||||
|
||||
# 1. 检查数据库连接
|
||||
try:
|
||||
await db.execute(select(1))
|
||||
except Exception as e:
|
||||
database_connected = False
|
||||
issues.append(f"数据库连接失败: {str(e)}")
|
||||
|
||||
if database_connected:
|
||||
# 2. 统计总记录数
|
||||
try:
|
||||
projects_count = len((await db.execute(
|
||||
select(Project).where(Project.owner_id == current_user.id)
|
||||
)).scalars().all())
|
||||
|
||||
tasks_count = len((await db.execute(
|
||||
select(AuditTask).where(AuditTask.created_by == current_user.id)
|
||||
)).scalars().all())
|
||||
|
||||
analyses_count = len((await db.execute(
|
||||
select(InstantAnalysis).where(InstantAnalysis.user_id == current_user.id)
|
||||
)).scalars().all())
|
||||
|
||||
total_records = projects_count + tasks_count + analyses_count
|
||||
except Exception as e:
|
||||
warnings.append(f"统计记录数时出错: {str(e)}")
|
||||
|
||||
# 3. 检查数据完整性
|
||||
try:
|
||||
# 检查孤立的任务(项目不存在)
|
||||
tasks_result = await db.execute(
|
||||
select(AuditTask).where(AuditTask.created_by == current_user.id)
|
||||
)
|
||||
tasks = tasks_result.scalars().all()
|
||||
orphan_tasks = 0
|
||||
for task in tasks:
|
||||
project = await db.get(Project, task.project_id)
|
||||
if not project:
|
||||
orphan_tasks += 1
|
||||
|
||||
if orphan_tasks > 0:
|
||||
warnings.append(f"发现 {orphan_tasks} 个孤立任务(关联的项目不存在)")
|
||||
|
||||
# 检查孤立的问题(任务不存在)
|
||||
if tasks:
|
||||
task_ids = [task.id for task in tasks]
|
||||
issues_result = await db.execute(
|
||||
select(AuditIssue).where(AuditIssue.task_id.in_(task_ids))
|
||||
)
|
||||
issues_list = issues_result.scalars().all()
|
||||
orphan_issues = 0
|
||||
for issue in issues_list:
|
||||
task = await db.get(AuditTask, issue.task_id)
|
||||
if not task:
|
||||
orphan_issues += 1
|
||||
|
||||
if orphan_issues > 0:
|
||||
warnings.append(f"发现 {orphan_issues} 个孤立问题(关联的任务不存在)")
|
||||
except Exception as e:
|
||||
warnings.append(f"数据完整性检查时出错: {str(e)}")
|
||||
|
||||
# 4. 确定健康状态
|
||||
if not database_connected or issues:
|
||||
status = "error"
|
||||
elif warnings:
|
||||
status = "warning"
|
||||
else:
|
||||
status = "healthy"
|
||||
|
||||
return DatabaseHealthResponse(
|
||||
status=status,
|
||||
database_connected=database_connected,
|
||||
total_records=total_records,
|
||||
last_backup_date=last_backup_date,
|
||||
issues=issues,
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"健康检查失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"健康检查失败: {str(e)}")
|
||||
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
from typing import Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db
|
||||
from app.models.project import Project, ProjectMember
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Schemas
|
||||
class UserSchema(BaseModel):
|
||||
id: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ProjectMemberSchema(BaseModel):
|
||||
id: str
|
||||
project_id: str
|
||||
user_id: str
|
||||
role: str
|
||||
permissions: Optional[str] = None
|
||||
joined_at: datetime
|
||||
created_at: datetime
|
||||
user: Optional[UserSchema] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AddMemberRequest(BaseModel):
|
||||
user_id: str
|
||||
role: str = "member"
|
||||
|
||||
|
||||
class UpdateMemberRequest(BaseModel):
|
||||
role: Optional[str] = None
|
||||
permissions: Optional[str] = None
|
||||
|
||||
|
||||
@router.get("/{project_id}/members", response_model=List[ProjectMemberSchema])
|
||||
async def get_project_members(
|
||||
project_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get all members of a project.
|
||||
"""
|
||||
# Verify project exists
|
||||
project = await db.get(Project, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.options(selectinload(ProjectMember.user))
|
||||
.where(ProjectMember.project_id == project_id)
|
||||
.order_by(ProjectMember.joined_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/{project_id}/members", response_model=ProjectMemberSchema)
|
||||
async def add_project_member(
|
||||
project_id: str,
|
||||
member_in: AddMemberRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Add a member to a project.
|
||||
"""
|
||||
# Verify project exists
|
||||
project = await db.get(Project, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Check if user is project owner or admin
|
||||
if project.owner_id != current_user.id and not current_user.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
|
||||
# Check if user exists
|
||||
user = await db.get(User, member_in.user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# Check if already a member
|
||||
existing = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(
|
||||
ProjectMember.project_id == project_id,
|
||||
ProjectMember.user_id == member_in.user_id
|
||||
)
|
||||
)
|
||||
if existing.scalars().first():
|
||||
raise HTTPException(status_code=400, detail="用户已是项目成员")
|
||||
|
||||
# Create member
|
||||
member = ProjectMember(
|
||||
project_id=project_id,
|
||||
user_id=member_in.user_id,
|
||||
role=member_in.role,
|
||||
permissions="{}"
|
||||
)
|
||||
db.add(member)
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
|
||||
# Reload with user relationship
|
||||
result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.options(selectinload(ProjectMember.user))
|
||||
.where(ProjectMember.id == member.id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
@router.put("/{project_id}/members/{member_id}", response_model=ProjectMemberSchema)
|
||||
async def update_project_member(
|
||||
project_id: str,
|
||||
member_id: str,
|
||||
member_update: UpdateMemberRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a project member's role or permissions.
|
||||
"""
|
||||
# Verify project exists
|
||||
project = await db.get(Project, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Check permissions
|
||||
if project.owner_id != current_user.id and not current_user.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
|
||||
# Get member
|
||||
result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.id == member_id, ProjectMember.project_id == project_id)
|
||||
)
|
||||
member = result.scalars().first()
|
||||
if not member:
|
||||
raise HTTPException(status_code=404, detail="成员不存在")
|
||||
|
||||
# Update fields
|
||||
if member_update.role:
|
||||
member.role = member_update.role
|
||||
if member_update.permissions:
|
||||
member.permissions = member_update.permissions
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(member)
|
||||
|
||||
# Reload with user relationship
|
||||
result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.options(selectinload(ProjectMember.user))
|
||||
.where(ProjectMember.id == member.id)
|
||||
)
|
||||
return result.scalars().first()
|
||||
|
||||
|
||||
@router.delete("/{project_id}/members/{member_id}")
|
||||
async def remove_project_member(
|
||||
project_id: str,
|
||||
member_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Remove a member from a project.
|
||||
"""
|
||||
# Verify project exists
|
||||
project = await db.get(Project, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Check permissions
|
||||
if project.owner_id != current_user.id and not current_user.is_superuser:
|
||||
raise HTTPException(status_code=403, detail="权限不足")
|
||||
|
||||
# Get member
|
||||
result = await db.execute(
|
||||
select(ProjectMember)
|
||||
.where(ProjectMember.id == member_id, ProjectMember.project_id == project_id)
|
||||
)
|
||||
member = result.scalars().first()
|
||||
if not member:
|
||||
raise HTTPException(status_code=404, detail="成员不存在")
|
||||
|
||||
await db.delete(member)
|
||||
await db.commit()
|
||||
|
||||
return {"message": "成员已移除"}
|
||||
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
from typing import Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db, AsyncSessionLocal
|
||||
from app.models.project import Project
|
||||
from app.models.user import User
|
||||
from app.models.audit import AuditTask, AuditIssue
|
||||
from app.models.user_config import UserConfig
|
||||
from app.services.scanner import scan_repo_task
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Schemas
|
||||
class ProjectCreate(BaseModel):
|
||||
name: str
|
||||
repository_url: Optional[str] = None
|
||||
repository_type: Optional[str] = "other"
|
||||
description: Optional[str] = None
|
||||
default_branch: Optional[str] = "main"
|
||||
programming_languages: Optional[List[str]] = None
|
||||
|
||||
class ProjectUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
repository_url: Optional[str] = None
|
||||
repository_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
default_branch: Optional[str] = None
|
||||
programming_languages: Optional[List[str]] = None
|
||||
|
||||
class OwnerSchema(BaseModel):
|
||||
id: str
|
||||
email: Optional[str] = None
|
||||
full_name: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
role: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
repository_url: Optional[str] = None
|
||||
repository_type: Optional[str] = None
|
||||
default_branch: Optional[str] = None
|
||||
programming_languages: Optional[str] = None
|
||||
owner_id: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
owner: Optional[OwnerSchema] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class StatsResponse(BaseModel):
|
||||
total_projects: int
|
||||
active_projects: int
|
||||
total_tasks: int
|
||||
completed_tasks: int
|
||||
total_issues: int
|
||||
resolved_issues: int
|
||||
|
||||
@router.post("/", response_model=ProjectResponse)
|
||||
async def create_project(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
project_in: ProjectCreate,
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new project.
|
||||
"""
|
||||
import json
|
||||
project = Project(
|
||||
name=project_in.name,
|
||||
repository_url=project_in.repository_url,
|
||||
repository_type=project_in.repository_type or "other",
|
||||
description=project_in.description,
|
||||
default_branch=project_in.default_branch or "main",
|
||||
programming_languages=json.dumps(project_in.programming_languages or []),
|
||||
owner_id=current_user.id
|
||||
)
|
||||
db.add(project)
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
|
||||
@router.get("/", response_model=List[ProjectResponse])
|
||||
async def read_projects(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
include_deleted: bool = False,
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve projects.
|
||||
"""
|
||||
query = select(Project).options(selectinload(Project.owner))
|
||||
if not include_deleted:
|
||||
query = query.where(Project.is_active == True)
|
||||
query = query.order_by(Project.created_at.desc()).offset(skip).limit(limit)
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.get("/deleted", response_model=List[ProjectResponse])
|
||||
async def read_deleted_projects(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve deleted (soft-deleted) projects for current user.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Project)
|
||||
.options(selectinload(Project.owner))
|
||||
.where(Project.owner_id == current_user.id)
|
||||
.where(Project.is_active == False)
|
||||
.order_by(Project.updated_at.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
@router.get("/stats", response_model=StatsResponse)
|
||||
async def get_stats(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get overall statistics.
|
||||
"""
|
||||
projects_result = await db.execute(select(Project))
|
||||
projects = projects_result.scalars().all()
|
||||
|
||||
tasks_result = await db.execute(select(AuditTask))
|
||||
tasks = tasks_result.scalars().all()
|
||||
|
||||
issues_result = await db.execute(select(AuditIssue))
|
||||
issues = issues_result.scalars().all()
|
||||
|
||||
return {
|
||||
"total_projects": len(projects),
|
||||
"active_projects": len([p for p in projects if p.is_active]),
|
||||
"total_tasks": len(tasks),
|
||||
"completed_tasks": len([t for t in tasks if t.status == "completed"]),
|
||||
"total_issues": len(issues),
|
||||
"resolved_issues": len([i for i in issues if i.status == "resolved"]),
|
||||
}
|
||||
|
||||
@router.get("/{id}", response_model=ProjectResponse)
|
||||
async def read_project(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get project by ID.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(Project)
|
||||
.options(selectinload(Project.owner))
|
||||
.where(Project.id == id)
|
||||
)
|
||||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
return project
|
||||
|
||||
@router.put("/{id}", response_model=ProjectResponse)
|
||||
async def update_project(
|
||||
id: str,
|
||||
*,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
project_in: ProjectUpdate,
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update project.
|
||||
"""
|
||||
import json
|
||||
result = await db.execute(select(Project).where(Project.id == id))
|
||||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
update_data = project_in.model_dump(exclude_unset=True)
|
||||
if "programming_languages" in update_data and update_data["programming_languages"] is not None:
|
||||
update_data["programming_languages"] = json.dumps(update_data["programming_languages"])
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(project, field, value)
|
||||
|
||||
project.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
|
||||
@router.delete("/{id}")
|
||||
async def delete_project(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Soft delete project.
|
||||
"""
|
||||
result = await db.execute(select(Project).where(Project.id == id))
|
||||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以删除
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权删除此项目")
|
||||
|
||||
project.is_active = False
|
||||
project.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
return {"message": "项目已删除"}
|
||||
|
||||
@router.post("/{id}/restore")
|
||||
async def restore_project(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Restore soft-deleted project.
|
||||
"""
|
||||
result = await db.execute(select(Project).where(Project.id == id))
|
||||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以恢复
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权恢复此项目")
|
||||
|
||||
project.is_active = True
|
||||
project.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
return {"message": "项目已恢复"}
|
||||
|
||||
@router.delete("/{id}/permanent")
|
||||
async def permanently_delete_project(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Permanently delete project.
|
||||
"""
|
||||
result = await db.execute(select(Project).where(Project.id == id))
|
||||
project = result.scalars().first()
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# 检查权限:只有项目所有者可以永久删除
|
||||
if project.owner_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权永久删除此项目")
|
||||
|
||||
await db.delete(project)
|
||||
await db.commit()
|
||||
return {"message": "项目已永久删除"}
|
||||
|
||||
@router.post("/{id}/scan")
|
||||
async def scan_project(
|
||||
id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Start a scan task.
|
||||
"""
|
||||
project = await db.get(Project, id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Create Task Record
|
||||
task = AuditTask(
|
||||
project_id=project.id,
|
||||
created_by=current_user.id,
|
||||
task_type="repository",
|
||||
status="pending"
|
||||
)
|
||||
db.add(task)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
|
||||
# 获取用户配置
|
||||
from sqlalchemy.future import select
|
||||
import json
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == current_user.id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
user_config = {}
|
||||
if config:
|
||||
user_config = {
|
||||
'llmConfig': json.loads(config.llm_config) if config.llm_config else {},
|
||||
'otherConfig': json.loads(config.other_config) if config.other_config else {},
|
||||
}
|
||||
|
||||
# Trigger Background Task
|
||||
background_tasks.add_task(scan_repo_task, task.id, AsyncSessionLocal, user_config)
|
||||
|
||||
return {"task_id": task.id, "status": "started"}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
from fastapi import APIRouter, UploadFile, File, Depends, BackgroundTasks, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from typing import Any, List, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import shutil
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
import asyncio
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db, AsyncSessionLocal
|
||||
from app.models.audit import AuditTask, AuditIssue
|
||||
from app.models.user import User
|
||||
from app.models.project import Project
|
||||
from app.models.analysis import InstantAnalysis
|
||||
from app.models.user_config import UserConfig
|
||||
from app.services.llm.service import LLMService
|
||||
from app.services.scanner import task_control, is_text_file, should_exclude, get_language_from_path
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# 支持的文件扩展名
|
||||
TEXT_EXTENSIONS = [
|
||||
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs",
|
||||
".cpp", ".c", ".h", ".cc", ".hh", ".cs", ".php", ".rb",
|
||||
".kt", ".swift", ".sql", ".sh", ".json", ".yml", ".yaml"
|
||||
]
|
||||
|
||||
|
||||
async def process_zip_task(task_id: str, file_path: str, db_session_factory, user_config: dict = None):
|
||||
"""后台ZIP文件处理任务"""
|
||||
async with db_session_factory() as db:
|
||||
task = await db.get(AuditTask, task_id)
|
||||
if not task:
|
||||
return
|
||||
|
||||
try:
|
||||
task.status = "running"
|
||||
task.started_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
# 创建使用用户配置的LLM服务实例
|
||||
llm_service = LLMService(user_config=user_config or {})
|
||||
|
||||
# Extract ZIP
|
||||
extract_dir = Path(f"/tmp/{task_id}")
|
||||
extract_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(file_path, 'r') as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
# Find files
|
||||
files_to_scan = []
|
||||
for root, dirs, files in os.walk(extract_dir):
|
||||
# 排除常见非代码目录
|
||||
dirs[:] = [d for d in dirs if d not in ['node_modules', '__pycache__', '.git', 'dist', 'build', 'vendor']]
|
||||
|
||||
for file in files:
|
||||
full_path = Path(root) / file
|
||||
rel_path = str(full_path.relative_to(extract_dir))
|
||||
|
||||
# 检查文件类型和排除规则
|
||||
if is_text_file(rel_path) and not should_exclude(rel_path):
|
||||
try:
|
||||
content = full_path.read_text(errors='ignore')
|
||||
if len(content) <= settings.MAX_FILE_SIZE_BYTES:
|
||||
files_to_scan.append({
|
||||
"path": rel_path,
|
||||
"content": content
|
||||
})
|
||||
except:
|
||||
pass
|
||||
|
||||
# 限制文件数量
|
||||
files_to_scan = files_to_scan[:settings.MAX_ANALYZE_FILES]
|
||||
|
||||
task.total_files = len(files_to_scan)
|
||||
await db.commit()
|
||||
|
||||
print(f"📊 ZIP任务 {task_id}: 找到 {len(files_to_scan)} 个文件")
|
||||
|
||||
total_issues = 0
|
||||
total_lines = 0
|
||||
quality_scores = []
|
||||
scanned_files = 0
|
||||
failed_files = 0
|
||||
|
||||
for file_info in files_to_scan:
|
||||
# 检查是否取消
|
||||
if task_control.is_cancelled(task_id):
|
||||
print(f"🛑 ZIP任务 {task_id} 已被取消")
|
||||
task.status = "cancelled"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
task_control.cleanup_task(task_id)
|
||||
return
|
||||
|
||||
try:
|
||||
content = file_info['content']
|
||||
total_lines += content.count('\n') + 1
|
||||
language = get_language_from_path(file_info['path'])
|
||||
|
||||
result = await llm_service.analyze_code(content, language)
|
||||
|
||||
issues = result.get("issues", [])
|
||||
for i in issues:
|
||||
issue = AuditIssue(
|
||||
task_id=task.id,
|
||||
file_path=file_info['path'],
|
||||
line_number=i.get('line', 1),
|
||||
column_number=i.get('column'),
|
||||
issue_type=i.get('type', 'maintainability'),
|
||||
severity=i.get('severity', 'low'),
|
||||
title=i.get('title', 'Issue'),
|
||||
message=i.get('title', 'Issue'),
|
||||
description=i.get('description'),
|
||||
suggestion=i.get('suggestion'),
|
||||
code_snippet=i.get('code_snippet'),
|
||||
ai_explanation=json.dumps(i.get('xai')) if i.get('xai') else None,
|
||||
status="open"
|
||||
)
|
||||
db.add(issue)
|
||||
total_issues += 1
|
||||
|
||||
if "quality_score" in result:
|
||||
quality_scores.append(result["quality_score"])
|
||||
|
||||
scanned_files += 1
|
||||
task.scanned_files = scanned_files
|
||||
task.total_lines = total_lines
|
||||
task.issues_count = total_issues
|
||||
await db.commit()
|
||||
|
||||
print(f"📈 ZIP任务 {task_id}: 进度 {scanned_files}/{len(files_to_scan)}")
|
||||
|
||||
# 请求间隔
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
|
||||
except Exception as file_error:
|
||||
failed_files += 1
|
||||
print(f"❌ ZIP任务分析文件失败 ({file_info['path']}): {file_error}")
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
|
||||
# 完成任务
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
task.scanned_files = scanned_files
|
||||
task.total_lines = total_lines
|
||||
task.issues_count = total_issues
|
||||
task.quality_score = sum(quality_scores) / len(quality_scores) if quality_scores else 100.0
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ ZIP任务 {task_id} 完成: 扫描 {scanned_files} 个文件, 发现 {total_issues} 个问题")
|
||||
task_control.cleanup_task(task_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ ZIP扫描失败: {e}")
|
||||
task.status = "failed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
task_control.cleanup_task(task_id)
|
||||
finally:
|
||||
# Cleanup
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
if extract_dir.exists():
|
||||
shutil.rmtree(extract_dir)
|
||||
|
||||
|
||||
@router.post("/upload-zip")
|
||||
async def scan_zip(
|
||||
project_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
file: UploadFile = File(...),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Upload and scan a ZIP file.
|
||||
"""
|
||||
# Verify project exists
|
||||
project = await db.get(Project, project_id)
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="项目不存在")
|
||||
|
||||
# Validate file
|
||||
if not file.filename.lower().endswith('.zip'):
|
||||
raise HTTPException(status_code=400, detail="请上传ZIP格式文件")
|
||||
|
||||
# Save Uploaded File
|
||||
file_id = str(uuid.uuid4())
|
||||
file_path = f"/tmp/{file_id}.zip"
|
||||
with open(file_path, "wb") as buffer:
|
||||
shutil.copyfileobj(file.file, buffer)
|
||||
|
||||
# Check file size
|
||||
file_size = os.path.getsize(file_path)
|
||||
if file_size > 100 * 1024 * 1024: # 100MB limit
|
||||
os.remove(file_path)
|
||||
raise HTTPException(status_code=400, detail="文件大小不能超过100MB")
|
||||
|
||||
# Create Task
|
||||
task = AuditTask(
|
||||
project_id=project_id,
|
||||
created_by=current_user.id,
|
||||
task_type="zip_upload",
|
||||
status="pending",
|
||||
scan_config="{}"
|
||||
)
|
||||
db.add(task)
|
||||
await db.commit()
|
||||
await db.refresh(task)
|
||||
|
||||
# 获取用户配置
|
||||
user_config = await get_user_config_dict(db, current_user.id)
|
||||
|
||||
# Trigger Background Task
|
||||
background_tasks.add_task(process_zip_task, task.id, file_path, AsyncSessionLocal, user_config)
|
||||
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
|
||||
class InstantAnalysisRequest(BaseModel):
|
||||
code: str
|
||||
language: str
|
||||
|
||||
|
||||
class InstantAnalysisResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
language: str
|
||||
issues_count: int
|
||||
quality_score: float
|
||||
analysis_time: float
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
async def get_user_config_dict(db: AsyncSession, user_id: str) -> dict:
|
||||
"""获取用户配置字典"""
|
||||
result = await db.execute(
|
||||
select(UserConfig).where(UserConfig.user_id == user_id)
|
||||
)
|
||||
config = result.scalar_one_or_none()
|
||||
if not config:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'llmConfig': json.loads(config.llm_config) if config.llm_config else {},
|
||||
'otherConfig': json.loads(config.other_config) if config.other_config else {},
|
||||
}
|
||||
|
||||
|
||||
@router.post("/instant")
|
||||
async def instant_analysis(
|
||||
req: InstantAnalysisRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Perform instant code analysis.
|
||||
"""
|
||||
# 获取用户配置
|
||||
user_config = await get_user_config_dict(db, current_user.id)
|
||||
|
||||
# 创建使用用户配置的LLM服务实例
|
||||
llm_service = LLMService(user_config=user_config)
|
||||
|
||||
start_time = datetime.utcnow()
|
||||
result = await llm_service.analyze_code(req.code, req.language)
|
||||
end_time = datetime.utcnow()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
# Save record
|
||||
analysis = InstantAnalysis(
|
||||
user_id=current_user.id,
|
||||
language=req.language,
|
||||
code_content="", # Do not persist code for privacy
|
||||
analysis_result=json.dumps(result),
|
||||
issues_count=len(result.get("issues", [])),
|
||||
quality_score=result.get("quality_score", 0),
|
||||
analysis_time=duration
|
||||
)
|
||||
db.add(analysis)
|
||||
await db.commit()
|
||||
await db.refresh(analysis)
|
||||
|
||||
# Return result directly to frontend
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/instant/history", response_model=List[InstantAnalysisResponse])
|
||||
async def get_instant_analysis_history(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
limit: int = 20,
|
||||
) -> Any:
|
||||
"""
|
||||
Get user's instant analysis history.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(InstantAnalysis)
|
||||
.where(InstantAnalysis.user_id == current_user.id)
|
||||
.order_by(InstantAnalysis.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
from typing import Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
|
||||
from app.api import deps
|
||||
from app.db.session import get_db
|
||||
from app.models.audit import AuditTask, AuditIssue
|
||||
from app.models.project import Project
|
||||
from app.models.user import User
|
||||
from app.services.scanner import task_control
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# Schemas
|
||||
class AuditIssueSchema(BaseModel):
|
||||
id: str
|
||||
task_id: str
|
||||
file_path: str
|
||||
line_number: Optional[int] = None
|
||||
column_number: Optional[int] = None
|
||||
issue_type: str
|
||||
severity: str
|
||||
title: Optional[str] = None
|
||||
message: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
suggestion: Optional[str] = None
|
||||
code_snippet: Optional[str] = None
|
||||
ai_explanation: Optional[str] = None
|
||||
status: str
|
||||
resolved_by: Optional[str] = None
|
||||
resolved_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class IssueUpdateSchema(BaseModel):
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class ProjectSchema(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
repository_url: Optional[str] = None
|
||||
repository_type: Optional[str] = None
|
||||
default_branch: Optional[str] = None
|
||||
programming_languages: Optional[str] = None
|
||||
owner_id: str
|
||||
is_active: bool
|
||||
created_at: datetime
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class AuditTaskSchema(BaseModel):
|
||||
id: str
|
||||
project_id: str
|
||||
task_type: str
|
||||
status: str
|
||||
branch_name: Optional[str] = None
|
||||
exclude_patterns: Optional[str] = None
|
||||
scan_config: Optional[str] = None
|
||||
total_files: int = 0
|
||||
scanned_files: int = 0
|
||||
total_lines: int = 0
|
||||
issues_count: int = 0
|
||||
quality_score: float = 0.0
|
||||
started_at: Optional[datetime] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
created_by: str
|
||||
created_at: datetime
|
||||
project: Optional[ProjectSchema] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/", response_model=List[AuditTaskSchema])
|
||||
async def list_tasks(
|
||||
project_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
List all tasks, optionally filtered by project.
|
||||
"""
|
||||
query = select(AuditTask).options(selectinload(AuditTask.project))
|
||||
if project_id:
|
||||
query = query.where(AuditTask.project_id == project_id)
|
||||
query = query.order_by(AuditTask.created_at.desc())
|
||||
result = await db.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.get("/{id}", response_model=AuditTaskSchema)
|
||||
async def read_task(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get task status by ID.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AuditTask)
|
||||
.options(selectinload(AuditTask.project))
|
||||
.where(AuditTask.id == id)
|
||||
)
|
||||
task = result.scalars().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
return task
|
||||
|
||||
|
||||
@router.post("/{id}/cancel")
|
||||
async def cancel_task(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Cancel a running task.
|
||||
"""
|
||||
result = await db.execute(select(AuditTask).where(AuditTask.id == id))
|
||||
task = result.scalars().first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
|
||||
if task.status not in ["pending", "running"]:
|
||||
raise HTTPException(status_code=400, detail="只能取消待处理或运行中的任务")
|
||||
|
||||
# 标记任务为取消
|
||||
task_control.cancel_task(id)
|
||||
|
||||
# 更新数据库状态
|
||||
task.status = "cancelled"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
return {"message": "任务已取消", "task_id": id}
|
||||
|
||||
|
||||
@router.get("/{id}/issues", response_model=List[AuditIssueSchema])
|
||||
async def read_task_issues(
|
||||
id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get issues for a specific task.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.task_id == id)
|
||||
.order_by(
|
||||
# 按严重程度排序
|
||||
AuditIssue.severity.desc(),
|
||||
AuditIssue.created_at.desc()
|
||||
)
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.patch("/{task_id}/issues/{issue_id}", response_model=AuditIssueSchema)
|
||||
async def update_issue(
|
||||
task_id: str,
|
||||
issue_id: str,
|
||||
issue_update: IssueUpdateSchema,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update issue status (e.g., resolve, mark as false positive).
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(AuditIssue)
|
||||
.where(AuditIssue.id == issue_id, AuditIssue.task_id == task_id)
|
||||
)
|
||||
issue = result.scalars().first()
|
||||
if not issue:
|
||||
raise HTTPException(status_code=404, detail="问题不存在")
|
||||
|
||||
if issue_update.status:
|
||||
issue.status = issue_update.status
|
||||
if issue_update.status == "resolved":
|
||||
issue.resolved_by = current_user.id
|
||||
issue.resolved_at = datetime.utcnow()
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(issue)
|
||||
return issue
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
from typing import Any, List
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from app.api import deps
|
||||
from app.core import security
|
||||
from app.db.session import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[UserSchema])
|
||||
async def read_users(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve users.
|
||||
"""
|
||||
result = await db.execute(select(User).offset(skip).limit(limit))
|
||||
users = result.scalars().all()
|
||||
return users
|
||||
|
||||
@router.post("/", response_model=UserSchema)
|
||||
async def create_user(
|
||||
*,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user_in: UserCreate,
|
||||
) -> Any:
|
||||
"""
|
||||
Create new user.
|
||||
"""
|
||||
result = await db.execute(select(User).where(User.email == user_in.email))
|
||||
user = result.scalars().first()
|
||||
if user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="The user with this username already exists in the system.",
|
||||
)
|
||||
|
||||
db_user = User(
|
||||
email=user_in.email,
|
||||
hashed_password=security.get_password_hash(user_in.password),
|
||||
full_name=user_in.full_name,
|
||||
is_active=user_in.is_active,
|
||||
is_superuser=user_in.is_superuser,
|
||||
)
|
||||
db.add(db_user)
|
||||
await db.commit()
|
||||
await db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
@router.get("/me", response_model=UserSchema)
|
||||
async def read_user_me(
|
||||
current_user: User = Depends(deps.get_current_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get current user.
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
from typing import List, Union, Optional
|
||||
from pydantic import AnyHttpUrl, validator
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "XCodeReviewer"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
|
||||
# SECURITY
|
||||
SECRET_KEY: str = "changethis_in_production_to_a_long_random_string"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
|
||||
|
||||
# CORS
|
||||
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||
|
||||
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
|
||||
if isinstance(v, str) and not v.startswith("["):
|
||||
return [i.strip() for i in v.split(",")]
|
||||
elif isinstance(v, (list, str)):
|
||||
return v
|
||||
raise ValueError(v)
|
||||
|
||||
# POSTGRES
|
||||
POSTGRES_SERVER: str = "db"
|
||||
POSTGRES_USER: str = "postgres"
|
||||
POSTGRES_PASSWORD: str = "postgres"
|
||||
POSTGRES_DB: str = "xcodereviewer"
|
||||
DATABASE_URL: str | None = None
|
||||
|
||||
@validator("DATABASE_URL", pre=True)
|
||||
def assemble_db_connection(cls, v: str | None, values: dict[str, any]) -> str:
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
return str(f"postgresql+asyncpg://{values.get('POSTGRES_USER')}:{values.get('POSTGRES_PASSWORD')}@{values.get('POSTGRES_SERVER')}/{values.get('POSTGRES_DB')}")
|
||||
|
||||
# LLM配置
|
||||
LLM_PROVIDER: str = "openai" # gemini, openai, claude, qwen, deepseek, zhipu, moonshot, baidu, minimax, doubao, ollama
|
||||
LLM_API_KEY: Optional[str] = None
|
||||
LLM_MODEL: Optional[str] = None # 不指定时使用provider的默认模型
|
||||
LLM_BASE_URL: Optional[str] = None # 自定义API端点(如中转站)
|
||||
LLM_TIMEOUT: int = 150 # 超时时间(秒)
|
||||
LLM_TEMPERATURE: float = 0.1
|
||||
LLM_MAX_TOKENS: int = 4096
|
||||
|
||||
# 各LLM提供商的API Key配置(兼容单独配置)
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
OPENAI_BASE_URL: Optional[str] = None
|
||||
GEMINI_API_KEY: Optional[str] = None
|
||||
CLAUDE_API_KEY: Optional[str] = None
|
||||
QWEN_API_KEY: Optional[str] = None
|
||||
DEEPSEEK_API_KEY: Optional[str] = None
|
||||
ZHIPU_API_KEY: Optional[str] = None
|
||||
MOONSHOT_API_KEY: Optional[str] = None
|
||||
BAIDU_API_KEY: Optional[str] = None # 格式: api_key:secret_key
|
||||
MINIMAX_API_KEY: Optional[str] = None
|
||||
DOUBAO_API_KEY: Optional[str] = None
|
||||
OLLAMA_BASE_URL: Optional[str] = "http://localhost:11434/v1"
|
||||
|
||||
# GitHub配置
|
||||
GITHUB_TOKEN: Optional[str] = None
|
||||
|
||||
# GitLab配置
|
||||
GITLAB_TOKEN: Optional[str] = None
|
||||
|
||||
# 扫描配置
|
||||
MAX_ANALYZE_FILES: int = 50 # 最大分析文件数
|
||||
MAX_FILE_SIZE_BYTES: int = 200 * 1024 # 最大文件大小 200KB
|
||||
LLM_CONCURRENCY: int = 3 # LLM并发数
|
||||
LLM_GAP_MS: int = 2000 # LLM请求间隔(毫秒)
|
||||
|
||||
# 输出语言配置 - 支持 zh-CN(中文)和 en-US(英文)
|
||||
OUTPUT_LANGUAGE: str = "zh-CN"
|
||||
|
||||
class Config:
|
||||
case_sensitive = True
|
||||
env_file = ".env"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union
|
||||
from jose import jwt
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
ALGORITHM = settings.ALGORITHM
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
from sqlalchemy.orm import as_declarative, declared_attr
|
||||
|
||||
@as_declarative()
|
||||
class Base:
|
||||
id: str
|
||||
__name__: str
|
||||
|
||||
# Generate __tablename__ automatically
|
||||
@declared_attr
|
||||
def __tablename__(cls) -> str:
|
||||
return cls.__name__.lower() + "s"
|
||||
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=True, future=True)
|
||||
|
||||
AsyncSessionLocal = sessionmaker(
|
||||
engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async def get_db():
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.core.config import settings
|
||||
from app.api.v1.api import api_router
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json"
|
||||
)
|
||||
|
||||
# Configure CORS - Allow all origins in development
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # In production, replace with specific frontend URL
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to XCodeReviewer API", "docs": "/docs"}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from .user import User
|
||||
from .project import Project, ProjectMember
|
||||
from .audit import AuditTask, AuditIssue
|
||||
from .analysis import InstantAnalysis
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, String, Integer, DateTime, Float, Text, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
class InstantAnalysis(Base):
|
||||
__tablename__ = "instant_analyses"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=True) # Can be anonymous? Logic says usually logged in, but localDB allowed check.
|
||||
|
||||
language = Column(String, nullable=False)
|
||||
code_content = Column(Text, default="")
|
||||
analysis_result = Column(Text, default="{}")
|
||||
issues_count = Column(Integer, default=0)
|
||||
quality_score = Column(Float, default=0.0)
|
||||
analysis_time = Column(Float, default=0.0)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="instant_analyses")
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, Text, Float
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class AuditTask(Base):
|
||||
__tablename__ = "audit_tasks"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
project_id = Column(String, ForeignKey("projects.id"), nullable=False)
|
||||
created_by = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
task_type = Column(String, nullable=False)
|
||||
status = Column(String, default="pending", index=True)
|
||||
branch_name = Column(String, nullable=True)
|
||||
|
||||
exclude_patterns = Column(Text, default="[]")
|
||||
scan_config = Column(Text, default="{}")
|
||||
|
||||
# Stats
|
||||
total_files = Column(Integer, default=0)
|
||||
scanned_files = Column(Integer, default=0)
|
||||
total_lines = Column(Integer, default=0)
|
||||
issues_count = Column(Integer, default=0)
|
||||
quality_score = Column(Float, default=0.0)
|
||||
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", back_populates="tasks")
|
||||
creator = relationship("User", foreign_keys=[created_by])
|
||||
issues = relationship("AuditIssue", back_populates="task", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class AuditIssue(Base):
|
||||
__tablename__ = "audit_issues"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
task_id = Column(String, ForeignKey("audit_tasks.id"), nullable=False)
|
||||
|
||||
file_path = Column(String, nullable=False)
|
||||
line_number = Column(Integer, nullable=True)
|
||||
column_number = Column(Integer, nullable=True)
|
||||
issue_type = Column(String, nullable=False)
|
||||
severity = Column(String, nullable=False) # critical, high, medium, low
|
||||
|
||||
# 问题信息
|
||||
title = Column(String, nullable=True) # 问题标题
|
||||
message = Column(Text, nullable=True) # 兼容旧字段,同title
|
||||
description = Column(Text, nullable=True) # 详细描述
|
||||
suggestion = Column(Text, nullable=True) # 修复建议
|
||||
code_snippet = Column(Text, nullable=True) # 问题代码片段
|
||||
ai_explanation = Column(Text, nullable=True) # AI解释(JSON格式的xai字段)
|
||||
|
||||
status = Column(String, default="open") # open, resolved, false_positive
|
||||
resolved_by = Column(String, ForeignKey("users.id"), nullable=True)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
task = relationship("AuditTask", back_populates="issues")
|
||||
resolver = relationship("User", foreign_keys=[resolved_by])
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, String, Boolean, DateTime, ForeignKey, Text
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String, index=True, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
repository_url = Column(String, nullable=True)
|
||||
repository_type = Column(String, default="other")
|
||||
default_branch = Column(String, default="main")
|
||||
programming_languages = Column(Text, default="[]") # Stored as JSON string
|
||||
|
||||
owner_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
owner = relationship("User", backref="projects")
|
||||
members = relationship("ProjectMember", back_populates="project", cascade="all, delete-orphan")
|
||||
tasks = relationship("AuditTask", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
class ProjectMember(Base):
|
||||
__tablename__ = "project_members"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
project_id = Column(String, ForeignKey("projects.id"), nullable=False)
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=False)
|
||||
role = Column(String, default="member")
|
||||
permissions = Column(Text, default="{}")
|
||||
|
||||
joined_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
project = relationship("Project", back_populates="members")
|
||||
user = relationship("User", backref="project_memberships")
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, String, Boolean, DateTime
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
full_name = Column(String, index=True)
|
||||
is_active = Column(Boolean(), default=True)
|
||||
is_superuser = Column(Boolean(), default=False)
|
||||
|
||||
# Profile fields
|
||||
phone = Column(String, nullable=True)
|
||||
avatar_url = Column(String, nullable=True)
|
||||
role = Column(String, default="member")
|
||||
github_username = Column(String, nullable=True)
|
||||
gitlab_username = Column(String, nullable=True)
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
用户配置模型 - 存储用户的LLM和其他配置
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from sqlalchemy import Column, String, Text, DateTime, ForeignKey
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy.orm import relationship
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class UserConfig(Base):
|
||||
"""用户配置表"""
|
||||
__tablename__ = "user_configs"
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = Column(String, ForeignKey("users.id"), nullable=False, unique=True)
|
||||
|
||||
# LLM配置(JSON格式存储)
|
||||
llm_config = Column(Text, default="{}")
|
||||
|
||||
# 其他配置(JSON格式存储)
|
||||
other_config = Column(Text, default="{}")
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="config")
|
||||
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenPayload(BaseModel):
|
||||
sub: Optional[str] = None
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
from typing import Optional
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: Optional[EmailStr] = None
|
||||
is_active: Optional[bool] = True
|
||||
is_superuser: bool = False
|
||||
full_name: Optional[str] = None
|
||||
|
||||
# Profile fields
|
||||
phone: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
role: str = "member"
|
||||
github_username: Optional[str] = None
|
||||
gitlab_username: Optional[str] = None
|
||||
|
||||
class UserCreate(UserBase):
|
||||
email: EmailStr
|
||||
password: str
|
||||
full_name: str
|
||||
|
||||
class UserUpdate(UserBase):
|
||||
password: Optional[str] = None
|
||||
|
||||
class UserInDBBase(UserBase):
|
||||
id: str
|
||||
created_at: Optional[object] = None # Datetime
|
||||
updated_at: Optional[object] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class User(UserInDBBase):
|
||||
pass
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
"""
|
||||
LLM适配器模块
|
||||
"""
|
||||
|
||||
from .openai_adapter import OpenAIAdapter
|
||||
from .gemini_adapter import GeminiAdapter
|
||||
from .claude_adapter import ClaudeAdapter
|
||||
from .deepseek_adapter import DeepSeekAdapter
|
||||
from .qwen_adapter import QwenAdapter
|
||||
from .zhipu_adapter import ZhipuAdapter
|
||||
from .moonshot_adapter import MoonshotAdapter
|
||||
from .baidu_adapter import BaiduAdapter
|
||||
from .minimax_adapter import MinimaxAdapter
|
||||
from .doubao_adapter import DoubaoAdapter
|
||||
from .ollama_adapter import OllamaAdapter
|
||||
|
||||
__all__ = [
|
||||
'OpenAIAdapter',
|
||||
'GeminiAdapter',
|
||||
'ClaudeAdapter',
|
||||
'DeepSeekAdapter',
|
||||
'QwenAdapter',
|
||||
'ZhipuAdapter',
|
||||
'MoonshotAdapter',
|
||||
'BaiduAdapter',
|
||||
'MinimaxAdapter',
|
||||
'DoubaoAdapter',
|
||||
'OllamaAdapter',
|
||||
]
|
||||
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
百度文心一言适配器
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import json
|
||||
from typing import Optional
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMConfig, LLMRequest, LLMResponse, LLMError
|
||||
|
||||
|
||||
class BaiduAdapter(BaseLLMAdapter):
|
||||
"""百度文心一言API适配器"""
|
||||
|
||||
# 模型名称到API端点的映射
|
||||
MODEL_ENDPOINTS = {
|
||||
"ERNIE-4.0": "completions_pro",
|
||||
"ERNIE-3.5-8K": "completions",
|
||||
"ERNIE-3.5-128K": "ernie-3.5-128k",
|
||||
"ERNIE-Speed": "ernie_speed",
|
||||
"ERNIE-Lite": "ernie-lite-8k",
|
||||
}
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._access_token: Optional[str] = None
|
||||
self._base_url = config.base_url or "https://aip.baidubce.com"
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
"""获取百度API的access_token
|
||||
|
||||
注意:百度API使用API Key和Secret Key来获取access_token
|
||||
这里假设api_key格式为: "api_key:secret_key"
|
||||
"""
|
||||
if self._access_token:
|
||||
return self._access_token
|
||||
|
||||
# 解析API Key和Secret Key
|
||||
if ":" not in self.config.api_key:
|
||||
raise LLMError(
|
||||
"百度API需要同时提供API Key和Secret Key,格式:api_key:secret_key",
|
||||
provider="baidu"
|
||||
)
|
||||
|
||||
api_key, secret_key = self.config.api_key.split(":", 1)
|
||||
|
||||
url = f"{self._base_url}/oauth/2.0/token"
|
||||
params = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": api_key,
|
||||
"client_secret": secret_key,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.post(url, params=params)
|
||||
if response.status_code != 200:
|
||||
raise LLMError(
|
||||
f"获取百度access_token失败: {response.text}",
|
||||
provider="baidu",
|
||||
status_code=response.status_code
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
self._access_token = data.get("access_token")
|
||||
if not self._access_token:
|
||||
raise LLMError(
|
||||
f"百度API返回的access_token为空: {response.text}",
|
||||
provider="baidu"
|
||||
)
|
||||
|
||||
return self._access_token
|
||||
|
||||
async def _do_complete(self, request: LLMRequest) -> LLMResponse:
|
||||
"""执行实际的API调用"""
|
||||
access_token = await self._get_access_token()
|
||||
|
||||
# 获取模型对应的API端点
|
||||
model = self.config.model or "ERNIE-3.5-8K"
|
||||
endpoint = self.MODEL_ENDPOINTS.get(model, "completions")
|
||||
|
||||
url = f"{self._base_url}/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{endpoint}?access_token={access_token}"
|
||||
|
||||
messages = [{"role": m.role, "content": m.content} for m in request.messages]
|
||||
|
||||
payload = {
|
||||
"messages": messages,
|
||||
"temperature": request.temperature or self.config.temperature,
|
||||
"top_p": request.top_p or self.config.top_p,
|
||||
}
|
||||
|
||||
if request.max_tokens or self.config.max_tokens:
|
||||
payload["max_output_tokens"] = request.max_tokens or self.config.max_tokens
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.config.timeout) as client:
|
||||
response = await client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise LLMError(
|
||||
f"百度API错误: {response.text}",
|
||||
provider="baidu",
|
||||
status_code=response.status_code
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "error_code" in data:
|
||||
raise LLMError(
|
||||
f"百度API错误: {data.get('error_msg', '未知错误')}",
|
||||
provider="baidu",
|
||||
status_code=data.get("error_code")
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=data.get("result", ""),
|
||||
model=model,
|
||||
usage=data.get("usage"),
|
||||
finish_reason=data.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
"""验证配置是否有效"""
|
||||
try:
|
||||
await self._get_access_token()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_provider(self) -> str:
|
||||
return "baidu"
|
||||
|
||||
def get_model(self) -> str:
|
||||
return self.config.model or "ERNIE-3.5-8K"
|
||||
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Anthropic Claude适配器
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class ClaudeAdapter(BaseLLMAdapter):
|
||||
"""Claude适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.CLAUDE, "https://api.anthropic.com/v1")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "Claude API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# Claude API需要将system消息分离
|
||||
system_message = None
|
||||
messages = []
|
||||
|
||||
for msg in request.messages:
|
||||
if msg.role == "system":
|
||||
system_message = msg.content
|
||||
else:
|
||||
messages.append({
|
||||
"role": msg.role,
|
||||
"content": msg.content
|
||||
})
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens or 4096,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
|
||||
if system_message:
|
||||
request_body["system"] = system_message
|
||||
|
||||
# 构建请求头
|
||||
headers = {
|
||||
"x-api-key": self.config.api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/messages"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
|
||||
if not data.get("content") or not data["content"][0]:
|
||||
raise Exception("API响应格式异常: 缺少content字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("input_tokens", 0),
|
||||
completion_tokens=data["usage"].get("output_tokens", 0),
|
||||
total_tokens=data["usage"].get("input_tokens", 0) + data["usage"].get("output_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=data["content"][0].get("text", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=data.get("stop_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model.startswith("claude-"):
|
||||
raise Exception(f"无效的Claude模型: {self.config.model}")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
"""
|
||||
DeepSeek适配器 - 兼容OpenAI格式
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class DeepSeekAdapter(BaseLLMAdapter):
|
||||
"""DeepSeek适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.DEEPSEEK, "https://api.deepseek.com")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "DeepSeek API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# DeepSeek API兼容OpenAI格式
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
"frequency_penalty": self.config.frequency_penalty,
|
||||
"presence_penalty": self.config.presence_penalty,
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/v1/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model:
|
||||
raise Exception("未指定DeepSeek模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
字节跳动豆包适配器
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMConfig, LLMRequest, LLMResponse, LLMError
|
||||
|
||||
|
||||
class DoubaoAdapter(BaseLLMAdapter):
|
||||
"""字节跳动豆包API适配器
|
||||
|
||||
豆包使用OpenAI兼容的API格式
|
||||
"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._base_url = config.base_url or "https://ark.cn-beijing.volces.com/api/v3"
|
||||
|
||||
async def _do_complete(self, request: LLMRequest) -> LLMResponse:
|
||||
"""执行实际的API调用"""
|
||||
url = f"{self._base_url}/chat/completions"
|
||||
|
||||
messages = [{"role": m.role, "content": m.content} for m in request.messages]
|
||||
|
||||
payload = {
|
||||
"model": self.config.model or "doubao-pro-32k",
|
||||
"messages": messages,
|
||||
"temperature": request.temperature or self.config.temperature,
|
||||
"top_p": request.top_p or self.config.top_p,
|
||||
}
|
||||
|
||||
if request.max_tokens or self.config.max_tokens:
|
||||
payload["max_tokens"] = request.max_tokens or self.config.max_tokens
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.config.timeout) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise LLMError(
|
||||
f"豆包API错误: {response.text}",
|
||||
provider="doubao",
|
||||
status_code=response.status_code
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if "error" in data:
|
||||
raise LLMError(
|
||||
f"豆包API错误: {data['error'].get('message', '未知错误')}",
|
||||
provider="doubao"
|
||||
)
|
||||
|
||||
choices = data.get("choices", [])
|
||||
if not choices:
|
||||
raise LLMError("豆包API返回空响应", provider="doubao")
|
||||
|
||||
return LLMResponse(
|
||||
content=choices[0].get("message", {}).get("content", ""),
|
||||
model=data.get("model", self.config.model or "doubao-pro-32k"),
|
||||
usage=data.get("usage"),
|
||||
finish_reason=choices[0].get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
"""验证配置是否有效"""
|
||||
try:
|
||||
test_request = LLMRequest(
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
max_tokens=10
|
||||
)
|
||||
await self._do_complete(test_request)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_provider(self) -> str:
|
||||
return "doubao"
|
||||
|
||||
def get_model(self) -> str:
|
||||
return self.config.model or "doubao-pro-32k"
|
||||
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
Google Gemini适配器 - 支持官方API和中转站
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class GeminiAdapter(BaseLLMAdapter):
|
||||
"""Gemini适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.GEMINI, "https://generativelanguage.googleapis.com/v1beta")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._generate_content(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "Gemini API调用失败")
|
||||
|
||||
async def _generate_content(self, request: LLMRequest) -> LLMResponse:
|
||||
# 转换消息格式为 Gemini 格式
|
||||
contents: List[Dict[str, Any]] = []
|
||||
system_content = ""
|
||||
|
||||
for msg in request.messages:
|
||||
if msg.role == "system":
|
||||
system_content = msg.content
|
||||
else:
|
||||
role = "model" if msg.role == "assistant" else "user"
|
||||
contents.append({
|
||||
"role": role,
|
||||
"parts": [{"text": msg.content}]
|
||||
})
|
||||
|
||||
# 将系统消息合并到第一条用户消息
|
||||
if system_content and contents:
|
||||
contents[0]["parts"][0]["text"] = f"{system_content}\n\n{contents[0]['parts'][0]['text']}"
|
||||
|
||||
# 构建请求体
|
||||
request_body = {
|
||||
"contents": contents,
|
||||
"generationConfig": {
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"maxOutputTokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens,
|
||||
"topP": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
}
|
||||
|
||||
# API Key 在 URL 参数中
|
||||
url = f"{self.base_url}/models/{self.config.model}:generateContent?key={self.config.api_key}"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
|
||||
# 解析 Gemini 响应格式
|
||||
candidates = data.get("candidates", [])
|
||||
if not candidates:
|
||||
# 检查是否有错误信息
|
||||
if "error" in data:
|
||||
error_msg = data["error"].get("message", "未知错误")
|
||||
raise Exception(f"Gemini API错误: {error_msg}")
|
||||
raise Exception("API响应格式异常: 缺少candidates字段")
|
||||
|
||||
candidate = candidates[0]
|
||||
if not candidate or "content" not in candidate:
|
||||
raise Exception("API响应格式异常: 缺少content字段")
|
||||
|
||||
text_parts = candidate.get("content", {}).get("parts", [])
|
||||
if not text_parts:
|
||||
raise Exception("API响应格式异常: content.parts为空")
|
||||
|
||||
text = "".join(part.get("text", "") for part in text_parts)
|
||||
|
||||
# 检查响应内容是否为空
|
||||
if not text or not text.strip():
|
||||
finish_reason = candidate.get("finishReason", "unknown")
|
||||
raise Exception(f"Gemini返回空响应 - Finish Reason: {finish_reason}")
|
||||
|
||||
usage = None
|
||||
if "usageMetadata" in data:
|
||||
usage_data = data["usageMetadata"]
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=usage_data.get("promptTokenCount", 0),
|
||||
completion_tokens=usage_data.get("candidatesTokenCount", 0),
|
||||
total_tokens=usage_data.get("totalTokenCount", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=text,
|
||||
model=self.config.model,
|
||||
usage=usage,
|
||||
finish_reason=candidate.get("finishReason", "stop")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model.startswith("gemini-"):
|
||||
raise Exception(f"无效的Gemini模型: {self.config.model}")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
"""
|
||||
MiniMax适配器
|
||||
"""
|
||||
|
||||
import httpx
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMConfig, LLMRequest, LLMResponse, LLMError
|
||||
|
||||
|
||||
class MinimaxAdapter(BaseLLMAdapter):
|
||||
"""MiniMax API适配器"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
super().__init__(config)
|
||||
self._base_url = config.base_url or "https://api.minimax.chat/v1"
|
||||
|
||||
async def _do_complete(self, request: LLMRequest) -> LLMResponse:
|
||||
"""执行实际的API调用"""
|
||||
url = f"{self._base_url}/text/chatcompletion_v2"
|
||||
|
||||
messages = [{"role": m.role, "content": m.content} for m in request.messages]
|
||||
|
||||
payload = {
|
||||
"model": self.config.model or "abab6.5-chat",
|
||||
"messages": messages,
|
||||
"temperature": request.temperature or self.config.temperature,
|
||||
"top_p": request.top_p or self.config.top_p,
|
||||
}
|
||||
|
||||
if request.max_tokens or self.config.max_tokens:
|
||||
payload["max_tokens"] = request.max_tokens or self.config.max_tokens
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.config.timeout) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise LLMError(
|
||||
f"MiniMax API错误: {response.text}",
|
||||
provider="minimax",
|
||||
status_code=response.status_code
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if data.get("base_resp", {}).get("status_code") != 0:
|
||||
raise LLMError(
|
||||
f"MiniMax API错误: {data.get('base_resp', {}).get('status_msg', '未知错误')}",
|
||||
provider="minimax"
|
||||
)
|
||||
|
||||
choices = data.get("choices", [])
|
||||
if not choices:
|
||||
raise LLMError("MiniMax API返回空响应", provider="minimax")
|
||||
|
||||
return LLMResponse(
|
||||
content=choices[0].get("message", {}).get("content", ""),
|
||||
model=self.config.model or "abab6.5-chat",
|
||||
usage=data.get("usage"),
|
||||
finish_reason=choices[0].get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
"""验证配置是否有效"""
|
||||
try:
|
||||
test_request = LLMRequest(
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
max_tokens=10
|
||||
)
|
||||
await self._do_complete(test_request)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_provider(self) -> str:
|
||||
return "minimax"
|
||||
|
||||
def get_model(self) -> str:
|
||||
return self.config.model or "abab6.5-chat"
|
||||
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
月之暗面 Kimi适配器 - 兼容OpenAI格式
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class MoonshotAdapter(BaseLLMAdapter):
|
||||
"""月之暗面Kimi适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.MOONSHOT, "https://api.moonshot.cn/v1")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "Moonshot API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# Moonshot API兼容OpenAI格式
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model:
|
||||
raise Exception("未指定Moonshot模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
"""
|
||||
Ollama本地大模型适配器 - 兼容OpenAI格式
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class OllamaAdapter(BaseLLMAdapter):
|
||||
"""Ollama本地模型适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.OLLAMA, "http://localhost:11434/v1")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
# Ollama本地运行,跳过API Key验证
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "Ollama API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# Ollama兼容OpenAI格式
|
||||
headers = {}
|
||||
if self.config.api_key:
|
||||
headers["Authorization"] = f"Bearer {self.config.api_key}"
|
||||
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
|
||||
# Ollama的max_tokens参数名可能不同
|
||||
if request.max_tokens or self.config.max_tokens:
|
||||
request_body["num_predict"] = request.max_tokens or self.config.max_tokens
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers) if headers else self.build_headers(),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
# Ollama本地运行,不需要API Key
|
||||
if not self.config.model:
|
||||
raise Exception("未指定Ollama模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
"""
|
||||
OpenAI适配器 (支持GPT系列和OpenAI兼容API)
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class OpenAIAdapter(BaseLLMAdapter):
|
||||
"""OpenAI适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.OPENAI, "https://api.openai.com/v1")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "OpenAI API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# 构建请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
# 检测是否为推理模型(o1/o3系列)
|
||||
model_name = self.config.model.lower()
|
||||
is_reasoning_model = "o1" in model_name or "o3" in model_name
|
||||
|
||||
# 构建请求体
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
"frequency_penalty": self.config.frequency_penalty,
|
||||
"presence_penalty": self.config.presence_penalty,
|
||||
}
|
||||
|
||||
# 推理模型使用max_completion_tokens,其他模型使用max_tokens
|
||||
max_tokens = request.max_tokens if request.max_tokens is not None else self.config.max_tokens
|
||||
if is_reasoning_model:
|
||||
request_body["max_completion_tokens"] = max_tokens
|
||||
else:
|
||||
request_body["max_tokens"] = max_tokens
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model:
|
||||
raise Exception("未指定OpenAI模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
阿里云通义千问适配器 - 兼容OpenAI格式
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class QwenAdapter(BaseLLMAdapter):
|
||||
"""通义千问适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.QWEN, "https://dashscope.aliyuncs.com/compatible-mode/v1")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "通义千问 API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# 通义千问兼容OpenAI格式
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model:
|
||||
raise Exception("未指定通义千问模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
智谱AI适配器 (GLM系列) - 兼容OpenAI格式
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from ..base_adapter import BaseLLMAdapter
|
||||
from ..types import LLMRequest, LLMResponse, LLMUsage, DEFAULT_BASE_URLS, LLMProvider
|
||||
|
||||
|
||||
class ZhipuAdapter(BaseLLMAdapter):
|
||||
"""智谱AI适配器"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return self.config.base_url or DEFAULT_BASE_URLS.get(LLMProvider.ZHIPU, "https://open.bigmodel.cn/api/paas/v4")
|
||||
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
try:
|
||||
await self.validate_config()
|
||||
return await self.retry(lambda: self._send_request(request))
|
||||
except Exception as error:
|
||||
self.handle_error(error, "智谱AI API调用失败")
|
||||
|
||||
async def _send_request(self, request: LLMRequest) -> LLMResponse:
|
||||
# 智谱AI兼容OpenAI格式
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.config.api_key}",
|
||||
}
|
||||
|
||||
messages = [{"role": msg.role, "content": msg.content} for msg in request.messages]
|
||||
|
||||
request_body: Dict[str, Any] = {
|
||||
"model": self.config.model,
|
||||
"messages": messages,
|
||||
"temperature": request.temperature if request.temperature is not None else self.config.temperature,
|
||||
"max_tokens": request.max_tokens if request.max_tokens is not None else self.config.max_tokens,
|
||||
"top_p": request.top_p if request.top_p is not None else self.config.top_p,
|
||||
}
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/chat/completions"
|
||||
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers=self.build_headers(headers),
|
||||
json=request_body
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
error_data = response.json() if response.text else {}
|
||||
error_msg = error_data.get("error", {}).get("message", f"HTTP {response.status_code}")
|
||||
raise Exception(f"{error_msg}")
|
||||
|
||||
data = response.json()
|
||||
choice = data.get("choices", [{}])[0]
|
||||
|
||||
if not choice:
|
||||
raise Exception("API响应格式异常: 缺少choices字段")
|
||||
|
||||
usage = None
|
||||
if "usage" in data:
|
||||
usage = LLMUsage(
|
||||
prompt_tokens=data["usage"].get("prompt_tokens", 0),
|
||||
completion_tokens=data["usage"].get("completion_tokens", 0),
|
||||
total_tokens=data["usage"].get("total_tokens", 0)
|
||||
)
|
||||
|
||||
return LLMResponse(
|
||||
content=choice.get("message", {}).get("content", ""),
|
||||
model=data.get("model"),
|
||||
usage=usage,
|
||||
finish_reason=choice.get("finish_reason")
|
||||
)
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
await super().validate_config()
|
||||
if not self.config.model:
|
||||
raise Exception("未指定智谱AI模型")
|
||||
return True
|
||||
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
LLM适配器基类
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
import httpx
|
||||
|
||||
from .types import LLMConfig, LLMRequest, LLMResponse, LLMProvider, LLMError
|
||||
|
||||
|
||||
class BaseLLMAdapter(ABC):
|
||||
"""LLM适配器基类"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
self.config = config
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
|
||||
@property
|
||||
def client(self) -> httpx.AsyncClient:
|
||||
"""获取HTTP客户端"""
|
||||
if self._client is None:
|
||||
self._client = httpx.AsyncClient(timeout=self.config.timeout)
|
||||
return self._client
|
||||
|
||||
@abstractmethod
|
||||
async def complete(self, request: LLMRequest) -> LLMResponse:
|
||||
"""发送请求并获取响应"""
|
||||
pass
|
||||
|
||||
def get_provider(self) -> LLMProvider:
|
||||
"""获取提供商名称"""
|
||||
return self.config.provider
|
||||
|
||||
def get_model(self) -> str:
|
||||
"""获取模型名称"""
|
||||
return self.config.model
|
||||
|
||||
async def validate_config(self) -> bool:
|
||||
"""验证配置是否有效"""
|
||||
if not self.config.api_key:
|
||||
raise LLMError(
|
||||
"API Key未配置",
|
||||
self.config.provider
|
||||
)
|
||||
return True
|
||||
|
||||
async def with_timeout(self, coro, timeout_seconds: Optional[int] = None) -> Any:
|
||||
"""处理超时"""
|
||||
timeout = timeout_seconds or self.config.timeout
|
||||
try:
|
||||
return await asyncio.wait_for(coro, timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
raise LLMError(
|
||||
f"请求超时 ({timeout}s)",
|
||||
self.config.provider
|
||||
)
|
||||
|
||||
def handle_error(self, error: Any, context: str = "") -> None:
|
||||
"""处理API错误"""
|
||||
message = str(error)
|
||||
status_code = getattr(error, 'status_code', None)
|
||||
|
||||
# 针对不同错误类型提供更详细的信息
|
||||
if "超时" in message or "timeout" in message.lower():
|
||||
message = f"请求超时 ({self.config.timeout}s)。建议:\n" \
|
||||
f"1. 检查网络连接是否正常\n" \
|
||||
f"2. 尝试增加超时时间\n" \
|
||||
f"3. 验证API端点是否正确"
|
||||
elif status_code == 401 or status_code == 403:
|
||||
message = f"API认证失败。建议:\n" \
|
||||
f"1. 检查API Key是否正确配置\n" \
|
||||
f"2. 确认API Key是否有效且未过期\n" \
|
||||
f"3. 验证API Key权限是否充足"
|
||||
elif status_code == 429:
|
||||
message = f"API调用频率超限。建议:\n" \
|
||||
f"1. 等待一段时间后重试\n" \
|
||||
f"2. 降低并发数\n" \
|
||||
f"3. 增加请求间隔"
|
||||
elif status_code and status_code >= 500:
|
||||
message = f"API服务异常 ({status_code})。建议:\n" \
|
||||
f"1. 稍后重试\n" \
|
||||
f"2. 检查服务商状态页面\n" \
|
||||
f"3. 尝试切换其他LLM提供商"
|
||||
|
||||
full_message = f"{context}: {message}" if context else message
|
||||
|
||||
raise LLMError(
|
||||
full_message,
|
||||
self.config.provider,
|
||||
status_code,
|
||||
error
|
||||
)
|
||||
|
||||
async def retry(self, fn, max_attempts: int = 3, delay: float = 1.0) -> Any:
|
||||
"""重试逻辑"""
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return await fn()
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
status_code = getattr(error, 'status_code', None)
|
||||
|
||||
# 如果是4xx错误(客户端错误),不重试
|
||||
if status_code and 400 <= status_code < 500:
|
||||
raise error
|
||||
|
||||
# 最后一次尝试时不等待
|
||||
if attempt < max_attempts - 1:
|
||||
# 指数退避
|
||||
await asyncio.sleep(delay * (2 ** attempt))
|
||||
|
||||
raise last_error
|
||||
|
||||
def build_headers(self, additional_headers: Dict[str, str] = None) -> Dict[str, str]:
|
||||
"""构建请求头"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
if additional_headers:
|
||||
headers.update(additional_headers)
|
||||
if self.config.custom_headers:
|
||||
headers.update(self.config.custom_headers)
|
||||
return headers
|
||||
|
||||
async def close(self):
|
||||
"""关闭客户端"""
|
||||
if self._client:
|
||||
await self._client.aclose()
|
||||
self._client = None
|
||||
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
LLM工厂类 - 统一创建和管理LLM适配器
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
from .types import LLMConfig, LLMProvider, DEFAULT_MODELS
|
||||
from .base_adapter import BaseLLMAdapter
|
||||
from .adapters import (
|
||||
OpenAIAdapter,
|
||||
GeminiAdapter,
|
||||
ClaudeAdapter,
|
||||
DeepSeekAdapter,
|
||||
QwenAdapter,
|
||||
ZhipuAdapter,
|
||||
MoonshotAdapter,
|
||||
BaiduAdapter,
|
||||
MinimaxAdapter,
|
||||
DoubaoAdapter,
|
||||
OllamaAdapter,
|
||||
)
|
||||
|
||||
|
||||
class LLMFactory:
|
||||
"""LLM工厂类"""
|
||||
|
||||
_adapters: Dict[str, BaseLLMAdapter] = {}
|
||||
|
||||
@classmethod
|
||||
def create_adapter(cls, config: LLMConfig) -> BaseLLMAdapter:
|
||||
"""创建LLM适配器实例"""
|
||||
cache_key = cls._get_cache_key(config)
|
||||
|
||||
# 从缓存中获取
|
||||
if cache_key in cls._adapters:
|
||||
return cls._adapters[cache_key]
|
||||
|
||||
# 创建新的适配器实例
|
||||
adapter = cls._instantiate_adapter(config)
|
||||
|
||||
# 缓存实例
|
||||
cls._adapters[cache_key] = adapter
|
||||
|
||||
return adapter
|
||||
|
||||
@classmethod
|
||||
def _instantiate_adapter(cls, config: LLMConfig) -> BaseLLMAdapter:
|
||||
"""根据提供商类型实例化适配器"""
|
||||
# 如果未指定模型,使用默认模型
|
||||
if not config.model:
|
||||
config.model = DEFAULT_MODELS.get(config.provider, "gpt-4o-mini")
|
||||
|
||||
adapter_map = {
|
||||
LLMProvider.OPENAI: OpenAIAdapter,
|
||||
LLMProvider.GEMINI: GeminiAdapter,
|
||||
LLMProvider.CLAUDE: ClaudeAdapter,
|
||||
LLMProvider.DEEPSEEK: DeepSeekAdapter,
|
||||
LLMProvider.QWEN: QwenAdapter,
|
||||
LLMProvider.ZHIPU: ZhipuAdapter,
|
||||
LLMProvider.MOONSHOT: MoonshotAdapter,
|
||||
LLMProvider.BAIDU: BaiduAdapter,
|
||||
LLMProvider.MINIMAX: MinimaxAdapter,
|
||||
LLMProvider.DOUBAO: DoubaoAdapter,
|
||||
LLMProvider.OLLAMA: OllamaAdapter,
|
||||
}
|
||||
|
||||
adapter_class = adapter_map.get(config.provider)
|
||||
if not adapter_class:
|
||||
raise ValueError(f"不支持的LLM提供商: {config.provider}")
|
||||
|
||||
return adapter_class(config)
|
||||
|
||||
@classmethod
|
||||
def _get_cache_key(cls, config: LLMConfig) -> str:
|
||||
"""生成缓存键"""
|
||||
api_key_prefix = config.api_key[:8] if config.api_key else "no-key"
|
||||
return f"{config.provider.value}:{config.model}:{api_key_prefix}"
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls) -> None:
|
||||
"""清除缓存"""
|
||||
cls._adapters.clear()
|
||||
|
||||
@classmethod
|
||||
def get_supported_providers(cls) -> List[LLMProvider]:
|
||||
"""获取支持的提供商列表"""
|
||||
return list(LLMProvider)
|
||||
|
||||
@classmethod
|
||||
def get_default_model(cls, provider: LLMProvider) -> str:
|
||||
"""获取提供商的默认模型"""
|
||||
return DEFAULT_MODELS.get(provider, "gpt-4o-mini")
|
||||
|
||||
@classmethod
|
||||
def get_available_models(cls, provider: LLMProvider) -> List[str]:
|
||||
"""获取提供商的可用模型列表"""
|
||||
models = {
|
||||
LLMProvider.GEMINI: [
|
||||
"gemini-2.5-flash",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-pro",
|
||||
],
|
||||
LLMProvider.OPENAI: [
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4-turbo",
|
||||
"gpt-4",
|
||||
"gpt-3.5-turbo",
|
||||
"o1-preview",
|
||||
"o1-mini",
|
||||
],
|
||||
LLMProvider.CLAUDE: [
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-haiku-20240307",
|
||||
],
|
||||
LLMProvider.QWEN: [
|
||||
"qwen-turbo",
|
||||
"qwen-plus",
|
||||
"qwen-max",
|
||||
"qwen-max-longcontext",
|
||||
],
|
||||
LLMProvider.DEEPSEEK: [
|
||||
"deepseek-chat",
|
||||
"deepseek-coder",
|
||||
],
|
||||
LLMProvider.ZHIPU: [
|
||||
"glm-4-flash",
|
||||
"glm-4",
|
||||
"glm-4-plus",
|
||||
"glm-4-long",
|
||||
],
|
||||
LLMProvider.MOONSHOT: [
|
||||
"moonshot-v1-8k",
|
||||
"moonshot-v1-32k",
|
||||
"moonshot-v1-128k",
|
||||
],
|
||||
LLMProvider.BAIDU: [
|
||||
"ERNIE-3.5-8K",
|
||||
"ERNIE-4.0-8K",
|
||||
"ERNIE-Speed-8K",
|
||||
],
|
||||
LLMProvider.MINIMAX: [
|
||||
"abab6.5-chat",
|
||||
"abab6.5s-chat",
|
||||
"abab5.5-chat",
|
||||
],
|
||||
LLMProvider.DOUBAO: [
|
||||
"doubao-pro-32k",
|
||||
"doubao-pro-128k",
|
||||
"doubao-lite-32k",
|
||||
],
|
||||
LLMProvider.OLLAMA: [
|
||||
"llama3",
|
||||
"llama3.1",
|
||||
"llama3.2",
|
||||
"codellama",
|
||||
"mistral",
|
||||
"deepseek-coder-v2",
|
||||
"qwen2.5-coder",
|
||||
],
|
||||
}
|
||||
return models.get(provider, [])
|
||||
|
||||
|
|
@ -0,0 +1,586 @@
|
|||
"""
|
||||
LLM服务 - 代码分析核心服务
|
||||
支持中英文双语输出
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from .types import LLMConfig, LLMProvider, LLMMessage, LLMRequest, DEFAULT_MODELS
|
||||
from .factory import LLMFactory
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMService:
|
||||
"""LLM服务类"""
|
||||
|
||||
def __init__(self, user_config: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
初始化LLM服务
|
||||
|
||||
Args:
|
||||
user_config: 用户配置字典,包含llmConfig字段
|
||||
"""
|
||||
self._config: Optional[LLMConfig] = None
|
||||
self._user_config = user_config or {}
|
||||
|
||||
@property
|
||||
def config(self) -> LLMConfig:
|
||||
"""获取LLM配置(优先使用用户配置,然后使用系统配置)"""
|
||||
if self._config is None:
|
||||
user_llm_config = self._user_config.get('llmConfig', {})
|
||||
|
||||
# 优先使用用户配置的provider,否则使用系统配置
|
||||
provider_str = user_llm_config.get('llmProvider') or getattr(settings, 'LLM_PROVIDER', 'openai')
|
||||
provider = self._parse_provider(provider_str)
|
||||
|
||||
# 获取API Key - 优先级:用户配置 > 系统通用配置 > 系统平台专属配置
|
||||
api_key = (
|
||||
user_llm_config.get('llmApiKey') or
|
||||
getattr(settings, 'LLM_API_KEY', '') or
|
||||
self._get_provider_api_key_from_user_config(provider, user_llm_config) or
|
||||
self._get_provider_api_key(provider)
|
||||
)
|
||||
|
||||
# 获取Base URL
|
||||
base_url = (
|
||||
user_llm_config.get('llmBaseUrl') or
|
||||
getattr(settings, 'LLM_BASE_URL', None) or
|
||||
self._get_provider_base_url(provider)
|
||||
)
|
||||
|
||||
# 获取模型
|
||||
model = (
|
||||
user_llm_config.get('llmModel') or
|
||||
getattr(settings, 'LLM_MODEL', '') or
|
||||
DEFAULT_MODELS.get(provider, 'gpt-4o-mini')
|
||||
)
|
||||
|
||||
# 获取超时时间(用户配置是毫秒,系统配置是秒)
|
||||
timeout_ms = user_llm_config.get('llmTimeout')
|
||||
if timeout_ms:
|
||||
# 用户配置是毫秒,转换为秒
|
||||
timeout = int(timeout_ms / 1000) if timeout_ms > 1000 else int(timeout_ms)
|
||||
else:
|
||||
# 系统配置是秒
|
||||
timeout = int(getattr(settings, 'LLM_TIMEOUT', 150))
|
||||
|
||||
# 获取温度
|
||||
temperature = user_llm_config.get('llmTemperature') if user_llm_config.get('llmTemperature') is not None else float(getattr(settings, 'LLM_TEMPERATURE', 0.1))
|
||||
|
||||
# 获取最大token数
|
||||
max_tokens = user_llm_config.get('llmMaxTokens') or int(getattr(settings, 'LLM_MAX_TOKENS', 4096))
|
||||
|
||||
self._config = LLMConfig(
|
||||
provider=provider,
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
timeout=timeout,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
return self._config
|
||||
|
||||
def _get_provider_api_key_from_user_config(self, provider: LLMProvider, user_llm_config: Dict[str, Any]) -> Optional[str]:
|
||||
"""从用户配置中获取平台专属API Key"""
|
||||
provider_key_map = {
|
||||
LLMProvider.OPENAI: 'openaiApiKey',
|
||||
LLMProvider.GEMINI: 'geminiApiKey',
|
||||
LLMProvider.CLAUDE: 'claudeApiKey',
|
||||
LLMProvider.QWEN: 'qwenApiKey',
|
||||
LLMProvider.DEEPSEEK: 'deepseekApiKey',
|
||||
LLMProvider.ZHIPU: 'zhipuApiKey',
|
||||
LLMProvider.MOONSHOT: 'moonshotApiKey',
|
||||
LLMProvider.BAIDU: 'baiduApiKey',
|
||||
LLMProvider.MINIMAX: 'minimaxApiKey',
|
||||
LLMProvider.DOUBAO: 'doubaoApiKey',
|
||||
}
|
||||
key_name = provider_key_map.get(provider)
|
||||
if key_name:
|
||||
return user_llm_config.get(key_name)
|
||||
return None
|
||||
|
||||
def _get_provider_api_key(self, provider: LLMProvider) -> str:
|
||||
"""根据提供商获取API Key"""
|
||||
provider_key_map = {
|
||||
LLMProvider.OPENAI: 'OPENAI_API_KEY',
|
||||
LLMProvider.GEMINI: 'GEMINI_API_KEY',
|
||||
LLMProvider.CLAUDE: 'CLAUDE_API_KEY',
|
||||
LLMProvider.QWEN: 'QWEN_API_KEY',
|
||||
LLMProvider.DEEPSEEK: 'DEEPSEEK_API_KEY',
|
||||
LLMProvider.ZHIPU: 'ZHIPU_API_KEY',
|
||||
LLMProvider.MOONSHOT: 'MOONSHOT_API_KEY',
|
||||
LLMProvider.BAIDU: 'BAIDU_API_KEY',
|
||||
LLMProvider.MINIMAX: 'MINIMAX_API_KEY',
|
||||
LLMProvider.DOUBAO: 'DOUBAO_API_KEY',
|
||||
LLMProvider.OLLAMA: None, # Ollama 不需要 API Key
|
||||
}
|
||||
key_name = provider_key_map.get(provider)
|
||||
if key_name:
|
||||
return getattr(settings, key_name, '') or ''
|
||||
return 'ollama' # Ollama的默认值
|
||||
|
||||
def _get_provider_base_url(self, provider: LLMProvider) -> Optional[str]:
|
||||
"""根据提供商获取Base URL"""
|
||||
if provider == LLMProvider.OPENAI:
|
||||
return getattr(settings, 'OPENAI_BASE_URL', None)
|
||||
elif provider == LLMProvider.OLLAMA:
|
||||
return getattr(settings, 'OLLAMA_BASE_URL', 'http://localhost:11434/v1')
|
||||
return None
|
||||
|
||||
def _parse_provider(self, provider_str: str) -> LLMProvider:
|
||||
"""解析provider字符串"""
|
||||
provider_map = {
|
||||
'gemini': LLMProvider.GEMINI,
|
||||
'openai': LLMProvider.OPENAI,
|
||||
'claude': LLMProvider.CLAUDE,
|
||||
'qwen': LLMProvider.QWEN,
|
||||
'deepseek': LLMProvider.DEEPSEEK,
|
||||
'zhipu': LLMProvider.ZHIPU,
|
||||
'moonshot': LLMProvider.MOONSHOT,
|
||||
'baidu': LLMProvider.BAIDU,
|
||||
'minimax': LLMProvider.MINIMAX,
|
||||
'doubao': LLMProvider.DOUBAO,
|
||||
'ollama': LLMProvider.OLLAMA,
|
||||
}
|
||||
return provider_map.get(provider_str.lower(), LLMProvider.OPENAI)
|
||||
|
||||
def _get_output_language(self) -> str:
|
||||
"""获取输出语言配置(优先使用用户配置)"""
|
||||
user_other_config = self._user_config.get('otherConfig', {})
|
||||
return user_other_config.get('outputLanguage') or getattr(settings, 'OUTPUT_LANGUAGE', 'zh-CN')
|
||||
|
||||
def _build_system_prompt(self, is_chinese: bool) -> str:
|
||||
"""构建系统提示词(支持中英文)"""
|
||||
schema = """{
|
||||
"issues": [
|
||||
{
|
||||
"type": "security|bug|performance|style|maintainability",
|
||||
"severity": "critical|high|medium|low",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"suggestion": "string",
|
||||
"line": 1,
|
||||
"column": 1,
|
||||
"code_snippet": "string",
|
||||
"ai_explanation": "string",
|
||||
"xai": {
|
||||
"what": "string",
|
||||
"why": "string",
|
||||
"how": "string",
|
||||
"learn_more": "string(optional)"
|
||||
}
|
||||
}
|
||||
],
|
||||
"quality_score": 0-100,
|
||||
"summary": {
|
||||
"total_issues": number,
|
||||
"critical_issues": number,
|
||||
"high_issues": number,
|
||||
"medium_issues": number,
|
||||
"low_issues": number
|
||||
},
|
||||
"metrics": {
|
||||
"complexity": 0-100,
|
||||
"maintainability": 0-100,
|
||||
"security": 0-100,
|
||||
"performance": 0-100
|
||||
}
|
||||
}"""
|
||||
|
||||
if is_chinese:
|
||||
return f"""⚠️⚠️⚠️ 只输出JSON,禁止输出其他任何格式!禁止markdown!禁止文本分析!⚠️⚠️⚠️
|
||||
|
||||
你是一个专业的代码审计助手。你的任务是分析代码并返回严格符合JSON Schema的结果。
|
||||
|
||||
【最重要】输出格式要求:
|
||||
1. 必须只输出纯JSON对象,从{{开始,到}}结束
|
||||
2. 禁止在JSON前后添加任何文字、说明、markdown标记
|
||||
3. 禁止输出```json或###等markdown语法
|
||||
4. 如果是文档文件(如README),也必须以JSON格式输出分析结果
|
||||
|
||||
【内容要求】:
|
||||
1. 所有文本内容必须统一使用简体中文
|
||||
2. JSON字符串值中的特殊字符必须正确转义(换行用\\n,双引号用\\",反斜杠用\\\\)
|
||||
3. code_snippet字段必须使用\\n表示换行
|
||||
|
||||
请从以下维度全面分析代码:
|
||||
- 编码规范和代码风格
|
||||
- 潜在的 Bug 和逻辑错误
|
||||
- 性能问题和优化建议
|
||||
- 安全漏洞和风险
|
||||
- 可维护性和可读性
|
||||
- 最佳实践和设计模式
|
||||
|
||||
输出格式必须严格符合以下 JSON Schema:
|
||||
|
||||
{schema}
|
||||
|
||||
注意:
|
||||
- title: 问题的简短标题(中文)
|
||||
- description: 详细描述问题(中文)
|
||||
- suggestion: 具体的修复建议(中文)
|
||||
- line: 问题所在的行号(从1开始计数,必须准确对应代码中的行号)
|
||||
- column: 问题所在的列号(从1开始计数,指向问题代码的起始位置)
|
||||
- code_snippet: 包含问题的代码片段(建议包含问题行及其前后1-2行作为上下文,保持原始缩进格式)
|
||||
- ai_explanation: AI 的深入解释(中文)
|
||||
- xai.what: 这是什么问题(中文)
|
||||
- xai.why: 为什么会有这个问题(中文)
|
||||
- xai.how: 如何修复这个问题(中文)
|
||||
|
||||
【重要】关于行号和代码片段:
|
||||
1. line 必须是问题代码的行号!!!代码左侧有"行号|"标注,例如"25| const x = 1"表示第25行,line字段必须填25
|
||||
2. column 是问题代码在该行中的起始列位置(从1开始,不包括"行号|"前缀部分)
|
||||
3. code_snippet 应该包含问题代码及其上下文(前后各1-2行),去掉"行号|"前缀,保持原始代码的缩进
|
||||
4. 如果代码片段包含多行,必须使用 \\n 表示换行符(这是JSON的要求)
|
||||
5. 如果无法确定准确的行号,不要填写line和column字段(不要填0)
|
||||
|
||||
【严格禁止】:
|
||||
- 禁止在任何字段中使用英文,所有内容必须是简体中文
|
||||
- 禁止在JSON字符串值中使用真实换行符,必须用\\n转义
|
||||
- 禁止输出markdown代码块标记(如```json)
|
||||
|
||||
⚠️ 重要提醒:line字段必须从代码左侧的行号标注中读取,不要猜测或填0!"""
|
||||
else:
|
||||
return f"""⚠️⚠️⚠️ OUTPUT JSON ONLY! NO OTHER FORMAT! NO MARKDOWN! NO TEXT ANALYSIS! ⚠️⚠️⚠️
|
||||
|
||||
You are a professional code auditing assistant. Your task is to analyze code and return results in strict JSON Schema format.
|
||||
|
||||
【MOST IMPORTANT】Output format requirements:
|
||||
1. MUST output pure JSON object only, starting with {{ and ending with }}
|
||||
2. NO text, explanation, or markdown markers before or after JSON
|
||||
3. NO ```json or ### markdown syntax
|
||||
4. Even for document files (like README), output analysis in JSON format
|
||||
|
||||
【Content requirements】:
|
||||
1. All text content MUST be in English ONLY
|
||||
2. Special characters in JSON strings must be properly escaped (\\n for newlines, \\" for quotes, \\\\ for backslashes)
|
||||
3. code_snippet field MUST use \\n for newlines
|
||||
|
||||
Please comprehensively analyze the code from the following dimensions:
|
||||
- Coding standards and code style
|
||||
- Potential bugs and logical errors
|
||||
- Performance issues and optimization suggestions
|
||||
- Security vulnerabilities and risks
|
||||
- Maintainability and readability
|
||||
- Best practices and design patterns
|
||||
|
||||
The output format MUST strictly conform to the following JSON Schema:
|
||||
|
||||
{schema}
|
||||
|
||||
Note:
|
||||
- title: Brief title of the issue (in English)
|
||||
- description: Detailed description of the issue (in English)
|
||||
- suggestion: Specific fix suggestions (in English)
|
||||
- line: Line number where the issue occurs (1-indexed, must accurately correspond to the line in the code)
|
||||
- column: Column number where the issue starts (1-indexed, pointing to the start position of the problematic code)
|
||||
- code_snippet: Code snippet containing the issue (should include the problem line plus 1-2 lines before and after for context, preserve original indentation)
|
||||
- ai_explanation: AI's in-depth explanation (in English)
|
||||
- xai.what: What is this issue (in English)
|
||||
- xai.why: Why does this issue exist (in English)
|
||||
- xai.how: How to fix this issue (in English)
|
||||
|
||||
【IMPORTANT】About line numbers and code snippets:
|
||||
1. 'line' MUST be the line number from code!!! Code has "lineNumber|" prefix, e.g. "25| const x = 1" means line 25, you MUST set line to 25
|
||||
2. 'column' is the starting column position in that line (1-indexed, excluding the "lineNumber|" prefix)
|
||||
3. 'code_snippet' should include the problematic code with context (1-2 lines before/after), remove "lineNumber|" prefix, preserve indentation
|
||||
4. If code snippet has multiple lines, use \\n for newlines (JSON requirement)
|
||||
5. If you cannot determine the exact line number, do NOT fill line and column fields (don't use 0)
|
||||
|
||||
【STRICTLY PROHIBITED】:
|
||||
- NO Chinese characters in any field - English ONLY
|
||||
- NO real newline characters in JSON string values - must use \\n
|
||||
- NO markdown code block markers (like ```json)
|
||||
|
||||
⚠️ CRITICAL: Read line numbers from the "lineNumber|" prefix on the left of each code line. Do NOT guess or use 0!"""
|
||||
|
||||
async def analyze_code(self, code: str, language: str) -> Dict[str, Any]:
|
||||
"""
|
||||
分析代码并返回结构化问题
|
||||
支持中英文输出
|
||||
"""
|
||||
# 获取输出语言配置
|
||||
output_language = self._get_output_language()
|
||||
is_chinese = output_language == 'zh-CN'
|
||||
|
||||
# 添加行号帮助LLM定位问题
|
||||
code_with_lines = '\n'.join(
|
||||
f"{i+1}| {line}" for i, line in enumerate(code.split('\n'))
|
||||
)
|
||||
|
||||
# 构建系统提示词
|
||||
system_prompt = self._build_system_prompt(is_chinese)
|
||||
|
||||
# 构建用户提示词
|
||||
if is_chinese:
|
||||
user_prompt = f"""编程语言: {language}
|
||||
|
||||
⚠️ 代码已标注行号(格式:行号| 代码内容),请根据行号准确填写 line 字段!
|
||||
|
||||
请分析以下代码:
|
||||
|
||||
{code_with_lines}"""
|
||||
else:
|
||||
user_prompt = f"""Programming Language: {language}
|
||||
|
||||
⚠️ Code is annotated with line numbers (format: lineNumber| code), please fill the 'line' field accurately based on these numbers!
|
||||
|
||||
Please analyze the following code:
|
||||
|
||||
{code_with_lines}"""
|
||||
|
||||
try:
|
||||
adapter = LLMFactory.create_adapter(self.config)
|
||||
|
||||
request = LLMRequest(
|
||||
messages=[
|
||||
LLMMessage(role="system", content=system_prompt),
|
||||
LLMMessage(role="user", content=user_prompt)
|
||||
],
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
response = await adapter.complete(request)
|
||||
content = response.content
|
||||
|
||||
# 检查响应内容是否为空
|
||||
if not content or not content.strip():
|
||||
logger.warning(f"LLM返回空响应 - Provider: {self.config.provider.value}, Model: {self.config.model}")
|
||||
logger.warning(f"响应详情 - Finish Reason: {response.finish_reason}, Usage: {response.usage}")
|
||||
return self._get_default_response()
|
||||
|
||||
# 尝试从响应中提取JSON
|
||||
result = self._parse_json(content)
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM Analysis failed: {e}", exc_info=True)
|
||||
logger.error(f"Provider: {self.config.provider.value}, Model: {self.config.model}")
|
||||
return self._get_default_response()
|
||||
|
||||
def _parse_json(self, text: str) -> Dict[str, Any]:
|
||||
"""从LLM响应中解析JSON(增强版)"""
|
||||
|
||||
# 检查输入是否为空
|
||||
if not text or not text.strip():
|
||||
logger.warning("LLM响应内容为空,无法解析JSON")
|
||||
return self._get_default_response()
|
||||
|
||||
def clean_text(s: str) -> str:
|
||||
"""清理文本中的控制字符"""
|
||||
# 移除BOM和零宽字符
|
||||
s = s.replace('\ufeff', '').replace('\u200b', '').replace('\u200c', '').replace('\u200d', '')
|
||||
return s
|
||||
|
||||
def fix_json_format(s: str) -> str:
|
||||
"""修复常见的JSON格式问题"""
|
||||
s = s.strip()
|
||||
# 移除尾部逗号
|
||||
s = re.sub(r',(\s*[}\]])', r'\1', s)
|
||||
# 修复未转义的换行符(在字符串值中)
|
||||
s = re.sub(r':\s*"([^"]*)\n([^"]*)"', r': "\1\\n\2"', s)
|
||||
return s
|
||||
|
||||
def aggressive_fix_json(s: str) -> str:
|
||||
"""激进的JSON修复:尝试修复更多格式问题"""
|
||||
s = clean_text(s)
|
||||
s = s.strip()
|
||||
|
||||
# 找到第一个 { 和最后一个 }
|
||||
start_idx = s.find('{')
|
||||
if start_idx == -1:
|
||||
raise ValueError("No JSON object found")
|
||||
|
||||
# 尝试找到最后一个 }
|
||||
last_brace = s.rfind('}')
|
||||
if last_brace > start_idx:
|
||||
s = s[start_idx:last_brace + 1]
|
||||
|
||||
# 修复常见的JSON问题
|
||||
# 1. 移除尾部逗号
|
||||
s = re.sub(r',(\s*[}\]])', r'\1', s)
|
||||
# 2. 修复单引号为双引号(仅在键名中,小心处理)
|
||||
s = re.sub(r"'(\w+)'\s*:", r'"\1":', s)
|
||||
# 3. 修复未转义的控制字符(在字符串值中,但不在键名中)
|
||||
# 只移除不在引号内的控制字符,或未转义的换行符/制表符
|
||||
lines = []
|
||||
in_string = False
|
||||
escape_next = False
|
||||
for char in s:
|
||||
if escape_next:
|
||||
escape_next = False
|
||||
lines.append(char)
|
||||
continue
|
||||
if char == '\\':
|
||||
escape_next = True
|
||||
lines.append(char)
|
||||
continue
|
||||
if char == '"':
|
||||
in_string = not in_string
|
||||
lines.append(char)
|
||||
continue
|
||||
# 如果在字符串外,移除控制字符;如果在字符串内,保留(假设已转义)
|
||||
if not in_string and ord(char) < 32 and char not in ['\n', '\t', '\r']:
|
||||
continue # 跳过控制字符
|
||||
lines.append(char)
|
||||
s = ''.join(lines)
|
||||
|
||||
return s
|
||||
|
||||
# 尝试多种方式解析
|
||||
attempts = [
|
||||
# 1. 直接解析
|
||||
lambda: json.loads(text),
|
||||
# 2. 清理后解析
|
||||
lambda: json.loads(fix_json_format(clean_text(text))),
|
||||
# 3. 从markdown代码块提取
|
||||
lambda: self._extract_from_markdown(text),
|
||||
# 4. 智能提取JSON对象
|
||||
lambda: self._extract_json_object(clean_text(text)),
|
||||
# 5. 修复截断的JSON
|
||||
lambda: self._fix_truncated_json(clean_text(text)),
|
||||
# 6. 激进修复后解析
|
||||
lambda: json.loads(aggressive_fix_json(text)),
|
||||
]
|
||||
|
||||
last_error = None
|
||||
for i, attempt in enumerate(attempts):
|
||||
try:
|
||||
result = attempt()
|
||||
if result and isinstance(result, dict):
|
||||
if i > 0:
|
||||
logger.info(f"✅ JSON解析成功(方法 {i + 1}/{len(attempts)})")
|
||||
return result
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
if i == 0:
|
||||
logger.debug(f"直接解析失败,尝试其他方法... {e}")
|
||||
|
||||
# 所有尝试都失败
|
||||
logger.warning("⚠️ 无法解析LLM响应为JSON")
|
||||
logger.warning(f"原始内容长度: {len(text)} 字符")
|
||||
logger.warning(f"原始内容(前500字符): {text[:500]}")
|
||||
logger.warning(f"原始内容(后500字符): {text[-500:] if len(text) > 500 else text}")
|
||||
if last_error:
|
||||
logger.warning(f"最后错误: {type(last_error).__name__}: {str(last_error)}")
|
||||
return self._get_default_response()
|
||||
|
||||
def _extract_from_markdown(self, text: str) -> Dict[str, Any]:
|
||||
"""从markdown代码块提取JSON"""
|
||||
match = re.search(r'```(?:json)?\s*(\{[\s\S]*?\})\s*```', text)
|
||||
if match:
|
||||
return json.loads(match.group(1))
|
||||
raise ValueError("No markdown code block found")
|
||||
|
||||
def _extract_json_object(self, text: str) -> Dict[str, Any]:
|
||||
"""智能提取JSON对象"""
|
||||
start_idx = text.find('{')
|
||||
if start_idx == -1:
|
||||
raise ValueError("No JSON object found")
|
||||
|
||||
# 考虑字符串内的花括号和转义字符
|
||||
brace_count = 0
|
||||
bracket_count = 0
|
||||
in_string = False
|
||||
escape_next = False
|
||||
end_idx = -1
|
||||
|
||||
for i in range(start_idx, len(text)):
|
||||
char = text[i]
|
||||
|
||||
if escape_next:
|
||||
escape_next = False
|
||||
continue
|
||||
|
||||
if char == '\\':
|
||||
escape_next = True
|
||||
continue
|
||||
|
||||
if char == '"' and not escape_next:
|
||||
in_string = not in_string
|
||||
continue
|
||||
|
||||
if not in_string:
|
||||
if char == '{':
|
||||
brace_count += 1
|
||||
elif char == '}':
|
||||
brace_count -= 1
|
||||
if brace_count == 0 and bracket_count == 0:
|
||||
end_idx = i + 1
|
||||
break
|
||||
elif char == '[':
|
||||
bracket_count += 1
|
||||
elif char == ']':
|
||||
bracket_count -= 1
|
||||
|
||||
if end_idx == -1:
|
||||
# 如果找不到完整的JSON,尝试使用最后一个 }
|
||||
last_brace = text.rfind('}')
|
||||
if last_brace > start_idx:
|
||||
end_idx = last_brace + 1
|
||||
else:
|
||||
raise ValueError("Incomplete JSON object")
|
||||
|
||||
json_str = text[start_idx:end_idx]
|
||||
# 修复格式问题
|
||||
json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
|
||||
# 尝试修复未闭合的括号
|
||||
open_braces = json_str.count('{') - json_str.count('}')
|
||||
open_brackets = json_str.count('[') - json_str.count(']')
|
||||
if open_braces > 0:
|
||||
json_str += '}' * open_braces
|
||||
if open_brackets > 0:
|
||||
json_str += ']' * open_brackets
|
||||
|
||||
return json.loads(json_str)
|
||||
|
||||
def _fix_truncated_json(self, text: str) -> Dict[str, Any]:
|
||||
"""修复截断的JSON"""
|
||||
start_idx = text.find('{')
|
||||
if start_idx == -1:
|
||||
raise ValueError("Cannot fix truncated JSON")
|
||||
|
||||
json_str = text[start_idx:]
|
||||
|
||||
# 计算缺失的闭合符号
|
||||
open_braces = json_str.count('{')
|
||||
close_braces = json_str.count('}')
|
||||
open_brackets = json_str.count('[')
|
||||
close_brackets = json_str.count(']')
|
||||
|
||||
# 补全缺失的闭合符号
|
||||
json_str += ']' * max(0, open_brackets - close_brackets)
|
||||
json_str += '}' * max(0, open_braces - close_braces)
|
||||
|
||||
# 修复格式
|
||||
json_str = re.sub(r',(\s*[}\]])', r'\1', json_str)
|
||||
return json.loads(json_str)
|
||||
|
||||
def _get_default_response(self) -> Dict[str, Any]:
|
||||
"""返回默认响应"""
|
||||
return {
|
||||
"issues": [],
|
||||
"quality_score": 80,
|
||||
"summary": {
|
||||
"total_issues": 0,
|
||||
"critical_issues": 0,
|
||||
"high_issues": 0,
|
||||
"medium_issues": 0,
|
||||
"low_issues": 0
|
||||
},
|
||||
"metrics": {
|
||||
"complexity": 80,
|
||||
"maintainability": 80,
|
||||
"security": 80,
|
||||
"performance": 80
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 全局服务实例
|
||||
llm_service = LLMService()
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
"""
|
||||
LLM服务类型定义
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Optional, Dict, Any, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class LLMProvider(str, Enum):
|
||||
"""支持的LLM提供商类型"""
|
||||
GEMINI = "gemini" # Google Gemini
|
||||
OPENAI = "openai" # OpenAI (GPT系列)
|
||||
CLAUDE = "claude" # Anthropic Claude
|
||||
QWEN = "qwen" # 阿里云通义千问
|
||||
DEEPSEEK = "deepseek" # DeepSeek
|
||||
ZHIPU = "zhipu" # 智谱AI (GLM系列)
|
||||
MOONSHOT = "moonshot" # 月之暗面 Kimi
|
||||
BAIDU = "baidu" # 百度文心一言
|
||||
MINIMAX = "minimax" # MiniMax
|
||||
DOUBAO = "doubao" # 字节豆包
|
||||
OLLAMA = "ollama" # Ollama 本地大模型
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMConfig:
|
||||
"""LLM配置"""
|
||||
provider: LLMProvider
|
||||
api_key: str
|
||||
model: str
|
||||
base_url: Optional[str] = None
|
||||
timeout: int = 150
|
||||
temperature: float = 0.2
|
||||
max_tokens: int = 4096
|
||||
top_p: float = 1.0
|
||||
frequency_penalty: float = 0
|
||||
presence_penalty: float = 0
|
||||
custom_headers: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMMessage:
|
||||
"""LLM请求消息"""
|
||||
role: str # 'system', 'user', 'assistant'
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMRequest:
|
||||
"""LLM请求参数"""
|
||||
messages: List[LLMMessage]
|
||||
temperature: Optional[float] = None
|
||||
max_tokens: Optional[int] = None
|
||||
top_p: Optional[float] = None
|
||||
stream: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMUsage:
|
||||
"""Token使用统计"""
|
||||
prompt_tokens: int
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMResponse:
|
||||
"""LLM响应"""
|
||||
content: str
|
||||
model: Optional[str] = None
|
||||
usage: Optional[LLMUsage] = None
|
||||
finish_reason: Optional[str] = None
|
||||
|
||||
|
||||
class LLMError(Exception):
|
||||
"""LLM错误"""
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
provider: Optional[LLMProvider] = None,
|
||||
status_code: Optional[int] = None,
|
||||
original_error: Optional[Any] = None
|
||||
):
|
||||
super().__init__(message)
|
||||
self.provider = provider
|
||||
self.status_code = status_code
|
||||
self.original_error = original_error
|
||||
|
||||
|
||||
# 各平台默认模型
|
||||
DEFAULT_MODELS: Dict[LLMProvider, str] = {
|
||||
LLMProvider.GEMINI: "gemini-2.5-flash",
|
||||
LLMProvider.OPENAI: "gpt-4o-mini",
|
||||
LLMProvider.CLAUDE: "claude-3-5-sonnet-20241022",
|
||||
LLMProvider.QWEN: "qwen-turbo",
|
||||
LLMProvider.DEEPSEEK: "deepseek-chat",
|
||||
LLMProvider.ZHIPU: "glm-4-flash",
|
||||
LLMProvider.MOONSHOT: "moonshot-v1-8k",
|
||||
LLMProvider.BAIDU: "ERNIE-3.5-8K",
|
||||
LLMProvider.MINIMAX: "abab6.5-chat",
|
||||
LLMProvider.DOUBAO: "doubao-pro-32k",
|
||||
LLMProvider.OLLAMA: "llama3",
|
||||
}
|
||||
|
||||
|
||||
# 各平台API端点
|
||||
DEFAULT_BASE_URLS: Dict[LLMProvider, str] = {
|
||||
LLMProvider.OPENAI: "https://api.openai.com/v1",
|
||||
LLMProvider.QWEN: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
LLMProvider.DEEPSEEK: "https://api.deepseek.com",
|
||||
LLMProvider.ZHIPU: "https://open.bigmodel.cn/api/paas/v4",
|
||||
LLMProvider.MOONSHOT: "https://api.moonshot.cn/v1",
|
||||
LLMProvider.BAIDU: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1",
|
||||
LLMProvider.MINIMAX: "https://api.minimax.chat/v1",
|
||||
LLMProvider.DOUBAO: "https://ark.cn-beijing.volces.com/api/v3",
|
||||
LLMProvider.OLLAMA: "http://localhost:11434/v1",
|
||||
LLMProvider.GEMINI: "https://generativelanguage.googleapis.com/v1beta",
|
||||
LLMProvider.CLAUDE: "https://api.anthropic.com/v1",
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
"""
|
||||
仓库扫描服务 - 支持GitHub和GitLab仓库扫描
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse, quote
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.audit import AuditTask, AuditIssue
|
||||
from app.models.project import Project
|
||||
from app.services.llm.service import LLMService
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
# 支持的文本文件扩展名
|
||||
TEXT_EXTENSIONS = [
|
||||
".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".go", ".rs",
|
||||
".cpp", ".c", ".h", ".cc", ".hh", ".cs", ".php", ".rb",
|
||||
".kt", ".swift", ".sql", ".sh", ".json", ".yml", ".yaml"
|
||||
]
|
||||
|
||||
# 排除的目录和文件模式
|
||||
EXCLUDE_PATTERNS = [
|
||||
"node_modules/", "vendor/", "dist/", "build/", ".git/",
|
||||
"__pycache__/", ".pytest_cache/", "coverage/", ".nyc_output/",
|
||||
".vscode/", ".idea/", ".vs/", "target/", "out/",
|
||||
"__MACOSX/", ".DS_Store", "package-lock.json", "yarn.lock",
|
||||
"pnpm-lock.yaml", ".min.js", ".min.css", ".map"
|
||||
]
|
||||
|
||||
|
||||
def is_text_file(path: str) -> bool:
|
||||
"""检查是否为文本文件"""
|
||||
return any(path.lower().endswith(ext) for ext in TEXT_EXTENSIONS)
|
||||
|
||||
|
||||
def should_exclude(path: str, exclude_patterns: List[str] = None) -> bool:
|
||||
"""检查是否应该排除该文件"""
|
||||
all_patterns = EXCLUDE_PATTERNS + (exclude_patterns or [])
|
||||
return any(pattern in path for pattern in all_patterns)
|
||||
|
||||
|
||||
def get_language_from_path(path: str) -> str:
|
||||
"""从文件路径获取语言类型"""
|
||||
ext = path.split('.')[-1].lower() if '.' in path else ''
|
||||
language_map = {
|
||||
'js': 'javascript', 'jsx': 'javascript',
|
||||
'ts': 'typescript', 'tsx': 'typescript',
|
||||
'py': 'python', 'java': 'java', 'go': 'go',
|
||||
'rs': 'rust', 'cpp': 'cpp', 'c': 'cpp',
|
||||
'cc': 'cpp', 'h': 'cpp', 'hh': 'cpp',
|
||||
'cs': 'csharp', 'php': 'php', 'rb': 'ruby',
|
||||
'kt': 'kotlin', 'swift': 'swift'
|
||||
}
|
||||
return language_map.get(ext, 'text')
|
||||
|
||||
|
||||
class TaskControlManager:
|
||||
"""任务控制管理器 - 用于取消运行中的任务"""
|
||||
|
||||
def __init__(self):
|
||||
self._cancelled_tasks: set = set()
|
||||
|
||||
def cancel_task(self, task_id: str):
|
||||
"""取消任务"""
|
||||
self._cancelled_tasks.add(task_id)
|
||||
print(f"🛑 任务 {task_id} 已标记为取消")
|
||||
|
||||
def is_cancelled(self, task_id: str) -> bool:
|
||||
"""检查任务是否被取消"""
|
||||
return task_id in self._cancelled_tasks
|
||||
|
||||
def cleanup_task(self, task_id: str):
|
||||
"""清理已完成任务的控制状态"""
|
||||
self._cancelled_tasks.discard(task_id)
|
||||
|
||||
|
||||
# 全局任务控制器
|
||||
task_control = TaskControlManager()
|
||||
|
||||
|
||||
async def github_api(url: str, token: str = None) -> Any:
|
||||
"""调用GitHub API"""
|
||||
headers = {"Accept": "application/vnd.github+json"}
|
||||
t = token or settings.GITHUB_TOKEN
|
||||
if t:
|
||||
headers["Authorization"] = f"Bearer {t}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
if response.status_code == 403:
|
||||
raise Exception("GitHub API 403:请配置 GITHUB_TOKEN 或确认仓库权限/频率限制")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"GitHub API {response.status_code}: {url}")
|
||||
return response.json()
|
||||
|
||||
|
||||
async def gitlab_api(url: str, token: str = None) -> Any:
|
||||
"""调用GitLab API"""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
t = token or settings.GITLAB_TOKEN
|
||||
if t:
|
||||
headers["PRIVATE-TOKEN"] = t
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
if response.status_code == 401:
|
||||
raise Exception("GitLab API 401:请配置 GITLAB_TOKEN 或确认仓库权限")
|
||||
if response.status_code == 403:
|
||||
raise Exception("GitLab API 403:请确认仓库权限/频率限制")
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"GitLab API {response.status_code}: {url}")
|
||||
return response.json()
|
||||
|
||||
|
||||
async def fetch_file_content(url: str, headers: Dict[str, str] = None) -> Optional[str]:
|
||||
"""获取文件内容"""
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
try:
|
||||
response = await client.get(url, headers=headers or {})
|
||||
if response.status_code == 200:
|
||||
return response.text
|
||||
except Exception as e:
|
||||
print(f"获取文件内容失败: {url}, 错误: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def get_github_files(repo_url: str, branch: str, token: str = None) -> List[Dict[str, str]]:
|
||||
"""获取GitHub仓库文件列表"""
|
||||
# 解析仓库URL
|
||||
match = repo_url.rstrip('/').rstrip('.git')
|
||||
if 'github.com/' in match:
|
||||
parts = match.split('github.com/')[-1].split('/')
|
||||
if len(parts) >= 2:
|
||||
owner, repo = parts[0], parts[1]
|
||||
else:
|
||||
raise Exception("GitHub 仓库 URL 格式错误")
|
||||
else:
|
||||
raise Exception("GitHub 仓库 URL 格式错误")
|
||||
|
||||
# 获取仓库文件树
|
||||
tree_url = f"https://api.github.com/repos/{owner}/{repo}/git/trees/{quote(branch)}?recursive=1"
|
||||
tree_data = await github_api(tree_url, token)
|
||||
|
||||
files = []
|
||||
for item in tree_data.get("tree", []):
|
||||
if item.get("type") == "blob" and is_text_file(item["path"]) and not should_exclude(item["path"]):
|
||||
size = item.get("size", 0)
|
||||
if size <= settings.MAX_FILE_SIZE_BYTES:
|
||||
files.append({
|
||||
"path": item["path"],
|
||||
"url": f"https://raw.githubusercontent.com/{owner}/{repo}/{quote(branch)}/{item['path']}"
|
||||
})
|
||||
|
||||
return files
|
||||
|
||||
|
||||
async def get_gitlab_files(repo_url: str, branch: str, token: str = None) -> List[Dict[str, str]]:
|
||||
"""获取GitLab仓库文件列表"""
|
||||
parsed = urlparse(repo_url)
|
||||
base = f"{parsed.scheme}://{parsed.netloc}"
|
||||
|
||||
# 从URL中提取token(如果存在)
|
||||
extracted_token = token
|
||||
if parsed.username:
|
||||
if parsed.username == 'oauth2' and parsed.password:
|
||||
extracted_token = parsed.password
|
||||
elif parsed.username and not parsed.password:
|
||||
extracted_token = parsed.username
|
||||
|
||||
# 解析项目路径
|
||||
path = parsed.path.strip('/').rstrip('.git')
|
||||
if not path:
|
||||
raise Exception("GitLab 仓库 URL 格式错误")
|
||||
|
||||
project_path = quote(path, safe='')
|
||||
|
||||
# 获取仓库文件树
|
||||
tree_url = f"{base}/api/v4/projects/{project_path}/repository/tree?ref={quote(branch)}&recursive=true&per_page=100"
|
||||
tree_data = await gitlab_api(tree_url, extracted_token)
|
||||
|
||||
files = []
|
||||
for item in tree_data:
|
||||
if item.get("type") == "blob" and is_text_file(item["path"]) and not should_exclude(item["path"]):
|
||||
files.append({
|
||||
"path": item["path"],
|
||||
"url": f"{base}/api/v4/projects/{project_path}/repository/files/{quote(item['path'], safe='')}/raw?ref={quote(branch)}",
|
||||
"token": extracted_token
|
||||
})
|
||||
|
||||
return files
|
||||
|
||||
|
||||
async def scan_repo_task(task_id: str, db_session_factory, user_config: dict = None):
|
||||
"""
|
||||
后台仓库扫描任务
|
||||
|
||||
Args:
|
||||
task_id: 任务ID
|
||||
db_session_factory: 数据库会话工厂
|
||||
user_config: 用户配置字典(包含llmConfig和otherConfig)
|
||||
"""
|
||||
async with db_session_factory() as db:
|
||||
task = await db.get(AuditTask, task_id)
|
||||
if not task:
|
||||
return
|
||||
|
||||
try:
|
||||
# 1. 更新状态为运行中
|
||||
task.status = "running"
|
||||
task.started_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
|
||||
# 创建使用用户配置的LLM服务实例
|
||||
llm_service = LLMService(user_config=user_config or {})
|
||||
|
||||
# 2. 获取项目信息
|
||||
project = await db.get(Project, task.project_id)
|
||||
if not project or not project.repository_url:
|
||||
raise Exception("仓库地址不存在")
|
||||
|
||||
repo_url = project.repository_url
|
||||
branch = task.branch_name or project.default_branch or "main"
|
||||
repo_type = project.repository_type or "other"
|
||||
|
||||
print(f"🚀 开始扫描仓库: {repo_url}, 分支: {branch}, 类型: {repo_type}")
|
||||
|
||||
# 3. 获取文件列表
|
||||
# 从用户配置中读取 GitHub/GitLab Token(优先使用用户配置,然后使用系统配置)
|
||||
user_other_config = (user_config or {}).get('otherConfig', {})
|
||||
github_token = user_other_config.get('githubToken') or settings.GITHUB_TOKEN
|
||||
gitlab_token = user_other_config.get('gitlabToken') or settings.GITLAB_TOKEN
|
||||
|
||||
files: List[Dict[str, str]] = []
|
||||
extracted_gitlab_token = None
|
||||
|
||||
if repo_type == "github":
|
||||
files = await get_github_files(repo_url, branch, github_token)
|
||||
elif repo_type == "gitlab":
|
||||
files = await get_gitlab_files(repo_url, branch, gitlab_token)
|
||||
# GitLab文件可能带有token
|
||||
if files and 'token' in files[0]:
|
||||
extracted_gitlab_token = files[0].get('token')
|
||||
else:
|
||||
raise Exception("不支持的仓库类型,仅支持 GitHub 和 GitLab 仓库")
|
||||
|
||||
# 限制文件数量
|
||||
files = files[:settings.MAX_ANALYZE_FILES]
|
||||
|
||||
task.total_files = len(files)
|
||||
await db.commit()
|
||||
|
||||
print(f"📊 获取到 {len(files)} 个文件,开始分析")
|
||||
|
||||
# 4. 分析文件
|
||||
total_issues = 0
|
||||
total_lines = 0
|
||||
quality_scores = []
|
||||
scanned_files = 0
|
||||
failed_files = 0
|
||||
consecutive_failures = 0
|
||||
MAX_CONSECUTIVE_FAILURES = 5
|
||||
|
||||
for file_info in files:
|
||||
# 检查是否取消
|
||||
if task_control.is_cancelled(task_id):
|
||||
print(f"🛑 任务 {task_id} 已被用户取消")
|
||||
task.status = "cancelled"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
task_control.cleanup_task(task_id)
|
||||
return
|
||||
|
||||
# 检查连续失败次数
|
||||
if consecutive_failures >= MAX_CONSECUTIVE_FAILURES:
|
||||
print(f"❌ 任务 {task_id}: 连续失败 {consecutive_failures} 次,停止分析")
|
||||
raise Exception(f"连续失败 {consecutive_failures} 次,可能是 LLM API 服务异常")
|
||||
|
||||
try:
|
||||
# 获取文件内容
|
||||
headers = {}
|
||||
# 使用提取的 GitLab token 或用户配置的 token
|
||||
token_to_use = extracted_gitlab_token or gitlab_token
|
||||
if token_to_use:
|
||||
headers["PRIVATE-TOKEN"] = token_to_use
|
||||
|
||||
content = await fetch_file_content(file_info["url"], headers)
|
||||
if not content:
|
||||
continue
|
||||
|
||||
if len(content) > settings.MAX_FILE_SIZE_BYTES:
|
||||
continue
|
||||
|
||||
total_lines += content.count('\n') + 1
|
||||
language = get_language_from_path(file_info["path"])
|
||||
|
||||
# LLM分析
|
||||
analysis = await llm_service.analyze_code(content, language)
|
||||
|
||||
# 再次检查是否取消(LLM分析后)
|
||||
if task_control.is_cancelled(task_id):
|
||||
print(f"🛑 任务 {task_id} 在LLM分析后被取消")
|
||||
task.status = "cancelled"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
task_control.cleanup_task(task_id)
|
||||
return
|
||||
|
||||
# 保存问题
|
||||
issues = analysis.get("issues", [])
|
||||
for issue in issues:
|
||||
audit_issue = AuditIssue(
|
||||
task_id=task.id,
|
||||
file_path=file_info["path"],
|
||||
line_number=issue.get("line", 1),
|
||||
column_number=issue.get("column"),
|
||||
issue_type=issue.get("type", "maintainability"),
|
||||
severity=issue.get("severity", "low"),
|
||||
title=issue.get("title", "Issue"),
|
||||
message=issue.get("description") or issue.get("title", "Issue"),
|
||||
suggestion=issue.get("suggestion"),
|
||||
code_snippet=issue.get("code_snippet"),
|
||||
ai_explanation=issue.get("ai_explanation"),
|
||||
status="open"
|
||||
)
|
||||
db.add(audit_issue)
|
||||
total_issues += 1
|
||||
|
||||
if "quality_score" in analysis:
|
||||
quality_scores.append(analysis["quality_score"])
|
||||
|
||||
consecutive_failures = 0 # 成功后重置
|
||||
scanned_files += 1
|
||||
|
||||
# 更新进度
|
||||
task.scanned_files = scanned_files
|
||||
task.total_lines = total_lines
|
||||
task.issues_count = total_issues
|
||||
await db.commit()
|
||||
|
||||
print(f"📈 任务 {task_id}: 进度 {scanned_files}/{len(files)} ({int(scanned_files/len(files)*100)}%)")
|
||||
|
||||
# 请求间隔
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
|
||||
except Exception as file_error:
|
||||
failed_files += 1
|
||||
consecutive_failures += 1
|
||||
print(f"❌ 分析文件失败 ({file_info['path']}): {file_error}")
|
||||
await asyncio.sleep(settings.LLM_GAP_MS / 1000)
|
||||
|
||||
# 5. 完成任务
|
||||
avg_quality_score = sum(quality_scores) / len(quality_scores) if quality_scores else 100.0
|
||||
|
||||
task.status = "completed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
task.scanned_files = scanned_files
|
||||
task.total_lines = total_lines
|
||||
task.issues_count = total_issues
|
||||
task.quality_score = avg_quality_score
|
||||
await db.commit()
|
||||
|
||||
print(f"✅ 任务 {task_id} 完成: 扫描 {scanned_files} 个文件, 发现 {total_issues} 个问题, 质量分 {avg_quality_score:.1f}")
|
||||
task_control.cleanup_task(task_id)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 扫描失败: {e}")
|
||||
task.status = "failed"
|
||||
task.completed_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
task_control.cleanup_task(task_id)
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
# =============================================
|
||||
# XCodeReviewer Backend 配置文件
|
||||
# 复制此文件为 .env 并填入实际配置
|
||||
# =============================================
|
||||
|
||||
# ------------ 数据库配置 ------------
|
||||
POSTGRES_SERVER=localhost
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=xcodereviewer
|
||||
|
||||
# ------------ 安全配置 ------------
|
||||
SECRET_KEY=your-super-secret-key-change-this-in-production
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=11520
|
||||
|
||||
# ------------ LLM配置 ------------
|
||||
# 支持的provider: openai, gemini, claude, qwen, deepseek, zhipu, moonshot, baidu, minimax, doubao, ollama
|
||||
LLM_PROVIDER=openai
|
||||
LLM_API_KEY=sk-your-api-key
|
||||
LLM_MODEL=
|
||||
LLM_BASE_URL=
|
||||
LLM_TIMEOUT=150
|
||||
LLM_TEMPERATURE=0.1
|
||||
LLM_MAX_TOKENS=4096
|
||||
|
||||
# ------------ 仓库扫描配置 ------------
|
||||
GITHUB_TOKEN=
|
||||
GITLAB_TOKEN=
|
||||
MAX_ANALYZE_FILES=50
|
||||
MAX_FILE_SIZE_BYTES=204800
|
||||
LLM_CONCURRENCY=3
|
||||
LLM_GAP_MS=2000
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
def main():
|
||||
print("Hello from xcodereviewer-backend!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
[project]
|
||||
name = "xcodereviewer-backend"
|
||||
version = "0.1.0"
|
||||
description = "XCodeReviewer Backend API"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"fastapi>=0.100.0",
|
||||
"uvicorn[standard]",
|
||||
"sqlalchemy>=2.0.0",
|
||||
"asyncpg",
|
||||
"alembic",
|
||||
"pydantic>=2.0.0",
|
||||
"pydantic-settings",
|
||||
"passlib[bcrypt]",
|
||||
"python-jose[cryptography]",
|
||||
"python-multipart",
|
||||
"httpx",
|
||||
"email-validator",
|
||||
"greenlet",
|
||||
"bcrypt<5.0.0",
|
||||
]
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile requirements.txt -o requirements-lock.txt
|
||||
alembic==1.17.2
|
||||
# via -r requirements.txt
|
||||
annotated-doc==0.0.4
|
||||
# via fastapi
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.11.0
|
||||
# via
|
||||
# httpx
|
||||
# starlette
|
||||
# watchfiles
|
||||
asyncpg==0.31.0
|
||||
# via -r requirements.txt
|
||||
bcrypt==5.0.0
|
||||
# via passlib
|
||||
certifi==2025.11.12
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
cffi==2.0.0
|
||||
# via cryptography
|
||||
click==8.3.1
|
||||
# via uvicorn
|
||||
cryptography==46.0.3
|
||||
# via python-jose
|
||||
ecdsa==0.19.1
|
||||
# via python-jose
|
||||
fastapi==0.122.0
|
||||
# via -r requirements.txt
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# uvicorn
|
||||
httpcore==1.0.9
|
||||
# via httpx
|
||||
httptools==0.7.1
|
||||
# via uvicorn
|
||||
httpx==0.28.1
|
||||
# via -r requirements.txt
|
||||
idna==3.11
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
mako==1.3.10
|
||||
# via alembic
|
||||
markupsafe==3.0.3
|
||||
# via mako
|
||||
passlib==1.7.4
|
||||
# via -r requirements.txt
|
||||
pyasn1==0.6.1
|
||||
# via
|
||||
# python-jose
|
||||
# rsa
|
||||
pycparser==2.23
|
||||
# via cffi
|
||||
pydantic==2.12.4
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# fastapi
|
||||
# pydantic-settings
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
pydantic-settings==2.12.0
|
||||
# via -r requirements.txt
|
||||
python-dotenv==1.2.1
|
||||
# via
|
||||
# pydantic-settings
|
||||
# uvicorn
|
||||
python-jose==3.5.0
|
||||
# via -r requirements.txt
|
||||
python-multipart==0.0.20
|
||||
# via -r requirements.txt
|
||||
pyyaml==6.0.3
|
||||
# via uvicorn
|
||||
rsa==4.9.1
|
||||
# via python-jose
|
||||
six==1.17.0
|
||||
# via ecdsa
|
||||
sniffio==1.3.1
|
||||
# via anyio
|
||||
sqlalchemy==2.0.44
|
||||
# via
|
||||
# -r requirements.txt
|
||||
# alembic
|
||||
starlette==0.50.0
|
||||
# via fastapi
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# alembic
|
||||
# fastapi
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# sqlalchemy
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
uvicorn==0.38.0
|
||||
# via -r requirements.txt
|
||||
uvloop==0.22.1
|
||||
# via uvicorn
|
||||
watchfiles==1.1.1
|
||||
# via uvicorn
|
||||
websockets==15.0.1
|
||||
# via uvicorn
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
fastapi>=0.100.0
|
||||
uvicorn[standard]
|
||||
sqlalchemy>=2.0.0
|
||||
asyncpg
|
||||
alembic
|
||||
pydantic>=2.0.0
|
||||
pydantic-settings
|
||||
passlib[bcrypt]
|
||||
python-jose[cryptography]
|
||||
python-multipart
|
||||
httpx
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/bash
|
||||
# 使用 uv 启动后端服务
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 启动 XCodeReviewer 后端服务..."
|
||||
|
||||
# 检查 uv 是否安装
|
||||
if ! command -v uv &> /dev/null; then
|
||||
echo "❌ 未找到 uv,请先安装:"
|
||||
echo " curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 同步依赖(如果需要)
|
||||
if [ ! -d ".venv" ]; then
|
||||
echo "📦 首次运行,正在安装依赖..."
|
||||
uv sync
|
||||
fi
|
||||
|
||||
# 运行数据库迁移
|
||||
echo "🔄 运行数据库迁移..."
|
||||
uv run alembic upgrade head
|
||||
|
||||
# 启动服务
|
||||
echo "✅ 启动后端服务..."
|
||||
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
|
|
@ -0,0 +1,907 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.17.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/02/a6/74c8cadc2882977d80ad756a13857857dbcf9bd405bc80b662eb10651282/alembic-1.17.2.tar.gz", hash = "sha256:bbe9751705c5e0f14877f02d46c53d10885e377e3d90eda810a016f9baa19e8e", size = 1988064, upload-time = "2025-11-14T20:35:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/88/6237e97e3385b57b5f1528647addea5cc03d4d65d5979ab24327d41fb00d/alembic-1.17.2-py3-none-any.whl", hash = "sha256:f483dd1fe93f6c5d49217055e4d15b905b425b6af906746abb35b69c1996c4e6", size = 248554, upload-time = "2025-11-14T20:35:05.699Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.31.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/1f/924e3caae75f471eae4b26bd13b698f6af2c44279f67af317439c2f4c46a/ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61", size = 201793, upload-time = "2025-03-13T11:52:43.25Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "dnspython" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.122.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b", size = 346436, upload-time = "2025-11-24T19:17:47.95Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671, upload-time = "2025-11-24T19:17:45.96Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httptools"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "passlib"
|
||||
version = "1.7.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
bcrypt = [
|
||||
{ name = "bcrypt" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.23"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-jose"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ecdsa" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "rsa" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/77/3a1c9039db7124eb039772b935f2244fbb73fc8ee65b9acf2375da1c07bf/python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b", size = 92726, upload-time = "2025-05-28T17:31:54.288Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
cryptography = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "4.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyasn1" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.44"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.50.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
standard = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "httptools" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvloop"
|
||||
version = "0.22.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xcodereviewer-backend"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "bcrypt" },
|
||||
{ name = "email-validator" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-jose", extra = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "alembic" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "bcrypt", specifier = "<5.0.0" },
|
||||
{ name = "email-validator" },
|
||||
{ name = "fastapi", specifier = ">=0.100.0" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "passlib", extras = ["bcrypt"] },
|
||||
{ name = "pydantic", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-jose", extras = ["cryptography"] },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.0" },
|
||||
{ name = "uvicorn", extras = ["standard"] },
|
||||
]
|
||||
|
|
@ -1,95 +1,52 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
# XCodeReviewer 前端应用
|
||||
xcodereviewer:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
# 构建参数 - 从 .env 文件或环境变量传入
|
||||
args:
|
||||
# LLM 通用配置
|
||||
- VITE_LLM_PROVIDER=${VITE_LLM_PROVIDER:-gemini}
|
||||
- VITE_LLM_API_KEY=${VITE_LLM_API_KEY}
|
||||
- VITE_LLM_MODEL=${VITE_LLM_MODEL}
|
||||
- VITE_LLM_BASE_URL=${VITE_LLM_BASE_URL}
|
||||
- VITE_LLM_TIMEOUT=${VITE_LLM_TIMEOUT:-150000}
|
||||
- VITE_LLM_TEMPERATURE=${VITE_LLM_TEMPERATURE:-0.2}
|
||||
- VITE_LLM_MAX_TOKENS=${VITE_LLM_MAX_TOKENS:-4096}
|
||||
|
||||
# Google Gemini 配置
|
||||
- VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY}
|
||||
- VITE_GEMINI_MODEL=${VITE_GEMINI_MODEL:-gemini-2.5-flash}
|
||||
- VITE_GEMINI_TIMEOUT_MS=${VITE_GEMINI_TIMEOUT_MS:-25000}
|
||||
|
||||
# OpenAI 配置
|
||||
- VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}
|
||||
- VITE_OPENAI_MODEL=${VITE_OPENAI_MODEL:-gpt-4o-mini}
|
||||
- VITE_OPENAI_BASE_URL=${VITE_OPENAI_BASE_URL}
|
||||
|
||||
# Claude 配置
|
||||
- VITE_CLAUDE_API_KEY=${VITE_CLAUDE_API_KEY}
|
||||
- VITE_CLAUDE_MODEL=${VITE_CLAUDE_MODEL:-claude-3-5-sonnet-20241022}
|
||||
|
||||
# 通义千问配置
|
||||
- VITE_QWEN_API_KEY=${VITE_QWEN_API_KEY}
|
||||
- VITE_QWEN_MODEL=${VITE_QWEN_MODEL:-qwen-turbo}
|
||||
|
||||
# DeepSeek 配置
|
||||
- VITE_DEEPSEEK_API_KEY=${VITE_DEEPSEEK_API_KEY}
|
||||
- VITE_DEEPSEEK_MODEL=${VITE_DEEPSEEK_MODEL:-deepseek-chat}
|
||||
|
||||
# 智谱AI 配置
|
||||
- VITE_ZHIPU_API_KEY=${VITE_ZHIPU_API_KEY}
|
||||
- VITE_ZHIPU_MODEL=${VITE_ZHIPU_MODEL:-glm-4-flash}
|
||||
|
||||
# Moonshot 配置
|
||||
- VITE_MOONSHOT_API_KEY=${VITE_MOONSHOT_API_KEY}
|
||||
- VITE_MOONSHOT_MODEL=${VITE_MOONSHOT_MODEL:-moonshot-v1-8k}
|
||||
|
||||
# 百度文心一言配置
|
||||
- VITE_BAIDU_API_KEY=${VITE_BAIDU_API_KEY}
|
||||
- VITE_BAIDU_MODEL=${VITE_BAIDU_MODEL:-ERNIE-3.5-8K}
|
||||
|
||||
# MiniMax 配置
|
||||
- VITE_MINIMAX_API_KEY=${VITE_MINIMAX_API_KEY}
|
||||
- VITE_MINIMAX_MODEL=${VITE_MINIMAX_MODEL:-abab6.5-chat}
|
||||
|
||||
# 豆包配置
|
||||
- VITE_DOUBAO_API_KEY=${VITE_DOUBAO_API_KEY}
|
||||
- VITE_DOUBAO_MODEL=${VITE_DOUBAO_MODEL:-doubao-pro-32k}
|
||||
|
||||
# Ollama 配置
|
||||
- VITE_OLLAMA_API_KEY=${VITE_OLLAMA_API_KEY:-ollama}
|
||||
- VITE_OLLAMA_MODEL=${VITE_OLLAMA_MODEL:-llama3}
|
||||
- VITE_OLLAMA_BASE_URL=${VITE_OLLAMA_BASE_URL:-http://localhost:11434/v1}
|
||||
|
||||
# 数据库配置
|
||||
- VITE_USE_LOCAL_DB=${VITE_USE_LOCAL_DB:-true}
|
||||
- VITE_SUPABASE_URL=${VITE_SUPABASE_URL}
|
||||
- VITE_SUPABASE_ANON_KEY=${VITE_SUPABASE_ANON_KEY}
|
||||
|
||||
# GitHub 配置
|
||||
- VITE_GITHUB_TOKEN=${VITE_GITHUB_TOKEN}
|
||||
|
||||
# 应用配置
|
||||
- VITE_APP_ID=${VITE_APP_ID:-xcodereviewer}
|
||||
- VITE_MAX_ANALYZE_FILES=${VITE_MAX_ANALYZE_FILES:-40}
|
||||
- VITE_LLM_CONCURRENCY=${VITE_LLM_CONCURRENCY:-2}
|
||||
- VITE_LLM_GAP_MS=${VITE_LLM_GAP_MS:-500}
|
||||
- VITE_OUTPUT_LANGUAGE=${VITE_OUTPUT_LANGUAGE:-zh-CN}
|
||||
|
||||
container_name: xcodereviewer-app
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=xcodereviewer
|
||||
ports:
|
||||
- "5174:80"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- xcodereviewer-network
|
||||
- "5432:5432"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
xcodereviewer-network:
|
||||
driver: bridge
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/xcodereviewer
|
||||
- SECRET_KEY=changethisdevsecret
|
||||
- ALGORITHM=HS256
|
||||
- ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
- VITE_API_BASE_URL=/api
|
||||
depends_on:
|
||||
- backend
|
||||
command: npm run dev -- --host
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml* package-lock.json* ./
|
||||
|
||||
RUN if [ -f pnpm-lock.yaml ]; then \
|
||||
corepack enable && pnpm install; \
|
||||
else \
|
||||
npm install; \
|
||||
fi
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 5173
|
||||
|
||||
CMD ["npm", "run", "dev", "--", "--host"]
|
||||
|
||||
|
|
@ -82,6 +82,7 @@
|
|||
"@types/lodash": "^4.17.16",
|
||||
"@types/react": "^19.0.12",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@types/video-react": "^0.15.8",
|
||||
"@typescript/native-preview": "7.0.0-dev.20250819.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 463 KiB After Width: | Height: | Size: 463 KiB |
|
Before Width: | Height: | Size: 368 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,53 @@
|
|||
import { BrowserRouter, Routes, Route, Navigate, Outlet } from "react-router-dom";
|
||||
import { Toaster } from "sonner";
|
||||
import Header from "@/components/layout/Header";
|
||||
import routes from "./routes";
|
||||
import { AuthProvider } from "@/shared/context/AuthContext";
|
||||
import { ProtectedRoute } from "./ProtectedRoute";
|
||||
import Login from "@/pages/Login";
|
||||
import Register from "@/pages/Register";
|
||||
import NotFound from "@/pages/NotFound";
|
||||
|
||||
function AppLayout() {
|
||||
return (
|
||||
<div className="min-h-screen gradient-bg">
|
||||
<Header />
|
||||
<main className="container-responsive py-4 md:py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<BrowserRouter>
|
||||
<Toaster position="top-right" />
|
||||
<Routes>
|
||||
{/* Public Routes */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
{/* Protected Routes */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route element={<AppLayout />}>
|
||||
{routes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
/>
|
||||
))}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Catch all */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '@/shared/context/AuthContext';
|
||||
|
||||
export const ProtectedRoute = () => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex h-screen items-center justify-center">Loading...</div>;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
|
|
@ -3,16 +3,9 @@ import { createRoot } from "react-dom/client";
|
|||
import "@/assets/styles/globals.css";
|
||||
import App from "./App.tsx";
|
||||
import { AppWrapper } from "@/components/layout/PageMeta";
|
||||
import { isLocalMode } from "@/shared/config/database";
|
||||
import { initLocalDatabase } from "@/shared/utils/initLocalDB";
|
||||
import { ErrorBoundary } from "@/components/common/ErrorBoundary";
|
||||
import "@/shared/utils/fetchWrapper"; // 初始化fetch拦截器
|
||||
|
||||
// 初始化本地数据库
|
||||
if (isLocalMode) {
|
||||
initLocalDatabase().catch(console.error);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||