chore: 升级到 Python 3.14 并现代化代码

- 升级 Docker 基础镜像从 Python 3.12 到 3.14
- 更新 pyproject.toml 支持 Python 3.13/3.14
- 移除 Python 3.8/3.9/3.10/3.11 分类器
- 更新 black 和 mypy 配置目标版本
- 将 get_event_loop() 替换为 get_running_loop() 加上 RuntimeError 处理
- 简化 compute_cost_sync 中的 asyncio.run 使用
- Dict/List/Tuple/Set → dict/list/tuple/set (PEP 585)
- Optional[T] → T | None (PEP 604)
- Union[A, B] → A | B (PEP 604)
- 移除废弃的 typing 导入
- 移除不必要的字符串引号注解
This commit is contained in:
AAEE86
2026-01-30 03:10:21 +08:00
parent 3e75bc8964
commit 24d24f6829
255 changed files with 4062 additions and 4173 deletions

View File

@@ -6,10 +6,9 @@
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import and_, func, or_
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session, joinedload
from src.api.base.adapter import ApiAdapter, ApiMode
@@ -41,10 +40,10 @@ router = APIRouter(prefix="/api/public", tags=["System Catalog"])
pipeline = ApiRequestPipeline()
@router.get("/providers", response_model=List[PublicProviderResponse])
@router.get("/providers", response_model=list[PublicProviderResponse])
async def get_public_providers(
request: Request,
is_active: Optional[bool] = Query(None, description="过滤活跃状态"),
is_active: bool | None = Query(None, description="过滤活跃状态"),
skip: int = Query(0, description="跳过记录数"),
limit: int = Query(100, description="返回记录数限制"),
db: Session = Depends(get_db),
@@ -77,11 +76,11 @@ async def get_public_providers(
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=ApiMode.PUBLIC)
@router.get("/models", response_model=List[PublicModelResponse])
@router.get("/models", response_model=list[PublicModelResponse])
async def get_public_models(
request: Request,
provider_id: Optional[str] = Query(None, description="提供商ID过滤"),
is_active: Optional[bool] = Query(None, description="过滤活跃状态"),
provider_id: str | None = Query(None, description="提供商ID过滤"),
is_active: bool | None = Query(None, description="过滤活跃状态"),
skip: int = Query(0, description="跳过记录数"),
limit: int = Query(100, description="返回记录数限制"),
db: Session = Depends(get_db),
@@ -145,7 +144,7 @@ async def get_public_stats(request: Request, db: Session = Depends(get_db)):
async def search_models(
request: Request,
q: str = Query(..., description="搜索关键词"),
provider_id: Optional[int] = Query(None, description="提供商ID过滤"),
provider_id: int | None = Query(None, description="提供商ID过滤"),
limit: int = Query(20, description="返回记录数限制"),
db: Session = Depends(get_db),
):
@@ -234,8 +233,8 @@ async def get_public_global_models(
request: Request,
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=1000, description="返回记录数限制"),
is_active: Optional[bool] = Query(None, description="过滤活跃状态"),
search: Optional[str] = Query(None, description="搜索关键词"),
is_active: bool | None = Query(None, description="过滤活跃状态"),
search: str | None = Query(None, description="搜索关键词"),
db: Session = Depends(get_db),
):
"""
@@ -283,7 +282,7 @@ class PublicApiAdapter(ApiAdapter):
@dataclass
class PublicProvidersAdapter(PublicApiAdapter):
is_active: Optional[bool]
is_active: bool | None
skip: int
limit: int
@@ -338,8 +337,8 @@ class PublicProvidersAdapter(PublicApiAdapter):
@dataclass
class PublicModelsAdapter(PublicApiAdapter):
provider_id: Optional[str]
is_active: Optional[bool]
provider_id: str | None
is_active: bool | None
skip: int
limit: int
@@ -426,7 +425,7 @@ class PublicStatsAdapter(PublicApiAdapter):
@dataclass
class PublicSearchModelsAdapter(PublicApiAdapter):
query: str
provider_id: Optional[int]
provider_id: int | None
limit: int
async def handle(self, context): # type: ignore[override]
@@ -508,7 +507,7 @@ class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
.all()
)
all_formats: List[str] = []
all_formats: list[str] = []
for (api_format_enum,) in active_formats:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
@@ -525,7 +524,7 @@ class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
)
.all()
)
endpoint_map: Dict[str, List[str]] = defaultdict(list)
endpoint_map: dict[str, list[str]] = defaultdict(list)
for api_format_enum, endpoint_id in endpoint_rows:
api_format = (
api_format_enum.value if hasattr(api_format_enum, "value") else str(api_format_enum)
@@ -551,7 +550,7 @@ class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
.all()
)
grouped_candidates: Dict[str, List[RequestCandidate]] = {}
grouped_candidates: dict[str, list[RequestCandidate]] = {}
for candidate, api_format_enum in rows:
api_format = (
@@ -564,7 +563,7 @@ class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
grouped_candidates[api_format].append(candidate)
# 3. 为所有活跃格式生成监控数据
monitors: List[PublicApiFormatHealthMonitor] = []
monitors: list[PublicApiFormatHealthMonitor] = []
for api_format in all_formats:
candidates = grouped_candidates.get(api_format, [])
@@ -579,7 +578,7 @@ class PublicApiFormatHealthMonitorAdapter(PublicApiAdapter):
success_rate = success_count / actual_completed if actual_completed > 0 else 1.0
# 转换为公开版事件列表(不含敏感信息如 provider_id, key_id
events: List[PublicHealthEvent] = []
events: list[PublicHealthEvent] = []
for c in candidates:
event_time = c.finished_at or c.started_at or c.created_at
events.append(
@@ -649,8 +648,8 @@ class PublicGlobalModelsAdapter(PublicApiAdapter):
skip: int
limit: int
is_active: Optional[bool]
search: Optional[str]
is_active: bool | None
search: str | None
async def handle(self, context): # type: ignore[override]
db = context.db

View File

@@ -7,8 +7,6 @@
- Authorization: Bearer (bearer) -> OpenAI 格式
"""
from typing import Optional, Tuple, Union
from fastapi import APIRouter, Depends, Query, Request
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
@@ -35,7 +33,6 @@ from src.core.logger import logger
from src.database import get_db
from src.models.database import ApiKey, User
from src.services.auth.service import AuthService
from src.services.system.config import SystemConfigService
router = APIRouter(tags=["System Catalog"])
@@ -54,7 +51,7 @@ _ALL_CHAT_FORMATS = [
def _extract_api_key_from_request(
request: Request, definition: ApiFormatDefinition
) -> Optional[str]:
) -> str | None:
"""根据格式定义从请求中提取 API Key"""
auth_header = definition.auth_header.lower()
auth_type = definition.auth_type
@@ -76,7 +73,7 @@ def _extract_api_key_from_request(
return header_value
def _detect_api_format_and_key(request: Request) -> Tuple[str, Optional[str]]:
def _detect_api_format_and_key(request: Request) -> tuple[str, str | None]:
"""
根据请求头检测 API 格式并提取 API Key
@@ -163,7 +160,7 @@ def _build_empty_list_response(api_format: str) -> dict:
def _filter_formats_by_restrictions(
formats: list[str], restrictions: AccessRestrictions, api_format: str
) -> Tuple[list[str], Optional[dict]]:
) -> tuple[list[str], dict | None]:
"""
根据访问限制过滤 API 格式
@@ -182,7 +179,7 @@ def _filter_formats_by_restrictions(
return filtered, None
def _authenticate(db: Session, api_key: Optional[str]) -> Tuple[Optional[User], Optional[ApiKey]]:
def _authenticate(db: Session, api_key: str | None) -> tuple[User | None, ApiKey | None]:
"""
认证 API Key
@@ -248,8 +245,8 @@ def _build_auth_error_response(api_format: str) -> JSONResponse:
def _build_claude_list_response(
models: list[ModelInfo],
before_id: Optional[str],
after_id: Optional[str],
before_id: str | None,
after_id: str | None,
limit: int,
) -> dict:
"""构建 Claude 格式的列表响应"""
@@ -309,7 +306,7 @@ def _build_openai_list_response(models: list[ModelInfo]) -> dict:
def _build_gemini_list_response(
models: list[ModelInfo],
page_size: int,
page_token: Optional[str],
page_token: str | None,
) -> dict:
"""构建 Gemini 格式的列表响应"""
# 处理分页
@@ -435,14 +432,14 @@ def _build_404_response(model_id: str, api_format: str) -> JSONResponse:
async def list_models(
request: Request,
# Claude 分页参数
before_id: Optional[str] = Query(None, description="返回此 ID 之前的结果 (Claude)"),
after_id: Optional[str] = Query(None, description="返回此 ID 之后的结果 (Claude)"),
before_id: str | None = Query(None, description="返回此 ID 之前的结果 (Claude)"),
after_id: str | None = Query(None, description="返回此 ID 之后的结果 (Claude)"),
limit: int = Query(20, ge=1, le=1000, description="返回数量限制 (Claude)"),
# Gemini 分页参数
page_size: int = Query(50, alias="pageSize", ge=1, le=1000, description="每页数量 (Gemini)"),
page_token: Optional[str] = Query(None, alias="pageToken", description="分页 token (Gemini)"),
page_token: str | None = Query(None, alias="pageToken", description="分页 token (Gemini)"),
db: Session = Depends(get_db),
) -> Union[dict, JSONResponse]:
) -> dict | JSONResponse:
"""
列出可用模型(统一端点)
@@ -556,7 +553,7 @@ async def retrieve_model(
model_id: str,
request: Request,
db: Session = Depends(get_db),
) -> Union[dict, JSONResponse]:
) -> dict | JSONResponse:
"""
获取单个模型详情(统一端点)
@@ -658,9 +655,9 @@ async def retrieve_model(
async def list_models_gemini(
request: Request,
page_size: int = Query(50, alias="pageSize", ge=1, le=1000),
page_token: Optional[str] = Query(None, alias="pageToken"),
page_token: str | None = Query(None, alias="pageToken"),
db: Session = Depends(get_db),
) -> Union[dict, JSONResponse]:
) -> dict | JSONResponse:
"""
列出可用模型Gemini v1beta 专用端点)
@@ -741,7 +738,7 @@ async def get_model_gemini(
request: Request,
model_name: str,
db: Session = Depends(get_db),
) -> Union[dict, JSONResponse]:
) -> dict | JSONResponse:
"""
获取单个模型详情Gemini v1beta 专用端点)

View File

@@ -1,12 +1,11 @@
"""公开模块状态 API供登录页等使用"""
from typing import List
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from src.core.modules import ModuleCategory, get_module_registry
from src.core.modules import get_module_registry
from src.database import get_db
router = APIRouter(prefix="/api/modules", tags=["Modules"])
@@ -20,7 +19,7 @@ class AuthModuleInfo(BaseModel):
active: bool
@router.get("/auth-status", response_model=List[AuthModuleInfo])
@router.get("/auth-status", response_model=list[AuthModuleInfo])
async def get_auth_modules_status(db: Session = Depends(get_db)):
"""
获取认证模块状态(公开接口)

View File

@@ -5,7 +5,7 @@ System Catalog / 健康检查相关端点
"""
from datetime import datetime, timezone
from typing import Any, Dict, Optional
from typing import Any
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request
@@ -28,7 +28,7 @@ router = APIRouter(tags=["System Catalog"])
# ============== 辅助函数 ==============
def _as_bool(value: Optional[str], default: bool) -> bool:
def _as_bool(value: str | None, default: bool) -> bool:
"""将字符串转换为布尔值"""
if value is None:
return default
@@ -39,9 +39,9 @@ def _serialize_provider(
provider: Provider,
include_models: bool,
include_endpoints: bool,
) -> Dict[str, Any]:
) -> dict[str, Any]:
"""序列化 Provider 对象"""
provider_data: Dict[str, Any] = {
provider_data: dict[str, Any] = {
"id": provider.id,
"name": provider.name,
"is_active": provider.is_active,
@@ -81,7 +81,7 @@ def _serialize_provider(
return provider_data
def _select_provider(db: Session, provider_name: Optional[str]) -> Optional[Provider]:
def _select_provider(db: Session, provider_name: str | None) -> Provider | None:
"""选择 Provider按 provider_priority 优先级选择)"""
query = db.query(Provider).filter(Provider.is_active == True)
if provider_name:
@@ -104,7 +104,7 @@ async def service_health(db: Session = Depends(get_db)):
)
active_models = db.query(func.count(Model.id)).filter(Model.is_active == True).scalar() or 0
redis_info: Dict[str, Any] = {"status": "unknown"}
redis_info: dict[str, Any] = {"status": "unknown"}
try:
redis = await get_redis_client()
if redis:
@@ -245,9 +245,9 @@ async def provider_detail(
async def test_connection(
request: Request,
db: Session = Depends(get_db),
provider: Optional[str] = Query(None),
provider: str | None = Query(None),
model: str = Query("claude-3-haiku-20240307"),
api_format: Optional[str] = Query(None),
api_format: str | None = Query(None),
):
"""测试 Provider 连接"""
selected_provider = _select_provider(db, provider)