From 4eede0c8c0a2ce664291f2d93a596ba4dfeae255 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 22 Oct 2025 08:14:42 +0200 Subject: [PATCH] better time usings --- fix_test_instruction.md | 105 +---------------------- src/server/database/base.py | 4 +- src/server/database/models.py | 4 +- src/server/database/service.py | 18 ++-- src/server/models/auth.py | 4 +- src/server/models/download.py | 6 +- src/server/models/websocket.py | 24 +++--- src/server/services/auth_service.py | 16 ++-- src/server/services/download_service.py | 14 +-- src/server/services/progress_service.py | 14 +-- src/server/services/websocket_service.py | 16 ++-- 11 files changed, 62 insertions(+), 163 deletions(-) diff --git a/fix_test_instruction.md b/fix_test_instruction.md index abc8802..d21e18b 100644 --- a/fix_test_instruction.md +++ b/fix_test_instruction.md @@ -313,107 +313,6 @@ conda run -n AniWorld python -m pytest tests/ -v -s --- -## ✅ Completed Sections (Can be removed from this document) - -The following sections have been successfully completed: - -### Deprecated Pydantic V1 API Usage - -**Count:** ~20 warnings - -**Location:** `src/config/settings.py` - -**Issue:** - -```python -# Deprecated usage -Field(default="value", env="ENV_VAR") - -# Should be: -Field(default="value", json_schema_extra={"env": "ENV_VAR"}) -``` - -**Files to Update:** - -- `src/config/settings.py` - Update Field definitions -- `src/server/models/config.py` - Update validators to `@field_validator` - ---- - -### Deprecated datetime.utcnow() Usage - -**Count:** ~100+ warnings - -**Issue:** - -```python -# Deprecated -datetime.utcnow() - -# Should use -datetime.now(datetime.UTC) -``` - -**Files to Update:** - -- `src/server/services/auth_service.py` -- `src/server/services/download_service.py` -- `src/server/services/progress_service.py` -- `src/server/services/websocket_service.py` -- `src/server/database/service.py` -- `src/server/database/models.py` -- All test files using `datetime.utcnow()` - ---- - -### Deprecated FastAPI on_event - -**Count:** 4 warnings - -**Location:** `src/server/fastapi_app.py` - -**Issue:** - -```python -# Deprecated -@app.on_event("startup") -@app.on_event("shutdown") - -# Should use lifespan -from contextlib import asynccontextmanager - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Startup - yield - # Shutdown -``` - ---- - -### Deprecated Pydantic .dict() Method - -**Count:** ~5 warnings - -**Issue:** - -```python -# Deprecated -session.dict() - -# Should be -session.model_dump() -``` - -**Files to Update:** - -- `src/server/middleware/auth.py` -- `src/server/utils/dependencies.py` - ---- - -## 📝 Task Checklist for AI Agent - ### Phase 4: Frontend Integration 🔄 IN PROGRESS - [ ] Fix frontend auth integration tests (42 total → 4 remaining failures) @@ -440,7 +339,7 @@ session.model_dump() - TestFrontendRealTimeUpdates (3 failures): All real-time tests - TestFrontendDataFormats (3 failures): All format tests -### Phase 5: WebSocket Integration ✅ MOSTLY COMPLETE +### Phase 5: WebSocket Integration - [x] Fix websocket integration tests (48 failures → 2 remaining) - [x] Test connection management @@ -452,7 +351,7 @@ session.model_dump() - `test_concurrent_broadcasts_to_different_rooms` - `test_multi_room_workflow` -### Phase 6: Download Flow ✅ MOSTLY COMPLETE +### Phase 6: Download Flow - [x] Fix download endpoint API tests (18 errors → All 20 tests passing!) - [x] Fix download flow integration tests (22+ errors → 11 remaining) diff --git a/src/server/database/base.py b/src/server/database/base.py index acc6308..55ec48a 100644 --- a/src/server/database/base.py +++ b/src/server/database/base.py @@ -3,7 +3,7 @@ This module provides the base class that all ORM models inherit from, along with common functionality and mixins. """ -from datetime import datetime +from datetime import datetime, timezone from typing import Any from sqlalchemy import DateTime, func @@ -67,7 +67,7 @@ class SoftDeleteMixin: def soft_delete(self) -> None: """Mark record as deleted without removing from database.""" - self.deleted_at = datetime.utcnow() + self.deleted_at = datetime.now(timezone.utc) def restore(self) -> None: """Restore a soft deleted record.""" diff --git a/src/server/database/models.py b/src/server/database/models.py index 3adb20e..2f4b992 100644 --- a/src/server/database/models.py +++ b/src/server/database/models.py @@ -11,7 +11,7 @@ Models: """ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import List, Optional @@ -422,7 +422,7 @@ class UserSession(Base, TimestampMixin): @property def is_expired(self) -> bool: """Check if session has expired.""" - return datetime.utcnow() > self.expires_at + return datetime.now(timezone.utc) > self.expires_at def revoke(self) -> None: """Revoke this session.""" diff --git a/src/server/database/service.py b/src/server/database/service.py index edb8fa4..4bb82cc 100644 --- a/src/server/database/service.py +++ b/src/server/database/service.py @@ -14,7 +14,7 @@ All services support both async and sync operations for flexibility. from __future__ import annotations import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, List, Optional from sqlalchemy import delete, select, update @@ -276,7 +276,7 @@ class EpisodeService: file_path=file_path, file_size=file_size, is_downloaded=is_downloaded, - download_date=datetime.utcnow() if is_downloaded else None, + download_date=datetime.now(timezone.utc) if is_downloaded else None, ) db.add(episode) await db.flush() @@ -380,7 +380,7 @@ class EpisodeService: episode.is_downloaded = True episode.file_path = file_path episode.file_size = file_size - episode.download_date = datetime.utcnow() + episode.download_date = datetime.now(timezone.utc) await db.flush() await db.refresh(episode) @@ -597,9 +597,9 @@ class DownloadQueueService: # Update timestamps based on status if status == DownloadStatus.DOWNLOADING and not item.started_at: - item.started_at = datetime.utcnow() + item.started_at = datetime.now(timezone.utc) elif status in (DownloadStatus.COMPLETED, DownloadStatus.FAILED): - item.completed_at = datetime.utcnow() + item.completed_at = datetime.now(timezone.utc) # Set error message for failed downloads if status == DownloadStatus.FAILED and error_message: @@ -807,7 +807,7 @@ class UserSessionService: """ query = select(UserSession).where( UserSession.is_active == True, - UserSession.expires_at > datetime.utcnow(), + UserSession.expires_at > datetime.now(timezone.utc), ) if user_id: @@ -833,8 +833,8 @@ class UserSessionService: session = await UserSessionService.get_by_session_id(db, session_id) if not session: return None - - session.last_activity = datetime.utcnow() + + session.last_activity = datetime.now(timezone.utc) await db.flush() await db.refresh(session) return session @@ -871,7 +871,7 @@ class UserSessionService: """ result = await db.execute( delete(UserSession).where( - UserSession.expires_at < datetime.utcnow() + UserSession.expires_at < datetime.now(timezone.utc) ) ) count = result.rowcount diff --git a/src/server/models/auth.py b/src/server/models/auth.py index 2bccefa..5666263 100644 --- a/src/server/models/auth.py +++ b/src/server/models/auth.py @@ -6,7 +6,7 @@ easy to validate and test. """ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from typing import Optional from pydantic import BaseModel, Field, constr @@ -53,5 +53,5 @@ class SessionModel(BaseModel): session_id: str = Field(..., description="Unique session identifier") user: Optional[str] = Field(None, description="Username or identifier") - created_at: datetime = Field(default_factory=datetime.utcnow) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) expires_at: Optional[datetime] = Field(None) diff --git a/src/server/models/download.py b/src/server/models/download.py index 6e70048..b7f1470 100644 --- a/src/server/models/download.py +++ b/src/server/models/download.py @@ -6,7 +6,7 @@ on serialization, validation, and OpenAPI documentation. """ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import List, Optional @@ -80,8 +80,8 @@ class DownloadItem(BaseModel): # Timestamps added_at: datetime = Field( - default_factory=datetime.utcnow, - description="When item was added to queue" + default_factory=lambda: datetime.now(timezone.utc), + description="When item was added to queue", ) started_at: Optional[datetime] = Field( None, description="When download started" diff --git a/src/server/models/websocket.py b/src/server/models/websocket.py index 4807e9c..5b9e0b0 100644 --- a/src/server/models/websocket.py +++ b/src/server/models/websocket.py @@ -6,7 +6,7 @@ for real-time updates. """ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Any, Dict, Optional @@ -56,7 +56,7 @@ class WebSocketMessage(BaseModel): ..., description="Type of the message" ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp when message was created", ) data: Dict[str, Any] = Field( @@ -72,7 +72,7 @@ class DownloadProgressMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -89,7 +89,7 @@ class DownloadCompleteMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -105,7 +105,7 @@ class DownloadFailedMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -121,7 +121,7 @@ class QueueStatusMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -137,7 +137,7 @@ class SystemMessage(BaseModel): ..., description="System message type" ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -152,7 +152,7 @@ class ErrorMessage(BaseModel): default=WebSocketMessageType.ERROR, description="Message type" ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -167,7 +167,7 @@ class ConnectionMessage(BaseModel): ..., description="Connection message type" ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -203,7 +203,7 @@ class ScanProgressMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -220,7 +220,7 @@ class ScanCompleteMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( @@ -237,7 +237,7 @@ class ScanFailedMessage(BaseModel): description="Message type", ) timestamp: str = Field( - default_factory=lambda: datetime.utcnow().isoformat(), + default_factory=lambda: datetime.now(timezone.utc).isoformat(), description="ISO 8601 timestamp", ) data: Dict[str, Any] = Field( diff --git a/src/server/services/auth_service.py b/src/server/services/auth_service.py index 017f63b..dc7160c 100644 --- a/src/server/services/auth_service.py +++ b/src/server/services/auth_service.py @@ -12,7 +12,7 @@ can call it from async routes via threadpool if needed. from __future__ import annotations import hashlib -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Dict, Optional from jose import JWTError, jwt # type: ignore @@ -103,10 +103,10 @@ class AuthService: def _record_failure(self, identifier: str) -> None: rec = self._get_fail_record(identifier) rec["count"] += 1 - rec["last"] = datetime.utcnow() + rec["last"] = datetime.now(timezone.utc) if rec["count"] >= self.max_attempts: rec["locked_until"] = ( - datetime.utcnow() + timedelta(seconds=self.lockout_seconds) + datetime.now(timezone.utc) + timedelta(seconds=self.lockout_seconds) ) def _clear_failures(self, identifier: str) -> None: @@ -116,11 +116,11 @@ class AuthService: def _check_locked(self, identifier: str) -> None: rec = self._get_fail_record(identifier) lu = rec.get("locked_until") - if lu and datetime.utcnow() < lu: + if lu and datetime.now(timezone.utc) < lu: raise LockedOutError( "Too many failed attempts - temporarily locked out" ) - if lu and datetime.utcnow() >= lu: + if lu and datetime.now(timezone.utc) >= lu: # lock expired, reset self._failed[identifier] = { "count": 0, @@ -155,13 +155,13 @@ class AuthService: def create_access_token( self, subject: str = "master", remember: bool = False ) -> LoginResponse: - expiry = datetime.utcnow() + timedelta( + expiry = datetime.now(timezone.utc) + timedelta( hours=(168 if remember else self.token_expiry_hours) ) payload = { "sub": subject, "exp": int(expiry.timestamp()), - "iat": int(datetime.utcnow().timestamp()), + "iat": int(datetime.now(timezone.utc).timestamp()), } token = jwt.encode(payload, self.secret, algorithm="HS256") @@ -180,7 +180,7 @@ class AuthService: data = self.decode_token(token) exp_val = data.get("exp") expires_at = ( - datetime.utcfromtimestamp(exp_val) if exp_val is not None else None + datetime.fromtimestamp(exp_val, timezone.utc) if exp_val is not None else None ) return SessionModel( session_id=hashlib.sha256(token.encode()).hexdigest(), diff --git a/src/server/services/download_service.py b/src/server/services/download_service.py index ad582a1..026dd65 100644 --- a/src/server/services/download_service.py +++ b/src/server/services/download_service.py @@ -11,7 +11,7 @@ import json import uuid from collections import deque from concurrent.futures import ThreadPoolExecutor -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Callable, Dict, List, Optional @@ -183,7 +183,7 @@ class DownloadService: item.model_dump(mode="json") for item in self._failed_items ], - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), } with open(self._persistence_path, "w", encoding="utf-8") as f: @@ -225,7 +225,7 @@ class DownloadService: episode=episode, status=DownloadStatus.PENDING, priority=priority, - added_at=datetime.utcnow(), + added_at=datetime.now(timezone.utc), ) # Insert based on priority @@ -284,7 +284,7 @@ class DownloadService: if item_id in self._active_downloads: item = self._active_downloads[item_id] item.status = DownloadStatus.CANCELLED - item.completed_at = datetime.utcnow() + item.completed_at = datetime.now(timezone.utc) self._failed_items.append(item) del self._active_downloads[item_id] removed_ids.append(item_id) @@ -673,7 +673,7 @@ class DownloadService: try: # Update status item.status = DownloadStatus.DOWNLOADING - item.started_at = datetime.utcnow() + item.started_at = datetime.now(timezone.utc) self._active_downloads[item.id] = item logger.info( @@ -715,7 +715,7 @@ class DownloadService: # Handle result if success: item.status = DownloadStatus.COMPLETED - item.completed_at = datetime.utcnow() + item.completed_at = datetime.now(timezone.utc) # Track downloaded size if item.progress and item.progress.downloaded_mb: @@ -757,7 +757,7 @@ class DownloadService: except Exception as e: # Handle failure item.status = DownloadStatus.FAILED - item.completed_at = datetime.utcnow() + item.completed_at = datetime.now(timezone.utc) item.error = str(e) self._failed_items.append(item) diff --git a/src/server/services/progress_service.py b/src/server/services/progress_service.py index f347674..03cc7f3 100644 --- a/src/server/services/progress_service.py +++ b/src/server/services/progress_service.py @@ -9,7 +9,7 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Any, Callable, Dict, Optional @@ -65,8 +65,8 @@ class ProgressUpdate: current: int = 0 total: int = 0 metadata: Dict[str, Any] = field(default_factory=dict) - started_at: datetime = field(default_factory=datetime.utcnow) - updated_at: datetime = field(default_factory=datetime.utcnow) + started_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) def to_dict(self) -> Dict[str, Any]: """Convert progress update to dictionary.""" @@ -254,7 +254,7 @@ class ProgressService: update.percent = 0.0 update.status = ProgressStatus.IN_PROGRESS - update.updated_at = datetime.utcnow() + update.updated_at = datetime.now(timezone.utc) # Only broadcast if significant change or forced percent_change = abs(update.percent - old_percent) @@ -296,7 +296,7 @@ class ProgressService: update.message = message update.percent = 100.0 update.current = update.total - update.updated_at = datetime.utcnow() + update.updated_at = datetime.now(timezone.utc) if metadata: update.metadata.update(metadata) @@ -345,7 +345,7 @@ class ProgressService: update = self._active_progress[progress_id] update.status = ProgressStatus.FAILED update.message = error_message - update.updated_at = datetime.utcnow() + update.updated_at = datetime.now(timezone.utc) if metadata: update.metadata.update(metadata) @@ -393,7 +393,7 @@ class ProgressService: update = self._active_progress[progress_id] update.status = ProgressStatus.CANCELLED update.message = message - update.updated_at = datetime.utcnow() + update.updated_at = datetime.now(timezone.utc) # Move to history del self._active_progress[progress_id] diff --git a/src/server/services/websocket_service.py b/src/server/services/websocket_service.py index 0d20a90..cf764e9 100644 --- a/src/server/services/websocket_service.py +++ b/src/server/services/websocket_service.py @@ -8,7 +8,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Set import structlog @@ -346,7 +346,7 @@ class WebSocketService: user_id: Optional user identifier for authentication """ metadata = { - "connected_at": datetime.utcnow().isoformat(), + "connected_at": datetime.now(timezone.utc).isoformat(), "user_id": user_id, } await self._manager.connect(websocket, connection_id, metadata) @@ -366,7 +366,7 @@ class WebSocketService: """ message = { "type": "download_progress", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": { "download_id": download_id, **progress_data, @@ -385,7 +385,7 @@ class WebSocketService: """ message = { "type": "download_complete", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": { "download_id": download_id, **result_data, @@ -404,7 +404,7 @@ class WebSocketService: """ message = { "type": "download_failed", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": { "download_id": download_id, **error_data, @@ -420,7 +420,7 @@ class WebSocketService: """ message = { "type": "queue_status", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": status_data, } await self._manager.broadcast_to_room(message, "downloads") @@ -436,7 +436,7 @@ class WebSocketService: """ message = { "type": f"system_{message_type}", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": data, } await self._manager.broadcast(message) @@ -453,7 +453,7 @@ class WebSocketService: """ message = { "type": "error", - "timestamp": datetime.utcnow().isoformat(), + "timestamp": datetime.now(timezone.utc).isoformat(), "data": { "code": error_code, "message": error_message,