Replace cleanup service datetime.utcnow calls (#1494)
datetime.utcnow() is deprecated in Python 3.12 and removed in 3.14. Swap the five calls in src/cleanup_service.py for a local _utcnow() helper returning naive UTC, matching the naive DateTime columns the archive/delete cutoffs compare against (same approach as the task-scheduler and core-database slices). Add a regression test asserting the helper stays naive so the cutoff math can't hit a naive/aware TypeError. Part of #1116
This commit is contained in:
committed by
GitHub
parent
6fd52cf317
commit
4f03f5ccdd
@@ -1,10 +1,20 @@
|
|||||||
# src/cleanup_service.py
|
# src/cleanup_service.py
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Tuple, Dict, Any, Optional
|
from typing import Tuple, Dict, Any, Optional
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _utcnow() -> datetime:
|
||||||
|
"""Naive UTC for this module's DB-bound timestamps.
|
||||||
|
|
||||||
|
Mirrors the naive DateTime columns these values are compared against,
|
||||||
|
without the deprecated stdlib UTC-now call (removed in Python 3.14).
|
||||||
|
"""
|
||||||
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
class CleanupConfig:
|
class CleanupConfig:
|
||||||
"""Configuration constants for cleanup operations."""
|
"""Configuration constants for cleanup operations."""
|
||||||
ARCHIVE_AFTER_DAYS = 7
|
ARCHIVE_AFTER_DAYS = 7
|
||||||
@@ -38,7 +48,7 @@ async def archive_inactive_sessions(session_manager, owner: Optional[str] = None
|
|||||||
Returns:
|
Returns:
|
||||||
Number of sessions archived
|
Number of sessions archived
|
||||||
"""
|
"""
|
||||||
cutoff_date = datetime.utcnow() - timedelta(days=CleanupConfig.ARCHIVE_AFTER_DAYS)
|
cutoff_date = _utcnow() - timedelta(days=CleanupConfig.ARCHIVE_AFTER_DAYS)
|
||||||
archived_count = 0
|
archived_count = 0
|
||||||
|
|
||||||
from src.database import SessionLocal, Session as DbSession
|
from src.database import SessionLocal, Session as DbSession
|
||||||
@@ -53,7 +63,7 @@ async def archive_inactive_sessions(session_manager, owner: Optional[str] = None
|
|||||||
|
|
||||||
for session in sessions_to_archive:
|
for session in sessions_to_archive:
|
||||||
session.archived = True
|
session.archived = True
|
||||||
session.updated_at = datetime.utcnow()
|
session.updated_at = _utcnow()
|
||||||
archived_count += 1
|
archived_count += 1
|
||||||
|
|
||||||
if archived_count > 0:
|
if archived_count > 0:
|
||||||
@@ -79,7 +89,7 @@ async def cleanup_old_sessions(session_manager, owner: Optional[str] = None) ->
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (number of sessions deleted, space freed in MB)
|
Tuple of (number of sessions deleted, space freed in MB)
|
||||||
"""
|
"""
|
||||||
cutoff_date = datetime.utcnow() - timedelta(days=CleanupConfig.DELETE_AFTER_DAYS)
|
cutoff_date = _utcnow() - timedelta(days=CleanupConfig.DELETE_AFTER_DAYS)
|
||||||
deleted_count = 0
|
deleted_count = 0
|
||||||
space_freed = 0
|
space_freed = 0
|
||||||
|
|
||||||
@@ -158,8 +168,8 @@ async def get_cleanup_preview(owner: Optional[str] = None) -> Dict[str, Any]:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary containing preview information
|
Dictionary containing preview information
|
||||||
"""
|
"""
|
||||||
cutoff_archive = datetime.utcnow() - timedelta(days=CleanupConfig.ARCHIVE_AFTER_DAYS)
|
cutoff_archive = _utcnow() - timedelta(days=CleanupConfig.ARCHIVE_AFTER_DAYS)
|
||||||
cutoff_delete = datetime.utcnow() - timedelta(days=CleanupConfig.DELETE_AFTER_DAYS)
|
cutoff_delete = _utcnow() - timedelta(days=CleanupConfig.DELETE_AFTER_DAYS)
|
||||||
|
|
||||||
sessions_to_archive = []
|
sessions_to_archive = []
|
||||||
sessions_to_delete = []
|
sessions_to_delete = []
|
||||||
|
|||||||
25
tests/test_cleanup_service_utcnow.py
Normal file
25
tests/test_cleanup_service_utcnow.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Regression tests for the datetime.utcnow() removal in src/cleanup_service.py (#1116).
|
||||||
|
|
||||||
|
Importing src.cleanup_service is cheap and dependency-free: its only module-level
|
||||||
|
imports are logging/datetime/typing, and the `from src.database import ...` calls are
|
||||||
|
lazy (inside the functions), so no DB/sqlalchemy stack is pulled in here.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
from src.cleanup_service import _utcnow
|
||||||
|
|
||||||
|
|
||||||
|
def test_utcnow_returns_naive_utc():
|
||||||
|
now = _utcnow()
|
||||||
|
# Must be naive to match the naive DateTime columns this module compares against.
|
||||||
|
assert now.tzinfo is None
|
||||||
|
ref = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
assert abs((ref - now).total_seconds()) < 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_cutoff_math_stays_naive_and_comparable():
|
||||||
|
# Guards the archive/delete cutoffs against a naive/aware TypeError regression:
|
||||||
|
# an aware _utcnow() would raise when compared with the naive last_accessed column.
|
||||||
|
cutoff = _utcnow() - timedelta(days=7)
|
||||||
|
assert cutoff.tzinfo is None
|
||||||
|
assert cutoff < _utcnow()
|
||||||
Reference in New Issue
Block a user