better time usings

This commit is contained in:
Lukas 2025-10-22 08:14:42 +02:00
parent 04b516a52d
commit 4eede0c8c0
11 changed files with 62 additions and 163 deletions

View File

@ -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)

View File

@ -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."""

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -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(

View File

@ -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(),

View File

@ -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)

View File

@ -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]

View File

@ -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,