style: normalize python formatting gates

This commit is contained in:
Xiaoxia AI
2026-06-21 06:52:19 +08:00
parent 0809a079c5
commit bfbaddbd9a
129 changed files with 3024 additions and 2485 deletions
+31 -34
View File
@@ -5,17 +5,14 @@ Disabled because it depends on the removed DI container. Rebuild it around the
canonical JWT settings and SQLAlchemy-backed user repository before reuse.
"""
raise RuntimeError(
"apps.api.app.middleware.auth is disabled: rebuild auth dependency wiring before importing it"
)
raise RuntimeError("apps.api.app.middleware.auth is disabled: rebuild auth dependency wiring before importing it")
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from apps.api.app.dependencies import get_container
from packages.domain.auth import jwt_service
from packages.domain.entities import User
from apps.api.app.dependencies import get_container
security = HTTPBearer()
@@ -25,42 +22,42 @@ async def get_current_user(
) -> User:
"""
获取当前登录用户
从 Authorization header 中提取 JWT token 并验证
Raises:
HTTPException: Token 无效或过期
Returns:
当前用户对象
"""
token = credentials.credentials
try:
# 验证 token
payload = jwt_service.verify_token(token)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token: missing user_id",
headers={"WWW-Authenticate": "Bearer"},
)
# 从数据库获取用户
container = get_container()
user = container.user_repository.find_by_id(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
headers={"WWW-Authenticate": "Bearer"},
)
return user
except Exception as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -74,15 +71,15 @@ async def get_current_user_optional(
) -> User | None:
"""
获取当前登录用户(可选)
如果没有提供 token,返回 None 而不是抛出异常
Returns:
当前用户对象或 None
"""
if not credentials:
return None
try:
return await get_current_user(credentials)
except HTTPException:
@@ -92,78 +89,78 @@ async def get_current_user_optional(
def require_workspace_access(workspace_id: str, user: User = Depends(get_current_user)) -> tuple[str, str]:
"""
要求用户可以访问指定工作空间
Args:
workspace_id: 工作空间 ID
user: 当前用户
Raises:
HTTPException: 用户没有访问权限
Returns:
(workspace_id, user_role)
"""
container = get_container()
permission_checker = container.permission_checker
has_access, role = permission_checker.check_workspace_access(workspace_id, user.id)
if not has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have access to this workspace",
)
return workspace_id, role
def require_workspace_admin(workspace_id: str, user: User = Depends(get_current_user)) -> str:
"""
要求用户是工作空间的 Admin 或 Owner
Args:
workspace_id: 工作空间 ID
user: 当前用户
Raises:
HTTPException: 用户没有管理权限
Returns:
workspace_id
"""
container = get_container()
permission_checker = container.permission_checker
if not permission_checker.check_is_admin_or_owner(workspace_id, user.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only workspace owner or admin can perform this action",
)
return workspace_id
def require_workspace_owner(workspace_id: str, user: User = Depends(get_current_user)) -> str:
"""
要求用户是工作空间的 Owner
Args:
workspace_id: 工作空间 ID
user: 当前用户
Raises:
HTTPException: 用户不是 Owner
Returns:
workspace_id
"""
container = get_container()
permission_checker = container.permission_checker
if not permission_checker.check_is_owner(workspace_id, user.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only workspace owner can perform this action",
)
return workspace_id
+21 -17
View File
@@ -1,19 +1,21 @@
"""
全局异常处理和错误响应
"""
from fastapi import Request, status
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
import traceback
import logging
import traceback
from fastapi import Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
logger = logging.getLogger(__name__)
class APIException(Exception):
"""API 异常基类"""
def __init__(
self,
message: str,
@@ -28,7 +30,7 @@ class APIException(Exception):
class AuthenticationError(APIException):
"""认证错误"""
def __init__(self, message: str = "Authentication failed"):
super().__init__(
message=message,
@@ -39,7 +41,7 @@ class AuthenticationError(APIException):
class PermissionDeniedError(APIException):
"""权限拒绝"""
def __init__(self, message: str = "Permission denied"):
super().__init__(
message=message,
@@ -50,7 +52,7 @@ class PermissionDeniedError(APIException):
class ResourceNotFoundError(APIException):
"""资源不存在"""
def __init__(self, resource: str = "Resource"):
super().__init__(
message=f"{resource} not found",
@@ -61,7 +63,7 @@ class ResourceNotFoundError(APIException):
class ValidationError(APIException):
"""验证错误"""
def __init__(self, message: str):
super().__init__(
message=message,
@@ -100,12 +102,14 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
"""请求验证异常处理"""
errors = []
for error in exc.errors():
errors.append({
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
"type": error["type"],
})
errors.append(
{
"field": ".".join(str(loc) for loc in error["loc"]),
"message": error["msg"],
"type": error["type"],
}
)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
@@ -121,7 +125,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
async def general_exception_handler(request: Request, exc: Exception):
"""通用异常处理"""
logger.error(f"Unhandled exception: {exc}", exc_info=True)
# 生产环境不返回详细错误信息
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+23 -24
View File
@@ -1,8 +1,10 @@
"""
请求日志中间件
"""
import time
import logging
import time
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
@@ -11,58 +13,57 @@ logger = logging.getLogger(__name__)
class RequestLoggingMiddleware(BaseHTTPMiddleware):
"""请求日志中间件"""
async def dispatch(self, request: Request, call_next):
# 记录请求开始时间
start_time = time.time()
# 记录请求信息
logger.info(f"Request: {request.method} {request.url.path}")
# 处理请求
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
# 记录响应信息
logger.info(
f"Response: {request.method} {request.url.path} "
f"status={response.status_code} time={process_time:.3f}s"
f"Response: {request.method} {request.url.path} " f"status={response.status_code} time={process_time:.3f}s"
)
# 添加响应头
response.headers["X-Process-Time"] = str(process_time)
return response
class RateLimitMiddleware(BaseHTTPMiddleware):
"""简单的速率限制中间件(基于内存)"""
def __init__(self, app, max_requests: int = 100, window_seconds: int = 60):
super().__init__(app)
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # {ip: [(timestamp, ...)]}
async def dispatch(self, request: Request, call_next):
# 获取客户端 IP
client_ip = request.client.host
current_time = time.time()
# 清理过期记录
if client_ip in self.requests:
self.requests[client_ip] = [
ts for ts in self.requests[client_ip]
if current_time - ts < self.window_seconds
ts for ts in self.requests[client_ip] if current_time - ts < self.window_seconds
]
# 检查速率限制
request_count = len(self.requests.get(client_ip, []))
if request_count >= self.max_requests:
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=429,
content={
@@ -72,19 +73,17 @@ class RateLimitMiddleware(BaseHTTPMiddleware):
}
},
)
# 记录请求
if client_ip not in self.requests:
self.requests[client_ip] = []
self.requests[client_ip].append(current_time)
# 处理请求
response = await call_next(request)
# 添加速率限制信息到响应头
response.headers["X-RateLimit-Limit"] = str(self.max_requests)
response.headers["X-RateLimit-Remaining"] = str(
self.max_requests - len(self.requests[client_ip])
)
response.headers["X-RateLimit-Remaining"] = str(self.max_requests - len(self.requests[client_ip]))
return response
+28 -26
View File
@@ -1,9 +1,11 @@
"""
性能监控中间件
"""
import time
import logging
import time
from typing import Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
@@ -12,30 +14,30 @@ logger = logging.getLogger(__name__)
class PerformanceMonitoringMiddleware(BaseHTTPMiddleware):
"""性能监控中间件"""
def __init__(self, app, slow_request_threshold: float = 1.0):
super().__init__(app)
self.slow_request_threshold = slow_request_threshold # 慢请求阈值(秒)
async def dispatch(self, request: Request, call_next: Callable):
# 记录请求开始时间
start_time = time.time()
# 生成请求 ID
request_id = self._generate_request_id()
request.state.request_id = request_id
# 处理请求
try:
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
# 添加响应头
response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = f"{process_time:.3f}"
# 记录慢请求
if process_time > self.slow_request_threshold:
logger.warning(
@@ -43,55 +45,55 @@ class PerformanceMonitoringMiddleware(BaseHTTPMiddleware):
f"took {process_time:.3f}s (threshold: {self.slow_request_threshold}s) "
f"[request_id={request_id}]"
)
# 记录请求日志
logger.info(
f"{request.method} {request.url.path} "
f"status={response.status_code} time={process_time:.3f}s "
f"[request_id={request_id}]"
)
return response
except Exception as e:
process_time = time.time() - start_time
logger.error(
f"Request failed: {request.method} {request.url.path} "
f"error={str(e)} time={process_time:.3f}s "
f"[request_id={request_id}]",
exc_info=True
exc_info=True,
)
raise
def _generate_request_id(self) -> str:
"""生成请求 ID"""
import uuid
return str(uuid.uuid4())
class DatabaseQueryLogger:
"""数据库查询日志记录器"""
def __init__(self):
self.queries = []
self.total_time = 0
def log_query(self, query: str, params: tuple, duration: float):
"""记录查询"""
self.queries.append({
"query": query,
"params": params,
"duration": duration,
})
self.queries.append(
{
"query": query,
"params": params,
"duration": duration,
}
)
self.total_time += duration
# 记录慢查询(超过 100ms
if duration > 0.1:
logger.warning(
f"Slow query detected: {query[:100]}... "
f"took {duration:.3f}s with params {params}"
)
logger.warning(f"Slow query detected: {query[:100]}... " f"took {duration:.3f}s with params {params}")
def get_stats(self):
"""获取统计信息"""
return {
+21 -20
View File
@@ -1,14 +1,16 @@
"""
API 版本管理中间件
"""
from datetime import datetime
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from datetime import datetime
class APIVersionMiddleware(BaseHTTPMiddleware):
"""API 版本管理中间件"""
# 版本配置
VERSIONS = {
"v1": {
@@ -24,33 +26,31 @@ class APIVersionMiddleware(BaseHTTPMiddleware):
"release_date": None,
},
}
async def dispatch(self, request: Request, call_next):
# 提取版本号
version = self._extract_version(request.url.path)
# 处理请求
response = await call_next(request)
# 添加版本信息头
if version:
response.headers["X-API-Version"] = version
# 添加弃用警告
version_info = self.VERSIONS.get(version, {})
if version_info.get("deprecated"):
response.headers["X-API-Deprecated"] = "true"
sunset_date = version_info.get("sunset_date")
if sunset_date:
response.headers["X-API-Sunset-Date"] = sunset_date
response.headers["X-API-Deprecation-Info"] = (
f"https://docs.xiaoxia-saas.com/api/deprecation/{version}"
)
response.headers["X-API-Deprecation-Info"] = f"https://docs.xiaoxia-saas.com/api/deprecation/{version}"
return response
def _extract_version(self, path: str) -> str:
"""从路径中提取版本号"""
parts = path.split("/")
@@ -62,14 +62,15 @@ class APIVersionMiddleware(BaseHTTPMiddleware):
class VersionNotFoundMiddleware(BaseHTTPMiddleware):
"""处理已下线的 API 版本"""
SUNSET_VERSIONS = [] # 已下线的版本列表
async def dispatch(self, request: Request, call_next):
version = self._extract_version(request.url.path)
if version in self.SUNSET_VERSIONS:
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=410,
content={
@@ -77,13 +78,13 @@ class VersionNotFoundMiddleware(BaseHTTPMiddleware):
"code": "API_VERSION_SUNSET",
"message": f"API {version} has been sunset and is no longer available",
"sunset_date": "2028-07-01",
"migration_guide": f"https://docs.xiaoxia-saas.com/api/migration/{version}"
"migration_guide": f"https://docs.xiaoxia-saas.com/api/migration/{version}",
}
}
},
)
return await call_next(request)
def _extract_version(self, path: str) -> str:
"""从路径中提取版本号"""
parts = path.split("/")