diff --git a/apps/api/app/api/routes/_helpers.py b/apps/api/app/api/routes/_helpers.py index a17a2a6d3..a97dcb2c8 100755 --- a/apps/api/app/api/routes/_helpers.py +++ b/apps/api/app/api/routes/_helpers.py @@ -1,6 +1,6 @@ """路由层共享辅助函数 — 消除跨文件重复定义。""" -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from fastapi import HTTPException, status @@ -138,4 +138,4 @@ def format_utc_datetime(dt: datetime | None) -> str: return dt if dt.tzinfo is None: return dt.isoformat() + "Z" - return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + return dt.astimezone(UTC).isoformat().replace("+00:00", "Z") diff --git a/apps/api/app/api/routes/ai.py b/apps/api/app/api/routes/ai.py index 8373177b2..3de6c13b3 100755 --- a/apps/api/app/api/routes/ai.py +++ b/apps/api/app/api/routes/ai.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import List, Literal +from typing import Literal from app.services.ai_service import TITLE_STYLES, generate_smart_titles, semantic_match_assets from fastapi import APIRouter @@ -31,7 +31,7 @@ class GenerateTitlesRequest(BaseModel): class GenerateTitlesResponse(BaseModel): """智能标题生成响应.""" - titles: List[str] = Field(..., description="生成的标题列表") + titles: list[str] = Field(..., description="生成的标题列表") style: str = Field(..., description="实际使用的风格") source: str = Field(..., description="来源:doubao 或 fallback") description: str = Field(..., description="原始描述") @@ -53,7 +53,7 @@ class AssetMatchItem(BaseModel): id: str = Field(..., description="素材ID") name: str = Field(default="", description="素材名称") - tags: List[str] = Field(default_factory=list, description="标签列表") + tags: list[str] = Field(default_factory=list, description="标签列表") description: str = Field(default="", description="素材描述") @@ -61,7 +61,7 @@ class SemanticMatchRequest(BaseModel): """语义匹配请求.""" description: str = Field(..., min_length=1, max_length=500, description="目标视频内容描述") - assets: List[AssetMatchItem] = Field(..., min_length=1, max_length=100, description="待匹配素材列表") + assets: list[AssetMatchItem] = Field(..., min_length=1, max_length=100, description="待匹配素材列表") top_k: int = Field(default=0, ge=0, le=100, description="返回前K个,0返回全部") @@ -75,7 +75,7 @@ class SemanticMatchResultItem(AssetMatchItem): class SemanticMatchResponse(BaseModel): """语义匹配响应.""" - matches: List[SemanticMatchResultItem] = Field(..., description="按匹配度降序排列的素材列表") + matches: list[SemanticMatchResultItem] = Field(..., description="按匹配度降序排列的素材列表") source: str = Field(..., description="来源:doubao / fallback") description: str = Field(..., description="原始描述") total: int = Field(..., description="输入素材总数") @@ -99,7 +99,7 @@ def generate_titles(request: GenerateTitlesRequest): return GenerateTitlesResponse(**result) -@router.get("/titles/styles", response_model=List[TitleStyleInfo]) +@router.get("/titles/styles", response_model=list[TitleStyleInfo]) def list_title_styles(): """获取支持的标题风格列表.""" return [ diff --git a/apps/api/app/api/routes/ai_avatar_render.py b/apps/api/app/api/routes/ai_avatar_render.py index 3277ccb45..63f12ef6d 100644 --- a/apps/api/app/api/routes/ai_avatar_render.py +++ b/apps/api/app/api/routes/ai_avatar_render.py @@ -11,7 +11,7 @@ from __future__ import annotations import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_db_session @@ -82,7 +82,7 @@ def create_render_job( logger.exception("Celery 任务投递失败(创建): job_id=%s err=%s", job.id, exc) job.status = "failed" job.error_message = f"任务提交失败:{exc}" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) svc.db.commit() svc.db.refresh(job) return AiAvatarRenderJobResponse.model_validate(job) @@ -183,7 +183,7 @@ def retry_render_job( logger.exception("Celery 任务投递失败(重试): job_id=%s err=%s", job.id, exc) job.status = "failed" job.error_message = f"任务提交失败:{exc}" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) svc.db.commit() svc.db.refresh(job) return AiAvatarRenderJobResponse.model_validate(job) @@ -246,7 +246,7 @@ def generate_render_smart_cover( "url": cover_url, } job.output_cover_url = cover_url - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() except Exception as exc: logger.warning("更新 render_job 封面字段失败(不影响返回): job_id=%s err=%s", job_id, exc) diff --git a/apps/api/app/api/routes/assets.py b/apps/api/app/api/routes/assets.py index 89fd775b1..107a4dba9 100755 --- a/apps/api/app/api/routes/assets.py +++ b/apps/api/app/api/routes/assets.py @@ -1,5 +1,5 @@ import logging -from typing import Any, List, Optional +from typing import Any, Optional from app.api.routes._helpers import check_project_access, format_utc_datetime from app.auth import AuthenticatedUser, get_current_user @@ -390,7 +390,7 @@ def update_asset_review_status( return _to_asset_response(updated) -@router.post("/batch", response_model=List[AssetResponse]) +@router.post("/batch", response_model=list[AssetResponse]) def batch_get_assets( request: BatchGetRequest, authenticated_user: AuthenticatedUser = Depends(get_current_user), diff --git a/apps/api/app/api/routes/chunked_upload.py b/apps/api/app/api/routes/chunked_upload.py index 8407c842d..2cd4a50ab 100644 --- a/apps/api/app/api/routes/chunked_upload.py +++ b/apps/api/app/api/routes/chunked_upload.py @@ -8,7 +8,7 @@ import json import logging import shutil import tempfile -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any from uuid import uuid4 @@ -156,7 +156,7 @@ def _cleanup_expired_uploads() -> int: if not CHUNK_STORAGE_ROOT.exists(): return 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) cleaned = 0 for meta_file in CHUNK_STORAGE_ROOT.glob("*.meta.json"): @@ -166,7 +166,7 @@ def _cleanup_expired_uploads() -> int: expires_at = datetime.fromisoformat(meta["expires_at"]) if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) + expires_at = expires_at.replace(tzinfo=UTC) # Only cleanup uploads that are not actively being uploaded if expires_at < now and meta.get("status") != "uploading": @@ -226,7 +226,7 @@ async def init_chunked_upload( # Generate upload ID upload_id = uuid4().hex - now = datetime.now(timezone.utc) + now = datetime.now(UTC) expires_at = now + timedelta(hours=CHUNK_EXPIRY_HOURS) # Create chunk directory @@ -421,9 +421,9 @@ async def upload_chunk( # Check expiry expires_at = datetime.fromisoformat(meta["expires_at"]) if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) + expires_at = expires_at.replace(tzinfo=UTC) - if expires_at < datetime.now(timezone.utc): + if expires_at < datetime.now(UTC): raise HTTPException(status_code=status.HTTP_410_GONE, detail="Upload has expired") # Validate chunk index diff --git a/apps/api/app/api/routes/generation_cover.py b/apps/api/app/api/routes/generation_cover.py index 258c9b786..540aaefd2 100644 --- a/apps/api/app/api/routes/generation_cover.py +++ b/apps/api/app/api/routes/generation_cover.py @@ -11,7 +11,7 @@ from __future__ import annotations import ipaddress import logging import re -from typing import Any, List, Optional +from typing import Any, Optional from urllib.parse import urlparse from app.auth import AuthenticatedUser, get_current_user @@ -41,7 +41,7 @@ router = APIRouter(tags=["Generation"]) class GenerateCoverRequest(BaseModel): """AI 封面生成请求体""" - asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)") + asset_ids: list[str] = Field(default_factory=list, description="素材 ID 列表(确定视频来源)") cover_type: str = Field( default="ai_frame", description="封面类型: ai_frame / manual / upload / ai_regenerate", diff --git a/apps/api/app/api/routes/health.py b/apps/api/app/api/routes/health.py index f16465695..cd436df6a 100644 --- a/apps/api/app/api/routes/health.py +++ b/apps/api/app/api/routes/health.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime import psycopg import redis @@ -13,7 +13,7 @@ router = APIRouter(tags=["Health"]) async def health_check(): return { "status": "healthy", - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "version": settings.APP_VERSION, } @@ -33,7 +33,7 @@ async def startup_check(): all_ready = all(check["status"] == "healthy" for check in checks.values()) response = { "status": "started" if all_ready else "starting", - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "checks": checks, } if not all_ready: diff --git a/apps/api/app/api/routes/lipsync.py b/apps/api/app/api/routes/lipsync.py index dd4d4b125..ff3d84b6b 100644 --- a/apps/api/app/api/routes/lipsync.py +++ b/apps/api/app/api/routes/lipsync.py @@ -12,6 +12,7 @@ from __future__ import annotations import logging +from datetime import UTC from app.auth import AuthenticatedUser, get_current_user from app.dependencies import ( @@ -196,9 +197,8 @@ def get_lipsync_job( # 三层防御 ①:如果距上次更新超过 30 秒,同步刷新一次(避免 background task # 静默失败导致前端永远看到 running);否则挂后台异步刷新(避免阻塞轮询)。 from datetime import datetime as _dt - from datetime import timezone as _tz - _now = _dt.now(_tz.utc) + _now = _dt.now(UTC) _stale = job.updated_at is None or (_now - job.updated_at).total_seconds() > 30 if _stale: try: diff --git a/apps/api/app/api/routes/subscription.py b/apps/api/app/api/routes/subscription.py index ae7944a74..e66fac515 100755 --- a/apps/api/app/api/routes/subscription.py +++ b/apps/api/app/api/routes/subscription.py @@ -4,8 +4,7 @@ from __future__ import annotations import logging from dataclasses import replace -from datetime import datetime, timezone -from typing import List +from datetime import UTC, datetime from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_user_repository @@ -67,7 +66,7 @@ def _get_plan_price(plan_id: str, billing_cycle: str) -> float: def _build_subscription_info(user: AuthenticatedUser) -> SubscriptionInfo: """构建订阅信息响应""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if user.user.subscription_expires_at: period_end = user.user.subscription_expires_at.isoformat() period_start = now.isoformat() @@ -100,10 +99,10 @@ async def get_current_subscription( return _build_subscription_info(current_user) -@router.get("/billing-records", response_model=List[BillingRecord]) +@router.get("/billing-records", response_model=list[BillingRecord]) async def get_billing_records( current_user: AuthenticatedUser = Depends(get_current_user), -) -> List[BillingRecord]: +) -> list[BillingRecord]: """获取账单记录列表""" from packages.adapters.sqlalchemy_impl.billing_repository import SQLAlchemyBillingRepository from packages.adapters.sqlalchemy_impl.session import SessionLocal @@ -251,7 +250,7 @@ async def payment_callback( # 计算到期时间 days = 365 if billing_cycle == "yearly" else 30 - expires_at = datetime.now(timezone.utc) + timedelta(days=days) + expires_at = datetime.now(UTC) + timedelta(days=days) repo.update_subscription_on_payment(user_id, plan, expires_at) return {"success": True, "message": "支付成功", "record_id": record_id} diff --git a/apps/api/app/api/routes/templates_editor/schemas.py b/apps/api/app/api/routes/templates_editor/schemas.py index 516d49cd6..a8ac79589 100755 --- a/apps/api/app/api/routes/templates_editor/schemas.py +++ b/apps/api/app/api/routes/templates_editor/schemas.py @@ -6,7 +6,7 @@ from __future__ import annotations import re as _re -from typing import Any, List, Optional +from typing import Any, Optional from pydantic import BaseModel, Field, model_validator, validator @@ -21,7 +21,7 @@ _EXPORT_VALID_FORMATS = {"mp4", "mov"} class AIRecommendRequest(BaseModel): """AI 推荐片段方案请求体""" - asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表") + asset_ids: list[str] = Field(default_factory=list, description="素材 ID 列表") editing_mode: str = Field(default="one_take", description="剪辑模式: one_take / pip / voice_over / voice_pip") target_duration: float = Field(default=30.0, ge=1.0, le=600.0, description="目标时长(秒)") @@ -44,7 +44,7 @@ class AIRecommendResponse(BaseModel): """AI 推荐片段方案响应体""" plan_id: str = Field(..., description="剪辑计划 ID") - clips: List[AIRecommendClipItem] = Field(..., description="推荐的片段列表") + clips: list[AIRecommendClipItem] = Field(..., description="推荐的片段列表") config: dict[str, Any] = Field(..., description="推荐的 plan config(cover/title/subtitle/bgm)") total_duration: float = Field(..., ge=0.0, description="推荐方案总时长(秒)") confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)") @@ -137,7 +137,7 @@ class ClipReorderItem(BaseModel): class ClipReorderRequest(BaseModel): """片段重排序请求""" - items: List[ClipReorderItem] = Field(..., min_length=1, max_length=500, description="重排序条目列表") + items: list[ClipReorderItem] = Field(..., min_length=1, max_length=500, description="重排序条目列表") class ClipReorderResponse(BaseModel): @@ -151,7 +151,7 @@ class ClipReorderResponse(BaseModel): class ClipBatchDeleteRequest(BaseModel): """批量删除片段请求""" - clip_ids: List[str] = Field(..., min_length=1, max_length=500, description="要删除的片段ID列表") + clip_ids: list[str] = Field(..., min_length=1, max_length=500, description="要删除的片段ID列表") class ClipBatchDeleteResponse(BaseModel): @@ -169,7 +169,7 @@ _UNSET = object() class ClipsFromAssetsRequest(BaseModel): """从素材批量创建片段请求""" - asset_ids: List[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾") + asset_ids: list[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾") clip_type: str = Field(default="main", description="片段类型,默认 main") clip_count: Optional[int] = Field( default=None, @@ -218,7 +218,7 @@ class ClipsFromAssetsResponse(BaseModel): created_count: int plan_id: str = "" message: str = "" - clip_ids: List[str] = Field(default_factory=list, description="创建的片段ID列表") + clip_ids: list[str] = Field(default_factory=list, description="创建的片段ID列表") duplicate_warning: Optional[str] = Field(default=None, description="查重率超标警告") exhaustion_warning: Optional[str] = Field(default=None, description="素材耗尽警告") @@ -302,7 +302,7 @@ class ExportPresetItem(BaseModel): class ExportPresetListResponse(BaseModel): """导出预设列表响应""" - items: List[ExportPresetItem] + items: list[ExportPresetItem] total: int @@ -316,7 +316,7 @@ class FilterPresetResponse(BaseModel): name: str category: str description: str - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) class FilterConfigResponse(BaseModel): @@ -346,7 +346,7 @@ class FilterUpdateRequest(BaseModel): class FilterPresetListResponse(BaseModel): """滤镜预设列表响应""" - items: List[FilterPresetResponse] + items: list[FilterPresetResponse] total: int @@ -360,7 +360,7 @@ class TransitionPresetResponse(BaseModel): name: str category: str description: str - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) default_duration: float min_duration: float max_duration: float @@ -402,7 +402,7 @@ class BatchTransitionResponse(BaseModel): class TransitionPresetListResponse(BaseModel): """转场预设列表响应""" - items: List[TransitionPresetResponse] + items: list[TransitionPresetResponse] total: int @@ -458,7 +458,7 @@ class EditorClipResponse(BaseModel): class EditorClipListResponse(BaseModel): """片段列表响应""" - items: List[EditorClipResponse] + items: list[EditorClipResponse] total: int @@ -496,7 +496,7 @@ class EditorClipBatchItem(BaseModel): class EditorClipBatchUpdateRequest(BaseModel): """批量替换clips请求(全量覆盖)""" - clips: List[EditorClipBatchItem] = Field(default_factory=list) + clips: list[EditorClipBatchItem] = Field(default_factory=list) class EditorClipBatchUpdateResponse(BaseModel): @@ -584,4 +584,4 @@ class EditorTimelineResponse(BaseModel): plan_id: str total_duration: float - scenes: List[EditorTimelineSceneResponse] + scenes: list[EditorTimelineSceneResponse] diff --git a/apps/api/app/dependencies.py b/apps/api/app/dependencies.py index 682d62699..ce622df26 100644 --- a/apps/api/app/dependencies.py +++ b/apps/api/app/dependencies.py @@ -6,7 +6,7 @@ ensuring proper lifecycle management and testability. from __future__ import annotations -from typing import Generator +from collections.abc import Generator import redis from app.config import settings diff --git a/apps/api/app/middleware/monitoring.py b/apps/api/app/middleware/monitoring.py index 69a64a43d..f0a794af7 100644 --- a/apps/api/app/middleware/monitoring.py +++ b/apps/api/app/middleware/monitoring.py @@ -4,7 +4,7 @@ import logging import time -from typing import Callable +from collections.abc import Callable from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware diff --git a/apps/api/app/middleware/prometheus_metrics.py b/apps/api/app/middleware/prometheus_metrics.py index 18601209a..736e04fea 100644 --- a/apps/api/app/middleware/prometheus_metrics.py +++ b/apps/api/app/middleware/prometheus_metrics.py @@ -10,7 +10,7 @@ Exposes: import re import time -from typing import Callable +from collections.abc import Callable from fastapi import Request, Response from prometheus_client import ( diff --git a/apps/api/app/schemas/script.py b/apps/api/app/schemas/script.py index fb06738c8..bd9b5b4b3 100644 --- a/apps/api/app/schemas/script.py +++ b/apps/api/app/schemas/script.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -20,8 +20,8 @@ class ScriptResponse(BaseModel): user_id: str title: str content: str - segments: List[ScriptSegment] = Field(default_factory=list) - tags: List[str] = Field(default_factory=list) + segments: list[ScriptSegment] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) created_at: datetime updated_at: datetime @@ -34,12 +34,12 @@ class ScriptListResponse(BaseModel): class CreateScriptRequest(BaseModel): title: str = Field(..., min_length=1, max_length=255) content: str = "" - segments: List[ScriptSegment] = Field(default_factory=list) - tags: List[str] = Field(default_factory=list) + segments: list[ScriptSegment] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) class UpdateScriptRequest(BaseModel): title: Optional[str] = Field(None, min_length=1, max_length=255) content: Optional[str] = None - segments: Optional[List[ScriptSegment]] = None - tags: Optional[List[str]] = None + segments: Optional[list[ScriptSegment]] = None + tags: Optional[list[str]] = None diff --git a/apps/api/app/schemas/template.py b/apps/api/app/schemas/template.py index 903cd632a..dd10d493a 100755 --- a/apps/api/app/schemas/template.py +++ b/apps/api/app/schemas/template.py @@ -8,7 +8,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Optional from pydantic import BaseModel, Field @@ -42,12 +42,12 @@ class TemplateResponse(BaseModel): name: str mode: str category: str = "" - tags: List[str] = Field(default_factory=list) - title_config: Dict[str, Any] = Field(default_factory=dict) - subtitle_config: Dict[str, Any] = Field(default_factory=dict) - bgm_config: Dict[str, Any] = Field(default_factory=dict) + tags: list[str] = Field(default_factory=list) + title_config: dict[str, Any] = Field(default_factory=dict) + subtitle_config: dict[str, Any] = Field(default_factory=dict) + bgm_config: dict[str, Any] = Field(default_factory=dict) estimated_duration: float = 0.0 - segments: List[SegmentResponse] = Field(default_factory=list) + segments: list[SegmentResponse] = Field(default_factory=list) is_active: bool = True is_favorite: bool = False usage_count: int = 0 @@ -56,7 +56,7 @@ class TemplateResponse(BaseModel): class ListTemplatesResponse(BaseModel): - items: List[TemplateResponse] + items: list[TemplateResponse] total: int = 0 @@ -67,12 +67,12 @@ class CreateTemplateRequest(BaseModel): name: str mode: str category: str = "" - tags: List[str] = Field(default_factory=list) - title_config: Dict[str, Any] = Field(default_factory=dict) - subtitle_config: Dict[str, Any] = Field(default_factory=dict) - bgm_config: Dict[str, Any] = Field(default_factory=dict) + tags: list[str] = Field(default_factory=list) + title_config: dict[str, Any] = Field(default_factory=dict) + subtitle_config: dict[str, Any] = Field(default_factory=dict) + bgm_config: dict[str, Any] = Field(default_factory=dict) estimated_duration: float = 0.0 - segments: List[SegmentRequest] = Field(default_factory=list) + segments: list[SegmentRequest] = Field(default_factory=list) class GenerateWarningResponse(BaseModel): @@ -80,4 +80,4 @@ class GenerateWarningResponse(BaseModel): code: str message: str - details: Dict[str, Any] = Field(default_factory=dict) + details: dict[str, Any] = Field(default_factory=dict) diff --git a/apps/api/app/schemas/title_library.py b/apps/api/app/schemas/title_library.py index b955d1169..37e197ff5 100644 --- a/apps/api/app/schemas/title_library.py +++ b/apps/api/app/schemas/title_library.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -15,7 +15,7 @@ class TitleLibraryItemResponse(BaseModel): text: str category: str = "default" description: str = "" - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) usage_count: int = 0 is_active: bool = True created_at: datetime @@ -32,7 +32,7 @@ class CreateTitleLibraryRequest(BaseModel): text: str = Field(..., min_length=1, max_length=500) category: str = "default" description: str = "" - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) class UpdateTitleLibraryRequest(BaseModel): @@ -40,4 +40,4 @@ class UpdateTitleLibraryRequest(BaseModel): text: Optional[str] = Field(None, min_length=1, max_length=500) category: Optional[str] = None description: Optional[str] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None diff --git a/apps/api/app/schemas/tts.py b/apps/api/app/schemas/tts.py index 1dcf3e5f4..39dd4f380 100644 --- a/apps/api/app/schemas/tts.py +++ b/apps/api/app/schemas/tts.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Optional from pydantic import BaseModel, Field @@ -23,7 +23,7 @@ class TTSSynthesizeRequest(BaseModel): voice_model: str = Field("", description="语音模型名称") voice_clone_profile_id: str = Field("", description="关联的音色克隆档案 ID") format: str = Field("mp3", description="输出格式(mp3/wav/pcm)") - metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") + metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") class Config: populate_by_name = True @@ -49,7 +49,7 @@ class TTSJobResponse(BaseModel): error_message: str = "" retry_count: int = 0 max_retries: int = 3 - metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") + metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") started_at: Optional[datetime] = None completed_at: Optional[datetime] = None created_at: datetime @@ -83,7 +83,7 @@ class TTSSynthesizeResponse(BaseModel): class ListTTSJobResponse(BaseModel): """TTS 任务列表响应。""" - items: List[TTSJobResponse] + items: list[TTSJobResponse] total: int page: int page_size: int diff --git a/apps/api/app/schemas/video_share.py b/apps/api/app/schemas/video_share.py index df0590422..e8dc51251 100755 --- a/apps/api/app/schemas/video_share.py +++ b/apps/api/app/schemas/video_share.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -61,7 +61,7 @@ class ShareResponse(BaseModel): class ShareListResponse(BaseModel): """分享列表响应.""" - items: List[ShareResponse] + items: list[ShareResponse] total: int = 0 skip: int = 0 limit: int = 20 diff --git a/apps/api/app/schemas/voice.py b/apps/api/app/schemas/voice.py index 41d4424c1..38bf5382d 100644 --- a/apps/api/app/schemas/voice.py +++ b/apps/api/app/schemas/voice.py @@ -6,7 +6,7 @@ from __future__ import annotations from datetime import datetime -from typing import List, Literal, Optional +from typing import Literal, Optional from pydantic import BaseModel, Field @@ -56,7 +56,7 @@ class UnifiedVoiceItemResponse(BaseModel): status: str = "completed" """状态""" - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) """标签列表""" # 克隆音色特有字段 @@ -113,7 +113,7 @@ class PresetVoiceItemResponse(BaseModel): preview_url: str = "" """预览音频 URL""" - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) """标签列表""" diff --git a/apps/api/app/schemas/voice_clone.py b/apps/api/app/schemas/voice_clone.py index 261fbfaf2..e9d82a802 100755 --- a/apps/api/app/schemas/voice_clone.py +++ b/apps/api/app/schemas/voice_clone.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Optional from pydantic import BaseModel, Field @@ -19,7 +19,7 @@ class CreateVoiceCloneRequest(BaseModel): language: str = Field("zh-CN", description="语言") gender: str = Field("unknown", description="性别") max_retries: int = Field(3, ge=1, le=10, description="最大重试次数") - metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") + metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") class Config: populate_by_name = True @@ -41,7 +41,7 @@ class VoiceCloneProfileResponse(BaseModel): error_message: str = "" retry_count: int = 0 max_retries: int = 3 - metadata_: Optional[Dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") + metadata_: Optional[dict[str, Any]] = Field(default=None, alias="metadata", description="额外元数据") created_at: datetime updated_at: datetime @@ -62,7 +62,7 @@ class VoiceCloneStatusResponse(BaseModel): class ListVoiceCloneResponse(BaseModel): """音色克隆列表响应。""" - items: List[VoiceCloneProfileResponse] + items: list[VoiceCloneProfileResponse] total: int diff --git a/apps/api/app/schemas/voice_library.py b/apps/api/app/schemas/voice_library.py index b21ea4362..9d3c25be3 100644 --- a/apps/api/app/schemas/voice_library.py +++ b/apps/api/app/schemas/voice_library.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -21,7 +21,7 @@ class VoiceLibraryItemResponse(BaseModel): file_size: int = 0 status: str = "completed" project_id: Optional[str] = None - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) created_at: datetime updated_at: datetime @@ -42,7 +42,7 @@ class CreateVoiceLibraryRequest(BaseModel): file_size: int = 0 status: str = "completed" project_id: Optional[str] = None - tags: List[str] = Field(default_factory=list) + tags: list[str] = Field(default_factory=list) class UpdateVoiceLibraryRequest(BaseModel): @@ -55,4 +55,4 @@ class UpdateVoiceLibraryRequest(BaseModel): duration: Optional[float] = None file_size: Optional[int] = None status: Optional[str] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None diff --git a/apps/api/app/services/ai_avatar_render_service.py b/apps/api/app/services/ai_avatar_render_service.py index ca6965b0c..0e84914e5 100644 --- a/apps/api/app/services/ai_avatar_render_service.py +++ b/apps/api/app/services/ai_avatar_render_service.py @@ -16,7 +16,7 @@ import os import subprocess import tempfile import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, Optional from sqlalchemy.orm import Session @@ -117,7 +117,7 @@ class AiAvatarRenderService: self.db.add(job) self.db.flush() - job.submitted_at = datetime.now(timezone.utc) + job.submitted_at = datetime.now(UTC) self.db.commit() self.db.refresh(job) return job @@ -164,7 +164,7 @@ class AiAvatarRenderService: return None if job.status in ("pending", "submitted"): job.status = "cancelled" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() self.db.refresh(job) return job @@ -186,7 +186,7 @@ class AiAvatarRenderService: job.output_duration = 0.0 job.started_at = None job.completed_at = None - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() self.db.refresh(job) return job @@ -215,9 +215,9 @@ class AiAvatarRenderService: try: # 更新状态为 processing job.status = "processing" - job.started_at = datetime.now(timezone.utc) + job.started_at = datetime.now(UTC) job.progress = 5 - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() # 获取对口型任务信息 @@ -394,8 +394,8 @@ class AiAvatarRenderService: # 6. 完成 job.status = "completed" job.progress = 100 - job.completed_at = datetime.now(timezone.utc) - job.updated_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) + job.updated_at = datetime.now(UTC) self.db.commit() logger.info("渲染任务完成: %s", job_id) @@ -406,14 +406,14 @@ class AiAvatarRenderService: except AiAvatarRenderError as exc: job.status = "failed" job.error_message = str(exc) - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() logger.error("渲染任务失败 [%s]: %s", job_id, exc) raise except Exception as exc: job.status = "failed" job.error_message = f"渲染异常: {str(exc)}" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() logger.exception("渲染任务异常 [%s]", job_id) raise @@ -494,7 +494,7 @@ class AiAvatarRenderService: # 同步更新 cover_config,保持 smart-cover 路径一致 if isinstance(job.cover_config, dict): job.cover_config = {**job.cover_config, "mode": "auto_frame", "url": cover_url.strip()} - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() return self._persist_to_library(job, cover_url=cover_url) diff --git a/apps/api/app/services/ai_service.py b/apps/api/app/services/ai_service.py index 6e1291a11..e25c72b9a 100755 --- a/apps/api/app/services/ai_service.py +++ b/apps/api/app/services/ai_service.py @@ -13,7 +13,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, List, Optional +from typing import Any, Optional from packages.domain.ai_parsing import generate_titles_fallback as _generate_titles_fallback_base from packages.domain.ai_parsing import keyword_match_fallback as _semantic_match_fallback_base @@ -64,7 +64,7 @@ def _generate_titles_fallback( description: str, style: str = "viral", count: int = 5, -) -> List[str]: +) -> list[str]: """本地降级:基于模板规则生成标题(薄包装,转发到 ai_parsing 模块).""" style_info = TITLE_STYLES.get(style, TITLE_STYLES["viral"]) return _generate_titles_fallback_base(description, style_info, count) @@ -74,7 +74,7 @@ def generate_smart_titles( description: str, style: str = "viral", count: int = 5, -) -> Dict[str, Any]: +) -> dict[str, Any]: """生成智能标题. Args: @@ -164,16 +164,16 @@ def generate_smart_titles( def _semantic_match_fallback( description: str, - assets: List[Dict[str, Any]], -) -> List[Dict[str, Any]]: + assets: list[dict[str, Any]], +) -> list[dict[str, Any]]: """本地降级:基于关键词的简单匹配(薄包装,转发到 ai_parsing 模块).""" return _semantic_match_fallback_base(description, assets) def _parse_semantic_match_response( content: str, - asset_ids: List[str], -) -> Optional[Dict[str, float]]: + asset_ids: list[str], +) -> Optional[dict[str, float]]: """从模型返回中解析素材匹配度(薄包装,转发到 ai_parsing 模块).""" result = _parse_semantic_match_base(content, asset_ids) if result is None: @@ -183,9 +183,9 @@ def _parse_semantic_match_response( def semantic_match_assets( description: str, - assets: List[Dict[str, Any]], + assets: list[dict[str, Any]], top_k: int = 0, -) -> Dict[str, Any]: +) -> dict[str, Any]: """智能素材语义匹配. 根据用户描述,评估每个素材的语义匹配度并排序。 @@ -336,13 +336,13 @@ class AIService: description: str, style: str = "viral", count: int = 5, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: return generate_smart_titles(description, style, count) def semantic_match( self, description: str, - assets: List[Dict[str, Any]], + assets: list[dict[str, Any]], top_k: int = 0, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: return semantic_match_assets(description, assets, top_k) diff --git a/apps/api/app/services/asset_segment_tracker.py b/apps/api/app/services/asset_segment_tracker.py index c9122bb02..07af35daa 100644 --- a/apps/api/app/services/asset_segment_tracker.py +++ b/apps/api/app/services/asset_segment_tracker.py @@ -28,8 +28,8 @@ from __future__ import annotations import json import logging -from datetime import datetime, timezone -from typing import Callable +from collections.abc import Callable +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -57,7 +57,7 @@ _REUSE_OVERLAP_RATIO = 0.6 def _now_iso() -> str: - return datetime.now(timezone.utc).isoformat() + return datetime.now(UTC).isoformat() def _read_meta(model) -> dict: @@ -156,7 +156,7 @@ def record_used_segments( r["plan_id"] = plan_id meta[USED_RANGES_KEY] = ranges model.classification_result = json.dumps(meta, ensure_ascii=False) - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) return ranges.append( @@ -171,7 +171,7 @@ def record_used_segments( ) meta[USED_RANGES_KEY] = ranges model.classification_result = json.dumps(meta, ensure_ascii=False) - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) def remove_used_segment( @@ -215,7 +215,7 @@ def remove_used_segment( if removed: meta[USED_RANGES_KEY] = remaining model.classification_result = json.dumps(meta, ensure_ascii=False) - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) return removed @@ -231,7 +231,7 @@ def reset_used_segments(db: Session, asset_id: str) -> None: if meta.get(USED_RANGES_KEY): meta[USED_RANGES_KEY] = [] model.classification_result = json.dumps(meta, ensure_ascii=False) - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) logger.info("[片段追踪] 素材区间记录手动清空: asset_id=%s", asset_id) diff --git a/apps/api/app/services/edit_plan_service.py b/apps/api/app/services/edit_plan_service.py index f9f752404..24944060a 100755 --- a/apps/api/app/services/edit_plan_service.py +++ b/apps/api/app/services/edit_plan_service.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging -from typing import Any, Dict, List, Optional +from typing import Any, Optional from app.services.asset_segment_tracker import ( REUSE_RATIO_LIMIT, @@ -206,7 +206,7 @@ class EditPlanService: status: Optional[EditPlanClipStatus] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: """列出计划的片段""" # 确保计划存在 self.get_plan_or_raise(plan_id) @@ -503,7 +503,7 @@ class EditPlanService: source = self.get_plan_or_raise(source_plan_id) # 分页读取源 plan 全部片段 - clips: List[EditPlanClip] = [] + clips: list[EditPlanClip] = [] skip, page = 0, 500 while True: batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page) @@ -665,7 +665,7 @@ class EditPlanService: source = self.get_plan_or_raise(source_plan_id) # 分页读取源 plan 全部片段 - clips: List[EditPlanClip] = [] + clips: list[EditPlanClip] = [] skip, page = 0, 500 while True: batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page) @@ -797,7 +797,7 @@ class EditPlanService: except Exception: pass - clips: List[EditPlanClip] = [] + clips: list[EditPlanClip] = [] skip, page = 0, 500 while True: batch = self._clip_repo.list_by_plan(plan_id, skip=skip, limit=page) @@ -1063,7 +1063,7 @@ class EditPlanService: # ── 片段分割与合并 ────────────────────────────────────────────────────── - def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]: + def split_clip(self, clip_id: str, split_time: float) -> dict[str, Any]: """将一个片段从指定位置分割为两个片段 Args: @@ -1151,7 +1151,7 @@ class EditPlanService: "right_clip": created_right, } - def merge_clips(self, clip_ids: List[str]) -> EditPlanClip: + def merge_clips(self, clip_ids: list[str]) -> EditPlanClip: """合并多个连续片段为一个片段 Args: @@ -1217,7 +1217,7 @@ class EditPlanService: # ── 渲染生成流程 ──────────────────────────────────────────────────────── - def get_generation_status(self, plan_id: str) -> Dict[str, Any]: + def get_generation_status(self, plan_id: str) -> dict[str, Any]: """获取渲染进度状态 Returns: @@ -1364,7 +1364,7 @@ class EditPlanService: ) return count - def update_plan_config(self, plan_id: str, config_updates: Dict[str, Any]) -> EditPlan: + def update_plan_config(self, plan_id: str, config_updates: dict[str, Any]) -> EditPlan: """更新计划配置(合并更新) Args: diff --git a/apps/api/app/services/edit_template_service.py b/apps/api/app/services/edit_template_service.py index fe3057c50..cb607ec66 100755 --- a/apps/api/app/services/edit_template_service.py +++ b/apps/api/app/services/edit_template_service.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging -from typing import Any, List, Optional +from typing import Any, Optional from sqlalchemy.orm import Session @@ -76,7 +76,7 @@ class EditTemplateService: active_only: bool = False, skip: int = 0, limit: int = 50, - ) -> List[EditTemplate]: + ) -> list[EditTemplate]: """列出模板 Args: @@ -227,7 +227,7 @@ class EditTemplateService: clip_type: Optional[ClipType] = None, skip: int = 0, limit: int = 100, - ) -> List[TemplateClipConfig]: + ) -> list[TemplateClipConfig]: """列出模板的片段配置 注意:本方法要求模板存在于新表 ``edit_templates``(全局模板库), @@ -253,7 +253,7 @@ class EditTemplateService: clip_type: Optional[ClipType] = None, skip: int = 0, limit: int = 100, - ) -> List[TemplateClipConfig]: + ) -> list[TemplateClipConfig]: """编辑器读取模板片段配置的单一数据源入口. 片段配置主表是 ``template_clip_configs``(直接读取,不抛异常、不降级)。 @@ -404,8 +404,8 @@ class EditTemplateService: def reorder_clip_configs( self, template_id: str, - config_ids: List[str], - ) -> List[TemplateClipConfig]: + config_ids: list[str], + ) -> list[TemplateClipConfig]: """重新排序片段配置 Args: @@ -560,7 +560,7 @@ class EditTemplateService: ) # 5. 转换每个片段为模板片段配置 - created_configs: List[TemplateClipConfig] = [] + created_configs: list[TemplateClipConfig] = [] for clip_config_obj in clips_to_template_clip_configs(created_template.id, clips): created = self._clip_config_repo.create(clip_config_obj) created_configs.append(created) diff --git a/apps/api/app/services/lipsync_service.py b/apps/api/app/services/lipsync_service.py index ea9176b66..9ea698354 100644 --- a/apps/api/app/services/lipsync_service.py +++ b/apps/api/app/services/lipsync_service.py @@ -17,7 +17,7 @@ from __future__ import annotations import io import logging import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from urllib.parse import urlparse @@ -229,7 +229,7 @@ class LipsyncService: ) job.mediakit_task_id = result["task_id"] job.status = "submitted" - job.submitted_at = datetime.now(timezone.utc) + job.submitted_at = datetime.now(UTC) self.db.commit() logger.info( "[lipsync] 直传音频已提交 MediaKit: job_id=%s task_id=%s", @@ -338,7 +338,7 @@ class LipsyncService: job.status = "failed" job.error_message = f"Celery 任务投递失败: {exc}" job.error_code = "AsyncDispatchFailed" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() else: # 2b/2c. 直接音频 / 预合成音频:同步路径 @@ -498,8 +498,8 @@ class LipsyncService: temp_url = result.get("video_url", "") job.output_video_url = temp_url job.output_duration = result.get("duration", 0.0) - job.completed_at = datetime.now(timezone.utc) - job.updated_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) + job.updated_at = datetime.now(UTC) self.db.commit() # 异步转存自家 OSS try: @@ -517,12 +517,12 @@ class LipsyncService: job.status = "failed" job.error_message = error.get("message", "任务执行失败") job.error_code = error.get("code", "TaskFailed") - job.completed_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) else: # 中间状态(running/processing/queued 等)同步到 DB,避免前端永远卡在 submitted if isinstance(mk_status, str) and mk_status: job.status = mk_status - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() except Exception as exc: # noqa: BLE001 - DB 提交失败必须记录日志并重试,否则后台任务静默失败 logger.error( @@ -589,7 +589,7 @@ class LipsyncService: if job.status in ("pending", "tts_processing", "submitted"): job.status = "cancelled" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) self.db.commit() self.db.refresh(job) diff --git a/apps/api/app/services/plan_generator_service.py b/apps/api/app/services/plan_generator_service.py index 619c8abd0..04418ae2b 100755 --- a/apps/api/app/services/plan_generator_service.py +++ b/apps/api/app/services/plan_generator_service.py @@ -14,7 +14,7 @@ from __future__ import annotations import logging import random -from typing import Any, List +from typing import Any from sqlalchemy.orm import Session @@ -62,8 +62,8 @@ class PlanGeneratorService: def generate_from_template( self, template: EditTemplate, - clip_configs: List[TemplateClipConfig], - asset_ids: List[str], + clip_configs: list[TemplateClipConfig], + asset_ids: list[str], *, project_id: str = "", created_by_user_id: str = "", @@ -135,7 +135,7 @@ class PlanGeneratorService: ) # 5. 持久化所有 clips 并计算总时长 - created_clips: List[EditPlanClip] = [] + created_clips: list[EditPlanClip] = [] total_duration = 0.0 for clip in clips: saved = self._clip_repo.create(clip) @@ -184,15 +184,15 @@ class PlanGeneratorService: def _create_clips_from_configs( self, plan_id: str, - clip_configs: List[TemplateClipConfig], - ) -> List[EditPlanClip]: + clip_configs: list[TemplateClipConfig], + ) -> list[EditPlanClip]: """从 TemplateClipConfig 列表创建 EditPlanClip 列表(未持久化). 委托给 plan_generator_utils.create_clips_from_configs 纯函数。 """ return create_clips_from_configs(plan_id, clip_configs) - def _map_clip_types_for_mode(self, clips: List[EditPlanClip], editing_mode: str) -> None: + def _map_clip_types_for_mode(self, clips: list[EditPlanClip], editing_mode: str) -> None: """将 MAIN 类型片段按 editing_mode 映射为对应角色类型. 委托给 plan_generator_utils.map_clip_types_for_mode 纯函数。 @@ -204,7 +204,7 @@ class PlanGeneratorService: plan_id: str, editing_mode: str, asset_count: int, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: """无 clip_configs 时,根据 editing_mode 生成默认 clip 结构. 委托给 plan_generator_utils.generate_default_clips 纯函数。 @@ -213,8 +213,8 @@ class PlanGeneratorService: def _distribute_assets( self, - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], editing_mode: str, *, random_selection: bool = False, @@ -259,7 +259,7 @@ class PlanGeneratorService: external_used_segments=external_used_segments, ) - def _fetch_asset_scene_points(self, asset_ids: List[str]) -> dict[str, list[float]]: + def _fetch_asset_scene_points(self, asset_ids: list[str]) -> dict[str, list[float]]: """从素材 metadata 读取场景切换点缓存(无缓存的素材不包含在结果中)。""" points_map: dict[str, list[float]] = {} if not self._asset_repo: @@ -272,7 +272,7 @@ class PlanGeneratorService: points_map[asset_id] = points return points_map - def _sort_assets_by_smart_score(self, asset_ids: List[str]) -> List[str]: + def _sort_assets_by_smart_score(self, asset_ids: list[str]) -> list[str]: """按 smart_match 综合评分降序排列素材 ID(注入随机噪声)。 评分高的素材(质量好、时长合适、新鲜、使用次数少)倾向排在前面; @@ -295,7 +295,7 @@ class PlanGeneratorService: ) return [aid for aid, _ in scored] - def _fetch_asset_durations(self, asset_ids: List[str]) -> dict[str, float]: + def _fetch_asset_durations(self, asset_ids: list[str]) -> dict[str, float]: """从数据库获取素材时长信息. Args: diff --git a/apps/api/app/services/script_service.py b/apps/api/app/services/script_service.py index 113281d8d..26a6d55a6 100644 --- a/apps/api/app/services/script_service.py +++ b/apps/api/app/services/script_service.py @@ -6,7 +6,7 @@ from __future__ import annotations import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from sqlalchemy.orm import Session @@ -93,7 +93,7 @@ class ScriptService: script.segments = segments if tags is not None: script.tags = tags - script.updated_at = datetime.now(timezone.utc) + script.updated_at = datetime.now(UTC) self.db.commit() self.db.refresh(script) return script diff --git a/apps/api/app/tasks/lipsync_tts.py b/apps/api/app/tasks/lipsync_tts.py index 0ab7d32f5..dce535485 100644 --- a/apps/api/app/tasks/lipsync_tts.py +++ b/apps/api/app/tasks/lipsync_tts.py @@ -20,7 +20,7 @@ _estimate_sentence_timings_by_chars 等内部函数已复用共享实现,避 import io import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from urllib.parse import urlparse from celery import shared_task @@ -167,7 +167,7 @@ def tts_synthesize_and_submit( job.status = "failed" job.error_message = f"TTS 合成失败: {exc}" job.error_code = "TTSSynthesisFailed" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() return except ValueError as exc: @@ -175,7 +175,7 @@ def tts_synthesize_and_submit( job.status = "failed" job.error_message = f"TTS 参数错误: {exc}" job.error_code = "TTSInvalidParam" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() return @@ -185,7 +185,7 @@ def tts_synthesize_and_submit( job.status = "failed" job.error_message = "TTS 未返回音频 URL" job.error_code = "TTSNoAudio" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() return @@ -271,7 +271,7 @@ def tts_synthesize_and_submit( ) job.mediakit_task_id = mk_result["task_id"] job.status = "submitted" - job.submitted_at = datetime.now(timezone.utc) + job.submitted_at = datetime.now(UTC) logger.info( "[lipsync_tts] 已提交 MediaKit: job_id=%s task_id=%s", job_id, @@ -305,7 +305,7 @@ def tts_synthesize_and_submit( job.status = "failed" job.error_message = "TTS 异步任务执行异常" job.error_code = "AsyncTaskError" - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() except Exception: logger.exception("[lipsync_tts] 回写失败状态时异常: job_id=%s", job_id) @@ -374,8 +374,8 @@ def poll_mediakit_status(self, job_id: str, user_id: str): logger.warning("[lipsync_poll] 转存 OSS 失败,保留临时 URL: job_id=%s err=%s", job_id, exc) job.output_video_url = output_url job.output_duration = result.get("duration", 0.0) - job.completed_at = datetime.now(timezone.utc) - job.updated_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) + job.updated_at = datetime.now(UTC) db.commit() logger.info("[lipsync_poll] 任务完成: job_id=%s", job_id) elif mk_status in ("failed", "error", _SF): @@ -383,13 +383,13 @@ def poll_mediakit_status(self, job_id: str, user_id: str): job.status = "failed" job.error_message = error.get("message", "任务执行失败") job.error_code = error.get("code", "TaskFailed") - job.completed_at = datetime.now(timezone.utc) - job.updated_at = datetime.now(timezone.utc) + job.completed_at = datetime.now(UTC) + job.updated_at = datetime.now(UTC) db.commit() logger.info("[lipsync_poll] 任务失败: job_id=%s err=%s", job_id, job.error_message) else: # 中间状态,更新时间戳,继续重试 - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) if isinstance(mk_status, str) and mk_status: job.status = mk_status db.commit() @@ -446,7 +446,7 @@ def persist_output_video_task(job_id: str, user_id: str, temp_url: str): permanent_url = storage.upload_file(io.BytesIO(data), storage_key, content_type="video/mp4") final_url = _sign_media_url(permanent_url) if permanent_url else temp_url job.output_video_url = final_url - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) db.commit() logger.info("[lipsync_tts.persist] 输出视频已转存 OSS: job_id=%s key=%s", job_id, storage_key) except Exception as exc: diff --git a/apps/worker/video_processing/processor.py b/apps/worker/video_processing/processor.py index 702418f4d..657e2c703 100644 --- a/apps/worker/video_processing/processor.py +++ b/apps/worker/video_processing/processor.py @@ -6,7 +6,6 @@ import logging import os import tempfile from dataclasses import dataclass -from typing import List import ffmpeg from shared.ffmpeg_utils import FFMPEG_ENCODE_CRF, FFMPEG_ENCODE_PRESET @@ -41,7 +40,7 @@ class VideoProcessor: def concatenate_videos( self, - input_paths: List[str], + input_paths: list[str], output_path: str, resolution: tuple[int, int] = (1920, 1080), fps: int = 25, diff --git a/apps/worker/video_processing/render_adapter.py b/apps/worker/video_processing/render_adapter.py index defb59068..874b310d0 100755 --- a/apps/worker/video_processing/render_adapter.py +++ b/apps/worker/video_processing/render_adapter.py @@ -18,9 +18,10 @@ from __future__ import annotations import logging import subprocess import tempfile +from collections.abc import Callable from dataclasses import dataclass from pathlib import Path -from typing import Any, Callable +from typing import Any from sqlalchemy.orm import Session from video_processing.oss_helpers import download_asset, upload_to_oss diff --git a/apps/worker/worker_app/core/asset_usage.py b/apps/worker/worker_app/core/asset_usage.py index 97caed1cf..092150fb3 100644 --- a/apps/worker/worker_app/core/asset_usage.py +++ b/apps/worker/worker_app/core/asset_usage.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime def mark_asset_used_for_generation(asset, times: int = 1) -> None: @@ -15,6 +15,6 @@ def mark_asset_used_for_generation(asset, times: int = 1) -> None: asset.metadata = { **asset.metadata, "generation_use_count": int(asset.metadata.get("generation_use_count") or 0) + times, - "last_used_at": datetime.now(timezone.utc).isoformat(), + "last_used_at": datetime.now(UTC).isoformat(), "review_status": asset.metadata.get("review_status") or "pending_review", } diff --git a/apps/worker/worker_app/core/title_usage.py b/apps/worker/worker_app/core/title_usage.py index 0d0ffc981..a57aabb85 100644 --- a/apps/worker/worker_app/core/title_usage.py +++ b/apps/worker/worker_app/core/title_usage.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from packages.adapters.sqlalchemy_impl.models import TitleLibraryModel @@ -10,6 +10,6 @@ def mark_title_used_for_generation(db, task) -> None: if title is None: return title.usage_count = int(title.usage_count or 0) + 1 - title.updated_at = datetime.now(timezone.utc) + title.updated_at = datetime.now(UTC) db.add(title) db.commit() diff --git a/apps/worker/worker_app/tasks/_startup.py b/apps/worker/worker_app/tasks/_startup.py index afe329704..8853ebafe 100644 --- a/apps/worker/worker_app/tasks/_startup.py +++ b/apps/worker/worker_app/tasks/_startup.py @@ -1,6 +1,7 @@ """Worker 启动时的初始化任务 — 孤儿任务清理等.""" import logging +from datetime import UTC from celery.signals import worker_ready from worker_app.db import SessionLocal @@ -130,14 +131,14 @@ def cleanup_stale_jobs(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> in Returns: 清理的任务数量 """ - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta from packages.adapters.sqlalchemy_impl.models import JobModel from packages.domain.job import JobStatus try: session = SessionLocal() - cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes) stale_jobs = ( session.query(JobModel) .filter( diff --git a/apps/worker/worker_app/tasks/generation_plan_builder.py b/apps/worker/worker_app/tasks/generation_plan_builder.py index f35506048..2039219eb 100755 --- a/apps/worker/worker_app/tasks/generation_plan_builder.py +++ b/apps/worker/worker_app/tasks/generation_plan_builder.py @@ -12,7 +12,7 @@ from __future__ import annotations import traceback from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any # ── 数据类 ─────────────────────────────────────────────────────────────────── @@ -322,5 +322,5 @@ def build_error_info(error: Exception, stage: str = "render") -> dict[str, Any]: "message": str(error), "stack_trace": tb_summary, "stage": stage, - "failed_at": datetime.now(timezone.utc).isoformat(), + "failed_at": datetime.now(UTC).isoformat(), } diff --git a/apps/worker/worker_app/tasks/ingest.py b/apps/worker/worker_app/tasks/ingest.py index 4d7094952..870f843e3 100755 --- a/apps/worker/worker_app/tasks/ingest.py +++ b/apps/worker/worker_app/tasks/ingest.py @@ -2,7 +2,7 @@ import json import shutil import subprocess import tempfile -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from celery.utils.log import get_task_logger @@ -447,7 +447,7 @@ def ingest_asset(job_id: str) -> dict: # Update job status to PROCESSING job.status = IngestJobStatus.PROCESSING - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) job_repo.update(job) db.commit() @@ -703,7 +703,7 @@ def ingest_asset(job_id: str) -> dict: if codec_val: asset.codec = str(codec_val) asset.status = AssetStatus.ERROR - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) asset_repo.update(asset) else: # 旧链路兜底:无占位记录(如历史 job 重跑)才新建 @@ -729,7 +729,7 @@ def ingest_asset(job_id: str) -> dict: job.status = IngestJobStatus.FAILED job.error_message = f"Invalid media file: {error_reason}" job.result_asset_id = asset.id - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) job_repo.update(job) db.commit() @@ -797,13 +797,13 @@ def ingest_asset(job_id: str) -> dict: pass asset.status = AssetStatus.READY asset.thumbnail_url = thumbnail_url - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) asset_repo.update(asset) # Update job status to COMPLETED job.status = IngestJobStatus.COMPLETED job.result_asset_id = asset.id - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) job_repo.update(job) db.commit() @@ -825,7 +825,7 @@ def ingest_asset(job_id: str) -> dict: if job: job.status = IngestJobStatus.FAILED job.error_message = str(e) - job.updated_at = datetime.now(timezone.utc) + job.updated_at = datetime.now(UTC) job_repo.update(job) # 将上传时创建的占位 Asset(PROCESSING/UPLOADING)标记为 ERROR, @@ -857,7 +857,7 @@ def ingest_asset(job_id: str) -> dict: ): existing.status = AssetStatus.ERROR existing.metadata = {**(existing.metadata or {}), "ingest_error": str(e)} - existing.updated_at = datetime.now(timezone.utc) + existing.updated_at = datetime.now(UTC) asset_repo.update(existing) logger.info( "Marked asset as ERROR due to ingest failure: asset_id=%s job_id=%s", diff --git a/packages/adapters/in_memory/asset_repository.py b/packages/adapters/in_memory/asset_repository.py index 3e6486db8..4b3d6270a 100755 --- a/packages/adapters/in_memory/asset_repository.py +++ b/packages/adapters/in_memory/asset_repository.py @@ -1,5 +1,7 @@ """Asset InMemory Repository 实现""" +from datetime import UTC + from packages.domain import Asset @@ -45,7 +47,7 @@ class InMemoryAssetRepository: def batch_delete(self, asset_ids: list[str]) -> int: """批量删除素材(软删除,标记 status=deleted),返回实际影响数量。""" - from datetime import datetime, timezone + from datetime import datetime from packages.domain import AssetStatus @@ -54,26 +56,26 @@ class InMemoryAssetRepository: asset = self._assets.get(aid) if asset and asset.status != AssetStatus.DELETED: asset.status = AssetStatus.DELETED - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count def batch_update_metadata(self, asset_ids: list[str], metadata_patch: dict[str, object]) -> int: """批量更新素材 metadata(合并 patch),返回实际影响数量。""" - from datetime import datetime, timezone + from datetime import datetime count = 0 for aid in asset_ids: asset = self._assets.get(aid) if asset: asset.metadata = {**asset.metadata, **metadata_patch} - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count def batch_add_tags(self, asset_ids: list[str], tag_ids: list[str]) -> int: """批量给素材添加标签(合并去重),返回实际影响数量。""" - from datetime import datetime, timezone + from datetime import datetime count = 0 for aid in asset_ids: @@ -85,20 +87,20 @@ class InMemoryAssetRepository: asset.tag_ids.append(tid) changed = True if changed: - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count def batch_replace_tags(self, asset_ids: list[str], tag_ids: list[str]) -> int: """批量替换素材标签(全量覆盖),返回实际影响数量。""" - from datetime import datetime, timezone + from datetime import datetime count = 0 for aid in asset_ids: asset = self._assets.get(aid) if asset: asset.tag_ids = list(tag_ids) - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count @@ -168,13 +170,13 @@ class InMemoryAssetRepository: file_size: int = 0, ) -> Asset | None: """兜底去重:同库 + 同文件名(+同大小)且近期活动状态的素材。""" - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta if not name: return None from packages.domain import AssetStatus - cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes) candidates = [ a for a in self._assets.values() diff --git a/packages/adapters/in_memory/user_repository.py b/packages/adapters/in_memory/user_repository.py index 72cc8118d..e1aa903f8 100755 --- a/packages/adapters/in_memory/user_repository.py +++ b/packages/adapters/in_memory/user_repository.py @@ -3,7 +3,7 @@ """ import copy -from typing import Dict, Optional +from typing import Optional from packages.domain.entities import User from packages.ports.user_repository import UserRepository @@ -13,14 +13,14 @@ class InMemoryUserRepository(UserRepository): """用户仓储内存实现""" def __init__(self): - self._users: Dict[str, User] = {} - self._email_index: Dict[str, str] = {} # email -> user_id - self._username_index: Dict[str, str] = {} # username -> user_id - self._verification_token_index: Dict[str, str] = {} # token -> user_id - self._reset_token_index: Dict[str, str] = {} # token -> user_id - self._wechat_openid_index: Dict[str, str] = {} # openid -> user_id - self._wechat_unionid_index: Dict[str, str] = {} # unionid -> user_id - self._phone_index: Dict[str, str] = {} # phone -> user_id + self._users: dict[str, User] = {} + self._email_index: dict[str, str] = {} # email -> user_id + self._username_index: dict[str, str] = {} # username -> user_id + self._verification_token_index: dict[str, str] = {} # token -> user_id + self._reset_token_index: dict[str, str] = {} # token -> user_id + self._wechat_openid_index: dict[str, str] = {} # openid -> user_id + self._wechat_unionid_index: dict[str, str] = {} # unionid -> user_id + self._phone_index: dict[str, str] = {} # phone -> user_id def save(self, user: User) -> None: """保存用户(存储独立副本,避免外部修改影响内部状态)""" diff --git a/packages/adapters/redis/feature_flag_store.py b/packages/adapters/redis/feature_flag_store.py index 7e29f9cf5..604026412 100755 --- a/packages/adapters/redis/feature_flag_store.py +++ b/packages/adapters/redis/feature_flag_store.py @@ -14,6 +14,7 @@ from __future__ import annotations +import builtins import hashlib import json import logging @@ -21,7 +22,7 @@ import threading import time from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Optional, Set +from typing import Optional logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ class FeatureFlagConfig: name: str enabled: bool = False percentage: int = 0 # 0-100 - whitelist: Set[str] = field(default_factory=set) + whitelist: set[str] = field(default_factory=set) def to_dict(self) -> dict: return { @@ -171,7 +172,7 @@ class RedisFeatureFlagStore(FeatureFlagStore): def _redis_key(self, name: str) -> str: return f"{self._key_prefix}{name}" - def _parse_whitelist(self, raw: Optional[str]) -> Set[str]: + def _parse_whitelist(self, raw: Optional[str]) -> builtins.set[str]: if not raw: return set() try: diff --git a/packages/adapters/redis/session_store.py b/packages/adapters/redis/session_store.py index 3679c1a27..7db128567 100644 --- a/packages/adapters/redis/session_store.py +++ b/packages/adapters/redis/session_store.py @@ -5,7 +5,7 @@ Redis Session 存储 import json import logging -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Optional import redis @@ -118,7 +118,7 @@ class SessionStore(SessionStorePort): 是否保存成功 """ try: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) expires_at = now + timedelta(seconds=expires_in_seconds) session_data = { @@ -230,7 +230,7 @@ class SessionStore(SessionStorePort): if not session: return False - session["last_active_at"] = datetime.now(timezone.utc).isoformat() + session["last_active_at"] = datetime.now(UTC).isoformat() session_key = self._session_key(session_id) ttl = self.redis.ttl(session_key) diff --git a/packages/adapters/smtp/email_service.py b/packages/adapters/smtp/email_service.py index 165f084b6..a027faf4c 100755 --- a/packages/adapters/smtp/email_service.py +++ b/packages/adapters/smtp/email_service.py @@ -6,7 +6,7 @@ import smtplib from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -from typing import List, Optional +from typing import Optional from packages.domain.auth.email_service import EmailConfig, EmailServicePort @@ -37,8 +37,8 @@ class EmailService(EmailServicePort): subject: str, html_body: str, text_body: Optional[str] = None, - cc: Optional[List[str]] = None, - bcc: Optional[List[str]] = None, + cc: Optional[list[str]] = None, + bcc: Optional[list[str]] = None, ) -> tuple[bool, Optional[str]]: """ 发送邮件 diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index 88f9dceb2..f316fa543 100755 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -1,5 +1,5 @@ import json -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -112,7 +112,7 @@ class SQLAlchemyAssetRepository: return self.find_by_id(asset_id) def create(self, asset: Asset) -> Asset: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) model = AssetModel( id=asset.id, project_id=asset.project_id, @@ -176,7 +176,7 @@ class SQLAlchemyAssetRepository: model.file_hash = asset.file_hash or model.file_hash if getattr(model, "client_upload_id", None) is None and asset.client_upload_id: model.client_upload_id = asset.client_upload_id - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.flush() self._sync_asset_tags(asset.id, asset.tag_ids) self.session.commit() @@ -214,9 +214,9 @@ class SQLAlchemyAssetRepository: """批量删除素材(软删除,标记 status=deleted),返回实际影响数量。""" if not asset_ids: return 0 - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # 先查询待删除素材的库分布(用于更新计数) to_delete = ( self.session.query(AssetModel.asset_library_id, AssetModel.file_size) @@ -262,9 +262,9 @@ class SQLAlchemyAssetRepository: """批量更新素材 metadata(合并 patch),返回实际影响数量。""" if not asset_ids: return 0 - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # 逐条读取 + 合并 + 更新,保证 JSON 合并正确 models = self.session.query(AssetModel).filter(AssetModel.id.in_(asset_ids)).all() count = 0 @@ -286,9 +286,9 @@ class SQLAlchemyAssetRepository: """批量给素材添加标签(合并去重),返回实际影响数量。""" if not asset_ids or not tag_ids: return 0 - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) clean_tag_ids = list(set(tag_ids)) count = 0 for aid in asset_ids: @@ -313,9 +313,9 @@ class SQLAlchemyAssetRepository: """批量替换素材标签(全量覆盖),返回实际影响数量。""" if not asset_ids: return 0 - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) clean_tag_ids = list(set(tag_ids)) count = 0 for aid in asset_ids: @@ -556,13 +556,13 @@ class SQLAlchemyAssetRepository: file_size=0(大小未知)时直接返回 None——宁可漏判(极端情况下多建一条 占位)也不可仅凭同名 + processing 误杀内容全新的视频。 """ - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta if not name: return None if not file_size or file_size <= 0: return None - cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes) query = self.session.query(AssetModel).filter( AssetModel.asset_library_id == library_id, AssetModel.name == name, diff --git a/packages/adapters/sqlalchemy_impl/billing_repository.py b/packages/adapters/sqlalchemy_impl/billing_repository.py index 56e749eee..662194111 100644 --- a/packages/adapters/sqlalchemy_impl/billing_repository.py +++ b/packages/adapters/sqlalchemy_impl/billing_repository.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -36,7 +36,7 @@ class SQLAlchemyBillingRepository: model.status = "paid" model.payment_method = payment_method model.payment_id = payment_id - model.paid_at = datetime.now(timezone.utc) + model.paid_at = datetime.now(UTC) self.session.commit() return True diff --git a/packages/adapters/sqlalchemy_impl/daily_usage_repository.py b/packages/adapters/sqlalchemy_impl/daily_usage_repository.py index d4996b4fd..4fcaf0610 100644 --- a/packages/adapters/sqlalchemy_impl/daily_usage_repository.py +++ b/packages/adapters/sqlalchemy_impl/daily_usage_repository.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from sqlalchemy.orm import Session @@ -44,7 +44,7 @@ class SQLAlchemyDailyUsageRepository: if model is None: return record model.count = record.count - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.add(model) self.session.commit() return record @@ -69,14 +69,14 @@ class SQLAlchemyDailyUsageRepository: usage_date=record.usage_date, usage_type=record.usage_type, count=1, - updated_at=datetime.now(timezone.utc), + updated_at=datetime.now(UTC), ) self.session.add(model) self.session.commit() return record model.count += 1 - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.add(model) self.session.commit() return self._to_domain(model) diff --git a/packages/adapters/sqlalchemy_impl/edit_plan_clip_repository.py b/packages/adapters/sqlalchemy_impl/edit_plan_clip_repository.py index 16819092e..b76a46211 100755 --- a/packages/adapters/sqlalchemy_impl/edit_plan_clip_repository.py +++ b/packages/adapters/sqlalchemy_impl/edit_plan_clip_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -23,7 +23,7 @@ class SQLAlchemyEditPlanClipRepository: status: Optional[EditPlanClipStatus] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: """按计划列出片段,按 order 排序""" query = self.session.query(EditPlanClipModel).filter( EditPlanClipModel.plan_id == plan_id, diff --git a/packages/adapters/sqlalchemy_impl/edit_plan_repository.py b/packages/adapters/sqlalchemy_impl/edit_plan_repository.py index ad6c87eb5..804432d7c 100755 --- a/packages/adapters/sqlalchemy_impl/edit_plan_repository.py +++ b/packages/adapters/sqlalchemy_impl/edit_plan_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -23,7 +23,7 @@ class SQLAlchemyEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: """按模板列出剪辑计划""" query = self.session.query(EditPlanModel).filter( EditPlanModel.template_id == template_id, @@ -40,7 +40,7 @@ class SQLAlchemyEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: """列出所有剪辑计划""" query = self.session.query(EditPlanModel) if status: @@ -63,7 +63,7 @@ class SQLAlchemyEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: """按项目列出剪辑计划""" query = self.session.query(EditPlanModel).filter( EditPlanModel.project_id == project_id, @@ -81,7 +81,7 @@ class SQLAlchemyEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: """列出用户创建的剪辑计划""" query = self.session.query(EditPlanModel).filter( EditPlanModel.created_by_user_id == user_id, diff --git a/packages/adapters/sqlalchemy_impl/edit_template_repository.py b/packages/adapters/sqlalchemy_impl/edit_template_repository.py index 79dcafdce..7ad13bb5f 100755 --- a/packages/adapters/sqlalchemy_impl/edit_template_repository.py +++ b/packages/adapters/sqlalchemy_impl/edit_template_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -22,7 +22,7 @@ class SQLAlchemyEditTemplateRepository: template_type: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[EditTemplate]: + ) -> list[EditTemplate]: """列出所有激活的模板""" query = self.session.query(EditTemplateModel).filter( EditTemplateModel.status == EditTemplateStatus.ACTIVE, @@ -43,7 +43,7 @@ class SQLAlchemyEditTemplateRepository: status: Optional[EditTemplateStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditTemplate]: + ) -> list[EditTemplate]: """列出所有模板(含非激活)""" query = self.session.query(EditTemplateModel) if template_type: diff --git a/packages/adapters/sqlalchemy_impl/generation_task_repository.py b/packages/adapters/sqlalchemy_impl/generation_task_repository.py index 893e6a582..433aaa829 100755 --- a/packages/adapters/sqlalchemy_impl/generation_task_repository.py +++ b/packages/adapters/sqlalchemy_impl/generation_task_repository.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -339,7 +339,7 @@ class SQLAlchemyGenerationTaskRepository: """同 cleanup_stale_running,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。""" from datetime import timedelta - cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes) models = ( self.session.query(GenerationTaskModel) .filter( @@ -358,9 +358,9 @@ class SQLAlchemyGenerationTaskRepository: model.error_info = { "error_type": "WorkerInterrupted", "message": "任务在运行中中断,可能因 worker 重启或超时", - "failed_at": datetime.now(timezone.utc).isoformat(), + "failed_at": datetime.now(UTC).isoformat(), } - model.completed_at = datetime.now(timezone.utc) + model.completed_at = datetime.now(UTC) self.session.commit() return result @@ -377,7 +377,7 @@ class SQLAlchemyGenerationTaskRepository: """同 cleanup_stale_pending,但返回 [(task_id, celery_task_id), ...] 供撤销队列消息。""" from datetime import timedelta - cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=timeout_minutes) models = ( self.session.query(GenerationTaskModel) .filter( @@ -391,7 +391,7 @@ class SQLAlchemyGenerationTaskRepository: error_info = { "error_type": "PendingTimeout", "message": f"任务在 pending 状态停留超过 {timeout_minutes} 分钟,自动清理", - "failed_at": datetime.now(timezone.utc).isoformat(), + "failed_at": datetime.now(UTC).isoformat(), } result: list[tuple[str, str]] = [] for model in models: @@ -399,6 +399,6 @@ class SQLAlchemyGenerationTaskRepository: model.status = GenerationTaskStatus.FAILED.value model.error_message = "pending timeout: auto cleanup" model.error_info = error_info - model.completed_at = datetime.now(timezone.utc) + model.completed_at = datetime.now(UTC) self.session.commit() return result diff --git a/packages/adapters/sqlalchemy_impl/models.py b/packages/adapters/sqlalchemy_impl/models.py index 648aa9ca8..dc84bc3d4 100755 --- a/packages/adapters/sqlalchemy_impl/models.py +++ b/packages/adapters/sqlalchemy_impl/models.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from sqlalchemy import JSON, Boolean, Column, DateTime, Float, Index, Integer, String, Text, UniqueConstraint, text @@ -44,7 +44,7 @@ class UserModel(Base): member_type = Column(String(20), nullable=True) member_expires_at = Column(DateTime, nullable=True) points_balance = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class ProjectModel(Base): @@ -62,7 +62,7 @@ class ProjectModel(Base): shared_users = Column(JSON, nullable=False, default=list) # 被共享的用户 ID 列表 is_default = Column(Boolean, nullable=False, default=False, server_default="false") extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class AssetLibraryModel(Base): @@ -76,8 +76,8 @@ class AssetLibraryModel(Base): asset_count = Column(Float, nullable=False, default=0) total_size = Column(Float, nullable=False, default=0) extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class AssetModel(Base): @@ -108,8 +108,8 @@ class AssetModel(Base): file_hash = Column(String(64), nullable=True, index=True) client_upload_id = Column(String(64), nullable=True, index=True) extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), index=True) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC), index=True) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class TagModel(Base): @@ -120,7 +120,7 @@ class TagModel(Base): id = Column(String(36), primary_key=True) user_id = Column(String(36), nullable=False, index=True) name = Column(String(100), nullable=False) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) __table_args__ = (UniqueConstraint("user_id", "name", name="uq_tags_user_name"),) @@ -132,7 +132,7 @@ class AssetTagModel(Base): asset_id = Column(String(36), primary_key=True) tag_id = Column(String(36), primary_key=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class EditTemplateModel(Base): @@ -153,8 +153,8 @@ class EditTemplateModel(Base): sort_weight = Column(Integer, nullable=False, default=0, index=True) status = Column(String(20), nullable=False, default="active", index=True) version = Column(Integer, nullable=False, default=1) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class EditTemplateVersionModel(Base): @@ -174,7 +174,7 @@ class EditTemplateVersionModel(Base): clip_configs = Column(JSON, nullable=False, default=list) change_note = Column(String(500), nullable=False, default="") published_by = Column(String(36), nullable=False, default="") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class EditPlanModel(Base): @@ -194,8 +194,8 @@ class EditPlanModel(Base): source_edit_plan_id = Column(String(36), nullable=True, index=True) project_id = Column(String(36), nullable=False, default="", index=True) created_by_user_id = Column(String(36), nullable=False, default="", index=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class TemplateClipConfigModel(Base): @@ -216,8 +216,8 @@ class TemplateClipConfigModel(Base): material_requirements = Column(JSON, nullable=False, default=dict) transition_effect = Column(String(20), nullable=False, default="cut") config = Column(JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class EditPlanClipModel(Base): @@ -242,8 +242,8 @@ class EditPlanClipModel(Base): playback_speed = Column(Float, nullable=False, default=1.0) status = Column(String(20), nullable=False, default="pending", index=True) config = Column(JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class IngestJobModel(Base): @@ -259,8 +259,8 @@ class IngestJobModel(Base): file_hash = Column(String(64), nullable=True, index=True) asset_id = Column(String(36), nullable=False, default="", index=True) celery_task_id = Column(String(64), nullable=False, default="", server_default="") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class ClassificationJobModel(Base): @@ -273,8 +273,8 @@ class ClassificationJobModel(Base): classification = Column(String(50), nullable=False, default="") confidence = Column(Float, nullable=False, default=0.0) error_message = Column(Text, nullable=False, default="") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class GenerationTaskModel(Base): @@ -320,12 +320,12 @@ class GenerationTaskModel(Base): bgm_config = Column(JSON, nullable=False, default=dict) extra_meta = Column("metadata", JSON, nullable=False, default=dict) logs = Column(Text, nullable=False, default="[]", server_default="[]") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) updated_at = Column( DateTime, nullable=False, - default=lambda: datetime.now(timezone.utc), - onupdate=lambda: datetime.now(timezone.utc), + default=lambda: datetime.now(UTC), + onupdate=lambda: datetime.now(UTC), ) @@ -349,8 +349,8 @@ class GeneratedVideoModel(Base): status = Column(String(20), nullable=False, default="completed", index=True) review_status = Column(String(20), nullable=False, default="pending_review", index=True) generation_params = Column(Text, nullable=False, default="{}") - generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) updated_at = Column(DateTime, nullable=True) video_fingerprint = Column(Text, nullable=True) is_duplicate = Column(Boolean, nullable=False, default=False) @@ -373,8 +373,8 @@ class TitleLibraryModel(Base): usage_count = Column(Integer, nullable=False, default=0) is_active = Column(Boolean, nullable=False, default=True, index=True) extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class VoiceCloneProfileModel(Base): @@ -396,8 +396,8 @@ class VoiceCloneProfileModel(Base): retry_count = Column(Integer, nullable=False, default=0) max_retries = Column(Integer, nullable=False, default=3) metadata_ = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class VoiceLibraryModel(Base): @@ -417,8 +417,8 @@ class VoiceLibraryModel(Base): status = Column(String(20), nullable=False, default="completed", index=True) tags = Column(JSON, nullable=False, default=list) extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class DuplicationRecordModel(Base): @@ -438,8 +438,8 @@ class DuplicationRecordModel(Base): match_count = Column(Integer, nullable=True) video_fingerprint = Column(Text, nullable=True) error_message = Column(Text, nullable=False, default="") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class DuplicationSegmentModel(Base): @@ -467,8 +467,8 @@ class RecipeModel(Base): generation_params = Column(JSON, nullable=False, default=dict) is_active = Column(Boolean, nullable=False, default=True) extra_meta = Column("metadata", JSON, nullable=False, default=dict) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class RecipeItemModel(Base): @@ -496,8 +496,8 @@ class TemplateModel(Base): bgm_config = Column(JSON, nullable=False, default=dict) estimated_duration = Column(Float, nullable=False, default=0.0) is_active = Column(Boolean, nullable=False, default=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class TemplateSegmentModel(Base): @@ -509,8 +509,8 @@ class TemplateSegmentModel(Base): duration_min = Column(Float, nullable=False) duration_max = Column(Float, nullable=False) material_type = Column(String(20), nullable=True) # 仅 voice_over 模式: 人物/场景 - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class TemplateCategoryModel(Base): @@ -520,7 +520,7 @@ class TemplateCategoryModel(Base): user_id = Column(String(36), nullable=False, index=True) name = Column(String(100), nullable=False) sort_order = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class JobModel(Base): @@ -544,8 +544,8 @@ class JobModel(Base): created_by_user_id = Column(String(36), nullable=False, default="", index=True) started_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class TTSJobModel(Base): @@ -573,8 +573,8 @@ class TTSJobModel(Base): metadata_ = Column("metadata", JSON, nullable=False, default=dict) started_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class BillingRecordModel(Base): @@ -591,7 +591,7 @@ class BillingRecordModel(Base): payment_method = Column(String(50), nullable=True) payment_id = Column(String(100), nullable=True) # 第三方支付流水号 invoice_url = Column(String(500), nullable=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) paid_at = Column(DateTime, nullable=True) @@ -607,7 +607,7 @@ class VerificationCodeModel(Base): expires_at = Column(DateTime, nullable=False) used_at = Column(DateTime, nullable=True) attempts = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class VideoShareModel(Base): @@ -624,8 +624,8 @@ class VideoShareModel(Base): view_count = Column(Integer, nullable=False, default=0) download_count = Column(Integer, nullable=False, default=0) is_active = Column(Boolean, nullable=False, default=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) class CoverTemplateModel(Base): @@ -639,8 +639,8 @@ class CoverTemplateModel(Base): thumbnail_url = Column(String(1000), nullable=False, default="") is_system = Column(Boolean, nullable=False, default=False, index=True) config = Column(JSON, nullable=False, default=dict) - created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) class VideoFingerprintChunkModel(Base): @@ -657,7 +657,7 @@ class VideoFingerprintChunkModel(Base): phash_binary = Column(String(16), nullable=False) color_histogram = Column(JSON, nullable=False) frame_count = Column(Integer, nullable=False, default=1) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class ScriptModel(Base): @@ -671,8 +671,8 @@ class ScriptModel(Base): content = Column(Text, nullable=False, default="") segments = Column(JSON, nullable=False, default=list) tags = Column(JSON, nullable=False, default=list) - created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) class LipsyncJobModel(Base): @@ -714,8 +714,8 @@ class LipsyncJobModel(Base): # 时间戳 submitted_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class AiAvatarRenderJob(Base): @@ -746,8 +746,8 @@ class AiAvatarRenderJob(Base): submitted_at = Column(DateTime, nullable=True) started_at = Column(DateTime, nullable=True) completed_at = Column(DateTime, nullable=True) - created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC)) class PointsAccountModel(Base): @@ -760,8 +760,8 @@ class PointsAccountModel(Base): balance = Column(Integer, nullable=False, default=0) total_earned = Column(Integer, nullable=False, default=0) total_spent = Column(Integer, nullable=False, default=0) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class PointsTransactionModel(Base): @@ -778,7 +778,7 @@ class PointsTransactionModel(Base): balance_after = Column(Integer, nullable=False) description = Column(String(255), nullable=False, default="") ref_id = Column(String(100), nullable=False, default="") - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class PointsOrderModel(Base): @@ -798,7 +798,7 @@ class PointsOrderModel(Base): payment_method = Column(String(50), nullable=True) payment_id = Column(String(100), nullable=True) paid_at = Column(DateTime, nullable=True) - created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) class DailyUsageRecordModel(Base): @@ -812,4 +812,4 @@ class DailyUsageRecordModel(Base): usage_date = Column(DateTime, nullable=False) # stored as DATE in SQL but DateTime for ORM compat usage_type = Column(String(50), nullable=False, default="free_clip") count = Column(Integer, nullable=False, default=0) - updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc)) + updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC)) diff --git a/packages/adapters/sqlalchemy_impl/points_account_repository.py b/packages/adapters/sqlalchemy_impl/points_account_repository.py index 29c391caa..54a6fb362 100644 --- a/packages/adapters/sqlalchemy_impl/points_account_repository.py +++ b/packages/adapters/sqlalchemy_impl/points_account_repository.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy.orm import Session @@ -37,7 +37,7 @@ class SQLAlchemyPointsAccountRepository: model.balance = account.balance model.total_earned = account.total_earned model.total_spent = account.total_spent - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.add(model) self.session.commit() return account diff --git a/packages/adapters/sqlalchemy_impl/recipe_repository.py b/packages/adapters/sqlalchemy_impl/recipe_repository.py index 5dc223abd..b31207b4a 100644 --- a/packages/adapters/sqlalchemy_impl/recipe_repository.py +++ b/packages/adapters/sqlalchemy_impl/recipe_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -22,7 +22,7 @@ class SQLAlchemyRecipeRepository: *, skip: int = 0, limit: int = 50, - ) -> List[Recipe]: + ) -> list[Recipe]: models = ( self.session.query(RecipeModel) .filter( @@ -121,7 +121,7 @@ class SQLAlchemyRecipeRepository: .count() ) - def list_items(self, recipe_id: str) -> List[RecipeItem]: + def list_items(self, recipe_id: str) -> list[RecipeItem]: models = ( self.session.query(RecipeItemModel) .filter(RecipeItemModel.recipe_id == recipe_id) @@ -130,7 +130,7 @@ class SQLAlchemyRecipeRepository: ) return [self._item_model_to_entity(m) for m in models] - def create_items(self, items: List[RecipeItem]) -> List[RecipeItem]: + def create_items(self, items: list[RecipeItem]) -> list[RecipeItem]: for item in items: model = RecipeItemModel( id=item.id, diff --git a/packages/adapters/sqlalchemy_impl/template_clip_config_repository.py b/packages/adapters/sqlalchemy_impl/template_clip_config_repository.py index 19cc921d1..24e0f46d4 100755 --- a/packages/adapters/sqlalchemy_impl/template_clip_config_repository.py +++ b/packages/adapters/sqlalchemy_impl/template_clip_config_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -30,7 +30,7 @@ class SQLAlchemyTemplateClipConfigRepository: clip_type: Optional[ClipType] = None, skip: int = 0, limit: int = 100, - ) -> List[TemplateClipConfig]: + ) -> list[TemplateClipConfig]: """按模板列出片段配置,按 order 排序""" query = self.session.query(TemplateClipConfigModel).filter( TemplateClipConfigModel.template_id == template_id, diff --git a/packages/adapters/sqlalchemy_impl/template_repository.py b/packages/adapters/sqlalchemy_impl/template_repository.py index dce3a782e..e66584bfd 100755 --- a/packages/adapters/sqlalchemy_impl/template_repository.py +++ b/packages/adapters/sqlalchemy_impl/template_repository.py @@ -8,7 +8,7 @@ from __future__ import annotations import uuid -from typing import List, Optional +from typing import Optional from sqlalchemy import or_ from sqlalchemy.orm import Session @@ -56,7 +56,7 @@ class SQLAlchemyTemplateRepository: keyword: Optional[str] = None, mode: Optional[str] = None, valid_only: bool = False, - ) -> List[Template]: + ) -> list[Template]: query = self.session.query(TemplateModel).filter( TemplateModel.user_id == user_id, TemplateModel.is_active.is_(True), @@ -252,7 +252,7 @@ class SQLAlchemyTemplateRepository: created = self.create(new_template) # 复用 create_segments 写入 template_clip_configs - new_segments: List[TemplateSegment] = [] + new_segments: list[TemplateSegment] = [] for seg in source.segments: new_segments.append( TemplateSegment( @@ -274,7 +274,7 @@ class SQLAlchemyTemplateRepository: # ── Segments ── - def list_segments(self, template_id: str) -> List[TemplateSegment]: + def list_segments(self, template_id: str) -> list[TemplateSegment]: """优先从 template_clip_configs 读取,回退读 template_segments。""" clips = ( self.session.query(TemplateClipConfigModel) @@ -293,7 +293,7 @@ class SQLAlchemyTemplateRepository: ) return [self._segment_model_to_entity(m) for m in old] - def create_segments(self, segments: List[TemplateSegment]) -> List[TemplateSegment]: + def create_segments(self, segments: list[TemplateSegment]) -> list[TemplateSegment]: """写入 template_clip_configs 表。material_type 存入 config JSON。""" for seg in segments: config = {"material_type": seg.material_type} if seg.material_type else {} @@ -330,7 +330,7 @@ class SQLAlchemyTemplateRepository: # ── Categories ── - def list_categories(self, user_id: str) -> List[TemplateCategory]: + def list_categories(self, user_id: str) -> list[TemplateCategory]: models = ( self.session.query(TemplateCategoryModel) .filter(TemplateCategoryModel.user_id == user_id) @@ -380,7 +380,7 @@ class SQLAlchemyTemplateRepository: # ── Tags ── - def list_tags(self, user_id: str) -> List[str]: + def list_tags(self, user_id: str) -> list[str]: """获取用户所有模板的标签(去重)。""" models = ( self.session.query(TemplateModel) diff --git a/packages/adapters/sqlalchemy_impl/template_version_repository.py b/packages/adapters/sqlalchemy_impl/template_version_repository.py index 7512a221d..383e6dcfa 100755 --- a/packages/adapters/sqlalchemy_impl/template_version_repository.py +++ b/packages/adapters/sqlalchemy_impl/template_version_repository.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import List - from sqlalchemy.orm import Session from packages.domain.template_version import EditTemplateVersion @@ -51,7 +49,7 @@ class SQLAlchemyTemplateVersionRepository: return None return self._to_entity(model) - def list_by_template(self, template_id: str, limit: int = 50) -> List[EditTemplateVersion]: + def list_by_template(self, template_id: str, limit: int = 50) -> list[EditTemplateVersion]: """列出模板的所有历史版本(按版本号倒序)""" from packages.adapters.sqlalchemy_impl.models import EditTemplateVersionModel diff --git a/packages/adapters/sqlalchemy_impl/title_library_repository.py b/packages/adapters/sqlalchemy_impl/title_library_repository.py index 709e539c3..5539d1dab 100755 --- a/packages/adapters/sqlalchemy_impl/title_library_repository.py +++ b/packages/adapters/sqlalchemy_impl/title_library_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -24,7 +24,7 @@ class SQLAlchemyTitleLibraryRepository: is_active: bool = True, skip: int = 0, limit: int = 50, - ) -> List[TitleLibraryItem]: + ) -> list[TitleLibraryItem]: query = self.session.query(TitleLibraryModel).filter( TitleLibraryModel.user_id == user_id, TitleLibraryModel.is_active == is_active, diff --git a/packages/adapters/sqlalchemy_impl/tts_job_repository.py b/packages/adapters/sqlalchemy_impl/tts_job_repository.py index 2857c941d..6856e9c0a 100644 --- a/packages/adapters/sqlalchemy_impl/tts_job_repository.py +++ b/packages/adapters/sqlalchemy_impl/tts_job_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -98,7 +98,7 @@ class SQLAlchemyTTSJobRepository: status: Optional[str] = None, limit: int = 50, offset: int = 0, - ) -> List[TTSJob]: + ) -> list[TTSJob]: query = self.session.query(TTSJobModel).filter( TTSJobModel.user_id == user_id, TTSJobModel.status != "deleted", @@ -125,7 +125,7 @@ class SQLAlchemyTTSJobRepository: status: Optional[str] = None, limit: int = 50, offset: int = 0, - ) -> List[TTSJob]: + ) -> list[TTSJob]: query = self.session.query(TTSJobModel).filter( TTSJobModel.voice_clone_profile_id == voice_clone_profile_id, TTSJobModel.status != "deleted", diff --git a/packages/adapters/sqlalchemy_impl/verification_code_repository.py b/packages/adapters/sqlalchemy_impl/verification_code_repository.py index ce2a383c0..a54ab0822 100755 --- a/packages/adapters/sqlalchemy_impl/verification_code_repository.py +++ b/packages/adapters/sqlalchemy_impl/verification_code_repository.py @@ -4,7 +4,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from sqlalchemy.orm import Session @@ -51,7 +51,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository): return self._to_entity(self.session.get(VerificationCodeModel, code_id)) def count_today(self, recipient: str, code_type: str) -> int: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0) return ( self.session.query(VerificationCodeModel) @@ -75,7 +75,7 @@ class SQLAlchemyVerificationCodeRepository(VerificationCodeRepository): if dt is None: return None if dt.tzinfo is None: - return dt.replace(tzinfo=timezone.utc) + return dt.replace(tzinfo=UTC) return dt return VerificationCode( diff --git a/packages/adapters/sqlalchemy_impl/video_share_repository.py b/packages/adapters/sqlalchemy_impl/video_share_repository.py index fe15555f2..bc9c3f76e 100755 --- a/packages/adapters/sqlalchemy_impl/video_share_repository.py +++ b/packages/adapters/sqlalchemy_impl/video_share_repository.py @@ -2,8 +2,8 @@ from __future__ import annotations -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime +from typing import Optional from sqlalchemy.orm import Session @@ -69,7 +69,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): return None return _model_to_domain(model) - def list_by_video(self, video_id: str, user_id: str) -> List[VideoShare]: + def list_by_video(self, video_id: str, user_id: str) -> list[VideoShare]: models = ( self.session.query(VideoShareModel) .filter( @@ -81,7 +81,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): ) return [_model_to_domain(m) for m in models] - def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> List[VideoShare]: + def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> list[VideoShare]: models = ( self.session.query(VideoShareModel) .filter(VideoShareModel.user_id == user_id) @@ -104,7 +104,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): model.is_active = share.is_active model.view_count = share.view_count model.download_count = share.download_count - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.add(model) self.session.commit() return share @@ -121,7 +121,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): if model is None: return False model.is_active = False - model.updated_at = datetime.now(timezone.utc) + model.updated_at = datetime.now(UTC) self.session.add(model) self.session.commit() return True @@ -130,7 +130,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update( { "view_count": VideoShareModel.view_count + 1, - "updated_at": datetime.now(timezone.utc), + "updated_at": datetime.now(UTC), }, synchronize_session=False, ) @@ -140,7 +140,7 @@ class SQLAlchemyVideoShareRepository(VideoShareRepositoryPort): self.session.query(VideoShareModel).filter(VideoShareModel.id == share_id).update( { "download_count": VideoShareModel.download_count + 1, - "updated_at": datetime.now(timezone.utc), + "updated_at": datetime.now(UTC), }, synchronize_session=False, ) diff --git a/packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py b/packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py index 61fbebfc5..8d4f8369f 100644 --- a/packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py +++ b/packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -86,7 +86,7 @@ class SQLAlchemyVoiceCloneProfileRepository: status: Optional[str] = None, limit: int = 50, offset: int = 0, - ) -> List[VoiceCloneProfile]: + ) -> list[VoiceCloneProfile]: query = self.session.query(VoiceCloneProfileModel).filter( VoiceCloneProfileModel.user_id == user_id, VoiceCloneProfileModel.status != "deleted", @@ -119,7 +119,7 @@ class SQLAlchemyVoiceCloneProfileRepository: return None return self._model_to_entity(model) - def find_profile_ids_by_voice_ids(self, voice_ids: List[str]) -> Dict[str, str]: + def find_profile_ids_by_voice_ids(self, voice_ids: list[str]) -> dict[str, str]: """批量查询 voice_id → profile_id 映射。用于填充统一列表的 voice_clone_profile_id。""" if not voice_ids: return {} diff --git a/packages/adapters/sqlalchemy_impl/voice_library_repository.py b/packages/adapters/sqlalchemy_impl/voice_library_repository.py index 636c5c7c2..867b95b27 100644 --- a/packages/adapters/sqlalchemy_impl/voice_library_repository.py +++ b/packages/adapters/sqlalchemy_impl/voice_library_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from sqlalchemy.orm import Session @@ -23,7 +23,7 @@ class SQLAlchemyVoiceLibraryRepository: status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[VoiceLibraryItem]: + ) -> list[VoiceLibraryItem]: query = self.session.query(VoiceLibraryModel).filter( VoiceLibraryModel.user_id == user_id, ) diff --git a/packages/application/auth/bind_contact_use_case.py b/packages/application/auth/bind_contact_use_case.py index ad4924116..55f280cfd 100755 --- a/packages/application/auth/bind_contact_use_case.py +++ b/packages/application/auth/bind_contact_use_case.py @@ -5,7 +5,7 @@ from __future__ import annotations import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from packages.application.auth.verification_code_service import ( @@ -135,7 +135,7 @@ class BindContactUseCase: # 5. 判断是否完成绑定 if user.phone_verified and user.email_verified and "@wechat.local" not in user.email: - user.binding_completed_at = datetime.now(timezone.utc) + user.binding_completed_at = datetime.now(UTC) # 6. 保存 self.user_repo.save(user) diff --git a/packages/application/auth/jwt_handler.py b/packages/application/auth/jwt_handler.py index c7f0f6ea8..3f98e1396 100644 --- a/packages/application/auth/jwt_handler.py +++ b/packages/application/auth/jwt_handler.py @@ -12,7 +12,7 @@ JWT 处理器委托层 payload = jwt_handler.verify_access_token(token) """ -from typing import Any, Dict, Optional +from typing import Any, Optional from packages.application.auth.jwt_service import JWTConfig, JWTService @@ -45,7 +45,7 @@ class JWTHandler: self, user_id: str, role: str = "", - additional_claims: Optional[Dict[str, Any]] = None, + additional_claims: Optional[dict[str, Any]] = None, ) -> str: """ 创建 access_token @@ -64,7 +64,7 @@ class JWTHandler: additional_claims=additional_claims, ) - def verify_access_token(self, token: str) -> Dict[str, Any]: + def verify_access_token(self, token: str) -> dict[str, Any]: """ 验证 access_token @@ -80,7 +80,7 @@ class JWTHandler: """ return self._service.verify_access_token(token) - def verify_token(self, token: str) -> Dict[str, Any]: + def verify_token(self, token: str) -> dict[str, Any]: """ 验证任意 Token diff --git a/packages/application/auth/jwt_service.py b/packages/application/auth/jwt_service.py index 0d7f17b1b..63f5d6e6a 100644 --- a/packages/application/auth/jwt_service.py +++ b/packages/application/auth/jwt_service.py @@ -1,7 +1,7 @@ """JWT Token 生成、验证、解析服务""" -from datetime import datetime, timedelta, timezone -from typing import Any, Dict, Optional +from datetime import UTC, datetime, timedelta +from typing import Any, Optional import jwt from jwt.exceptions import ExpiredSignatureError, InvalidTokenError @@ -75,7 +75,7 @@ class JWTService(JWTServicePort): self, user_id: str, role: str = "", - additional_claims: Optional[Dict[str, Any]] = None, + additional_claims: Optional[dict[str, Any]] = None, ) -> str: """ 创建 access_token @@ -88,7 +88,7 @@ class JWTService(JWTServicePort): Returns: JWT Token 字符串 """ - now = datetime.now(timezone.utc) + now = datetime.now(UTC) expire = now + timedelta(minutes=self.config.ACCESS_TOKEN_EXPIRE_MINUTES) # noqa: E501 payload = { @@ -115,7 +115,7 @@ class JWTService(JWTServicePort): Returns: JWT Token 字符串 """ - now = datetime.now(timezone.utc) + now = datetime.now(UTC) expire = now + timedelta(days=self.config.REFRESH_TOKEN_EXPIRE_DAYS) payload = { @@ -128,7 +128,7 @@ class JWTService(JWTServicePort): return jwt.encode(payload, self.config.SECRET_KEY, algorithm=self.config.ALGORITHM) - def verify_token(self, token: str) -> Dict[str, Any]: + def verify_token(self, token: str) -> dict[str, Any]: """ 验证 Token @@ -154,7 +154,7 @@ class JWTService(JWTServicePort): except InvalidTokenError as e: raise InvalidTokenError(f"Invalid token: {str(e)}") from e - def verify_access_token(self, token: str) -> Dict[str, Any]: + def verify_access_token(self, token: str) -> dict[str, Any]: """ 验证 access_token @@ -171,7 +171,7 @@ class JWTService(JWTServicePort): return payload - def verify_refresh_token(self, token: str) -> Dict[str, Any]: + def verify_refresh_token(self, token: str) -> dict[str, Any]: """ 验证 refresh_token diff --git a/packages/application/auth/login_use_case.py b/packages/application/auth/login_use_case.py index c50d2aeb9..e9d991016 100755 --- a/packages/application/auth/login_use_case.py +++ b/packages/application/auth/login_use_case.py @@ -4,7 +4,7 @@ import hashlib import secrets -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Optional import jwt as pyjwt @@ -112,7 +112,7 @@ class LoginUseCase: refresh_token = secrets.token_urlsafe(32) # 5. 生成 JWT token - now = datetime.now(timezone.utc) + now = datetime.now(UTC) access_token_payload = { "sub": user.id, "sid": session_id, @@ -135,7 +135,7 @@ class LoginUseCase: ) # 6. 更新最后登录信息 - user.last_login_at = datetime.now(timezone.utc) + user.last_login_at = datetime.now(UTC) user.last_login_ip = request.ip_address self.user_repository.save(user) @@ -213,7 +213,7 @@ class RefreshTokenUseCase: return None, "User not found" # 4. 生成新的 access_token - now = datetime.now(timezone.utc) + now = datetime.now(UTC) access_token_payload = { "sub": user.id, "sid": session_id, diff --git a/packages/application/auth/password_handler.py b/packages/application/auth/password_handler.py index 4d97704bc..d07e6d9d3 100644 --- a/packages/application/auth/password_handler.py +++ b/packages/application/auth/password_handler.py @@ -12,7 +12,7 @@ is_valid = password_handler.verify_password("my_secure_password", hashed) """ -from typing import Optional, Tuple +from typing import Optional from packages.application.auth.password_hasher import PasswordHasher, PasswordValidator @@ -81,7 +81,7 @@ class PasswordHandler: """ return self._hasher.needs_rehash(hashed_password) - def validate_strength(self, password: str) -> Tuple[bool, Optional[str]]: + def validate_strength(self, password: str) -> tuple[bool, Optional[str]]: """ 验证密码强度 diff --git a/packages/application/auth/password_reset_use_case.py b/packages/application/auth/password_reset_use_case.py index c8ce88032..efce26b3b 100644 --- a/packages/application/auth/password_reset_use_case.py +++ b/packages/application/auth/password_reset_use_case.py @@ -4,7 +4,7 @@ import logging import secrets -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Optional from packages.adapters.smtp import get_email_service @@ -71,7 +71,7 @@ class RequestPasswordResetUseCase: # 设置令牌和过期时间 user.password_reset_token = reset_token - user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=self.token_expire_hours) + user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=self.token_expire_hours) # 保存用户 self.user_repository.save(user) @@ -141,8 +141,8 @@ class ResetPasswordUseCase: if user.password_reset_expires_at: expires_at = user.password_reset_expires_at if expires_at.tzinfo is None: - expires_at = expires_at.replace(tzinfo=timezone.utc) - if datetime.now(timezone.utc) > expires_at: + expires_at = expires_at.replace(tzinfo=UTC) + if datetime.now(UTC) > expires_at: return False, "Reset token has expired" # 哈希新密码 diff --git a/packages/application/auth/register_user_use_case.py b/packages/application/auth/register_user_use_case.py index 25de08e4a..284dd5195 100644 --- a/packages/application/auth/register_user_use_case.py +++ b/packages/application/auth/register_user_use_case.py @@ -4,7 +4,7 @@ import logging import secrets -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from uuid import uuid4 @@ -120,7 +120,7 @@ class RegisterUserUseCase: password_hash=hashed_password, email_verified=False, email_verification_token=verification_token, - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) # 8. 保存用户 diff --git a/packages/application/auth/verification_code_service.py b/packages/application/auth/verification_code_service.py index 23bb00af9..6dacd433c 100755 --- a/packages/application/auth/verification_code_service.py +++ b/packages/application/auth/verification_code_service.py @@ -9,7 +9,7 @@ from __future__ import annotations import logging import re -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Optional from packages.domain.verification_code import VerificationCode @@ -162,7 +162,7 @@ class VerificationCodeService: # 检查冷却时间 latest = self.repo.find_latest(recipient, code_type) if latest: - elapsed = (datetime.now(timezone.utc) - latest.created_at).total_seconds() + elapsed = (datetime.now(UTC) - latest.created_at).total_seconds() if elapsed < self.resend_cooldown: wait = int(self.resend_cooldown - elapsed) return False, wait diff --git a/packages/application/auth/wechat_sync_use_case.py b/packages/application/auth/wechat_sync_use_case.py index eae3b17e1..2baba7cde 100644 --- a/packages/application/auth/wechat_sync_use_case.py +++ b/packages/application/auth/wechat_sync_use_case.py @@ -10,7 +10,7 @@ """ import secrets -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Optional from uuid import uuid4 @@ -158,7 +158,7 @@ class WechatSyncUseCase: session_id = secrets.token_urlsafe(16) refresh_token = secrets.token_urlsafe(32) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) access_token_payload = { "sub": user.id, "sid": session_id, diff --git a/packages/application/common/pagination.py b/packages/application/common/pagination.py index 1219c3cf0..f7ac289b1 100644 --- a/packages/application/common/pagination.py +++ b/packages/application/common/pagination.py @@ -3,7 +3,7 @@ """ from math import ceil -from typing import Generic, List, TypeVar +from typing import Generic, TypeVar from pydantic import BaseModel, Field @@ -59,13 +59,13 @@ class PaginationMeta(BaseModel): class PaginatedResponse(BaseModel, Generic[T]): """分页响应""" - data: List[T] = Field(..., description="数据列表") + data: list[T] = Field(..., description="数据列表") pagination: PaginationMeta = Field(..., description="分页信息") @classmethod def create( cls, - data: List[T], + data: list[T], params: PaginationParams, total: int, ) -> "PaginatedResponse[T]": @@ -77,7 +77,7 @@ class PaginatedResponse(BaseModel, Generic[T]): def paginate( - items: List[T], + items: list[T], params: PaginationParams, ) -> PaginatedResponse[T]: """ diff --git a/packages/application/cosyvoice_service.py b/packages/application/cosyvoice_service.py index d2b9d4c63..c3ee745b4 100644 --- a/packages/application/cosyvoice_service.py +++ b/packages/application/cosyvoice_service.py @@ -14,8 +14,9 @@ from __future__ import annotations import logging import time +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Optional +from typing import Any, Optional import httpx diff --git a/packages/application/ingest_orphan_cleanup.py b/packages/application/ingest_orphan_cleanup.py index 22fbb09a4..fbf14b192 100644 --- a/packages/application/ingest_orphan_cleanup.py +++ b/packages/application/ingest_orphan_cleanup.py @@ -16,8 +16,9 @@ celery 消息会丢失(transcode 队列 worker_prefetch_multiplier=1,消息 from __future__ import annotations import logging -from datetime import datetime, timedelta, timezone -from typing import Any, Callable +from collections.abc import Callable +from datetime import UTC, datetime, timedelta +from typing import Any logger = logging.getLogger(__name__) @@ -39,7 +40,7 @@ _TERMINAL_ASSET_STATUSES = ("ready", "error", "deleted") def _now() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) def cleanup_stale_ingest_jobs( @@ -86,7 +87,7 @@ def cleanup_stale_ingest_jobs( for job_model in stale_jobs: ref_time = job_model.updated_at or job_model.created_at if ref_time.tzinfo is None: # SQLite 读回 naive datetime 的防御 - ref_time = ref_time.replace(tzinfo=timezone.utc) + ref_time = ref_time.replace(tzinfo=UTC) stale_minutes = int((now - ref_time).total_seconds() // 60) job_model.status = "failed" job_model.error_message = ( diff --git a/packages/application/recipe/commands.py b/packages/application/recipe/commands.py index 97757a2d4..7350d62b5 100644 --- a/packages/application/recipe/commands.py +++ b/packages/application/recipe/commands.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass @@ -21,7 +21,7 @@ class CreateRecipeCommand: description: str = "" template_id: str = "" generation_params: dict = field(default_factory=dict) - items: List[RecipeItemCommand] = field(default_factory=list) + items: list[RecipeItemCommand] = field(default_factory=list) metadata_: dict = field(default_factory=dict) @@ -33,5 +33,5 @@ class UpdateRecipeCommand: description: Optional[str] = None template_id: Optional[str] = None generation_params: Optional[dict] = None - items: Optional[List[RecipeItemCommand]] = None + items: Optional[list[RecipeItemCommand]] = None metadata_: Optional[dict] = None diff --git a/packages/application/recipe/use_cases.py b/packages/application/recipe/use_cases.py index e28e6c331..9766ef90f 100644 --- a/packages/application/recipe/use_cases.py +++ b/packages/application/recipe/use_cases.py @@ -4,7 +4,7 @@ from __future__ import annotations import uuid from dataclasses import dataclass -from typing import List, Optional +from typing import Optional from packages.adapters.sqlalchemy_impl.recipe_repository import SQLAlchemyRecipeRepository from packages.application.recipe.commands import ( @@ -75,7 +75,7 @@ class ListRecipesUseCase: *, skip: int = 0, limit: int = 50, - ) -> List[Recipe]: + ) -> list[Recipe]: return self.repository.list_by_user(user_id, skip=skip, limit=limit) @@ -144,7 +144,7 @@ class UseRecipeResult: """使用配方的结果""" recipe: Recipe - warnings: List[MissingAssetWarning] + warnings: list[MissingAssetWarning] class UseRecipeUseCase: @@ -173,7 +173,7 @@ class UseRecipeUseCase: raise NotFoundError(f"Recipe {recipe_id} not found") # 3. 校验引用的素材/标题/配音是否仍存在 - warnings: List[MissingAssetWarning] = [] + warnings: list[MissingAssetWarning] = [] # Note: 实际项目中这里需要注入 asset/title/voice repository # 来校验每个 item 是否仍然存在。当前版本返回空警告列表, # 由调用方(路由层)决定是否传入额外的校验逻辑。 diff --git a/packages/application/template/commands.py b/packages/application/template/commands.py index 90dbea846..de8b68799 100755 --- a/packages/application/template/commands.py +++ b/packages/application/template/commands.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass @@ -20,12 +20,12 @@ class CreateTemplateCommand: name: str mode: str category: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) title_config: dict = field(default_factory=dict) subtitle_config: dict = field(default_factory=dict) bgm_config: dict = field(default_factory=dict) estimated_duration: float = 0.0 - segments: List[SegmentCommand] = field(default_factory=list) + segments: list[SegmentCommand] = field(default_factory=list) @dataclass @@ -35,12 +35,12 @@ class UpdateTemplateCommand: name: Optional[str] = None mode: Optional[str] = None category: Optional[str] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None title_config: Optional[dict] = None subtitle_config: Optional[dict] = None bgm_config: Optional[dict] = None estimated_duration: Optional[float] = None - segments: Optional[List[SegmentCommand]] = None + segments: Optional[list[SegmentCommand]] = None @dataclass diff --git a/packages/application/template/use_cases.py b/packages/application/template/use_cases.py index e4e537c2f..52f6a51dd 100755 --- a/packages/application/template/use_cases.py +++ b/packages/application/template/use_cases.py @@ -4,7 +4,7 @@ from __future__ import annotations import uuid from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional from packages.application.template.commands import ( CopyTemplateCommand, @@ -37,7 +37,7 @@ class ValidateResult: """模板校验结果.""" template: Template - warnings: List[GenerateWarning] = field(default_factory=list) + warnings: list[GenerateWarning] = field(default_factory=list) # ── Template CRUD ── @@ -95,7 +95,7 @@ class ListTemplatesUseCase: skip: int = 0, limit: int = 50, filter: Optional[ListTemplatesFilter] = None, - ) -> List[Template]: + ) -> list[Template]: if filter is None: return self.repository.list_by_user(user_id, skip=skip, limit=limit) return self.repository.list_by_user( @@ -232,7 +232,7 @@ class ValidateTemplateUseCase: if template is None: raise NotFoundError(f"Template {command.template_id} not found") - warnings: List[GenerateWarning] = [] + warnings: list[GenerateWarning] = [] # 业务规则 1: one_take 必须恰好 1 个片段 if template.mode == EditingMode.ONE_TAKE.value: @@ -291,7 +291,7 @@ class ListCategoriesUseCase: def __init__(self, repository: TemplateRepositoryPort) -> None: self.repository = repository - def execute(self, user_id: str) -> List[TemplateCategory]: + def execute(self, user_id: str) -> list[TemplateCategory]: return self.repository.list_categories(user_id) @@ -310,7 +310,7 @@ class ListTagsUseCase: def __init__(self, repository: TemplateRepositoryPort) -> None: self.repository = repository - def execute(self, user_id: str) -> List[str]: + def execute(self, user_id: str) -> list[str]: return self.repository.list_tags(user_id) diff --git a/packages/application/title_library/commands.py b/packages/application/title_library/commands.py index 7615fe3eb..af83ea03e 100755 --- a/packages/application/title_library/commands.py +++ b/packages/application/title_library/commands.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass @@ -13,7 +13,7 @@ class CreateTitleLibraryCommand: text: str category: str = "default" description: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) metadata_: dict = field(default_factory=dict) @@ -25,7 +25,7 @@ class UpdateTitleLibraryCommand: text: Optional[str] = None category: Optional[str] = None description: Optional[str] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None is_active: Optional[bool] = None metadata_: Optional[dict] = None @@ -41,4 +41,4 @@ class IncrementTitleUsageCommand: class PickTitleCommand: user_id: str category: Optional[str] = None - exclude_ids: List[str] = field(default_factory=list) + exclude_ids: list[str] = field(default_factory=list) diff --git a/packages/application/title_library/use_cases.py b/packages/application/title_library/use_cases.py index 889b598ef..ba8b34cb1 100755 --- a/packages/application/title_library/use_cases.py +++ b/packages/application/title_library/use_cases.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from typing import List, Optional +from typing import Optional from packages.adapters.sqlalchemy_impl.title_library_repository import SQLAlchemyTitleLibraryRepository from packages.application.title_library.commands import ( @@ -28,7 +28,7 @@ class ListTitleLibraryUseCase: category: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[TitleLibraryItem]: + ) -> list[TitleLibraryItem]: return self.repository.list_by_user(user_id, category=category, skip=skip, limit=limit) diff --git a/packages/application/tts_job/use_cases.py b/packages/application/tts_job/use_cases.py index 07cf9dd98..a2e83c605 100644 --- a/packages/application/tts_job/use_cases.py +++ b/packages/application/tts_job/use_cases.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from packages.application.tts_job.exceptions import TTSJobNotFoundError from packages.domain.tts_job import TTSJob @@ -58,7 +58,7 @@ class ListTTSJobsUseCase: status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> tuple[List[TTSJob], int]: + ) -> tuple[list[TTSJob], int]: items = self.repository.list_by_user(user_id, status=status, limit=limit, offset=skip) total = self.repository.count_by_user(user_id, status=status) return items, total diff --git a/packages/application/video_share/use_cases.py b/packages/application/video_share/use_cases.py index 01a8fe5d5..22368729f 100755 --- a/packages/application/video_share/use_cases.py +++ b/packages/application/video_share/use_cases.py @@ -3,8 +3,8 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime +from typing import Optional from packages.application.video_share.commands import ( CreateShareCommand, @@ -143,7 +143,7 @@ class ListSharesByVideoUseCase: def __init__(self, share_repository: VideoShareRepositoryPort) -> None: self.share_repo = share_repository - def execute(self, video_id: str, user_id: str) -> List[VideoShare]: + def execute(self, video_id: str, user_id: str) -> list[VideoShare]: return self.share_repo.list_by_video(video_id, user_id) @@ -153,7 +153,7 @@ class ListSharesByUserUseCase: def __init__(self, share_repository: VideoShareRepositoryPort) -> None: self.share_repo = share_repository - def execute(self, user_id: str, skip: int = 0, limit: int = 20) -> tuple[List[VideoShare], int]: + def execute(self, user_id: str, skip: int = 0, limit: int = 20) -> tuple[list[VideoShare], int]: items = self.share_repo.list_by_user(user_id, skip=skip, limit=limit) total = self.share_repo.count_by_user(user_id) return items, total @@ -181,7 +181,7 @@ class UpdateShareUseCase: # expires_at=None表示不修改 if command.expires_at is not None: - if command.expires_at < datetime.now(timezone.utc): + if command.expires_at < datetime.now(UTC): raise ValueError("expires_at cannot be in the past") share.expires_at = command.expires_at diff --git a/packages/application/voice_clone/use_cases.py b/packages/application/voice_clone/use_cases.py index b3079c09d..b8479d640 100644 --- a/packages/application/voice_clone/use_cases.py +++ b/packages/application/voice_clone/use_cases.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional from packages.domain.voice_clone_profile import VoiceCloneProfile from packages.ports.voice_clone_profile_repository import VoiceCloneProfileRepository @@ -66,7 +66,7 @@ class ListVoiceClonesUseCase: status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> tuple[List[VoiceCloneProfile], int]: + ) -> tuple[list[VoiceCloneProfile], int]: items = self.repository.list_by_user(user_id, status=status, limit=limit, offset=skip) total = self.repository.count_by_user(user_id, status=status) return items, total diff --git a/packages/application/voice_library/commands.py b/packages/application/voice_library/commands.py index 1cfde76e6..2a00379bc 100644 --- a/packages/application/voice_library/commands.py +++ b/packages/application/voice_library/commands.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass @@ -19,7 +19,7 @@ class CreateVoiceLibraryCommand: file_size: int = 0 status: str = "completed" project_id: Optional[str] = None - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) metadata_: dict = field(default_factory=dict) @@ -36,5 +36,5 @@ class UpdateVoiceLibraryCommand: duration: Optional[float] = None file_size: Optional[int] = None status: Optional[str] = None - tags: Optional[List[str]] = None + tags: Optional[list[str]] = None metadata_: Optional[dict] = None diff --git a/packages/application/voice_library/use_cases.py b/packages/application/voice_library/use_cases.py index 2255699f6..9813cfb5d 100644 --- a/packages/application/voice_library/use_cases.py +++ b/packages/application/voice_library/use_cases.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from typing import List, Optional +from typing import Optional from packages.adapters.sqlalchemy_impl.voice_library_repository import SQLAlchemyVoiceLibraryRepository from packages.application.voice_library.commands import ( @@ -26,7 +26,7 @@ class ListVoiceLibraryUseCase: status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> tuple[List[VoiceLibraryItem], int]: + ) -> tuple[list[VoiceLibraryItem], int]: """返回 (items, total_count),避免调用方再单独查一次 count。""" items = self.repository.list_by_user(user_id, status=status, skip=skip, limit=limit) total = ( diff --git a/packages/config/base.py b/packages/config/base.py index 37ae13778..579e4fe45 100755 --- a/packages/config/base.py +++ b/packages/config/base.py @@ -6,7 +6,7 @@ API 和 Worker 各自的 Settings 类继承本类,只追加服务特有字段 """ import os -from typing import Optional, Type, TypeVar +from typing import Optional, TypeVar from pydantic_settings import BaseSettings, SettingsConfigDict @@ -105,7 +105,7 @@ def _get_env_file() -> str: return env_file if os.path.exists(env_file) else ".env" -def get_cached_settings(settings_class: Type[T], cache_key: Optional[str] = None) -> T: +def get_cached_settings(settings_class: type[T], cache_key: Optional[str] = None) -> T: """统一的 Settings 单例获取函数。 所有服务都通过这个函数获取配置,消除重复的单例实现。 diff --git a/packages/domain/auth/email_service.py b/packages/domain/auth/email_service.py index b1ace6460..36070338b 100755 --- a/packages/domain/auth/email_service.py +++ b/packages/domain/auth/email_service.py @@ -8,7 +8,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import List, Optional +from typing import Optional @dataclass @@ -34,8 +34,8 @@ class EmailServicePort(ABC): subject: str, html_body: str, text_body: Optional[str] = None, - cc: Optional[List[str]] = None, - bcc: Optional[List[str]] = None, + cc: Optional[list[str]] = None, + bcc: Optional[list[str]] = None, ) -> tuple[bool, Optional[str]]: """发送邮件""" ... diff --git a/packages/domain/auth/jwt_service.py b/packages/domain/auth/jwt_service.py index 29caf1cb1..ce52c20f1 100755 --- a/packages/domain/auth/jwt_service.py +++ b/packages/domain/auth/jwt_service.py @@ -7,7 +7,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Any, Dict +from typing import Any class JWTServicePort(ABC): @@ -18,7 +18,7 @@ class JWTServicePort(ABC): self, user_id: str, role: str = "", - additional_claims: Dict[str, Any] | None = None, + additional_claims: dict[str, Any] | None = None, ) -> str: """创建 access_token""" ... @@ -29,16 +29,16 @@ class JWTServicePort(ABC): ... @abstractmethod - def verify_token(self, token: str) -> Dict[str, Any]: + def verify_token(self, token: str) -> dict[str, Any]: """验证任意 Token""" ... @abstractmethod - def verify_access_token(self, token: str) -> Dict[str, Any]: + def verify_access_token(self, token: str) -> dict[str, Any]: """验证 access_token""" ... @abstractmethod - def verify_refresh_token(self, token: str) -> Dict[str, Any]: + def verify_refresh_token(self, token: str) -> dict[str, Any]: """验证 refresh_token""" ... diff --git a/packages/domain/auth/password_hasher.py b/packages/domain/auth/password_hasher.py index bc59ff19d..c32de07a6 100755 --- a/packages/domain/auth/password_hasher.py +++ b/packages/domain/auth/password_hasher.py @@ -7,7 +7,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import Optional, Tuple +from typing import Optional class PasswordHasherPort(ABC): @@ -33,6 +33,6 @@ class PasswordValidatorPort(ABC): """密码强度验证端口(抽象接口)""" @abstractmethod - def validate(self, password: str) -> Tuple[bool, Optional[str]]: + def validate(self, password: str) -> tuple[bool, Optional[str]]: """验证密码强度""" ... diff --git a/packages/domain/classification.py b/packages/domain/classification.py index c424aac31..ea8634bb7 100644 --- a/packages/domain/classification.py +++ b/packages/domain/classification.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -80,8 +80,8 @@ class ClassificationJob: classification: str = "" # Result: AssetClassification value confidence: float = 0.0 # 0.0 - 1.0 error_message: str = "" - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/cover_template.py b/packages/domain/cover_template.py index 60cc5da93..857bde360 100644 --- a/packages/domain/cover_template.py +++ b/packages/domain/cover_template.py @@ -1,7 +1,7 @@ """封面模板领域实体。""" from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import uuid4 @@ -16,8 +16,8 @@ class CoverTemplate: thumbnail_url: str is_system: bool config: dict[str, Any] - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create_system( @@ -75,4 +75,4 @@ class CoverTemplate: self.config = config if thumbnail_url is not None: self.thumbnail_url = thumbnail_url - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) diff --git a/packages/domain/daily_usage_record.py b/packages/domain/daily_usage_record.py index ba4ccb197..b396db666 100644 --- a/packages/domain/daily_usage_record.py +++ b/packages/domain/daily_usage_record.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from uuid import uuid4 @@ -12,7 +12,7 @@ class DailyUsageRecord: usage_date: date usage_type: str = "free_clip" count: int = 0 - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/duplication.py b/packages/domain/duplication.py index cc6c6d1df..ef77bee95 100644 --- a/packages/domain/duplication.py +++ b/packages/domain/duplication.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import uuid4 @@ -69,8 +69,8 @@ class DuplicationRecord: video_fingerprint: dict[str, Any] | None = None error_message: str = "" segments: list[DuplicateSegment] = field(default_factory=list) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -99,7 +99,7 @@ class DuplicationRecord: def mark_processing(self) -> None: self.status = "processing" - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_completed( self, @@ -118,12 +118,12 @@ class DuplicationRecord: self.segments = segments self.visual_similarity = visual_similarity self.match_count = match_count - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_failed(self, error_message: str) -> None: self.status = "failed" self.error_message = error_message - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def can_retry(self) -> bool: """ @@ -151,4 +151,4 @@ class DuplicationRecord: self.error_message = "" self.segments = [] self.video_fingerprint = None - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) diff --git a/packages/domain/edit_plan.py b/packages/domain/edit_plan.py index 0f8175190..f12c45359 100755 --- a/packages/domain/edit_plan.py +++ b/packages/domain/edit_plan.py @@ -4,7 +4,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -68,8 +68,8 @@ class EditPlan: project_id: str = "" created_by_user_id: str = "" config: dict[str, Any] = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -106,39 +106,39 @@ class EditPlan: if self.status != EditPlanStatus.DRAFT: raise ValueError(f"只有 draft 状态的计划可以开始编辑,当前状态: {self.status}") self.status = EditPlanStatus.EDITING - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def start_rendering(self) -> None: """开始渲染""" if self.status != EditPlanStatus.EDITING: raise ValueError(f"只有 editing 状态的计划可以开始渲染,当前状态: {self.status}") self.status = EditPlanStatus.RENDERING - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_completed(self) -> None: """标记为完成""" if self.status != EditPlanStatus.RENDERING: raise ValueError(f"只有 rendering 状态的计划可以标记完成,当前状态: {self.status}") self.status = EditPlanStatus.COMPLETED - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_failed(self) -> None: """标记为失败""" if self.status != EditPlanStatus.RENDERING: raise ValueError(f"只有 rendering 状态的计划可以标记失败,当前状态: {self.status}") self.status = EditPlanStatus.FAILED - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def resume_editing(self) -> None: """重新进入编辑状态(完成/失败后重新编辑)""" if self.status not in (EditPlanStatus.COMPLETED, EditPlanStatus.FAILED): raise ValueError(f"只有 completed/failed 状态的计划可以重新编辑,当前状态: {self.status}") self.status = EditPlanStatus.EDITING - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def reset_to_draft(self) -> None: """重置为草稿状态(仅从 failed 状态可重置)""" if self.status != EditPlanStatus.FAILED: raise ValueError(f"只有 failed 状态的计划可以重置,当前状态: {self.status}") self.status = EditPlanStatus.DRAFT - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) diff --git a/packages/domain/edit_plan_clip.py b/packages/domain/edit_plan_clip.py index a913b988a..d62ed13fd 100755 --- a/packages/domain/edit_plan_clip.py +++ b/packages/domain/edit_plan_clip.py @@ -8,7 +8,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -73,8 +73,8 @@ class EditPlanClip: playback_speed: float = 1.0 # 0 或 1.0 表示原速,范围 0.25~4.0 status: EditPlanClipStatus = EditPlanClipStatus.PENDING config: dict[str, Any] = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -139,28 +139,28 @@ class EditPlanClip: self.asset_id = asset_id.strip() if start_time is not None and start_time >= 0: self.start_time = start_time - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_ready(self) -> None: """标记为就绪""" if self.status != EditPlanClipStatus.PENDING: raise ValueError(f"只有 pending 状态的片段可以标记就绪,当前状态: {self.status}") self.status = EditPlanClipStatus.READY - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_rendered(self) -> None: """标记为已渲染""" if self.status != EditPlanClipStatus.READY: raise ValueError(f"只有 ready 状态的片段可以标记已渲染,当前状态: {self.status}") self.status = EditPlanClipStatus.RENDERED - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def mark_failed(self) -> None: """标记为失败""" if self.status != EditPlanClipStatus.READY: raise ValueError(f"只有 ready 状态的片段可以标记失败,当前状态: {self.status}") self.status = EditPlanClipStatus.FAILED - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) @property def end_time(self) -> float: diff --git a/packages/domain/edit_template.py b/packages/domain/edit_template.py index 57d0871c9..99d65e783 100755 --- a/packages/domain/edit_template.py +++ b/packages/domain/edit_template.py @@ -4,7 +4,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -52,8 +52,8 @@ class EditTemplate: sort_weight: int = 0 status: EditTemplateStatus = EditTemplateStatus.ACTIVE version: int = 1 - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -94,12 +94,12 @@ class EditTemplate: def activate(self) -> None: """激活模板""" self.status = EditTemplateStatus.ACTIVE - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def deactivate(self) -> None: """停用模板""" self.status = EditTemplateStatus.INACTIVE - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) @property def is_active(self) -> bool: @@ -109,4 +109,4 @@ class EditTemplate: def bump_version(self) -> None: """版本号+1,发布时调用""" self.version += 1 - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) diff --git a/packages/domain/entities.py b/packages/domain/entities.py index cb5329457..2c5c8a002 100755 --- a/packages/domain/entities.py +++ b/packages/domain/entities.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -60,7 +60,7 @@ class User: # 资料是否已完善(微信新用户首次设置昵称后置 True;邮箱注册默认 True) profile_completed: bool = True - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @dataclass(slots=True) @@ -71,7 +71,7 @@ class Project: description: str = "" shared_users: list[str] = field(default_factory=list) # 被共享的用户 ID 列表 is_default: bool = False # 是否为用户的默认项目(小程序自动创建),DB 部分唯一索引保证每人至多一个 - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create(cls, owner_user_id: str, name: str, description: str = "", is_default: bool = False) -> "Project": @@ -108,8 +108,8 @@ class AssetLibrary: kind: AssetLibraryKind asset_count: int = 0 total_size: int = 0 - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -181,8 +181,8 @@ class Asset: client_upload_id: str = "" metadata: dict[str, Any] = field(default_factory=dict) tag_ids: list[str] = field(default_factory=list) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @property def file_type(self) -> str: @@ -253,14 +253,14 @@ class Asset: raise ValueError("标签 ID 不能为空") if clean_id not in self.tag_ids: self.tag_ids.append(clean_id) - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def remove_tag(self, tag_id: str) -> None: """删除标签 ID。如果标签不存在,不报错(幂等性)。""" clean_id = tag_id.strip() if clean_id in self.tag_ids: self.tag_ids.remove(clean_id) - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) @dataclass(slots=True) @@ -275,8 +275,8 @@ class IngestJob: file_hash: str = "" asset_id: str = "" celery_task_id: str = "" - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/filter_presets.py b/packages/domain/filter_presets.py index f2344a391..a2317d4f1 100755 --- a/packages/domain/filter_presets.py +++ b/packages/domain/filter_presets.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass(frozen=True) @@ -18,7 +18,7 @@ class FilterPreset: name: str category: str # 分类:basic / cinematic / vintage / bw / style description: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) # FFmpeg eq 滤镜参数(基准值,实际应用时乘以强度系数) brightness: float = 0.0 # -1.0 ~ 1.0 contrast: float = 1.0 # 0.0 ~ 2.0,1.0为原值 @@ -34,7 +34,7 @@ class FilterPreset: # ── 预设库清单 ──────────────────────────────────────────────────────────────── -FILTER_PRESET_LIBRARY: List[FilterPreset] = [ +FILTER_PRESET_LIBRARY: list[FilterPreset] = [ # ── 基础 basic ───────────────────────────────────────────────────── FilterPreset( id="filter_none", @@ -281,7 +281,7 @@ def list_filter_presets( *, category: Optional[str] = None, keyword: Optional[str] = None, -) -> List[FilterPreset]: +) -> list[FilterPreset]: """筛选滤镜预设列表 Args: diff --git a/packages/domain/generated_video.py b/packages/domain/generated_video.py index ec4bbd9d0..c43f6f377 100755 --- a/packages/domain/generated_video.py +++ b/packages/domain/generated_video.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import uuid4 @@ -29,8 +29,8 @@ class GeneratedVideo: duplicate_rate: float | None = None match_count: int | None = None visual_similarity: float | None = None - generated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + generated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/generation_task.py b/packages/domain/generation_task.py index ad00c3a70..30897bb4e 100755 --- a/packages/domain/generation_task.py +++ b/packages/domain/generation_task.py @@ -11,7 +11,7 @@ from __future__ import annotations import json import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -124,8 +124,8 @@ class GenerationTask: title_config: dict = field(default_factory=dict) extra_meta: dict = field(default_factory=dict) logs: str = "[]" - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -245,7 +245,7 @@ class GenerationTask: ValueError: 当前状态不允许转换到 running """ self.transition_to(GenerationTaskStatus.RUNNING) - self.started_at = datetime.now(timezone.utc) + self.started_at = datetime.now(UTC) self.error_message = "" def mark_completed(self, result_count: int = 1) -> None: @@ -260,7 +260,7 @@ class GenerationTask: ValueError: 当前状态不允许转换到 completed """ self.transition_to(GenerationTaskStatus.COMPLETED) - self.completed_at = datetime.now(timezone.utc) + self.completed_at = datetime.now(UTC) self.progress = 100.0 self.result_count = result_count self.error_message = "" @@ -279,14 +279,14 @@ class GenerationTask: """ self.transition_to(GenerationTaskStatus.FAILED) self.error_message = error_message - self.completed_at = datetime.now(timezone.utc) + self.completed_at = datetime.now(UTC) if error_info is not None: self.error_info = error_info else: self.error_info = { "error_type": "UnknownError", "message": error_message, - "failed_at": datetime.now(timezone.utc).isoformat(), + "failed_at": datetime.now(UTC).isoformat(), } def mark_cancelled(self) -> None: @@ -298,7 +298,7 @@ class GenerationTask: ValueError: 当前状态不允许转换到 cancelled """ self.transition_to(GenerationTaskStatus.CANCELLED) - self.completed_at = datetime.now(timezone.utc) + self.completed_at = datetime.now(UTC) def mark_confirmed( self, @@ -325,7 +325,7 @@ class GenerationTask: self.title_config = dict(title_config) if extra_meta: self.extra_meta.update(extra_meta) - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) # ── 日志辅助 ──────────────────────────────────────────────────────────── @@ -345,7 +345,7 @@ class GenerationTask: except (json.JSONDecodeError, TypeError): entries = [] entry = { - "ts": datetime.now(timezone.utc).isoformat(), + "ts": datetime.now(UTC).isoformat(), "level": level, "stage": stage, "message": message, diff --git a/packages/domain/job.py b/packages/domain/job.py index 3f2262d35..0daa5b9ec 100755 --- a/packages/domain/job.py +++ b/packages/domain/job.py @@ -12,7 +12,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -110,8 +110,8 @@ class Job: created_by_user_id: str = "" started_at: datetime | None = None completed_at: datetime | None = None - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -192,7 +192,7 @@ class Job: f"允许: {{{', '.join(s.value for s in allowed)}}}" ) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) self.status = new_status self.updated_at = now @@ -242,7 +242,7 @@ class Job: self.progress = progress if stage: self.current_stage = stage - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def prepare_retry(self) -> None: """准备重试:重置状态为 pending。 diff --git a/packages/domain/plan_generator_utils.py b/packages/domain/plan_generator_utils.py index e7530472f..2ba0abeb8 100755 --- a/packages/domain/plan_generator_utils.py +++ b/packages/domain/plan_generator_utils.py @@ -13,7 +13,7 @@ from __future__ import annotations import logging import random -from typing import Callable, List +from collections.abc import Callable logger = logging.getLogger(__name__) @@ -162,8 +162,8 @@ def extract_scene_points_from_metadata(metadata: object) -> list[float] | None: def distribute_assets( - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], editing_mode: str, *, random_selection: bool = False, @@ -246,8 +246,8 @@ def _resolve_start_time( def _distribute_one_take( - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], asset_durations: dict[str, float] | None = None, asset_scene_points: dict[str, list[float]] | None = None, external_used_segments: dict[str, list[tuple[float, float]]] | None = None, @@ -272,8 +272,8 @@ def _distribute_one_take( def _distribute_pip( - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], asset_durations: dict[str, float] | None = None, asset_scene_points: dict[str, list[float]] | None = None, external_used_segments: dict[str, list[tuple[float, float]]] | None = None, @@ -314,8 +314,8 @@ def _distribute_pip( def _distribute_voice_over( - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], asset_durations: dict[str, float] | None = None, asset_scene_points: dict[str, list[float]] | None = None, external_used_segments: dict[str, list[tuple[float, float]]] | None = None, @@ -340,8 +340,8 @@ def _distribute_voice_over( def _distribute_voice_pip( - clips: List[EditPlanClip], - asset_ids: List[str], + clips: list[EditPlanClip], + asset_ids: list[str], asset_durations: dict[str, float] | None = None, asset_scene_points: dict[str, list[float]] | None = None, external_used_segments: dict[str, list[tuple[float, float]]] | None = None, @@ -509,7 +509,7 @@ def _calc_random_start_time( def map_clip_types_for_mode( - clips: List[EditPlanClip], + clips: list[EditPlanClip], editing_mode: str, ) -> None: """将 MAIN 类型片段按 editing_mode 映射为对应角色类型. @@ -556,7 +556,7 @@ def generate_default_clips( plan_id: str, editing_mode: str, asset_count: int, -) -> List[EditPlanClip]: +) -> list[EditPlanClip]: """无 clip_configs 时,根据 editing_mode 生成默认 clip 结构. - ONE_TAKE: N 个 main clips(N = asset_count,至少1个) @@ -573,7 +573,7 @@ def generate_default_clips( List[EditPlanClip]: 生成的默认剪辑片段列表 """ n = max(asset_count, 1) - clips: List[EditPlanClip] = [] + clips: list[EditPlanClip] = [] order = 0 if editing_mode == EditingMode.PIP.value: @@ -668,8 +668,8 @@ def generate_default_clips( def create_clips_from_configs( plan_id: str, - clip_configs: List[TemplateClipConfig], -) -> List[EditPlanClip]: + clip_configs: list[TemplateClipConfig], +) -> list[EditPlanClip]: """从 TemplateClipConfig 列表创建 EditPlanClip 列表. Args: @@ -679,7 +679,7 @@ def create_clips_from_configs( Returns: List[EditPlanClip]: 创建的剪辑片段列表(按 order 排序) """ - clips: List[EditPlanClip] = [] + clips: list[EditPlanClip] = [] # 按 order 排序 sorted_configs = sorted(clip_configs, key=lambda c: c.order) diff --git a/packages/domain/points_account.py b/packages/domain/points_account.py index d6374c328..3654aeb79 100644 --- a/packages/domain/points_account.py +++ b/packages/domain/points_account.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 @@ -12,8 +12,8 @@ class PointsAccount: balance: int = 0 total_earned: int = 0 total_spent: int = 0 - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create(cls, user_id: str) -> "PointsAccount": diff --git a/packages/domain/points_order.py b/packages/domain/points_order.py index 0666179c8..52b2e6c2d 100644 --- a/packages/domain/points_order.py +++ b/packages/domain/points_order.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 @@ -19,7 +19,7 @@ class PointsOrder: payment_method: str | None = None payment_id: str | None = None paid_at: datetime | None = None - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/points_service.py b/packages/domain/points_service.py index d6caba734..1a8706d9c 100644 --- a/packages/domain/points_service.py +++ b/packages/domain/points_service.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import Any from sqlalchemy.orm import Session @@ -328,7 +328,7 @@ class PointsService: def _daily_key(self, user_id: str) -> str: """生成 Redis 每日额度 key。格式: daily_usage:{user_id}:{YYYYMMDD}:free_clip""" - today = datetime.now(timezone.utc).strftime("%Y%m%d") + today = datetime.now(UTC).strftime("%Y%m%d") return f"daily_usage:{user_id}:{today}:free_clip" def check_daily_free_clip(self, user_id: str, db: Session) -> bool: @@ -349,7 +349,7 @@ class PointsService: # 降级到 DB _, _, _, DailyUsageRecordModel, _ = _get_models() - today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) record = ( db.query(DailyUsageRecordModel) .filter( @@ -384,7 +384,7 @@ class PointsService: # 降级/兜底到 DB(upsert 语义) _, _, _, DailyUsageRecordModel, _ = _get_models() - today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) record = ( db.query(DailyUsageRecordModel) @@ -403,7 +403,7 @@ class PointsService: id=uuid.uuid4().hex, user_id=user_id, usage_type="free_clip", - usage_date=datetime.now(timezone.utc), + usage_date=datetime.now(UTC), count=1, ) db.add(record) @@ -431,7 +431,7 @@ class PointsService: if used == 0: # 从 DB 查 _, _, _, DailyUsageRecordModel, _ = _get_models() - today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + today_start = datetime.now(UTC).replace(hour=0, minute=0, second=0, microsecond=0) record = ( db.query(DailyUsageRecordModel) .filter( @@ -443,7 +443,7 @@ class PointsService: ) used = record.count if record else 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) tomorrow = (now + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) return { @@ -529,7 +529,7 @@ class PointsService: # 更新订单状态 order.status = "paid" order.payment_id = payment_id - order.paid_at = datetime.now(timezone.utc) + order.paid_at = datetime.now(UTC) if order.order_type == "points": # 发放积分 @@ -550,7 +550,7 @@ class PointsService: user = db.query(UserModel).filter(UserModel.id == order.user_id).first() if user: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) current_expires = user.member_expires_at or now if current_expires < now: current_expires = now diff --git a/packages/domain/points_transaction.py b/packages/domain/points_transaction.py index 93defbbc6..194f6aca3 100644 --- a/packages/domain/points_transaction.py +++ b/packages/domain/points_transaction.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 @@ -16,7 +16,7 @@ class PointsTransaction: balance_after: int description: str = "" ref_id: str = "" - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/quota.py b/packages/domain/quota.py index 0ea9e573b..1f842df8e 100644 --- a/packages/domain/quota.py +++ b/packages/domain/quota.py @@ -14,7 +14,7 @@ from __future__ import annotations from dataclasses import dataclass, field from enum import Enum -from typing import Dict, List, Optional +from typing import Optional class QuotaDimension(str, Enum): @@ -39,7 +39,7 @@ class QuotaTier: """一个套餐等级的配额定义""" name: str - limits: Dict[str, float] = field(default_factory=dict) + limits: dict[str, float] = field(default_factory=dict) def get_limit(self, dimension: str) -> float: """获取指定维度的配额限制,未定义返回 0""" @@ -51,7 +51,7 @@ class QuotaTier: # 三个套餐等级的配额定义 -QUOTA_TIERS: Dict[str, QuotaTier] = { +QUOTA_TIERS: dict[str, QuotaTier] = { "free": QuotaTier( name="free", limits={ @@ -142,8 +142,8 @@ class QuotaRegistry: """ def __init__(self) -> None: - self._dimensions: Dict[str, str] = {} # dimension_name -> description - self._tiers: Dict[str, QuotaTier] = dict(QUOTA_TIERS) + self._dimensions: dict[str, str] = {} # dimension_name -> description + self._tiers: dict[str, QuotaTier] = dict(QUOTA_TIERS) # 初始化内置维度 for dim in QuotaDimension: @@ -153,7 +153,7 @@ class QuotaRegistry: self, dimension: str, description: str, - default_limits: Optional[Dict[str, float]] = None, + default_limits: Optional[dict[str, float]] = None, ) -> None: """注册新的配额维度 @@ -188,11 +188,11 @@ class QuotaRegistry: return 0 return tier.get_limit(dimension) - def list_dimensions(self) -> Dict[str, str]: + def list_dimensions(self) -> dict[str, str]: """列出所有已注册的配额维度""" return dict(self._dimensions) - def list_tiers(self) -> List[str]: + def list_tiers(self) -> list[str]: """列出所有套餐等级""" return list(self._tiers.keys()) @@ -240,8 +240,8 @@ class QuotaChecker: def check_multiple( self, plan_name: str, - usage: Dict[str, float], - ) -> List[QuotaCheckResult]: + usage: dict[str, float], + ) -> list[QuotaCheckResult]: """批量检查多个维度的配额""" return [self.check(plan_name, dim, used) for dim, used in usage.items()] diff --git a/packages/domain/recipe.py b/packages/domain/recipe.py index 1b11134d7..f88dc8669 100644 --- a/packages/domain/recipe.py +++ b/packages/domain/recipe.py @@ -3,8 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import List +from datetime import UTC, datetime @dataclass @@ -29,8 +28,8 @@ class Recipe: description: str = "" template_id: str = "" generation_params: dict = field(default_factory=dict) - items: List[RecipeItem] = field(default_factory=list) + items: list[RecipeItem] = field(default_factory=list) is_active: bool = True metadata_: dict = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) diff --git a/packages/domain/smart_match.py b/packages/domain/smart_match.py index 8f07cc461..85ba709d3 100755 --- a/packages/domain/smart_match.py +++ b/packages/domain/smart_match.py @@ -12,7 +12,7 @@ from __future__ import annotations import math import random from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any # 素材选取排序时注入的随机噪声上限(分)。 @@ -67,7 +67,7 @@ def score_asset( (total_score, breakdown_dict) """ if now is None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) breakdown: dict[str, float] = {} @@ -100,7 +100,7 @@ def score_asset( recency = 50.0 else: if created_at.tzinfo is None: - created_at = created_at.replace(tzinfo=timezone.utc) + created_at = created_at.replace(tzinfo=UTC) age_days = max(0, (now - created_at).total_seconds() / 86400) recency = 100.0 * math.exp(-0.05 * age_days) # ~14天半衰期 recency_component = recency * 0.2 diff --git a/packages/domain/subtitle.py b/packages/domain/subtitle.py index 8e055f7e5..21d874d7d 100755 --- a/packages/domain/subtitle.py +++ b/packages/domain/subtitle.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List @dataclass @@ -26,7 +25,7 @@ class SubtitleSegment: text: str start: float # 秒 end: float # 秒 - words: List[SubtitleWord] = field(default_factory=list) + words: list[SubtitleWord] = field(default_factory=list) @property def duration(self) -> float: @@ -41,7 +40,7 @@ class SubtitleSegment: class SubtitleTimeline: """完整的字幕时间轴,由多个片段组成。""" - segments: List[SubtitleSegment] = field(default_factory=list) + segments: list[SubtitleSegment] = field(default_factory=list) language: str = "zh" # zh / en / ja 等 total_duration: float = 0.0 # 音频总时长(秒) @@ -58,8 +57,8 @@ class SubtitleTimeline: if len(self.segments) <= 1: return self - merged: List[SubtitleSegment] = [] - buffer: List[SubtitleSegment] = [] + merged: list[SubtitleSegment] = [] + buffer: list[SubtitleSegment] = [] for seg in self.segments: buffer.append(seg) @@ -85,7 +84,7 @@ class SubtitleTimeline: def split_long_segments(self, max_chars: int = 20) -> SubtitleTimeline: """拆分过长的字幕片段,按语义断句。""" - new_segments: List[SubtitleSegment] = [] + new_segments: list[SubtitleSegment] = [] for seg in self.segments: if seg.char_count <= max_chars: @@ -132,7 +131,7 @@ class SubtitleTimeline: ) @staticmethod - def _merge_segments(segments: List[SubtitleSegment]) -> SubtitleSegment: + def _merge_segments(segments: list[SubtitleSegment]) -> SubtitleSegment: if not segments: return SubtitleSegment(text="", start=0, end=0) return SubtitleSegment( @@ -143,13 +142,13 @@ class SubtitleTimeline: ) @staticmethod - def _split_text_by_punctuation(text: str, max_chars: int) -> List[str]: + def _split_text_by_punctuation(text: str, max_chars: int) -> list[str]: """按标点符号智能拆分长文本。""" # 中文常见句末标点 sentence_end = "。!?!?" clause_pause = ",;:,;:" - parts: List[str] = [] + parts: list[str] = [] current = "" for char in text: diff --git a/packages/domain/tag.py b/packages/domain/tag.py index b83b1e8b6..62236b3d9 100644 --- a/packages/domain/tag.py +++ b/packages/domain/tag.py @@ -1,7 +1,7 @@ """标签领域实体。""" from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 @@ -10,7 +10,7 @@ class Tag: id: str user_id: str name: str - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create(cls, user_id: str, name: str) -> "Tag": diff --git a/packages/domain/template.py b/packages/domain/template.py index 85d17d829..9b6b83c55 100644 --- a/packages/domain/template.py +++ b/packages/domain/template.py @@ -3,8 +3,8 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime +from typing import Optional @dataclass @@ -17,8 +17,8 @@ class TemplateSegment: duration_min: float duration_max: float material_type: Optional[str] = None # 仅 voice_over_mix: 人物/场景; 其他模式 null - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @dataclass @@ -30,15 +30,15 @@ class Template: name: str mode: str # EditingMode 枚举值: pip / voice_pip / one_take / voice_over category: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) title_config: dict = field(default_factory=dict) subtitle_config: dict = field(default_factory=dict) bgm_config: dict = field(default_factory=dict) estimated_duration: float = 0.0 - segments: List[TemplateSegment] = field(default_factory=list) + segments: list[TemplateSegment] = field(default_factory=list) is_active: bool = True - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @dataclass @@ -48,4 +48,4 @@ class TemplateCategory: id: str user_id: str name: str - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) diff --git a/packages/domain/template_clip_config.py b/packages/domain/template_clip_config.py index 454bf7e94..318ebede8 100644 --- a/packages/domain/template_clip_config.py +++ b/packages/domain/template_clip_config.py @@ -7,7 +7,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -62,8 +62,8 @@ class TemplateClipConfig: material_requirements: dict[str, Any] = field(default_factory=dict) transition_effect: TransitionEffect = TransitionEffect.CUT config: dict[str, Any] = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/template_version.py b/packages/domain/template_version.py index 4a0dbfad9..a34b71c3a 100755 --- a/packages/domain/template_version.py +++ b/packages/domain/template_version.py @@ -3,7 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import uuid4 @@ -25,7 +25,7 @@ class EditTemplateVersion: clip_configs: list[dict[str, Any]] = field(default_factory=list) change_note: str = "" published_by: str = "" - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( diff --git a/packages/domain/title_library.py b/packages/domain/title_library.py index f1e5ad3db..6d80ac7a9 100644 --- a/packages/domain/title_library.py +++ b/packages/domain/title_library.py @@ -3,8 +3,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import List +from datetime import UTC, datetime @dataclass @@ -17,9 +16,9 @@ class TitleLibraryItem: text: str category: str = "default" description: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) usage_count: int = 0 is_active: bool = True metadata_: dict = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) diff --git a/packages/domain/transition_presets.py b/packages/domain/transition_presets.py index d6ec66724..d27764e2d 100755 --- a/packages/domain/transition_presets.py +++ b/packages/domain/transition_presets.py @@ -7,7 +7,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass(frozen=True) @@ -18,7 +18,7 @@ class TransitionPreset: name: str category: str # 分类:basic / fade / slide / zoom / warp / special description: str = "" - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) # FFmpeg xfade transition 名称 transition: str = "fade" # 默认时长(秒) @@ -32,7 +32,7 @@ class TransitionPreset: # ── 预设库清单 ──────────────────────────────────────────────────────────────── -TRANSITION_PRESET_LIBRARY: List[TransitionPreset] = [ +TRANSITION_PRESET_LIBRARY: list[TransitionPreset] = [ # ── 基础 basic ──────────────────────────────────────────────────── TransitionPreset( id="transition_none", @@ -275,7 +275,7 @@ def list_transition_presets( *, category: Optional[str] = None, keyword: Optional[str] = None, -) -> List[TransitionPreset]: +) -> list[TransitionPreset]: """筛选转场预设列表 Args: diff --git a/packages/domain/tts_job.py b/packages/domain/tts_job.py index 44116a37d..c2afd2672 100644 --- a/packages/domain/tts_job.py +++ b/packages/domain/tts_job.py @@ -12,7 +12,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -105,8 +105,8 @@ class TTSJob: metadata: dict = field(default_factory=dict) started_at: datetime | None = None completed_at: datetime | None = None - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -203,14 +203,14 @@ class TTSJob: f"允许: {{{', '.join(s.value for s in allowed)}}}" ) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) self.status = new_status self.updated_at = now def mark_processing(self) -> None: """标记为处理中。""" self.transition_to(TTSJobStatus.PROCESSING) - self.started_at = datetime.now(timezone.utc) + self.started_at = datetime.now(UTC) self.error_message = "" def mark_completed( @@ -236,7 +236,7 @@ class TTSJob: self.output_audio_key = output_audio_key.strip() self.duration = duration self.file_size = file_size - self.completed_at = datetime.now(timezone.utc) + self.completed_at = datetime.now(UTC) self.error_message = "" def mark_failed(self, error_message: str) -> None: diff --git a/packages/domain/variant_voice_resolver.py b/packages/domain/variant_voice_resolver.py index c9ca121ed..fad85c3c7 100644 --- a/packages/domain/variant_voice_resolver.py +++ b/packages/domain/variant_voice_resolver.py @@ -15,7 +15,7 @@ from __future__ import annotations -from typing import List, Optional +from typing import Optional class VariantVoiceError(ValueError): @@ -26,8 +26,8 @@ def resolve_variant_voice_ids( *, count: int, voice_library_id: Optional[str] = None, - voice_library_ids: Optional[List[str]] = None, -) -> List[str]: + voice_library_ids: Optional[list[str]] = None, +) -> list[str]: """解析每个变体使用的配音 voice_library_id。 Args: diff --git a/packages/domain/verification_code.py b/packages/domain/verification_code.py index e2cbb7b00..2b355d7fb 100755 --- a/packages/domain/verification_code.py +++ b/packages/domain/verification_code.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from uuid import uuid4 @@ -18,7 +18,7 @@ class VerificationCode: expires_at: datetime used_at: datetime | None = None attempts: int = 0 - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -32,7 +32,7 @@ class VerificationCode: import random code = custom_code or "".join(random.choices("0123456789", k=6)) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) return cls( id=uuid4().hex, recipient=recipient.strip(), @@ -45,7 +45,7 @@ class VerificationCode: @property def is_expired(self) -> bool: """是否已过期""" - return datetime.now(timezone.utc) > self.expires_at + return datetime.now(UTC) > self.expires_at @property def is_used(self) -> bool: @@ -59,7 +59,7 @@ class VerificationCode: def mark_used(self) -> None: """标记为已使用""" - self.used_at = datetime.now(timezone.utc) + self.used_at = datetime.now(UTC) def increment_attempts(self) -> None: """增加尝试次数""" diff --git a/packages/domain/video_share.py b/packages/domain/video_share.py index b3b28a1ee..ccd567ae4 100755 --- a/packages/domain/video_share.py +++ b/packages/domain/video_share.py @@ -4,7 +4,7 @@ from __future__ import annotations import secrets from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from hashlib import sha256 from typing import Optional from uuid import uuid4 @@ -42,8 +42,8 @@ class VideoShare: view_count: int = 0 download_count: int = 0 is_active: bool = True - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -58,7 +58,7 @@ class VideoShare: raise ValueError("video_id cannot be empty") if not user_id.strip(): raise ValueError("user_id cannot be empty") - if expires_at and expires_at < datetime.now(timezone.utc): + if expires_at and expires_at < datetime.now(UTC): raise ValueError("expires_at cannot be in the past") return cls( @@ -80,7 +80,7 @@ class VideoShare: """是否已过期.""" if not self.expires_at: return False - return datetime.now(timezone.utc) > self.expires_at + return datetime.now(UTC) > self.expires_at @property def is_accessible(self) -> bool: diff --git a/packages/domain/voice_clone_profile.py b/packages/domain/voice_clone_profile.py index 3e54db971..8ba4a6290 100644 --- a/packages/domain/voice_clone_profile.py +++ b/packages/domain/voice_clone_profile.py @@ -12,7 +12,7 @@ from __future__ import annotations import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime if sys.version_info >= (3, 11): from enum import StrEnum @@ -94,8 +94,8 @@ class VoiceCloneProfile: retry_count: int = 0 max_retries: int = 3 metadata: dict = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( @@ -187,7 +187,7 @@ class VoiceCloneProfile: f"允许: {{{', '.join(s.value for s in allowed)}}}" ) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) self.status = new_status self.updated_at = now diff --git a/packages/domain/voice_library.py b/packages/domain/voice_library.py index 2f8d10c2f..e488e85f9 100644 --- a/packages/domain/voice_library.py +++ b/packages/domain/voice_library.py @@ -3,8 +3,8 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import List, Optional +from datetime import UTC, datetime +from typing import Optional @dataclass @@ -23,7 +23,7 @@ class VoiceLibraryItem: file_size: int = 0 status: str = "completed" project_id: Optional[str] = None - tags: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) metadata_: dict = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) diff --git a/packages/infrastructure/feature_flags.py b/packages/infrastructure/feature_flags.py index ad30c93d6..13b085574 100644 --- a/packages/infrastructure/feature_flags.py +++ b/packages/infrastructure/feature_flags.py @@ -24,7 +24,7 @@ from __future__ import annotations import logging from dataclasses import dataclass, field -from typing import Dict, Optional +from typing import Optional logger = logging.getLogger(__name__) @@ -38,9 +38,9 @@ class FeatureFlag: # 全局开关,默认 True(启用) global_enabled: bool = True # 套餐级别覆盖:{plan_name: bool} - plan_overrides: Dict[str, bool] = field(default_factory=dict) + plan_overrides: dict[str, bool] = field(default_factory=dict) # 用户白名单:user_id -> bool(True=强制启用,False=强制禁用) - user_overrides: Dict[str, bool] = field(default_factory=dict) + user_overrides: dict[str, bool] = field(default_factory=dict) def is_enabled( self, @@ -80,7 +80,7 @@ class FeatureFlags: """ def __init__(self) -> None: - self._flags: Dict[str, FeatureFlag] = {} + self._flags: dict[str, FeatureFlag] = {} self._init_defaults() def _init_defaults(self) -> None: @@ -175,7 +175,7 @@ class FeatureFlags: flag.user_overrides[user_id] = enabled logger.info(f"Feature flag '{name}' user '{user_id}' set to {enabled}") - def list_flags(self) -> Dict[str, FeatureFlag]: + def list_flags(self) -> dict[str, FeatureFlag]: """列出所有 Feature Flags""" return dict(self._flags) diff --git a/packages/infrastructure/module_registry.py b/packages/infrastructure/module_registry.py index 278014bb8..3047ba541 100644 --- a/packages/infrastructure/module_registry.py +++ b/packages/infrastructure/module_registry.py @@ -12,9 +12,10 @@ from __future__ import annotations import logging +from collections.abc import Callable from dataclasses import dataclass, field from enum import Enum -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Optional logger = logging.getLogger(__name__) @@ -50,8 +51,8 @@ class ModuleCapability: name: str # 能力名,如 "generate_voice" description: str = "" # 人类可读描述 - quota_rules: List[QuotaRule] = field(default_factory=list) # 该能力消耗的配额规则 - metadata: Dict[str, Any] = field(default_factory=dict) # 扩展元数据 + quota_rules: list[QuotaRule] = field(default_factory=list) # 该能力消耗的配额规则 + metadata: dict[str, Any] = field(default_factory=dict) # 扩展元数据 @dataclass @@ -64,10 +65,10 @@ class Module: name: str # 模块唯一标识,如 "ai_voice" version: str = "1.0.0" # 模块版本 description: str = "" # 人类可读描述 - capabilities: List[ModuleCapability] = field(default_factory=list) - dependencies: List[str] = field(default_factory=list) # 依赖的其他模块名 + capabilities: list[ModuleCapability] = field(default_factory=list) + dependencies: list[str] = field(default_factory=list) # 依赖的其他模块名 status: ModuleStatus = ModuleStatus.REGISTERED - config: Dict[str, Any] = field(default_factory=dict) # 模块配置 + config: dict[str, Any] = field(default_factory=dict) # 模块配置 _init_func: Optional[Callable] = field(default=None, repr=False) # 初始化回调 def activate(self) -> None: @@ -113,7 +114,7 @@ class ModuleRegistry: """ def __init__(self) -> None: - self._modules: Dict[str, Module] = {} + self._modules: dict[str, Module] = {} def register(self, module: Module) -> None: """注册一个模块 @@ -159,7 +160,7 @@ class ModuleRegistry: """按名称获取模块,不存在返回 None""" return self._modules.get(name) - def list_modules(self, status: Optional[ModuleStatus] = None) -> List[Module]: + def list_modules(self, status: Optional[ModuleStatus] = None) -> list[Module]: """列出所有模块,可按状态过滤""" modules = list(self._modules.values()) if status is not None: @@ -186,7 +187,7 @@ class ModuleRegistry: return cap return None - def get_quota_rules(self, capability_name: str) -> List[QuotaRule]: + def get_quota_rules(self, capability_name: str) -> list[QuotaRule]: """获取指定能力的配额消耗规则""" cap = self.get_capability(capability_name) if cap is None: @@ -204,7 +205,7 @@ class ModuleRegistry: return False return True - def get_active_capabilities(self) -> Dict[str, List[str]]: + def get_active_capabilities(self) -> dict[str, list[str]]: """获取所有已激活模块的能力列表 Returns: diff --git a/packages/middleware/points_gate.py b/packages/middleware/points_gate.py index 4d4770cdb..95642ad0d 100644 --- a/packages/middleware/points_gate.py +++ b/packages/middleware/points_gate.py @@ -9,7 +9,8 @@ import asyncio import functools import inspect import logging -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from fastapi import HTTPException diff --git a/packages/ports/recipe_repository.py b/packages/ports/recipe_repository.py index fb6ceb347..80537a25e 100644 --- a/packages/ports/recipe_repository.py +++ b/packages/ports/recipe_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional, Protocol +from typing import Optional, Protocol from packages.domain.recipe import Recipe, RecipeItem @@ -16,7 +16,7 @@ class RecipeRepository(Protocol): *, skip: int = 0, limit: int = 50, - ) -> List[Recipe]: ... + ) -> list[Recipe]: ... def get(self, recipe_id: str, user_id: str) -> Optional[Recipe]: ... @@ -28,8 +28,8 @@ class RecipeRepository(Protocol): def count_by_user(self, user_id: str, is_active: bool = True) -> int: ... - def list_items(self, recipe_id: str) -> List[RecipeItem]: ... + def list_items(self, recipe_id: str) -> list[RecipeItem]: ... - def create_items(self, items: List[RecipeItem]) -> List[RecipeItem]: ... + def create_items(self, items: list[RecipeItem]) -> list[RecipeItem]: ... def delete_items_by_recipe(self, recipe_id: str) -> int: ... diff --git a/packages/ports/storage_port.py b/packages/ports/storage_port.py index b02fa4010..5afd7848a 100755 --- a/packages/ports/storage_port.py +++ b/packages/ports/storage_port.py @@ -8,7 +8,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional, Union +from typing import Optional class StoragePort(ABC): @@ -23,7 +23,7 @@ class StoragePort(ABC): @abstractmethod def upload_file( self, - file_or_path: Union[str, Path, object], + file_or_path: str | Path | object, storage_key: str, content_type: str = "application/octet-stream", ) -> str: @@ -40,7 +40,7 @@ class StoragePort(ABC): ... @abstractmethod - def download_file(self, storage_key_or_url: str, local_path: Union[str, Path]) -> bool: + def download_file(self, storage_key_or_url: str, local_path: str | Path) -> bool: """从存储下载文件到本地。 自动识别输入:完整URL走HTTP下载(支持预签名),存储键走SDK下载。 @@ -97,7 +97,7 @@ class StoragePort(ABC): # ── Asset 解析(Worker 用)──────────────────────────────────────── @abstractmethod - def resolve_asset_path(self, asset_id: str, work_dir: Union[str, Path]) -> Optional[Path]: + def resolve_asset_path(self, asset_id: str, work_dir: str | Path) -> Optional[Path]: """从 asset_id 解析到本地文件路径。 策略:本地路径 → 缓存命中 → OSS下载 → None diff --git a/packages/ports/template_repository.py b/packages/ports/template_repository.py index 8cd7f057b..6eb2e3e80 100755 --- a/packages/ports/template_repository.py +++ b/packages/ports/template_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional, Protocol +from typing import Optional, Protocol from packages.domain.template import Template, TemplateCategory, TemplateSegment @@ -19,7 +19,7 @@ class TemplateRepositoryPort(Protocol): keyword: Optional[str] = None, mode: Optional[str] = None, valid_only: bool = False, - ) -> List[Template]: ... + ) -> list[Template]: ... def get(self, template_id: str, user_id: str) -> Optional[Template]: ... def create(self, template: Template) -> Template: ... def update(self, template: Template) -> Template: ... @@ -35,12 +35,12 @@ class TemplateRepositoryPort(Protocol): valid_only: bool = False, ) -> int: ... def copy_template(self, template_id: str, user_id: str, new_name: str) -> Template: ... - def list_segments(self, template_id: str) -> List[TemplateSegment]: ... - def create_segments(self, segments: List[TemplateSegment]) -> List[TemplateSegment]: ... + def list_segments(self, template_id: str) -> list[TemplateSegment]: ... + def create_segments(self, segments: list[TemplateSegment]) -> list[TemplateSegment]: ... def delete_segments_by_template(self, template_id: str) -> int: ... - def list_categories(self, user_id: str) -> List[TemplateCategory]: ... + def list_categories(self, user_id: str) -> list[TemplateCategory]: ... def create_category(self, category: TemplateCategory) -> TemplateCategory: ... def get_category(self, category_id: str, user_id: str) -> Optional[TemplateCategory]: ... def delete_category(self, category_id: str, user_id: str) -> bool: ... - def list_tags(self, user_id: str) -> List[str]: ... + def list_tags(self, user_id: str) -> list[str]: ... def get_usage_count(self, template_id: str) -> int: ... diff --git a/packages/ports/title_library_repository.py b/packages/ports/title_library_repository.py index 22b020544..4b949a202 100644 --- a/packages/ports/title_library_repository.py +++ b/packages/ports/title_library_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional, Protocol +from typing import Optional, Protocol from packages.domain.title_library import TitleLibraryItem @@ -18,7 +18,7 @@ class TitleLibraryRepository(Protocol): is_active: bool = True, skip: int = 0, limit: int = 50, - ) -> List[TitleLibraryItem]: ... + ) -> list[TitleLibraryItem]: ... def get(self, title_id: str, user_id: str) -> Optional[TitleLibraryItem]: ... diff --git a/packages/ports/video_share_repository.py b/packages/ports/video_share_repository.py index 96f137d16..1f4f80a67 100755 --- a/packages/ports/video_share_repository.py +++ b/packages/ports/video_share_repository.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List, Optional +from typing import Optional from packages.domain.video_share import VideoShare @@ -27,12 +27,12 @@ class VideoShareRepositoryPort(ABC): ... @abstractmethod - def list_by_video(self, video_id: str, user_id: str) -> List[VideoShare]: + def list_by_video(self, video_id: str, user_id: str) -> list[VideoShare]: """列出某个视频的所有分享记录.""" ... @abstractmethod - def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> List[VideoShare]: + def list_by_user(self, user_id: str, skip: int = 0, limit: int = 20) -> list[VideoShare]: """列出用户创建的所有分享记录.""" ... diff --git a/packages/ports/voice_clone_profile_repository.py b/packages/ports/voice_clone_profile_repository.py index 81f1b8637..2a4f43b97 100644 --- a/packages/ports/voice_clone_profile_repository.py +++ b/packages/ports/voice_clone_profile_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List, Protocol +from typing import Protocol from packages.domain.voice_clone_profile import VoiceCloneProfile, VoiceCloneStatus @@ -50,6 +50,6 @@ class VoiceCloneProfileRepository(Protocol): """根据 CosyVoice 返回的音色 ID 查找档案。""" ... - def find_profile_ids_by_voice_ids(self, voice_ids: List[str]) -> Dict[str, str]: + def find_profile_ids_by_voice_ids(self, voice_ids: list[str]) -> dict[str, str]: """批量查询 voice_id → profile_id 映射。""" ... diff --git a/packages/ports/voice_library_repository.py b/packages/ports/voice_library_repository.py index 2e97d3730..89d41a8c9 100644 --- a/packages/ports/voice_library_repository.py +++ b/packages/ports/voice_library_repository.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List, Optional, Protocol +from typing import Optional, Protocol from packages.domain.voice_library import VoiceLibraryItem @@ -17,7 +17,7 @@ class VoiceLibraryRepository(Protocol): status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[VoiceLibraryItem]: ... + ) -> list[VoiceLibraryItem]: ... def get(self, voice_id: str, user_id: str) -> Optional[VoiceLibraryItem]: ... diff --git a/packages/shared/ai_client.py b/packages/shared/ai_client.py index 6ab2a5999..249c4b81d 100755 --- a/packages/shared/ai_client.py +++ b/packages/shared/ai_client.py @@ -14,7 +14,7 @@ from __future__ import annotations import logging import time -from typing import Any, Dict, List, Optional +from typing import Any, Optional import httpx @@ -45,7 +45,7 @@ class DoubaoClient: def chat_completion( self, - messages: List[Dict[str, str]], + messages: list[dict[str, str]], temperature: float = 0.7, max_tokens: int = 1024, ) -> Optional[str]: @@ -67,7 +67,7 @@ class DoubaoClient: "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } - payload: Dict[str, Any] = { + payload: dict[str, Any] = { "model": self.model, "messages": messages, "temperature": temperature, diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index 390a32a68..daa92e9c7 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -11,7 +11,7 @@ import json import logging import random import time -from typing import Any, Dict, List, Optional +from typing import Any, Optional from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG from packages.shared.ai_client import get_doubao_client @@ -25,10 +25,10 @@ logger = logging.getLogger(__name__) def _fallback_recommend_clips( plan_id: str, template_id: str, - asset_ids: List[str], + asset_ids: list[str], editing_mode: str, target_duration: float, -) -> Dict[str, Any]: +) -> dict[str, Any]: """本地降级推荐方案(原 stub 逻辑). 当豆包 API 不可用或调用失败时使用,基于模板规则生成模拟推荐数据。 @@ -37,7 +37,7 @@ def _fallback_recommend_clips( time.sleep(0.5) # 根据素材数量生成推荐片段 - clips: List[Dict[str, Any]] = [] + clips: list[dict[str, Any]] = [] order = 0 # 开场片段 @@ -102,9 +102,9 @@ def _fallback_recommend_clips( def _parse_recommend_response( content: str, - asset_ids: List[str], + asset_ids: list[str], target_duration: float, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """解析豆包返回的推荐方案. 期望返回结构: @@ -138,7 +138,7 @@ def _parse_recommend_response( if not isinstance(clips_data, list) or len(clips_data) == 0: return None - clips: List[Dict[str, Any]] = [] + clips: list[dict[str, Any]] = [] for _, clip in enumerate(clips_data): if not isinstance(clip, dict): continue @@ -193,11 +193,11 @@ def _parse_recommend_response( def _call_ai_recommend_service( plan_id: str, template_id: str, - asset_ids: List[str], + asset_ids: list[str], editing_mode: str, target_duration: float, - asset_analyses: Optional[Dict[str, str]] = None, -) -> Dict[str, Any]: + asset_analyses: Optional[dict[str, str]] = None, +) -> dict[str, Any]: """调用 AI 推荐服务生成片段编排方案. 优先使用豆包大模型生成,失败或未配置时降级为本地规则生成。 @@ -354,11 +354,11 @@ def _transfer_cover_frame_to_storage(frame_url: str, plan_id: str) -> str: def _call_ai_cover_service( plan_id: str, - asset_ids: List[str], + asset_ids: list[str], cover_type: str, frame_time: float | None = None, primary_video_url: str | None = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """调用 AI 封面生成服务. 统一封面管道下,封面已由渲染后视频抽帧生成并持久化到 GenerationTask.cover_url。 @@ -408,11 +408,11 @@ def _call_ai_cover_service( def run_ai_recommend( plan_id: str, template_id: str, - asset_ids: List[str], + asset_ids: list[str], editing_mode: str = "one_take", target_duration: float = 30.0, - asset_analyses: Optional[Dict[str, str]] = None, -) -> Dict[str, Any]: + asset_analyses: Optional[dict[str, str]] = None, +) -> dict[str, Any]: """执行 AI 推荐片段方案 Args: @@ -455,11 +455,11 @@ def run_ai_recommend( def run_generate_cover( plan_id: str, - asset_ids: List[str], + asset_ids: list[str], cover_type: str = "ai_frame", frame_time: float | None = None, primary_video_url: str | None = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """执行 AI 封面生成 Args: diff --git a/packages/shared/mediakit_client.py b/packages/shared/mediakit_client.py index 80c63f1ae..c3d6f891d 100755 --- a/packages/shared/mediakit_client.py +++ b/packages/shared/mediakit_client.py @@ -15,7 +15,7 @@ from __future__ import annotations import logging import time -from typing import Any, Dict, List, Optional +from typing import Any, Optional import httpx @@ -59,7 +59,7 @@ class MediaKitClient: poll_interval: float = 2.0, max_poll_attempts: int = 30, max_retries: int = 1, - ) -> Optional[List[Dict[str, Any]]]: + ) -> Optional[list[dict[str, Any]]]: """调用 MediaKit 视频抽帧接口. Args: @@ -125,7 +125,7 @@ class MediaKitClient: max_frames: int = 20, poll_interval: float = 2.0, max_poll_attempts: int = 30, - ) -> Optional[List[float]]: + ) -> Optional[list[float]]: """检测视频场景切换点,返回时间戳列表. 降级策略: @@ -221,13 +221,13 @@ class MediaKitClient: def analyze_videos( self, - video_urls: List[str], + video_urls: list[str], prompt: str, level: str = "Economy", poll_interval: float = 2.0, max_poll_attempts: int = 15, max_retries: int = 1, - ) -> Optional[List[str]]: + ) -> Optional[list[str]]: """调用 MediaKit 视频理解智能策略 API. 基于火山方舟视觉大模型,对输入的视频 URL 列表进行内容分析, @@ -289,7 +289,7 @@ class MediaKitClient: def _submit_video_understand_task( self, - video_urls: List[str], + video_urls: list[str], prompt: str, level: str, ) -> Optional[str]: @@ -333,7 +333,7 @@ class MediaKitClient: task_id: str, poll_interval: float, max_poll_attempts: int, - ) -> tuple[Optional[List[str]], Optional[str]]: + ) -> tuple[Optional[list[str]], Optional[str]]: """轮询视频理解任务结果,返回 (contents, error_msg). 成功时返回 (contents, None),失败时返回 (None, error_message)。 @@ -401,7 +401,7 @@ class MediaKitClient: task_id: str, poll_interval: float, max_poll_attempts: int, - ) -> Optional[List[str]]: + ) -> Optional[list[str]]: """轮询视频理解任务结果,返回 contents 列表(兼容旧接口).""" result, _ = self._poll_video_understand_result_with_error(task_id, poll_interval, max_poll_attempts) return result @@ -411,7 +411,7 @@ class MediaKitClient: task_id: str, poll_interval: float, max_poll_attempts: int, - ) -> tuple[Optional[List[Dict[str, Any]]], Optional[str]]: + ) -> tuple[Optional[list[dict[str, Any]]], Optional[str]]: """轮询抽帧任务状态,返回 (snapshots, error_msg). 成功时返回 (snapshots, None),失败时返回 (None, error_message)。 @@ -480,7 +480,7 @@ class MediaKitClient: task_id: str, poll_interval: float, max_poll_attempts: int, - ) -> Optional[List[Dict[str, Any]]]: + ) -> Optional[list[dict[str, Any]]]: """轮询抽帧任务状态,返回结果(兼容旧接口).""" result, _ = self._poll_task_result_with_error(task_id, poll_interval, max_poll_attempts) return result diff --git a/packages/shared/storage.py b/packages/shared/storage.py index 500ae1f29..11d6ad57e 100755 --- a/packages/shared/storage.py +++ b/packages/shared/storage.py @@ -18,7 +18,7 @@ import logging import os import threading from pathlib import Path -from typing import Optional, Union +from typing import Optional from urllib.parse import unquote, urlparse import requests @@ -139,7 +139,7 @@ class SharedStorageService(StoragePort): def upload_file( self, - file_or_path: Union[str, Path, object], + file_or_path: str | Path | object, storage_key: str, content_type: str = "application/octet-stream", ) -> str: @@ -164,7 +164,7 @@ class SharedStorageService(StoragePort): def upload_file_smart( self, - local_path: Union[str, Path], + local_path: str | Path, storage_key: str, ) -> Optional[str]: """智能上传:大文件自动分片+超时保护(从 oss_helpers 合并)。 @@ -242,7 +242,7 @@ class SharedStorageService(StoragePort): # ── 下载 ─────────────────────────────────────────────────────────── - def download_file(self, storage_key: str, local_path: Union[str, Path]) -> None: + def download_file(self, storage_key: str, local_path: str | Path) -> None: """从 OSS 下载文件(简单下载,API端原有行为)。 bucket未配置 → 抛 RuntimeError @@ -257,7 +257,7 @@ class SharedStorageService(StoragePort): except Exception as e: raise Exception(f"Failed to download file from OSS: {e}") from e - def download_asset(self, asset_storage_key: str, local_path: Union[str, Path]) -> bool: + def download_asset(self, asset_storage_key: str, local_path: str | Path) -> bool: """下载素材(从 oss_helpers 合并)。 自动识别输入类型: @@ -353,7 +353,7 @@ class SharedStorageService(StoragePort): if not normalized_key.startswith("uploads/"): raise ValueError("direct upload key must be under uploads/") - expiration = (dt.datetime.now(dt.timezone.utc) + dt.timedelta(seconds=expires_seconds)).strftime( + expiration = (dt.datetime.now(dt.UTC) + dt.timedelta(seconds=expires_seconds)).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ) policy = { @@ -412,7 +412,7 @@ class SharedStorageService(StoragePort): # ── Asset 路径解析(Worker 用)──────────────────────────────────── - def resolve_asset_path(self, asset_id: str, work_dir: Union[str, Path]) -> Optional[Path]: + def resolve_asset_path(self, asset_id: str, work_dir: str | Path) -> Optional[Path]: """从 asset_id 解析到本地文件路径。 策略(按优先级): @@ -479,7 +479,7 @@ class SharedStorageService(StoragePort): def resolve_asset_ids_to_paths( self, asset_ids: list[str], - work_dir: Union[str, Path], + work_dir: str | Path, ) -> dict[str, Path]: """批量解析 asset_id → 本地路径。""" result: dict[str, Path] = {} diff --git a/scripts/backfill_generated_videos.py b/scripts/backfill_generated_videos.py index 175d1c93b..84ce8cc6f 100755 --- a/scripts/backfill_generated_videos.py +++ b/scripts/backfill_generated_videos.py @@ -19,7 +19,7 @@ from __future__ import annotations import logging import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 # 配置日志 @@ -116,8 +116,8 @@ def main() -> int: status="completed", generation_params={"mode": "backfill", "plan_id": plan.id}, review_status="pending", - created_at=task.completed_at or datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=task.completed_at or datetime.now(UTC), + updated_at=datetime.now(UTC), ) db.add(video) db.commit() diff --git a/scripts/check_migration_safety.py b/scripts/check_migration_safety.py index 17649b270..cd4a1f0e3 100644 --- a/scripts/check_migration_safety.py +++ b/scripts/check_migration_safety.py @@ -39,7 +39,6 @@ import time import urllib.error import urllib.request from pathlib import Path -from typing import List, Tuple REPO_ROOT = Path(__file__).resolve().parents[1] ALEMBIC_VERSIONS_DIR = REPO_ROOT / "alembic" / "versions" @@ -139,7 +138,7 @@ def _api_get_with_retry(url: str, token: str, max_retries: int = 3) -> dict | li raise last_error # type: ignore[misc] -def get_new_migrations_via_api(diff_target: str) -> List[Path] | None: +def get_new_migrations_via_api(diff_target: str) -> list[Path] | None: """ 通过 Gitea/GitHub Contents API 对比目标分支,找出 alembic/versions/ 下新增的迁移文件。 返回 None 表示 API 方式不可用,调用方应尝试其他方式。 @@ -181,7 +180,7 @@ def get_new_migrations_via_api(diff_target: str) -> List[Path] | None: return None -def get_new_migrations_via_git(diff_target: str) -> List[Path] | None: +def get_new_migrations_via_git(diff_target: str) -> list[Path] | None: """ Fallback:通过本地 git diff 找出新增的迁移文件。 CI 环境中 git 可用时作为 API 失败后的兜底方案。 @@ -232,7 +231,7 @@ def get_new_migrations_via_git(diff_target: str) -> List[Path] | None: return None -def get_new_migrations_via_diff(diff_target: str) -> List[Path]: +def get_new_migrations_via_diff(diff_target: str) -> list[Path]: """ 找出相对目标分支新增的迁移文件,按优先级尝试多种方式: 1. Gitea/GitHub Contents API(最可靠,不受本地 checkout 深度影响) @@ -255,7 +254,7 @@ def get_new_migrations_via_diff(diff_target: str) -> List[Path]: return sorted(ALEMBIC_VERSIONS_DIR.glob("*.py")) -def find_new_migrations(since_revision: str | None = None, diff_against: str | None = None) -> List[Path]: +def find_new_migrations(since_revision: str | None = None, diff_against: str | None = None) -> list[Path]: """ 找出需要检查的迁移文件。 优先级:diff_against > since_revision > 全部 @@ -279,7 +278,7 @@ def find_new_migrations(since_revision: str | None = None, diff_against: str | N return result if found else all_migrations -def analyze_migration(file_path: Path) -> Tuple[List[str], List[str], List[str]]: +def analyze_migration(file_path: Path) -> tuple[list[str], list[str], list[str]]: """分析单个迁移文件 upgrade 部分的风险等级""" content = file_path.read_text() upgrade_content = extract_upgrade_content(content) diff --git a/scripts/ci/acr_cleanup.py b/scripts/ci/acr_cleanup.py index 30bf41114..5b3967b3d 100755 --- a/scripts/ci/acr_cleanup.py +++ b/scripts/ci/acr_cleanup.py @@ -36,7 +36,7 @@ import os import sys import urllib.error import urllib.request -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta # ========== 配置 ========== @@ -255,13 +255,13 @@ def gitea_get_pr_commits(pr_number): def parse_time(created_str): """解析ISO时间字符串""" if not created_str: - return datetime.min.replace(tzinfo=timezone.utc) + return datetime.min.replace(tzinfo=UTC) try: if created_str.endswith("Z"): created_str = created_str[:-1] + "+00:00" return datetime.fromisoformat(created_str) except Exception: - return datetime.min.replace(tzinfo=timezone.utc) + return datetime.min.replace(tzinfo=UTC) def is_version_tag(tag): @@ -443,7 +443,7 @@ def cleanup_repo(repo, keep_count, dry_run, protected_tags, pr_sha=None, pr_open # pr-days兜底:超过指定天数的打开PR镜像也清理 if pr_days > 0: - cutoff = datetime.now(timezone.utc) - timedelta(days=pr_days) + cutoff = datetime.now(UTC) - timedelta(days=pr_days) extra_old = [] for tag in pr_tags_list: sha = extract_sha_from_pr_tag(tag) @@ -463,7 +463,7 @@ def cleanup_repo(repo, keep_count, dry_run, protected_tags, pr_sha=None, pr_open else: # 无Gitea token,降级为按pr_days天保留(默认7天) print(" 模式: 按时间保留7天(无Gitea token降级)") - cutoff = datetime.now(timezone.utc) - timedelta(days=7) + cutoff = datetime.now(UTC) - timedelta(days=7) for tag in pr_tags_list: info = get_manifest_info(repo, tag, token_pull) created = parse_time(info["created"]) diff --git a/scripts/ci/ci_dashboard.py b/scripts/ci/ci_dashboard.py index 3c7c1f6f8..0f2a541b8 100644 --- a/scripts/ci/ci_dashboard.py +++ b/scripts/ci/ci_dashboard.py @@ -22,7 +22,7 @@ import sys import urllib.error import urllib.request from collections import defaultdict -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone # ── 配置 ────────────────────────────────────────────── DEFAULT_GITEA_URL = "https://git.xiaoxiajianji.com" @@ -111,7 +111,7 @@ def to_shanghai(dt): if dt is None: return None if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) + dt = dt.replace(tzinfo=UTC) return dt.astimezone(timezone(timedelta(hours=8))) diff --git a/scripts/ci/ci_failure_diagnosis.py b/scripts/ci/ci_failure_diagnosis.py index 5244484b2..8e8fcb3e6 100644 --- a/scripts/ci/ci_failure_diagnosis.py +++ b/scripts/ci/ci_failure_diagnosis.py @@ -25,7 +25,7 @@ import re import sys import urllib.request from dataclasses import dataclass, field -from typing import List, Optional +from typing import Optional @dataclass @@ -36,8 +36,8 @@ class FailureDiagnosis: category_cn: str # 中文分类名 severity: str # 严重程度: high / medium / low summary: str # 一句话摘要 - error_lines: List[str] = field(default_factory=list) # 关键错误行 - suggestions: List[str] = field(default_factory=list) # 修复建议 + error_lines: list[str] = field(default_factory=list) # 关键错误行 + suggestions: list[str] = field(default_factory=list) # 修复建议 auto_fixable: bool = False # 是否可以自动修复 related_docs: str = "" # 相关文档链接 diff --git a/scripts/ci/ci_repeated_failure_detector.py b/scripts/ci/ci_repeated_failure_detector.py index b5dc95d34..10d9a7ea1 100644 --- a/scripts/ci/ci_repeated_failure_detector.py +++ b/scripts/ci/ci_repeated_failure_detector.py @@ -14,7 +14,7 @@ import time import urllib.error import urllib.request from collections import defaultdict -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta def get_env(name, default=None, required=False): @@ -54,7 +54,7 @@ def api_get(path): def fetch_recent_runs(days=7, per_page=50, max_pages=10): """获取最近N天的runs""" - since = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat() + since = (datetime.now(UTC) - timedelta(days=days)).isoformat() all_runs = [] for page in range(1, max_pages + 1): @@ -237,7 +237,7 @@ def generate_report(critical, warning, info, days, total_runs): lines.append("") lines.append(f"**统计周期**: 最近{days}天") lines.append(f"**扫描Runs**: {total_runs}个") - lines.append(f"**生成时间**: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") + lines.append(f"**生成时间**: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M UTC')}") lines.append("") lines.append("## 概览") diff --git a/scripts/ci/runner_monitor/alert_manager.py b/scripts/ci/runner_monitor/alert_manager.py index e0f3b119e..1e566eb71 100644 --- a/scripts/ci/runner_monitor/alert_manager.py +++ b/scripts/ci/runner_monitor/alert_manager.py @@ -30,7 +30,7 @@ import json import os import sys import time -from datetime import datetime, timezone +from datetime import UTC, datetime # 复用 chatops 的飞书通知 _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -53,7 +53,7 @@ class Alert: self.description = description self.details = details or {} self.source = source - self.timestamp = datetime.now(timezone.utc).isoformat() + self.timestamp = datetime.now(UTC).isoformat() def to_dict(self): return { @@ -370,7 +370,7 @@ class AlertManager: alerts = self.detect_alerts() snapshot = { - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "runner_summary": status_result["runner_summary"], "offline_runners": status_result["offline_runners"], "queue_backlog": status_result["queue_backlog"], diff --git a/scripts/ci/runner_monitor/runner_status.py b/scripts/ci/runner_monitor/runner_status.py index 358aececa..a63e7b8d9 100755 --- a/scripts/ci/runner_monitor/runner_status.py +++ b/scripts/ci/runner_monitor/runner_status.py @@ -26,7 +26,7 @@ import json import os import sys import time -from datetime import datetime, timezone +from datetime import UTC, datetime # 复用 chatops 的 GiteaClient _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -153,7 +153,7 @@ class RunnerStatusChecker: "pending_runs": [], } - now = datetime.now(timezone.utc) + now = datetime.now(UTC) oldest_minutes = 0 for r in pending: created = r.get("created_at", "") @@ -191,7 +191,7 @@ class RunnerStatusChecker: ) return { - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "runner_summary": { "total": summary["total"], "online": summary["online"], diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index c67bf88d9..790e927f5 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -12,7 +12,7 @@ import logging import os import re import sys -from typing import Optional, Tuple +from typing import Optional import requests @@ -39,7 +39,7 @@ LLM_PROVIDER = os.getenv("LLM_PROVIDER", "coze").lower() # ============== 工具函数 ============== -def truncate_diff(diff_text: str, max_chars: int) -> Tuple[str, bool]: +def truncate_diff(diff_text: str, max_chars: int) -> tuple[str, bool]: """ 截断过大的 diff 内容,避免超出 LLM 上下文限制。 优先保留文件头和前面的变更,末尾加提示。 @@ -504,7 +504,7 @@ def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str: return prompt -def parse_blocker_result(review_text: str) -> Tuple[bool, int]: +def parse_blocker_result(review_text: str) -> tuple[bool, int]: """ 从审查结果中解析是否存在阻塞级问题。 返回 (has_blocker, blocker_count) diff --git a/scripts/seed_test_voice_clones.py b/scripts/seed_test_voice_clones.py index f8d74bbf3..7b869a683 100755 --- a/scripts/seed_test_voice_clones.py +++ b/scripts/seed_test_voice_clones.py @@ -17,7 +17,7 @@ from __future__ import annotations import logging import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") @@ -100,7 +100,7 @@ def create_clone_profile(session, user_id: str, clone_data: dict) -> str: from sqlalchemy import text profile_id = uuid4().hex - now = datetime.now(timezone.utc) + now = datetime.now(UTC) session.execute( text(""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 8dbac13d7..d9afe8421 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -10,10 +10,11 @@ from __future__ import annotations import os import sys import time +from collections.abc import Callable from contextlib import contextmanager from dataclasses import dataclass, field from pathlib import Path -from typing import Callable, Dict, List, Optional +from typing import Optional import pytest @@ -29,7 +30,7 @@ def _get_worker_id() -> Optional[str]: return os.environ.get("PYTEST_XDIST_WORKER") -def _parse_database_url(url: str) -> Dict[str, str]: +def _parse_database_url(url: str) -> dict[str, str]: """ 解析 DATABASE_URL,返回各组件。 支持 postgresql+psycopg://user:pass@host:port/dbname 格式 @@ -164,7 +165,7 @@ def _cleanup_worker_database(worker_id: str): # ── 性能阈值配置 ────────────────────────────────────────────────────────── -PERF_THRESHOLDS: Dict[str, int] = { +PERF_THRESHOLDS: dict[str, int] = { "core": 500, # 核心接口:500ms "normal": 1000, # 普通接口:1000ms "heavy": 3000, # 重操作:3000ms(涉及外部调用或复杂计算) @@ -188,7 +189,7 @@ class PerfResult: name: str threshold_ms: int - times_ms: List[float] = field(default_factory=list) + times_ms: list[float] = field(default_factory=list) status_code: Optional[int] = None @property @@ -246,7 +247,7 @@ class PerfAssert: def __init__(self, sample_count: int = PERF_SAMPLE_COUNT): self.sample_count = sample_count - self.results: List[PerfResult] = [] + self.results: list[PerfResult] = [] @contextmanager def __call__(self, threshold_level: str = "core", name: str = "", samples: Optional[int] = None): diff --git a/tests/integration/fixtures/subscription_routes.py b/tests/integration/fixtures/subscription_routes.py index 5d67ff46c..51224a56e 100644 --- a/tests/integration/fixtures/subscription_routes.py +++ b/tests/integration/fixtures/subscription_routes.py @@ -3,8 +3,7 @@ from __future__ import annotations from dataclasses import replace -from datetime import datetime, timezone -from typing import List +from datetime import UTC, datetime, timezone from app.auth import AuthenticatedUser, get_current_user from app.dependencies import get_user_repository @@ -64,7 +63,7 @@ def _get_plan_price(plan_id: str, billing_cycle: str) -> float: def _build_subscription_info(user: AuthenticatedUser) -> SubscriptionInfo: """构建订阅信息响应""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) if user.user.subscription_expires_at: period_end = user.user.subscription_expires_at.isoformat() period_start = now.isoformat() @@ -97,7 +96,7 @@ async def get_current_subscription( return _build_subscription_info(current_user) -@router.get("/billing-records", response_model=List[BillingRecord]) +@router.get("/billing-records", response_model=list[BillingRecord]) async def get_billing_records( current_user: AuthenticatedUser = Depends(get_current_user), ): diff --git a/tests/integration/test_assets_api.py b/tests/integration/test_assets_api.py index a68465541..77c131d33 100755 --- a/tests/integration/test_assets_api.py +++ b/tests/integration/test_assets_api.py @@ -32,6 +32,8 @@ from fastapi.testclient import TestClient sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) +from datetime import UTC + from app.api.routes.assets import router from app.auth import AuthenticatedUser, get_current_user from app.core.storage import get_storage_service @@ -156,7 +158,7 @@ class StubAssetRepository: asset = self._assets.get(aid) if asset and asset.status != AssetStatus.DELETED: asset.status = AssetStatus.DELETED - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count @@ -168,7 +170,7 @@ class StubAssetRepository: asset = self._assets.get(aid) if asset: asset.metadata = {**asset.metadata, **metadata_patch} - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count @@ -185,7 +187,7 @@ class StubAssetRepository: asset.tag_ids.append(tid) changed = True if changed: - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count @@ -197,7 +199,7 @@ class StubAssetRepository: asset = self._assets.get(aid) if asset: asset.tag_ids = list(tag_ids) - asset.updated_at = datetime.now(timezone.utc) + asset.updated_at = datetime.now(UTC) count += 1 return count @@ -254,7 +256,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_chunked_upload_api.py b/tests/integration/test_chunked_upload_api.py index d67bc3dc6..cf38c0152 100644 --- a/tests/integration/test_chunked_upload_api.py +++ b/tests/integration/test_chunked_upload_api.py @@ -29,6 +29,8 @@ from fastapi.testclient import TestClient sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) +from datetime import UTC + from app.api.routes.chunked_upload import ( CHUNK_STORAGE_ROOT, complete_chunked_upload, @@ -139,7 +141,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_duplication_api.py b/tests/integration/test_duplication_api.py index bdb57dd8d..c7ef8e7b8 100644 --- a/tests/integration/test_duplication_api.py +++ b/tests/integration/test_duplication_api.py @@ -14,7 +14,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from typing import Any from unittest.mock import MagicMock from uuid import uuid4 @@ -82,7 +82,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_duplication_upload_error_handling.py b/tests/integration/test_duplication_upload_error_handling.py index 9930e401d..e6c6d64dd 100644 --- a/tests/integration/test_duplication_upload_error_handling.py +++ b/tests/integration/test_duplication_upload_error_handling.py @@ -17,7 +17,7 @@ from __future__ import annotations import io import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock # ── 环境变量 & sys.path(必须在导入 app.* 之前设置) ────────────────────────── @@ -53,7 +53,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_generation_api.py b/tests/integration/test_generation_api.py index 458610265..2b5889791 100755 --- a/tests/integration/test_generation_api.py +++ b/tests/integration/test_generation_api.py @@ -16,7 +16,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path from unittest.mock import MagicMock, patch @@ -239,7 +239,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_generation_pipeline.py b/tests/integration/test_generation_pipeline.py index 0e80bd95a..3f10fb622 100755 --- a/tests/integration/test_generation_pipeline.py +++ b/tests/integration/test_generation_pipeline.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from packages.application import ( CreateGenerationTaskCommand, @@ -56,7 +56,7 @@ def simulate_generate_video( task.status = GenerationTaskStatus.RUNNING task.progress = 20.0 - task.started_at = task.started_at or datetime.now(timezone.utc) + task.started_at = task.started_at or datetime.now(UTC) task_repo.update(task) file_url = f"/projects/{task.project_id}/generated/{task.id}/{task.id}.mp4" @@ -76,7 +76,7 @@ def simulate_generate_video( task.status = GenerationTaskStatus.COMPLETED task.progress = 100.0 task.result_count = 1 - task.completed_at = datetime.now(timezone.utc) + task.completed_at = datetime.now(UTC) task_repo.update(task) return { diff --git a/tests/integration/test_subscription_api.py b/tests/integration/test_subscription_api.py index d462d5c59..209bb9a99 100644 --- a/tests/integration/test_subscription_api.py +++ b/tests/integration/test_subscription_api.py @@ -16,7 +16,7 @@ from __future__ import annotations import importlib.util import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from typing import Optional # ── 环境变量 & sys.path(必须在导入 app.* 之前设置) ────────────────────────── @@ -59,7 +59,7 @@ def _make_user(**overrides) -> User: subscription_expires_at=None, max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_task_center_api.py b/tests/integration/test_task_center_api.py index 67c7dc516..fc4721792 100755 --- a/tests/integration/test_task_center_api.py +++ b/tests/integration/test_task_center_api.py @@ -15,7 +15,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path from unittest.mock import MagicMock, patch @@ -210,7 +210,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_tts_api.py b/tests/integration/test_tts_api.py index 7c0acb081..ecc084cf7 100644 --- a/tests/integration/test_tts_api.py +++ b/tests/integration/test_tts_api.py @@ -17,7 +17,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone # ── 环境变量 & sys.path(必须在导入 app.* 之前设置) ────────────────────────── os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -268,7 +268,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/integration/test_voice_clone_api.py b/tests/integration/test_voice_clone_api.py index def454b87..dcff8f61f 100755 --- a/tests/integration/test_voice_clone_api.py +++ b/tests/integration/test_voice_clone_api.py @@ -17,7 +17,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone # ── 环境变量 & sys.path(必须在导入 app.* 之前设置) ────────────────────────── os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -183,7 +183,7 @@ def _make_user(**overrides) -> User: subscription_status="active", max_projects=3, max_storage_gb=10, - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), ) defaults.update(overrides) return User(**defaults) diff --git a/tests/unit/domain/test_edit_template.py b/tests/unit/domain/test_edit_template.py index 5d4609ba5..f126190f4 100755 --- a/tests/unit/domain/test_edit_template.py +++ b/tests/unit/domain/test_edit_template.py @@ -1,6 +1,6 @@ """edit_template 剪辑模板实体单测.""" -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest from domain.edit_template import EditTemplate, EditTemplateStatus @@ -233,7 +233,7 @@ class TestEditTemplateBasics: def test_direct_construction_full(self): # 完整构造:所有字段都传 - now = datetime(2025, 1, 1, tzinfo=timezone.utc) + now = datetime(2025, 1, 1, tzinfo=UTC) t = EditTemplate( id="full_id", name="完整构造", @@ -275,7 +275,7 @@ class TestEditTemplateBasics: assert t1 != t2 def test_same_id_equal(self): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) t1 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now) t2 = EditTemplate(id="same", name="同名", created_at=now, updated_at=now) assert t1 == t2 diff --git a/tests/unit/domain/test_entities.py b/tests/unit/domain/test_entities.py index ebfa20798..4495b1d90 100755 --- a/tests/unit/domain/test_entities.py +++ b/tests/unit/domain/test_entities.py @@ -4,7 +4,7 @@ User / Project / AssetLibrary / Asset / IngestJob 领域模型 """ -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -100,9 +100,9 @@ class TestUser: assert user.wechat_unionid is None def test_has_created_at(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) user = User(id="u1", email="t@e.com", display_name="T") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= user.created_at <= after def test_full_fields(self): @@ -160,9 +160,9 @@ class TestProjectCreate: assert p1.id != p2.id def test_create_has_timestamp(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) p = Project.create(owner_user_id="u1", name="P") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= p.created_at <= after @@ -237,9 +237,9 @@ class TestAssetLibraryCreate: assert lib1.id != lib2.id def test_create_has_timestamps(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) lib = AssetLibrary.create("p1", "L", AssetLibraryKind.VIDEO) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= lib.created_at <= after assert before <= lib.updated_at <= after @@ -370,9 +370,9 @@ class TestAssetCreate: assert a1.id != a2.id def test_create_has_timestamps(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) a = Asset.create("p1", "lib1", "v.mp4", "k", "video/mp4") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= a.created_at <= after assert before <= a.updated_at <= after @@ -513,8 +513,8 @@ class TestIngestJobCreate: assert j1.id != j2.id def test_create_has_timestamps(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) job = IngestJob.create("p1", "lib1", "k") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= job.created_at <= after assert before <= job.updated_at <= after diff --git a/tests/unit/domain/test_template_version.py b/tests/unit/domain/test_template_version.py index 5e3adcb7f..388c0919f 100755 --- a/tests/unit/domain/test_template_version.py +++ b/tests/unit/domain/test_template_version.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest from domain.template_version import EditTemplateVersion @@ -59,9 +59,9 @@ class TestEditTemplateVersionCreate: assert v1.id != v2.id def test_create_sets_created_at(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) v = EditTemplateVersion.create("t1", 1) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= v.created_at <= after @@ -69,7 +69,7 @@ class TestEditTemplateVersionConstruction: """直接构造测试.""" def test_direct_construction(self): - now = datetime(2026, 1, 1, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, tzinfo=UTC) v = EditTemplateVersion( id="v1", template_id="t1", @@ -116,19 +116,19 @@ class TestEditTemplateVersionEquality: """相等性测试.""" def test_equal_same_id_and_version(self): - now = datetime(2026, 1, 1, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, tzinfo=UTC) v1 = EditTemplateVersion(id="same", template_id="t1", version=1, created_at=now) v2 = EditTemplateVersion(id="same", template_id="t1", version=1, created_at=now) assert v1 == v2 def test_not_equal_different_id(self): - now = datetime(2026, 1, 1, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, tzinfo=UTC) v1 = EditTemplateVersion(id="v1", template_id="t1", version=1, created_at=now) v2 = EditTemplateVersion(id="v2", template_id="t1", version=1, created_at=now) assert v1 != v2 def test_not_equal_different_version(self): - now = datetime(2026, 1, 1, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, tzinfo=UTC) v1 = EditTemplateVersion(id="same", template_id="t1", version=1, created_at=now) v2 = EditTemplateVersion(id="same", template_id="t1", version=2, created_at=now) assert v1 != v2 diff --git a/tests/unit/domain/test_tts_job.py b/tests/unit/domain/test_tts_job.py index ff2c1dada..dfe40aae9 100755 --- a/tests/unit/domain/test_tts_job.py +++ b/tests/unit/domain/test_tts_job.py @@ -6,7 +6,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from packages.domain.tts_job import ( TERMINAL_STATUSES, @@ -146,9 +146,9 @@ class TestTTSJobCreate: assert job.format == "pcm" def test_create_sets_created_at(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) job = TTSJob.create(user_id="u1", input_text="hi") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= job.created_at <= after assert before <= job.updated_at <= after diff --git a/tests/unit/domain/test_verification_code.py b/tests/unit/domain/test_verification_code.py index d55e0f9d1..15f75ac19 100755 --- a/tests/unit/domain/test_verification_code.py +++ b/tests/unit/domain/test_verification_code.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import patch import pytest @@ -34,7 +34,7 @@ class TestVerificationCodeCreate: assert vc.code == "123456" def test_create_custom_ttl(self): - fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) @@ -42,7 +42,7 @@ class TestVerificationCodeCreate: assert vc.expires_at == fixed_now + timedelta(seconds=60) def test_create_default_ttl_300(self): - fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) @@ -77,7 +77,7 @@ class TestVerificationCodeIsExpired: """is_expired 属性测试.""" def test_not_expired_future(self): - future = datetime.now(timezone.utc) + timedelta(hours=1) + future = datetime.now(UTC) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -88,7 +88,7 @@ class TestVerificationCodeIsExpired: assert vc.is_expired is False def test_expired_past(self): - past = datetime.now(timezone.utc) - timedelta(hours=1) + past = datetime.now(UTC) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -100,7 +100,7 @@ class TestVerificationCodeIsExpired: def test_expired_boundary_exact(self): # 用mock固定时间,expires_at等于当前时间不算过期 - fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) with patch("domain.verification_code.datetime") as mock_dt: mock_dt.now.return_value = fixed_now mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw) @@ -131,7 +131,7 @@ class TestVerificationCodeIsValid: """is_valid 属性测试.""" def test_valid_fresh(self): - future = datetime.now(timezone.utc) + timedelta(hours=1) + future = datetime.now(UTC) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -142,7 +142,7 @@ class TestVerificationCodeIsValid: assert vc.is_valid is True def test_invalid_expired(self): - past = datetime.now(timezone.utc) - timedelta(hours=1) + past = datetime.now(UTC) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -153,7 +153,7 @@ class TestVerificationCodeIsValid: assert vc.is_valid is False def test_invalid_used(self): - future = datetime.now(timezone.utc) + timedelta(hours=1) + future = datetime.now(UTC) + timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -165,7 +165,7 @@ class TestVerificationCodeIsValid: assert vc.is_valid is False def test_invalid_expired_and_used(self): - past = datetime.now(timezone.utc) - timedelta(hours=1) + past = datetime.now(UTC) - timedelta(hours=1) vc = VerificationCode( id="1", recipient="a@b.com", @@ -183,9 +183,9 @@ class TestVerificationCodeMarkUsed: def test_mark_used_sets_timestamp(self): vc = VerificationCode.create("a@b.com", "email_login") assert vc.used_at is None - before = datetime.now(timezone.utc) + before = datetime.now(UTC) vc.mark_used() - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert vc.used_at is not None assert before <= vc.used_at <= after @@ -221,7 +221,7 @@ class TestVerificationCodeBasics: """基础构造和 slots 测试.""" def test_direct_construction(self): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) vc = VerificationCode( id="abc123", recipient="test@test.com", @@ -242,7 +242,7 @@ class TestVerificationCodeBasics: vc.new_field = "value" # type: ignore[attr-defined] def test_equality_same_id(self): - now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC) vc1 = VerificationCode( id="same", recipient="a@b.com", @@ -262,7 +262,7 @@ class TestVerificationCodeBasics: assert vc1 == vc2 def test_equality_different_id(self): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) vc1 = VerificationCode( id="id1", recipient="a@b.com", diff --git a/tests/unit/domain/test_video_share.py b/tests/unit/domain/test_video_share.py index 40064a6cb..417166530 100755 --- a/tests/unit/domain/test_video_share.py +++ b/tests/unit/domain/test_video_share.py @@ -3,7 +3,7 @@ from __future__ import annotations import re -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import pytest @@ -104,12 +104,12 @@ class TestVideoShareCreate: assert share.password_hash is None def test_create_with_expires_at(self): - future = datetime.now(timezone.utc) + timedelta(days=7) + future = datetime.now(UTC) + timedelta(days=7) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.expires_at == future def test_create_past_expires_at_raises(self): - past = datetime.now(timezone.utc) - timedelta(days=1) + past = datetime.now(UTC) - timedelta(days=1) with pytest.raises(ValueError, match="expires_at cannot be in the past"): VideoShare.create(video_id="v1", user_id="u1", expires_at=past) @@ -167,13 +167,13 @@ class TestVideoShareIsExpired: assert share.is_expired is False def test_future_expiry_not_expired(self): - future = datetime.now(timezone.utc) + timedelta(hours=1) + future = datetime.now(UTC) + timedelta(hours=1) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.is_expired is False def test_past_expiry_is_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1) + share.expires_at = datetime.now(UTC) - timedelta(seconds=1) assert share.is_expired is True @@ -192,13 +192,13 @@ class TestVideoShareIsAccessible: def test_expired_not_accessible(self): share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_accessible is False def test_revoked_and_expired_not_accessible(self): share = VideoShare.create(video_id="v1", user_id="u1") share.is_active = False - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_accessible is False diff --git a/tests/unit/domain/test_voice_clone_profile.py b/tests/unit/domain/test_voice_clone_profile.py index fc305f5d6..4e155b55f 100755 --- a/tests/unit/domain/test_voice_clone_profile.py +++ b/tests/unit/domain/test_voice_clone_profile.py @@ -6,7 +6,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from packages.domain.voice_clone_profile import ( TERMINAL_STATUSES, @@ -133,9 +133,9 @@ class TestVoiceCloneProfileCreate: assert profile.name == name def test_create_sets_created_at(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) profile = VoiceCloneProfile.create(user_id="u1", name="t") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= profile.created_at <= after assert before <= profile.updated_at <= after diff --git a/tests/unit/test_1286_can_generate_auto_repair.py b/tests/unit/test_1286_can_generate_auto_repair.py index 4562983c5..7d46ea3c4 100644 --- a/tests/unit/test_1286_can_generate_auto_repair.py +++ b/tests/unit/test_1286_can_generate_auto_repair.py @@ -17,7 +17,7 @@ from __future__ import annotations import os import sys from pathlib import Path -from typing import Any, List, Optional +from typing import Any, Optional from unittest.mock import MagicMock os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -118,7 +118,7 @@ class StubEditPlanClipRepository: status: Optional[EditPlanClipStatus] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: clips = [c for c in self._clips.values() if c.plan_id == plan_id] if status is not None: clips = [c for c in clips if c.status == status] diff --git a/tests/unit/test_ai_avatar_render_routes.py b/tests/unit/test_ai_avatar_render_routes.py index ee17b5079..78f632b00 100644 --- a/tests/unit/test_ai_avatar_render_routes.py +++ b/tests/unit/test_ai_avatar_render_routes.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone """AI数字人渲染 API 路由测试 — #1798. @@ -49,8 +49,8 @@ def _make_mock_render_job( m.submitted_at = None m.started_at = None m.completed_at = None - m.created_at = datetime(2026, 1, 1, tzinfo=timezone.utc) - m.updated_at = datetime(2026, 1, 1, tzinfo=timezone.utc) + m.created_at = datetime(2026, 1, 1, tzinfo=UTC) + m.updated_at = datetime(2026, 1, 1, tzinfo=UTC) return m diff --git a/tests/unit/test_asset_library_delete.py b/tests/unit/test_asset_library_delete.py index 3b595b079..f3846d688 100644 --- a/tests/unit/test_asset_library_delete.py +++ b/tests/unit/test_asset_library_delete.py @@ -19,6 +19,8 @@ os.environ.setdefault("DATABASE_URL", "sqlite:///test.db") sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) +from datetime import UTC + from fastapi import FastAPI from fastapi.testclient import TestClient @@ -100,7 +102,7 @@ class TestDeleteAssetLibrary: asset_repo = InMemoryAssetRepository() proj_repo = StubProjectRepository({"proj-1": StubProject("proj-1", "user-001")}) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) library = AssetLibrary( id="lib-1", project_id="proj-1", @@ -130,7 +132,7 @@ class TestDeleteAssetLibrary: asset_repo = InMemoryAssetRepository() proj_repo = StubProjectRepository({"proj-1": StubProject("proj-1", "user-001")}) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) library = AssetLibrary( id="lib-1", project_id="proj-1", @@ -201,7 +203,7 @@ class TestDeleteAssetLibrary: # 项目属于 user-002,当前用户是 user-001 proj_repo = StubProjectRepository({"proj-1": StubProject("proj-1", "user-002")}) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) library = AssetLibrary( id="lib-1", project_id="proj-1", @@ -233,7 +235,7 @@ class TestDeleteAssetLibrary: # 空的项目仓库,找不到项目 proj_repo = StubProjectRepository({}) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) library = AssetLibrary( id="lib-1", project_id="proj-missing", diff --git a/tests/unit/test_asset_list_ordering.py b/tests/unit/test_asset_list_ordering.py index 5291d323f..142017db3 100644 --- a/tests/unit/test_asset_list_ordering.py +++ b/tests/unit/test_asset_list_ordering.py @@ -1,7 +1,7 @@ """Unit tests for SQLAlchemyAssetRepository - list ordering (created_at DESC).""" import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) @@ -46,7 +46,7 @@ def _insert_asset_with_created_at( def test_find_by_library_ordered_by_created_at_desc(): """find_by_library 返回结果按 created_at 倒序排列,最新的在最前面。""" repository, session = _repository() - base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=timezone.utc) + base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=UTC) _insert_asset_with_created_at(session, "asset-oldest", "lib-1", "proj-1", base_time) _insert_asset_with_created_at(session, "asset-middle", "lib-1", "proj-1", base_time + timedelta(hours=1)) @@ -61,7 +61,7 @@ def test_find_by_library_ordered_by_created_at_desc(): def test_find_by_project_ordered_by_created_at_desc(): """find_by_project 返回结果按 created_at 倒序排列。""" repository, session = _repository() - base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=timezone.utc) + base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=UTC) _insert_asset_with_created_at(session, "asset-1", "lib-1", "proj-1", base_time) _insert_asset_with_created_at(session, "asset-2", "lib-2", "proj-1", base_time + timedelta(hours=1)) @@ -76,7 +76,7 @@ def test_find_by_project_ordered_by_created_at_desc(): def test_find_by_library_and_file_type_ordered_by_created_at_desc(): """find_by_library_and_file_type 返回结果按 created_at 倒序排列。""" repository, session = _repository() - base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=timezone.utc) + base_time = datetime(2026, 7, 18, 10, 0, 0, tzinfo=UTC) _insert_asset_with_created_at(session, "video-old", "lib-1", "proj-1", base_time, file_type="video") _insert_asset_with_created_at( diff --git a/tests/unit/test_asset_repo_fallback_dedup_1714.py b/tests/unit/test_asset_repo_fallback_dedup_1714.py index d68890773..eb9075919 100644 --- a/tests/unit/test_asset_repo_fallback_dedup_1714.py +++ b/tests/unit/test_asset_repo_fallback_dedup_1714.py @@ -5,7 +5,7 @@ file_size=0(未知)时必须返回 None(宁可漏判不可误杀); """ import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) @@ -35,7 +35,7 @@ def _mk_asset(name="IMG_2285.MOV", file_size=5_000_000, status=AssetStatus.PROCE file_size=file_size, ) asset.status = status - asset.created_at = datetime.now(timezone.utc) - timedelta(minutes=minutes_ago) + asset.created_at = datetime.now(UTC) - timedelta(minutes=minutes_ago) return asset diff --git a/tests/unit/test_asset_usage_deep.py b/tests/unit/test_asset_usage_deep.py index e643f2e29..b87e90d30 100755 --- a/tests/unit/test_asset_usage_deep.py +++ b/tests/unit/test_asset_usage_deep.py @@ -6,7 +6,7 @@ review_status 已有值不覆盖、多次调用递增。 from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from worker_app.core.asset_usage import mark_asset_used_for_generation @@ -117,9 +117,9 @@ class TestMarkAssetUsedForGeneration: def test_last_used_at_is_utc(self): """last_used_at 是 UTC 时间.""" asset = _asset() - before = datetime.now(timezone.utc) + before = datetime.now(UTC) mark_asset_used_for_generation(asset) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) ts = datetime.fromisoformat(asset.metadata["last_used_at"]) assert before <= ts <= after diff --git a/tests/unit/test_auth_bind_wechat_sync.py b/tests/unit/test_auth_bind_wechat_sync.py index a311350da..1a12aae33 100755 --- a/tests/unit/test_auth_bind_wechat_sync.py +++ b/tests/unit/test_auth_bind_wechat_sync.py @@ -6,7 +6,7 @@ Covers BindContactUseCase, SendVerificationCodeUseCase, WechatSyncUseCase. from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock from packages.application.auth.bind_contact_use_case import ( @@ -44,7 +44,7 @@ class FakeUser: email_verification_token: str | None = None password_reset_token: str | None = None password_reset_expires_at: datetime | None = None - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) class FakeUserRepository: @@ -93,8 +93,8 @@ class FakeUserRepository: class FakeVerificationCode: def __init__(self, code="123456", created_at=None, expires_at=None): self.code = code - self.created_at = created_at or datetime.now(timezone.utc) - self.expires_at = expires_at or (datetime.now(timezone.utc) + timedelta(minutes=5)) + self.created_at = created_at or datetime.now(UTC) + self.expires_at = expires_at or (datetime.now(UTC) + timedelta(minutes=5)) class FakeVerificationCodeService: diff --git a/tests/unit/test_auth_register_password_reset.py b/tests/unit/test_auth_register_password_reset.py index 95dcc2cd9..1c124f54e 100755 --- a/tests/unit/test_auth_register_password_reset.py +++ b/tests/unit/test_auth_register_password_reset.py @@ -6,7 +6,7 @@ RequestPasswordResetUseCase, ResetPasswordUseCase. from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock import pytest @@ -40,7 +40,7 @@ class FakeUser: self.email_verification_token = kwargs.get("email_verification_token", None) self.password_reset_token = kwargs.get("password_reset_token", None) self.password_reset_expires_at = kwargs.get("password_reset_expires_at", None) - self.created_at = kwargs.get("created_at", datetime.now(timezone.utc)) + self.created_at = kwargs.get("created_at", datetime.now(UTC)) class FakeUserRepository: @@ -481,10 +481,10 @@ class TestRequestPasswordResetUseCase: repo = FakeUserRepository(user=user) use_case = self._make_use_case(repo=repo, expire_hours=2) - before = datetime.now(timezone.utc) + before = datetime.now(UTC) req = RequestPasswordResetRequest(email="u@e.com") use_case.execute(req) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) expires_at = repo.saved_user.password_reset_expires_at assert expires_at is not None @@ -502,7 +502,7 @@ class TestRequestPasswordResetUseCase: email_service=FakeEmailService(), ) - before = datetime.now(timezone.utc) + before = datetime.now(UTC) req = RequestPasswordResetRequest(email="u@e.com") use_case.execute(req) @@ -575,7 +575,7 @@ class TestResetPasswordUseCase: user = FakeUser( password_hash=old_hash, password_reset_token=token, - password_reset_expires_at=datetime.now(timezone.utc) + timedelta(hours=1), + password_reset_expires_at=datetime.now(UTC) + timedelta(hours=1), ) repo = FakeUserRepository(user=user) use_case = ResetPasswordUseCase(user_repository=repo) @@ -634,7 +634,7 @@ class TestResetPasswordUseCase: token = "expired-token" user = FakeUser( password_reset_token=token, - password_reset_expires_at=datetime.now(timezone.utc) - timedelta(hours=1), + password_reset_expires_at=datetime.now(UTC) - timedelta(hours=1), ) repo = FakeUserRepository(user=user) use_case = ResetPasswordUseCase(user_repository=repo) @@ -648,7 +648,7 @@ class TestResetPasswordUseCase: def test_naive_datetime_expiry_treated_as_utc(self): token = "naive-token" # naive datetime representing UTC time 1 hour in the past - naive_expired = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(hours=1) + naive_expired = datetime.now(UTC).replace(tzinfo=None) - timedelta(hours=1) user = FakeUser( password_reset_token=token, password_reset_expires_at=naive_expired, @@ -682,7 +682,7 @@ class TestResetPasswordUseCase: token = "clear-me" user = FakeUser( password_reset_token=token, - password_reset_expires_at=datetime.now(timezone.utc) + timedelta(hours=1), + password_reset_expires_at=datetime.now(UTC) + timedelta(hours=1), ) repo = FakeUserRepository(user=user) use_case = ResetPasswordUseCase(user_repository=repo) diff --git a/tests/unit/test_auto_select_assets.py b/tests/unit/test_auto_select_assets.py index ed118e3eb..caf2cf597 100755 --- a/tests/unit/test_auto_select_assets.py +++ b/tests/unit/test_auto_select_assets.py @@ -8,6 +8,8 @@ from unittest.mock import MagicMock sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) +from datetime import UTC + import pytest from packages.domain import Asset, AssetLibrary, AssetLibraryKind, AssetStatus @@ -39,7 +41,7 @@ def _make_asset( metadata=metadata or {}, width=width, height=height, - created_at=created_at or datetime.now(timezone.utc), + created_at=created_at or datetime.now(UTC), ) @@ -303,7 +305,7 @@ class TestAutoSelectVideoAssetsCodecFilter: from app.api.routes._helpers import auto_select_video_assets - now = datetime.now(timezone.utc) + now = datetime.now(UTC) assets = [ _make_asset("oldest", codec="h264", created_at=now - timedelta(hours=2)), _make_asset("newest", codec="h264", created_at=now), diff --git a/tests/unit/test_bind_contact_use_case.py b/tests/unit/test_bind_contact_use_case.py index 1c8fdbdd0..1f360ce6c 100755 --- a/tests/unit/test_bind_contact_use_case.py +++ b/tests/unit/test_bind_contact_use_case.py @@ -1,6 +1,6 @@ """绑定联系方式 Use Case 单元测试.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock import pytest @@ -142,7 +142,7 @@ class TestBindContactResponse: phone="13800138000", phone_verified=True, email_verified=True, - binding_completed_at=datetime.now(timezone.utc), + binding_completed_at=datetime.now(UTC), ) resp = BindContactResponse(user=user) assert resp.to_dict()["user"]["binding_complete"] is True diff --git a/tests/unit/test_cover_templates_crud.py b/tests/unit/test_cover_templates_crud.py index 9ef2f88a1..5088255eb 100644 --- a/tests/unit/test_cover_templates_crud.py +++ b/tests/unit/test_cover_templates_crud.py @@ -7,7 +7,7 @@ 4. Schema:请求/响应序列化 """ -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest @@ -147,7 +147,7 @@ class TestCoverTemplateSchema: """测试响应序列化。""" from app.schemas.cover_template import CoverTemplateResponse - now = datetime.now(timezone.utc) + now = datetime.now(UTC) resp = CoverTemplateResponse( id="test-id", name="测试", diff --git a/tests/unit/test_duplication_domain.py b/tests/unit/test_duplication_domain.py index 172895dc5..2771bdd0e 100755 --- a/tests/unit/test_duplication_domain.py +++ b/tests/unit/test_duplication_domain.py @@ -1,6 +1,6 @@ """Duplication 领域模型单元测试。""" -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -267,7 +267,7 @@ class TestMarkProcessing: # 确保时间戳会变(datetime.now 精度问题,直接赋值模拟) record.mark_processing() assert record.status == "processing" - assert record.updated_at.tzinfo == timezone.utc + assert record.updated_at.tzinfo == UTC class TestMarkCompleted: @@ -307,7 +307,7 @@ class TestMarkCompleted: def test_mark_completed_updates_timestamp(self): record = DuplicationRecord.create("u", "f", 100, "k") record.mark_completed(50.0, 1, []) - assert record.updated_at.tzinfo == timezone.utc + assert record.updated_at.tzinfo == UTC class TestMarkFailed: diff --git a/tests/unit/test_edit_plan_domain.py b/tests/unit/test_edit_plan_domain.py index 474ac2cef..52def2e4a 100755 --- a/tests/unit/test_edit_plan_domain.py +++ b/tests/unit/test_edit_plan_domain.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -82,9 +82,9 @@ class TestEditPlanCreate: assert plan.template_id == "tpl_001" def test_create_timestamps_set(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) plan = EditPlan.create(template_id="tpl_001", name="测试") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= plan.created_at <= after assert before <= plan.updated_at <= after diff --git a/tests/unit/test_edit_plan_service.py b/tests/unit/test_edit_plan_service.py index b79bf5d18..906b6d7c9 100755 --- a/tests/unit/test_edit_plan_service.py +++ b/tests/unit/test_edit_plan_service.py @@ -13,9 +13,9 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path -from typing import Any, List, Optional +from typing import Any, Optional from unittest.mock import MagicMock os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -50,7 +50,7 @@ class StubEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: items = list(self._plans.values()) if status: items = [p for p in items if p.status == status] @@ -63,7 +63,7 @@ class StubEditPlanRepository: status: Optional[EditPlanStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: items = [p for p in self._plans.values() if p.template_id == template_id] if status: items = [p for p in items if p.status == status] @@ -81,8 +81,8 @@ class StubEditPlanRepository: status=plan.status, total_duration=plan.total_duration, config=plan.config, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) self._plans[plan.id] = plan return plan @@ -123,7 +123,7 @@ class StubEditPlanClipRepository: status: Optional[EditPlanClipStatus] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: items = [c for c in self._clips.values() if c.plan_id == plan_id] if status: items = [c for c in items if c.status == status] @@ -148,8 +148,8 @@ class StubEditPlanClipRepository: transition_effect=clip.transition_effect, status=clip.status, config=clip.config, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) self._clips[clip.id] = clip return clip diff --git a/tests/unit/test_edit_template_service.py b/tests/unit/test_edit_template_service.py index 4846b6b59..21dfa5dd3 100755 --- a/tests/unit/test_edit_template_service.py +++ b/tests/unit/test_edit_template_service.py @@ -14,9 +14,9 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path -from typing import List, Optional +from typing import Optional from unittest.mock import MagicMock os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -52,7 +52,7 @@ class StubEditTemplateRepository: status: Optional[EditTemplateStatus] = None, skip: int = 0, limit: int = 50, - ) -> List[EditTemplate]: + ) -> list[EditTemplate]: items = list(self._templates.values()) if template_type: items = [t for t in items if t.template_type == template_type] @@ -66,7 +66,7 @@ class StubEditTemplateRepository: template_type: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[EditTemplate]: + ) -> list[EditTemplate]: items = [t for t in self._templates.values() if t.status == EditTemplateStatus.ACTIVE] if template_type: items = [t for t in items if t.template_type == template_type] @@ -86,8 +86,8 @@ class StubEditTemplateRepository: preview_url=template.preview_url, sort_weight=template.sort_weight, status=template.status, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) self._templates[template.id] = template return template @@ -131,7 +131,7 @@ class StubTemplateClipConfigRepository: clip_type: Optional[ClipType] = None, skip: int = 0, limit: int = 100, - ) -> List[TemplateClipConfig]: + ) -> list[TemplateClipConfig]: items = [c for c in self._configs.values() if c.template_id == template_id] if clip_type: items = [c for c in items if c.clip_type == clip_type] @@ -154,8 +154,8 @@ class StubTemplateClipConfigRepository: material_requirements=config.material_requirements, transition_effect=config.transition_effect, config=config.config, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) self._configs[config.id] = config return config @@ -497,7 +497,7 @@ class StubEditPlanRepository: status: Optional[str] = None, skip: int = 0, limit: int = 50, - ) -> List[EditPlan]: + ) -> list[EditPlan]: items = [p for p in self._plans.values() if p.template_id == template_id] if status: items = [p for p in items if p.status.value == status] @@ -527,7 +527,7 @@ class StubEditPlanClipRepository: status: Optional[str] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: items = [c for c in self._clips.values() if c.plan_id == plan_id] if status: items = [c for c in items if c.status.value == status] diff --git a/tests/unit/test_generated_video_verification_code.py b/tests/unit/test_generated_video_verification_code.py index 0ac8facac..9e072540b 100755 --- a/tests/unit/test_generated_video_verification_code.py +++ b/tests/unit/test_generated_video_verification_code.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import timedelta +from datetime import UTC, timedelta import pytest @@ -214,13 +214,13 @@ class TestVerificationCodeCreate: """过期时间正确(5分钟后).""" from datetime import datetime, timezone - before = datetime.now(timezone.utc) + timedelta(seconds=299) + before = datetime.now(UTC) + timedelta(seconds=299) vc = VerificationCode.create( recipient="u@test.com", code_type="reset_password", ttl_seconds=300, ) - after = datetime.now(timezone.utc) + timedelta(seconds=301) + after = datetime.now(UTC) + timedelta(seconds=301) assert before <= vc.expires_at <= after def test_custom_ttl(self): @@ -232,7 +232,7 @@ class TestVerificationCodeCreate: ) from datetime import datetime, timezone - now = datetime.now(timezone.utc) + now = datetime.now(UTC) # 应该在1分钟左右过期 diff = (vc.expires_at - now).total_seconds() assert 0 < diff < 70 @@ -260,7 +260,7 @@ class TestVerificationCodeProperties: # 手动改过期时间到过去 from datetime import datetime, timedelta, timezone - vc.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1) + vc.expires_at = datetime.now(UTC) - timedelta(seconds=1) assert vc.is_expired is True def test_is_used_false_by_default(self): @@ -278,7 +278,7 @@ class TestVerificationCodeProperties: vc = VerificationCode.create(recipient="u@test.com", code_type="email_bind", ttl_seconds=1) from datetime import datetime, timedelta, timezone - vc.expires_at = datetime.now(timezone.utc) - timedelta(seconds=1) + vc.expires_at = datetime.now(UTC) - timedelta(seconds=1) assert vc.is_valid is False def test_is_valid_used(self): diff --git a/tests/unit/test_generation_preview.py b/tests/unit/test_generation_preview.py index 8522e2f54..8da468eb5 100644 --- a/tests/unit/test_generation_preview.py +++ b/tests/unit/test_generation_preview.py @@ -14,7 +14,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path from unittest.mock import MagicMock @@ -264,9 +264,9 @@ class TestPreviewGenerationTaskResponse: def test_completed_state_response(self): """completed 状态的响应""" - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) resp = PreviewGenerationTaskResponse( task_id="task_123", status="completed", @@ -345,8 +345,8 @@ class TestRepositoryIsPreviewCompatibility: mock_model.bgm_config = {} mock_model.is_preview = True mock_model.logs = "[]" - mock_model.created_at = datetime.now(timezone.utc) - mock_model.updated_at = datetime.now(timezone.utc) + mock_model.created_at = datetime.now(UTC) + mock_model.updated_at = datetime.now(UTC) from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain @@ -385,8 +385,8 @@ class TestRepositoryIsPreviewCompatibility: mock_model.bgm_config = {} mock_model.is_preview = False mock_model.logs = "[]" - mock_model.created_at = datetime.now(timezone.utc) - mock_model.updated_at = datetime.now(timezone.utc) + mock_model.created_at = datetime.now(UTC) + mock_model.updated_at = datetime.now(UTC) from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain @@ -606,7 +606,7 @@ def _make_task( task.is_preview = is_preview task.resolution = resolution task.error_message = error_message - task.created_at = created_at or datetime.now(timezone.utc) + task.created_at = created_at or datetime.now(UTC) task.started_at = started_at task.completed_at = completed_at task.extra_meta = extra_meta or {} @@ -664,7 +664,7 @@ class TestToPreviewResponse: """计算生成耗时""" from datetime import timedelta - started = datetime(2026, 8, 1, 10, 0, 0, tzinfo=timezone.utc) + started = datetime(2026, 8, 1, 10, 0, 0, tzinfo=UTC) completed = started + timedelta(seconds=45) task = _make_task(started_at=started, completed_at=completed) resp = _to_preview_response(task) diff --git a/tests/unit/test_generation_task_domain.py b/tests/unit/test_generation_task_domain.py index 0eb2f764d..41b4822bf 100755 --- a/tests/unit/test_generation_task_domain.py +++ b/tests/unit/test_generation_task_domain.py @@ -4,7 +4,7 @@ GenerationTask 领域模型单元测试 import json import time -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -520,11 +520,11 @@ class TestGenerationTaskTimestamps: def test_mark_processing_sets_started_at(self, new_task): assert new_task.started_at is None - before = datetime.now(timezone.utc) + before = datetime.now(UTC) time.sleep(0.01) new_task.mark_processing() time.sleep(0.01) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before < new_task.started_at < after def test_mark_completed_sets_completed_at(self, new_task): diff --git a/tests/unit/test_generation_task_orphan_cleanup.py b/tests/unit/test_generation_task_orphan_cleanup.py index f9756014d..325634039 100755 --- a/tests/unit/test_generation_task_orphan_cleanup.py +++ b/tests/unit/test_generation_task_orphan_cleanup.py @@ -1,7 +1,7 @@ """GenerationTaskRepository - cleanup_stale_running 孤儿任务清理单元测试。""" import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) @@ -65,7 +65,7 @@ def test_cleanup_stale_running_old_running_marked_failed(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET updated_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=30), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=30), "id": task.id}, ) conn.commit() @@ -88,7 +88,7 @@ def test_cleanup_stale_running_pending_not_touched(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET updated_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(hours=1), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(hours=1), "id": task.id}, ) conn.commit() @@ -114,7 +114,7 @@ def test_cleanup_stale_running_multiple_orphans(): for t in tasks[:2]: conn.execute( text("UPDATE generation_tasks SET updated_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=20), "id": t.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=20), "id": t.id}, ) conn.commit() diff --git a/tests/unit/test_generation_task_pending_cleanup.py b/tests/unit/test_generation_task_pending_cleanup.py index 252f1be19..da96f1238 100644 --- a/tests/unit/test_generation_task_pending_cleanup.py +++ b/tests/unit/test_generation_task_pending_cleanup.py @@ -1,7 +1,7 @@ """GenerationTaskRepository - cleanup_stale_pending 超时 pending 清理单元测试。""" import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) @@ -66,7 +66,7 @@ def test_cleanup_stale_pending_old_pending_marked_failed(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(hours=1), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(hours=1), "id": task.id}, ) conn.commit() @@ -94,7 +94,7 @@ def test_cleanup_stale_pending_running_not_touched(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(hours=1), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(hours=1), "id": task.id}, ) conn.commit() @@ -113,7 +113,7 @@ def test_cleanup_stale_pending_custom_timeout(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=20), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=20), "id": task.id}, ) conn.commit() @@ -141,7 +141,7 @@ def test_cleanup_stale_pending_multiple(): for t in tasks: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(hours=2), "id": t.id}, + {"ts": datetime.now(UTC) - timedelta(hours=2), "id": t.id}, ) conn.commit() diff --git a/tests/unit/test_ingest_orphan_cleanup_1714.py b/tests/unit/test_ingest_orphan_cleanup_1714.py index cceccf8d4..9e0b98e87 100644 --- a/tests/unit/test_ingest_orphan_cleanup_1714.py +++ b/tests/unit/test_ingest_orphan_cleanup_1714.py @@ -8,7 +8,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path import pytest @@ -41,7 +41,7 @@ def session(): def _mk_job(session, *, status="processing", celery_task_id="cel-1", asset_id="a-1", minutes_ago=90): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) job = IngestJobModel( id=f"job-{minutes_ago}-{status}-{celery_task_id}", project_id="p-1", @@ -59,7 +59,7 @@ def _mk_job(session, *, status="processing", celery_task_id="cel-1", asset_id="a def _mk_asset(session, *, id="a-1", status="processing", minutes_ago=90, file_size=0): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) asset = AssetModel( id=id, project_id="p-1", diff --git a/tests/unit/test_inmemory_small_repos.py b/tests/unit/test_inmemory_small_repos.py index efeab813d..4eacd46f1 100755 --- a/tests/unit/test_inmemory_small_repos.py +++ b/tests/unit/test_inmemory_small_repos.py @@ -1,6 +1,6 @@ """InMemory 小型仓储模块单元测试(asset_library/tag/project/ingest_job/classification_job).""" -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -429,7 +429,7 @@ class TestUserRepositoryUniqueness: phone="13800000001", wechat_openid="wx-openid-1", wechat_unionid="wx-unionid-1", - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) @pytest.fixture @@ -442,7 +442,7 @@ class TestUserRepositoryUniqueness: phone="13800000002", wechat_openid="wx-openid-2", wechat_unionid="wx-unionid-2", - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) def test_duplicate_email_raises(self, repo, user1, user2): diff --git a/tests/unit/test_inmemory_user_repository.py b/tests/unit/test_inmemory_user_repository.py index 6d8eb4153..e909fce5d 100755 --- a/tests/unit/test_inmemory_user_repository.py +++ b/tests/unit/test_inmemory_user_repository.py @@ -1,7 +1,7 @@ """InMemoryUserRepository 单元测试.""" import copy -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -27,7 +27,7 @@ def sample_user() -> User: wechat_openid="wx-openid-abc", wechat_unionid="wx-unionid-def", phone="13800138000", - created_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), ) diff --git a/tests/unit/test_jwt_service.py b/tests/unit/test_jwt_service.py index 4c4d5c65b..8451ad609 100755 --- a/tests/unit/test_jwt_service.py +++ b/tests/unit/test_jwt_service.py @@ -1,7 +1,7 @@ """JWT 服务与处理器单元测试.""" import time -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import jwt import pytest @@ -164,8 +164,8 @@ class TestCreateAccessToken: def test_expire_matches_config(self, service): token = service.create_access_token(user_id="user-123") payload = jwt.decode(token, STRONG_SECRET, algorithms=["HS256"]) - iat = datetime.fromtimestamp(payload["iat"], tz=timezone.utc) - exp = datetime.fromtimestamp(payload["exp"], tz=timezone.utc) + iat = datetime.fromtimestamp(payload["iat"], tz=UTC) + exp = datetime.fromtimestamp(payload["exp"], tz=UTC) delta = exp - iat assert delta.total_seconds() == 15 * 60 # 15分钟 diff --git a/tests/unit/test_lipsync_routes.py b/tests/unit/test_lipsync_routes.py index b6c994d5e..a755d3f3d 100644 --- a/tests/unit/test_lipsync_routes.py +++ b/tests/unit/test_lipsync_routes.py @@ -4,6 +4,7 @@ CI 增量映射: lipsync.py (route) + lipsync_service.py → test_lipsync_routes """ import os +from datetime import UTC from unittest.mock import MagicMock, patch import pytest @@ -734,7 +735,7 @@ class TestLipsyncRouteStaleRefresh: from app.services.lipsync_service import LipsyncService mock_job = _make_mock_job(status="submitted") - mock_job.updated_at = datetime.now(timezone.utc) - timedelta(seconds=45) + mock_job.updated_at = datetime.now(UTC) - timedelta(seconds=45) refreshed_job = _make_mock_job(status="completed", output_video_url="https://cdn/out.mp4", output_duration=12.0) mock_db = MagicMock() mock_query = MagicMock() @@ -760,7 +761,7 @@ class TestLipsyncRouteStaleRefresh: from app.services.lipsync_service import LipsyncService mock_job = _make_mock_job(status="submitted") - mock_job.updated_at = datetime.now(timezone.utc) + mock_job.updated_at = datetime.now(UTC) mock_db = MagicMock() svc = LipsyncService(mock_db, client=mock_mediakit, cosyvoice_service=mock_cosyvoice) svc.get_job = MagicMock(return_value=mock_job) @@ -798,7 +799,7 @@ class TestLipsyncRouteStaleRefresh: from app.services.lipsync_service import LipsyncService mock_job = _make_mock_job(status="submitted") - mock_job.updated_at = datetime.now(timezone.utc) - timedelta(seconds=60) + mock_job.updated_at = datetime.now(UTC) - timedelta(seconds=60) mock_db = MagicMock() svc = LipsyncService(mock_db, client=mock_mediakit, cosyvoice_service=mock_cosyvoice) svc.get_job = MagicMock(return_value=mock_job) diff --git a/tests/unit/test_password_reset_use_case.py b/tests/unit/test_password_reset_use_case.py index bd3f11e3c..15271d758 100755 --- a/tests/unit/test_password_reset_use_case.py +++ b/tests/unit/test_password_reset_use_case.py @@ -1,6 +1,6 @@ """密码重置 Use Case 单元测试.""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock, patch import pytest @@ -146,9 +146,9 @@ class TestRequestPasswordReset: email_service=mock_email_service, ) req = RequestPasswordResetRequest(email="user@example.com") - before = datetime.now(timezone.utc) + before = datetime.now(UTC) ok, _ = uc.execute(req) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert ok is True saved_user = mock_user_repo.save.call_args[0][0] @@ -272,7 +272,7 @@ class TestResetPassword: """成功重置密码.""" user = _make_user() user.password_reset_token = "valid-token-123" - user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=1) mock_user_repo.find_by_password_reset_token.return_value = user uc = ResetPasswordUseCase(user_repository=mock_user_repo) @@ -334,7 +334,7 @@ class TestResetPassword: """过期令牌返回错误.""" user = _make_user() user.password_reset_token = "expired-token" - user.password_reset_expires_at = datetime.now(timezone.utc) - timedelta(hours=1) + user.password_reset_expires_at = datetime.now(UTC) - timedelta(hours=1) mock_user_repo.find_by_password_reset_token.return_value = user uc = ResetPasswordUseCase(user_repository=mock_user_repo) @@ -369,7 +369,7 @@ class TestResetPassword: """弱密码被拒绝.""" user = _make_user() user.password_reset_token = "valid-token" - user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=1) mock_user_repo.find_by_password_reset_token.return_value = user uc = ResetPasswordUseCase(user_repository=mock_user_repo) @@ -420,7 +420,7 @@ class TestResetPassword: """成功重置后令牌被清除,防止重复使用.""" user = _make_user() user.password_reset_token = "valid-token" - user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=1) mock_user_repo.find_by_password_reset_token.return_value = user uc = ResetPasswordUseCase(user_repository=mock_user_repo) @@ -443,7 +443,7 @@ class TestResetPassword: """密码被哈希后保存.""" user = _make_user() user.password_reset_token = "valid-token" - user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=1) + user.password_reset_expires_at = datetime.now(UTC) + timedelta(hours=1) mock_user_repo.find_by_password_reset_token.return_value = user uc = ResetPasswordUseCase(user_repository=mock_user_repo) diff --git a/tests/unit/test_payment_callback.py b/tests/unit/test_payment_callback.py index 4b9beec51..9dd841715 100644 --- a/tests/unit/test_payment_callback.py +++ b/tests/unit/test_payment_callback.py @@ -17,7 +17,7 @@ from __future__ import annotations import os import sys from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, patch @@ -55,7 +55,7 @@ class MockBillingRecord: for k, v in kwargs.items(): setattr(self, k, v) if self.created_at is None: - self.created_at = datetime.now(timezone.utc) + self.created_at = datetime.now(UTC) class MockBillingRepository: @@ -90,7 +90,7 @@ class MockBillingRepository: model.status = "paid" model.payment_method = payment_method model.payment_id = payment_id - model.paid_at = datetime.now(timezone.utc) + model.paid_at = datetime.now(UTC) return True def update_subscription_on_payment(self, user_id: str, plan: str, expires_at: datetime) -> None: @@ -207,7 +207,7 @@ class TestPaymentCallbackSuccess: assert mock_repo.updated_subscriptions["user-002"]["plan"] == "standard" # 年付到期时间应为约 365 天后 expires_at = mock_repo.updated_subscriptions["user-002"]["expires_at"] - expected = datetime.now(timezone.utc) + timedelta(days=365) + expected = datetime.now(UTC) + timedelta(days=365) assert abs((expires_at - expected).days) <= 1 @patch("packages.adapters.sqlalchemy_impl.billing_repository.SQLAlchemyBillingRepository") @@ -566,7 +566,7 @@ class TestMockBillingRepository: def test_update_subscription_on_payment(self): """支付成功后更新订阅。""" repo = MockBillingRepository() - expires = datetime.now(timezone.utc) + timedelta(days=30) + expires = datetime.now(UTC) + timedelta(days=30) repo.update_subscription_on_payment("user-001", "pro", expires) diff --git a/tests/unit/test_plan_generator.py b/tests/unit/test_plan_generator.py index 2d328cbc3..5bb2eea3b 100755 --- a/tests/unit/test_plan_generator.py +++ b/tests/unit/test_plan_generator.py @@ -14,9 +14,9 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path -from typing import List, Optional +from typing import Optional from unittest.mock import MagicMock, patch os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -72,7 +72,7 @@ class StubEditPlanRepository: self._plans[plan.id] = plan return plan - def list_all(self, **kwargs) -> List[EditPlan]: + def list_all(self, **kwargs) -> list[EditPlan]: return list(self._plans.values()) def count(self, **kwargs) -> int: @@ -128,7 +128,7 @@ class StubEditPlanClipRepository: status: Optional[EditPlanClipStatus] = None, skip: int = 0, limit: int = 100, - ) -> List[EditPlanClip]: + ) -> list[EditPlanClip]: items = [c for c in self._clips.values() if c.plan_id == plan_id] items.sort(key=lambda c: c.order) if status: @@ -185,15 +185,15 @@ def _make_template( preview_url="", sort_weight=0, status=EditTemplateStatus.ACTIVE, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) def _make_clip_configs( template_id: str = "tpl-001", - specs: Optional[List[dict]] = None, -) -> List[TemplateClipConfig]: + specs: Optional[list[dict]] = None, +) -> list[TemplateClipConfig]: """创建测试用 TemplateClipConfig 列表 specs 示例: [{"clip_type": ClipType.INTRO, "order": 0}, ...] @@ -217,8 +217,8 @@ def _make_clip_configs( material_requirements=spec.get("material_requirements"), transition_effect=spec.get("transition_effect", TransitionEffect.CUT), config=spec.get("config"), - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) configs.append(cfg) return configs diff --git a/tests/unit/test_points_repositories.py b/tests/unit/test_points_repositories.py index 164664963..8578a4185 100644 --- a/tests/unit/test_points_repositories.py +++ b/tests/unit/test_points_repositories.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime, timezone import pytest from sqlalchemy import create_engine @@ -203,7 +203,7 @@ class TestPointsOrderRepository: ) order_repo.create(order) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) updated = order_repo.update_status(order.id, "paid", payment_id="pay-123", paid_at=now) assert updated is not None assert updated.status == "paid" @@ -247,9 +247,9 @@ class TestPointsOrderRepository: class TestDailyUsageRepository: def _today(self): # The model column is DateTime, so use datetime for comparison - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc) + now = datetime.now(UTC) return now.replace(hour=0, minute=0, second=0, microsecond=0) def test_create_and_get(self, daily_repo): diff --git a/tests/unit/test_script_service.py b/tests/unit/test_script_service.py index b79df9c64..0980fe7c0 100644 --- a/tests/unit/test_script_service.py +++ b/tests/unit/test_script_service.py @@ -5,7 +5,7 @@ CI 增量映射: script_service.py → test_script_service.py from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest @@ -29,8 +29,8 @@ def _make_mock_script( m.content = content m.segments = segments if segments is not None else [{"text": "第一段", "duration": None}] m.tags = tags if tags is not None else ["口播"] - m.created_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) - m.updated_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) + m.created_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC) + m.updated_at = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC) return m diff --git a/tests/unit/test_scripts_routes.py b/tests/unit/test_scripts_routes.py index a047d0035..c79d6631f 100644 --- a/tests/unit/test_scripts_routes.py +++ b/tests/unit/test_scripts_routes.py @@ -6,7 +6,7 @@ CI 增量映射: scripts.py → test_scripts.py from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest @@ -79,7 +79,7 @@ class TestUpdateScriptRequest: class TestScriptResponse: def test_response_construction(self): - now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) + now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC) r = ScriptResponse( id="s1", user_id="u1", @@ -101,7 +101,7 @@ class TestScriptListResponse: assert r.items == [] def test_with_items(self): - now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=timezone.utc) + now = datetime(2026, 9, 8, 12, 0, 0, tzinfo=UTC) item = ScriptResponse( id="s1", user_id="u1", @@ -141,8 +141,8 @@ class TestRouteHandlers: mock_script.content = "内容" mock_script.segments = [{"text": "段1", "duration": None}] mock_script.tags = [] - mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) - mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) + mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC) + mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC) svc.create_script.return_value = mock_script req = CreateScriptRequest(title="测试", content="内容") @@ -163,8 +163,8 @@ class TestRouteHandlers: mock_script.content = "" mock_script.segments = [] mock_script.tags = [] - mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) - mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) + mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC) + mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC) svc.list_scripts.return_value = ([mock_script], 1) auth = self._make_auth_user() @@ -183,8 +183,8 @@ class TestRouteHandlers: mock_script.content = "" mock_script.segments = [] mock_script.tags = [] - mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) - mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) + mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC) + mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC) svc.get_script.return_value = mock_script auth = self._make_auth_user() @@ -215,8 +215,8 @@ class TestRouteHandlers: mock_script.content = "原内容" mock_script.segments = [] mock_script.tags = [] - mock_script.created_at = datetime(2026, 9, 8, tzinfo=timezone.utc) - mock_script.updated_at = datetime(2026, 9, 8, tzinfo=timezone.utc) + mock_script.created_at = datetime(2026, 9, 8, tzinfo=UTC) + mock_script.updated_at = datetime(2026, 9, 8, tzinfo=UTC) svc.update_script.return_value = mock_script req = UpdateScriptRequest(title="新标题") diff --git a/tests/unit/test_smart_match.py b/tests/unit/test_smart_match.py index 00a46b8e0..6c28f2e92 100755 --- a/tests/unit/test_smart_match.py +++ b/tests/unit/test_smart_match.py @@ -2,7 +2,7 @@ import random from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from typing import Any import pytest @@ -36,7 +36,7 @@ class FakeAsset: quality_score: float | None = None status: str = "ready" metadata: dict = field(default_factory=dict) - created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @property def file_type(self) -> str: @@ -45,7 +45,7 @@ class FakeAsset: return self.mime_type -NOW = datetime(2026, 8, 5, 12, 0, 0, tzinfo=timezone.utc) +NOW = datetime(2026, 8, 5, 12, 0, 0, tzinfo=UTC) class _ZeroNoiseRandom(random.Random): diff --git a/tests/unit/test_smart_match_integration.py b/tests/unit/test_smart_match_integration.py index 5b724f8d0..9459d8711 100644 --- a/tests/unit/test_smart_match_integration.py +++ b/tests/unit/test_smart_match_integration.py @@ -12,7 +12,7 @@ import os import random import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -60,7 +60,7 @@ def _asset_with_use_count(asset_id: str, use_count: int) -> FakeAsset: id=asset_id, quality_score=70.0, duration=15.0, # 最优区间 5-30s - created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + created_at=datetime(2026, 1, 1, tzinfo=UTC), metadata={"generation_use_count": use_count}, ) diff --git a/tests/unit/test_smart_match_noise_1743.py b/tests/unit/test_smart_match_noise_1743.py index 03b035351..1705f70c9 100644 --- a/tests/unit/test_smart_match_noise_1743.py +++ b/tests/unit/test_smart_match_noise_1743.py @@ -12,7 +12,7 @@ from __future__ import annotations import random import sys from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[2] @@ -27,7 +27,7 @@ from packages.domain.smart_match import ( # noqa: E402 smart_select_assets, ) -NOW = datetime(2026, 9, 6, 12, 0, 0, tzinfo=timezone.utc) +NOW = datetime(2026, 9, 6, 12, 0, 0, tzinfo=UTC) @dataclass diff --git a/tests/unit/test_stale_task_revoke_1714.py b/tests/unit/test_stale_task_revoke_1714.py index 4ac086402..411f1bd74 100644 --- a/tests/unit/test_stale_task_revoke_1714.py +++ b/tests/unit/test_stale_task_revoke_1714.py @@ -11,7 +11,7 @@ from __future__ import annotations import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock @@ -65,7 +65,7 @@ def test_cleanup_stale_pending_returns_ids_with_celery_task_id(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=60), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=60), "id": task.id}, ) conn.commit() @@ -89,7 +89,7 @@ def test_cleanup_stale_running_returns_ids(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET updated_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=60), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=60), "id": task.id}, ) conn.commit() @@ -156,7 +156,7 @@ def test_stale_pending_cleanup_purges_redis_message(): with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET created_at = :ts WHERE id = :id"), - {"ts": datetime.now(timezone.utc) - timedelta(minutes=60), "id": task.id}, + {"ts": datetime.now(UTC) - timedelta(minutes=60), "id": task.id}, ) conn.commit() diff --git a/tests/unit/test_strenum_deserialization.py b/tests/unit/test_strenum_deserialization.py index 8f3275560..480a2ae47 100644 --- a/tests/unit/test_strenum_deserialization.py +++ b/tests/unit/test_strenum_deserialization.py @@ -9,7 +9,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -44,8 +44,8 @@ def _make_tts_model(status: str = "pending"): model.metadata_ = {} model.started_at = None model.completed_at = None - model.created_at = datetime.now(timezone.utc) - model.updated_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) + model.updated_at = datetime.now(UTC) return model @@ -66,8 +66,8 @@ def _make_voice_clone_model(status: str = "pending"): model.retry_count = 0 model.max_retries = 3 model.metadata_ = {} - model.created_at = datetime.now(timezone.utc) - model.updated_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) + model.updated_at = datetime.now(UTC) return model @@ -91,7 +91,7 @@ def _make_generation_task_model(status: str = "pending"): model.completed_at = None model.created_by_user_id = "user_001" model.source_edit_plan_id = "" - model.created_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) return model @@ -195,8 +195,8 @@ class TestAlreadyCorrectRepositories: model.project_id = "" model.created_by_user_id = "user_001" model.config = {} - model.created_at = datetime.now(timezone.utc) - model.updated_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) + model.updated_at = datetime.now(UTC) entity = SQLAlchemyEditPlanRepository._model_to_entity(model) assert isinstance(entity.status, EditPlanStatus) @@ -216,8 +216,8 @@ class TestAlreadyCorrectRepositories: model.preview_url = "" model.sort_weight = 0 model.status = "active" # 纯字符串 - model.created_at = datetime.now(timezone.utc) - model.updated_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) + model.updated_at = datetime.now(UTC) entity = SQLAlchemyEditTemplateRepository._model_to_entity(model) assert isinstance(entity.status, EditTemplateStatus) @@ -241,8 +241,8 @@ class TestAlreadyCorrectRepositories: model.transition_effect = "cut" model.status = "ready" # 纯字符串 model.config = {} - model.created_at = datetime.now(timezone.utc) - model.updated_at = datetime.now(timezone.utc) + model.created_at = datetime.now(UTC) + model.updated_at = datetime.now(UTC) entity = SQLAlchemyEditPlanClipRepository._model_to_entity(model) assert isinstance(entity.status, EditPlanClipStatus) diff --git a/tests/unit/test_task_fault_tolerance_1709.py b/tests/unit/test_task_fault_tolerance_1709.py index 756543d2a..93ec880fb 100644 --- a/tests/unit/test_task_fault_tolerance_1709.py +++ b/tests/unit/test_task_fault_tolerance_1709.py @@ -9,7 +9,7 @@ """ import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock @@ -55,10 +55,10 @@ def _age_task(engine, task_id, *, updated_minutes=None, created_minutes=None): sets, params = [], {"id": task_id} if updated_minutes is not None: sets.append("updated_at = :uts") - params["uts"] = datetime.now(timezone.utc) - timedelta(minutes=updated_minutes) + params["uts"] = datetime.now(UTC) - timedelta(minutes=updated_minutes) if created_minutes is not None: sets.append("created_at = :cts") - params["cts"] = datetime.now(timezone.utc) - timedelta(minutes=created_minutes) + params["cts"] = datetime.now(UTC) - timedelta(minutes=created_minutes) with engine.connect() as conn: conn.execute(text(f"UPDATE generation_tasks SET {', '.join(sets)} WHERE id = :id"), params) conn.commit() @@ -126,7 +126,7 @@ def _complete_task(repo, engine, task, duration_seconds: float): repo.update(task) task.mark_completed() repo.update(task) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) with engine.connect() as conn: conn.execute( text("UPDATE generation_tasks SET started_at = :s, completed_at = :c WHERE id = :id"), diff --git a/tests/unit/test_template_clip_config.py b/tests/unit/test_template_clip_config.py index d43dd315b..a1a59d9ff 100755 --- a/tests/unit/test_template_clip_config.py +++ b/tests/unit/test_template_clip_config.py @@ -1,6 +1,6 @@ """TemplateClipConfig domain entity unit tests (wave219).""" -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest from domain.template_clip_config import ( @@ -216,13 +216,13 @@ class TestTemplateClipConfigCreate: assert cfg1.id != cfg2.id def test_create_timestamps_are_utc(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) cfg = TemplateClipConfig.create("tpl", ClipType.MAIN, 1) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= cfg.created_at <= after assert before <= cfg.updated_at <= after - assert cfg.created_at.tzinfo is timezone.utc - assert cfg.updated_at.tzinfo is timezone.utc + assert cfg.created_at.tzinfo is UTC + assert cfg.updated_at.tzinfo is UTC # --------------------------------------------------------------------------- diff --git a/tests/unit/test_template_editing_mode.py b/tests/unit/test_template_editing_mode.py index 29a8fcad8..69ce68b3c 100644 --- a/tests/unit/test_template_editing_mode.py +++ b/tests/unit/test_template_editing_mode.py @@ -13,7 +13,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from pathlib import Path os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing") @@ -164,7 +164,7 @@ class TestEditTemplateEntityDirect: def test_direct_construction(self): """直接构造带 editing_mode 的实体""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) tpl = EditTemplate( id="tpl-test", name="直接构造", diff --git a/tests/unit/test_title_usage_pure.py b/tests/unit/test_title_usage_pure.py index 6b9715aaf..b08f57bd0 100755 --- a/tests/unit/test_title_usage_pure.py +++ b/tests/unit/test_title_usage_pure.py @@ -5,7 +5,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock from packages.adapters.sqlalchemy_impl.models import TitleLibraryModel @@ -116,9 +116,9 @@ class TestMarkTitleUsedForGeneration: db.query.return_value.filter.return_value.first.return_value = title task = self._make_task() - before = datetime.now(timezone.utc) + before = datetime.now(UTC) mark_title_used_for_generation(db, task) - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= title.updated_at <= after assert title.updated_at.tzinfo is not None # 带时区 diff --git a/tests/unit/test_tts_api.py b/tests/unit/test_tts_api.py index f4f4f8667..17055e104 100644 --- a/tests/unit/test_tts_api.py +++ b/tests/unit/test_tts_api.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -40,8 +40,8 @@ def _make_job(**kwargs) -> TTSJob: "metadata": {}, "started_at": None, "completed_at": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return TTSJob(**defaults) diff --git a/tests/unit/test_tts_job.py b/tests/unit/test_tts_job.py index c2ab30111..c87dc740d 100755 --- a/tests/unit/test_tts_job.py +++ b/tests/unit/test_tts_job.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from time import sleep import pytest @@ -571,7 +571,7 @@ class TestPrepareRetry: job = TTSJob.create(user_id="u1", input_text="hi") job.mark_processing() job.mark_failed("err") - job.completed_at = datetime.now(timezone.utc) # 模拟设置过 + job.completed_at = datetime.now(UTC) # 模拟设置过 job.prepare_retry() assert job.completed_at is None diff --git a/tests/unit/test_tts_oss_transfer.py b/tests/unit/test_tts_oss_transfer.py index 63203c2d4..b610ef936 100755 --- a/tests/unit/test_tts_oss_transfer.py +++ b/tests/unit/test_tts_oss_transfer.py @@ -7,7 +7,7 @@ from __future__ import annotations import unittest -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch from packages.application.cosyvoice_service import CosyVoiceService @@ -37,8 +37,8 @@ def _make_job(**kwargs) -> TTSJob: "metadata": {}, "started_at": None, "completed_at": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return TTSJob(**defaults) diff --git a/tests/unit/test_tts_save_to_library.py b/tests/unit/test_tts_save_to_library.py index b40711839..12b913a17 100644 --- a/tests/unit/test_tts_save_to_library.py +++ b/tests/unit/test_tts_save_to_library.py @@ -11,7 +11,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -41,10 +41,10 @@ def _make_completed_job(**kwargs) -> TTSJob: "retry_count": 0, "max_retries": 3, "metadata": {"speed": 1.0, "language": "zh-CN"}, - "started_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=timezone.utc), - "completed_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=timezone.utc), - "created_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=timezone.utc), + "started_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=UTC), + "completed_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=UTC), + "created_at": datetime(2026, 7, 7, 10, 0, 0, tzinfo=UTC), + "updated_at": datetime(2026, 7, 7, 10, 0, 5, tzinfo=UTC), } defaults.update(kwargs) return TTSJob(**defaults) @@ -74,8 +74,8 @@ def _make_voice_library_item(**kwargs) -> VoiceLibraryItem: "speed": 1.0, "language": "zh-CN", }, - "created_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=timezone.utc), - "updated_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=timezone.utc), + "created_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=UTC), + "updated_at": datetime(2026, 7, 7, 10, 1, 0, tzinfo=UTC), } defaults.update(kwargs) return VoiceLibraryItem(**defaults) diff --git a/tests/unit/test_tts_save_to_library_assets.py b/tests/unit/test_tts_save_to_library_assets.py index 92544dc31..1f7af3bbe 100644 --- a/tests/unit/test_tts_save_to_library_assets.py +++ b/tests/unit/test_tts_save_to_library_assets.py @@ -6,7 +6,7 @@ voice 素材库查找/自动创建/并发竞态兜底;未完成/无音频/下 from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock from packages.domain import AssetLibrary, AssetLibraryKind @@ -35,8 +35,8 @@ def _completed_job(**kwargs) -> TTSJob: "metadata": {"speed": 1.0}, "started_at": None, "completed_at": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return TTSJob(**defaults) diff --git a/tests/unit/test_tts_segment_synthesis.py b/tests/unit/test_tts_segment_synthesis.py index 5159683fe..e7f05aa48 100755 --- a/tests/unit/test_tts_segment_synthesis.py +++ b/tests/unit/test_tts_segment_synthesis.py @@ -10,7 +10,7 @@ from __future__ import annotations import os import tempfile -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest @@ -191,8 +191,8 @@ def _make_job(**kwargs) -> TTSJob: "metadata": {}, "started_at": None, "completed_at": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return TTSJob(**defaults) diff --git a/tests/unit/test_tts_synthesis_failure.py b/tests/unit/test_tts_synthesis_failure.py index 84094e6e7..5d121dda4 100644 --- a/tests/unit/test_tts_synthesis_failure.py +++ b/tests/unit/test_tts_synthesis_failure.py @@ -6,7 +6,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -43,8 +43,8 @@ def _make_job(**kwargs) -> TTSJob: "metadata": {}, "started_at": None, "completed_at": None, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return TTSJob(**defaults) diff --git a/tests/unit/test_tts_synthesize_clone_voice.py b/tests/unit/test_tts_synthesize_clone_voice.py index 821df2c55..c1c0a1684 100644 --- a/tests/unit/test_tts_synthesize_clone_voice.py +++ b/tests/unit/test_tts_synthesize_clone_voice.py @@ -6,7 +6,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch import pytest @@ -51,8 +51,8 @@ def _make_job(voice_id, voice_clone_profile_id=""): metadata={}, started_at=None, completed_at=None, - created_at=datetime.now(timezone.utc), - updated_at=datetime.now(timezone.utc), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), ) diff --git a/tests/unit/test_upload_complete_idempotency_1714.py b/tests/unit/test_upload_complete_idempotency_1714.py index c0436b065..544a36887 100644 --- a/tests/unit/test_upload_complete_idempotency_1714.py +++ b/tests/unit/test_upload_complete_idempotency_1714.py @@ -16,7 +16,7 @@ from __future__ import annotations import os import sys -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock @@ -76,7 +76,7 @@ class StubAssetRepository: # 严格模式(#1714):大小未知(0)直接不命中,宁可漏判不可误杀 if not file_size or file_size <= 0: return None - cutoff = datetime.now(timezone.utc) - timedelta(minutes=within_minutes) + cutoff = datetime.now(UTC) - timedelta(minutes=within_minutes) candidates = [ a for a in self._assets @@ -361,7 +361,7 @@ class TestDirectCompleteIdempotency: mime_type="video/quicktime", status=AssetStatus.PROCESSING, ) - stale.created_at = datetime.now(timezone.utc) - timedelta(minutes=45) + stale.created_at = datetime.now(UTC) - timedelta(minutes=45) client, asset_repo, ingest_repo, _ = _client(asset_repo=StubAssetRepository([stale])) r = client.post("/api/v1/direct/complete", json=COMPLETE_BODY) assert r.status_code == 200 diff --git a/tests/unit/test_verification_code_domain.py b/tests/unit/test_verification_code_domain.py index d4a01363d..e4906f7aa 100755 --- a/tests/unit/test_verification_code_domain.py +++ b/tests/unit/test_verification_code_domain.py @@ -2,7 +2,7 @@ VerificationCode 验证码领域模型单元测试 """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import pytest from domain.verification_code import VerificationCode @@ -27,16 +27,16 @@ class TestVerificationCodeCreate: assert vc.code == "123456" def test_create_default_ttl_300s(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) vc = VerificationCode.create("test@example.com", "email_login") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) expected_expiry_min = before + timedelta(seconds=300) expected_expiry_max = after + timedelta(seconds=300) assert expected_expiry_min <= vc.expires_at <= expected_expiry_max def test_create_custom_ttl(self): vc = VerificationCode.create("test@example.com", "reset_password", ttl_seconds=60) - expected = datetime.now(timezone.utc) + timedelta(seconds=60) + expected = datetime.now(UTC) + timedelta(seconds=60) diff = abs((vc.expires_at - expected).total_seconds()) assert diff < 2 @@ -67,14 +67,14 @@ class TestVerificationCodeExpiry: assert vc.is_expired is True def test_boundary_not_expired_at_expiry_time(self): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) vc = VerificationCode.create("test@example.com", "email_bind") vc.expires_at = now + timedelta(seconds=1) assert vc.is_expired is False def test_boundary_expired_right_after(self): vc = VerificationCode.create("test@example.com", "email_bind") - vc.expires_at = datetime.now(timezone.utc) - timedelta(microseconds=1) + vc.expires_at = datetime.now(UTC) - timedelta(microseconds=1) assert vc.is_expired is True @@ -94,9 +94,9 @@ class TestVerificationCodeUsed: def test_mark_used_sets_recent_time(self): vc = VerificationCode.create("test@example.com", "email_bind") - before = datetime.now(timezone.utc) + before = datetime.now(UTC) vc.mark_used() - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= vc.used_at <= after def test_mark_used_idempotent(self): diff --git a/tests/unit/test_verification_code_service.py b/tests/unit/test_verification_code_service.py index 2698e3096..e84e0ca09 100755 --- a/tests/unit/test_verification_code_service.py +++ b/tests/unit/test_verification_code_service.py @@ -1,7 +1,7 @@ """验证码服务单元测试.""" import re -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock import pytest @@ -48,7 +48,7 @@ def _make_code( created_at=None, ): """创建一个测试用验证码实体.""" - now = created_at or datetime.now(timezone.utc) + now = created_at or datetime.now(UTC) vc = VerificationCode( id="test-code-id", recipient=recipient, @@ -139,7 +139,7 @@ class TestGenerateRateLimit: def test_cooldown_active_rejects(self, service, mock_repo): """冷却期内拒绝重发.""" - recent = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=10)) + recent = _make_code(created_at=datetime.now(UTC) - timedelta(seconds=10)) mock_repo.find_latest.return_value = recent code, error = service.generate("u@e.com", CODE_TYPE_EMAIL_BIND) @@ -153,7 +153,7 @@ class TestGenerateRateLimit: def test_cooldown_expired_allows(self, service, mock_repo): """冷却期过后允许重发.""" - old = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=120)) + old = _make_code(created_at=datetime.now(UTC) - timedelta(seconds=120)) mock_repo.find_latest.return_value = old mock_repo.count_today.return_value = 1 @@ -191,7 +191,7 @@ class TestGenerateRateLimit: def test_custom_cooldown(self, mock_repo): """自定义冷却时间.""" svc = VerificationCodeService(repo=mock_repo, resend_cooldown=30) - recent = _make_code(created_at=datetime.now(timezone.utc) - timedelta(seconds=10)) + recent = _make_code(created_at=datetime.now(UTC) - timedelta(seconds=10)) mock_repo.find_latest.return_value = recent code, error = svc.generate("u@e.com", CODE_TYPE_EMAIL_BIND) diff --git a/tests/unit/test_video_share.py b/tests/unit/test_video_share.py index 4176b78de..34c11dd5a 100755 --- a/tests/unit/test_video_share.py +++ b/tests/unit/test_video_share.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import pytest from domain.video_share import ( @@ -98,7 +98,7 @@ class TestVideoShareCreate: assert len(share.password_hash) > 0 def test_create_with_expiration(self): - expire_time = datetime(2026, 12, 31, tzinfo=timezone.utc) + expire_time = datetime(2026, 12, 31, tzinfo=UTC) share = VideoShare.create(video_id="vid-1", user_id="user-1", expires_at=expire_time) assert share.expires_at == expire_time @@ -156,19 +156,19 @@ class TestVideoShareExpiration: assert share.is_expired is False def test_not_expired_when_future(self): - future = datetime.now(timezone.utc) + timedelta(days=7) + future = datetime.now(UTC) + timedelta(days=7) share = VideoShare.create(video_id="v", user_id="u", expires_at=future) assert share.is_expired is False def test_expired_when_past(self): share = VideoShare.create(video_id="v", user_id="u") # 直接设置过去的过期时间(create 方法会阻止过期时间在过去) - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_expired is True def test_create_rejects_past_expiry(self): """create 方法拒绝过去的过期时间。""" - past = datetime.now(timezone.utc) - timedelta(days=1) + past = datetime.now(UTC) - timedelta(days=1) with pytest.raises(ValueError, match="expires_at cannot be in the past"): VideoShare.create(video_id="v", user_id="u", expires_at=past) @@ -188,7 +188,7 @@ class TestVideoShareAccessible: def test_expired_not_accessible(self): share = VideoShare.create(video_id="v", user_id="u") # 直接设置过去的过期时间 - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_accessible is False diff --git a/tests/unit/test_video_share_domain.py b/tests/unit/test_video_share_domain.py index 8eef9b85a..7c64e27dc 100755 --- a/tests/unit/test_video_share_domain.py +++ b/tests/unit/test_video_share_domain.py @@ -1,6 +1,6 @@ """VideoShare 领域层单元测试 - video_share.py""" -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import pytest @@ -109,12 +109,12 @@ class TestVideoShareCreate: assert len(share.password_hash) == 64 # SHA-256 hex def test_create_with_expires_at(self): - future = datetime.now(timezone.utc) + timedelta(days=7) + future = datetime.now(UTC) + timedelta(days=7) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.expires_at == future def test_create_past_expires_at_raises(self): - past = datetime.now(timezone.utc) - timedelta(days=1) + past = datetime.now(UTC) - timedelta(days=1) with pytest.raises(ValueError, match="expires_at cannot be in the past"): VideoShare.create(video_id="v1", user_id="u1", expires_at=past) @@ -146,14 +146,14 @@ class TestVideoShareProperties: assert share.is_expired is False def test_is_expired_future_expiry(self): - future = datetime.now(timezone.utc) + timedelta(days=7) + future = datetime.now(UTC) + timedelta(days=7) share = VideoShare.create(video_id="v1", user_id="u1", expires_at=future) assert share.is_expired is False def test_is_expired_past_expiry(self): # 直接设置 expires_at 为过去时间(绕过 create 的校验) share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_expired is True def test_is_accessible_active_not_expired(self): @@ -167,13 +167,13 @@ class TestVideoShareProperties: def test_is_accessible_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_accessible is False def test_is_accessible_inactive_and_expired(self): share = VideoShare.create(video_id="v1", user_id="u1") share.is_active = False - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) assert share.is_accessible is False @@ -302,12 +302,12 @@ class TestVideoShareExtended: def test_expires_at_boundary_exact_now(self): """expires_at 恰好是现在,应该被认为过期""" share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) - timedelta(microseconds=1) + share.expires_at = datetime.now(UTC) - timedelta(microseconds=1) assert share.is_expired is True def test_expires_at_boundary_one_second_future(self): share = VideoShare.create(video_id="v1", user_id="u1") - share.expires_at = datetime.now(timezone.utc) + timedelta(seconds=1) + share.expires_at = datetime.now(UTC) + timedelta(seconds=1) assert share.is_expired is False def test_password_with_special_characters(self): diff --git a/tests/unit/test_video_share_use_cases.py b/tests/unit/test_video_share_use_cases.py index ebd10f78a..95bbbda9b 100755 --- a/tests/unit/test_video_share_use_cases.py +++ b/tests/unit/test_video_share_use_cases.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone from unittest.mock import MagicMock import pytest @@ -167,7 +167,7 @@ class TestGetShareByTokenUseCase: def test_expired_share_raises(self): share = _make_share() - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) repo = MagicMock() repo.get_by_token.return_value = share @@ -241,7 +241,7 @@ class TestAccessShareUseCase: def test_access_expired_share(self): share = _make_share() - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) share_repo = MagicMock() share_repo.get_by_token.return_value = share @@ -369,7 +369,7 @@ class TestUpdateShareUseCase: def test_update_expires_at(self): share = _make_share() - new_expiry = datetime.now(timezone.utc) + timedelta(days=30) + new_expiry = datetime.now(UTC) + timedelta(days=30) repo = MagicMock() repo.get_by_id.return_value = share repo.update.side_effect = lambda s: s @@ -382,7 +382,7 @@ class TestUpdateShareUseCase: def test_update_expires_at_past_raises(self): share = _make_share() - past = datetime.now(timezone.utc) - timedelta(days=1) + past = datetime.now(UTC) - timedelta(days=1) repo = MagicMock() repo.get_by_id.return_value = share @@ -471,7 +471,7 @@ class TestRecordShareDownloadUseCase: def test_record_download_expired_raises(self): share = _make_share() - share.expires_at = datetime.now(timezone.utc) - timedelta(days=1) + share.expires_at = datetime.now(UTC) - timedelta(days=1) repo = MagicMock() repo.get_by_token.return_value = share diff --git a/tests/unit/test_video_upload_fix_and_generated_api.py b/tests/unit/test_video_upload_fix_and_generated_api.py index 419a86739..14bb1d67b 100644 --- a/tests/unit/test_video_upload_fix_and_generated_api.py +++ b/tests/unit/test_video_upload_fix_and_generated_api.py @@ -6,7 +6,7 @@ """ import json -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock, patch # ─── 1. chunked_upload ALLOWED_MIME_TYPES 一致性 ─── @@ -109,8 +109,8 @@ class TestGeneratedVideoRepositoryNoNPlus1: model.video_fingerprint = None model.is_duplicate = False model.duplicate_of = None - model.generated_at = datetime(2026, 1, 1, tzinfo=timezone.utc) - model.created_at = datetime(2026, 1, 1, tzinfo=timezone.utc) + model.generated_at = datetime(2026, 1, 1, tzinfo=UTC) + model.created_at = datetime(2026, 1, 1, tzinfo=UTC) return model def test_list_by_project_uses_to_domain(self): diff --git a/tests/unit/test_voice_clone_api.py b/tests/unit/test_voice_clone_api.py index c5cd3877f..329cbdf4e 100644 --- a/tests/unit/test_voice_clone_api.py +++ b/tests/unit/test_voice_clone_api.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -36,8 +36,8 @@ def _make_profile(**kwargs) -> VoiceCloneProfile: "retry_count": 0, "max_retries": 3, "metadata": {}, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return VoiceCloneProfile(**defaults) diff --git a/tests/unit/test_voice_clone_preview.py b/tests/unit/test_voice_clone_preview.py index 75d33e7fb..a88d459eb 100755 --- a/tests/unit/test_voice_clone_preview.py +++ b/tests/unit/test_voice_clone_preview.py @@ -6,7 +6,7 @@ from __future__ import annotations import time -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -32,8 +32,8 @@ def _make_profile(**kwargs) -> VoiceCloneProfile: "retry_count": 0, "max_retries": 3, "metadata": {}, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), + "created_at": datetime.now(UTC), + "updated_at": datetime.now(UTC), } defaults.update(kwargs) return VoiceCloneProfile(**defaults) diff --git a/tests/unit/test_voices_clone_from_profile.py b/tests/unit/test_voices_clone_from_profile.py index 2ce8e027b..03e2104de 100755 --- a/tests/unit/test_voices_clone_from_profile.py +++ b/tests/unit/test_voices_clone_from_profile.py @@ -5,7 +5,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone from unittest.mock import MagicMock import pytest @@ -29,8 +29,8 @@ def _make_clone_profile(**kwargs) -> VoiceCloneProfile: "retry_count": 0, "max_retries": 3, "metadata": {}, - "created_at": datetime(2026, 7, 30, 10, 0, 0, tzinfo=timezone.utc), - "updated_at": datetime(2026, 7, 30, 10, 5, 0, tzinfo=timezone.utc), + "created_at": datetime(2026, 7, 30, 10, 0, 0, tzinfo=UTC), + "updated_at": datetime(2026, 7, 30, 10, 5, 0, tzinfo=UTC), } defaults.update(kwargs) return VoiceCloneProfile(**defaults) diff --git a/tests/unit/test_wave76_tts_job_tag.py b/tests/unit/test_wave76_tts_job_tag.py index 748a9965d..45c3a41ea 100755 --- a/tests/unit/test_wave76_tts_job_tag.py +++ b/tests/unit/test_wave76_tts_job_tag.py @@ -1,6 +1,6 @@ """第76波:TTSJob + Job + Tag 领域纯逻辑单测。""" -from datetime import datetime, timezone +from datetime import UTC, datetime, timezone import pytest @@ -39,9 +39,9 @@ class TestTagCreate: assert tag1.id != tag2.id def test_create_uses_utc_timezone(self): - before = datetime.now(timezone.utc) + before = datetime.now(UTC) tag = Tag.create(user_id="u1", name="t") - after = datetime.now(timezone.utc) + after = datetime.now(UTC) assert before <= tag.created_at <= after diff --git a/tests/unit/test_wechat_login_and_verification.py b/tests/unit/test_wechat_login_and_verification.py index 9cd55481c..26f4cc391 100755 --- a/tests/unit/test_wechat_login_and_verification.py +++ b/tests/unit/test_wechat_login_and_verification.py @@ -2,7 +2,7 @@ 验证码服务 + 绑定流程单元测试 """ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta, timezone import pytest @@ -44,7 +44,7 @@ class InMemoryVerificationCodeRepository: return self._codes.get(code_id) def count_today(self, recipient, code_type): - now = datetime.now(timezone.utc) + now = datetime.now(UTC) start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0) return sum( 1 @@ -148,8 +148,8 @@ class TestVerificationCodeService: def test_verify_expired_code(self, vc_service, vc_repo): # 手动创建一个已过期的验证码 expired = VerificationCode.create("13800138000", CODE_TYPE_PHONE_BIND, ttl_seconds=1) - expired.created_at = datetime.now(timezone.utc) - timedelta(seconds=10) - expired.expires_at = datetime.now(timezone.utc) - timedelta(seconds=5) + expired.created_at = datetime.now(UTC) - timedelta(seconds=10) + expired.expires_at = datetime.now(UTC) - timedelta(seconds=5) vc_repo.save(expired) ok, err = vc_service.verify("13800138000", CODE_TYPE_PHONE_BIND, expired.code)