diff --git a/web/src/padelnomics/core.py b/web/src/padelnomics/core.py index 5260ec0..c762d5f 100644 --- a/web/src/padelnomics/core.py +++ b/web/src/padelnomics/core.py @@ -10,7 +10,7 @@ import re import secrets import unicodedata from contextvars import ContextVar -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from functools import wraps from pathlib import Path @@ -88,6 +88,26 @@ class Config: config = Config() + +# ============================================================================= +# Datetime helpers +# ============================================================================= + + +def utcnow() -> datetime: + """Timezone-aware UTC now (replaces deprecated datetime.utcnow()).""" + return datetime.now(UTC) + + +def utcnow_iso() -> str: + """UTC now as naive ISO string for SQLite TEXT columns. + + Produces YYYY-MM-DDTHH:MM:SS (no +00:00 suffix) to match the existing + format stored in the DB so lexicographic SQL comparisons keep working. + """ + return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S") + + # ============================================================================= # Database # ============================================================================= @@ -528,17 +548,18 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t """ limit = limit or config.RATE_LIMIT_REQUESTS window = window or config.RATE_LIMIT_WINDOW - now = datetime.utcnow() + now = utcnow() window_start = now - timedelta(seconds=window) # Clean old entries and count recent await execute( - "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?", (key, window_start.isoformat()) + "DELETE FROM rate_limits WHERE key = ? AND timestamp < ?", + (key, window_start.strftime("%Y-%m-%dT%H:%M:%S")), ) result = await fetch_one( "SELECT COUNT(*) as count FROM rate_limits WHERE key = ? AND timestamp > ?", - (key, window_start.isoformat()), + (key, window_start.strftime("%Y-%m-%dT%H:%M:%S")), ) count = result["count"] if result else 0 @@ -552,7 +573,10 @@ async def check_rate_limit(key: str, limit: int = None, window: int = None) -> t return False, info # Record this request - await execute("INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)", (key, now.isoformat())) + await execute( + "INSERT INTO rate_limits (key, timestamp) VALUES (?, ?)", + (key, now.strftime("%Y-%m-%dT%H:%M:%S")), + ) return True, info @@ -628,7 +652,7 @@ async def soft_delete(table: str, id: int) -> bool: """Mark record as deleted.""" result = await execute( f"UPDATE {table} SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL", - (datetime.utcnow().isoformat(), id), + (utcnow_iso(), id), ) return result > 0 @@ -647,7 +671,7 @@ async def hard_delete(table: str, id: int) -> bool: async def purge_deleted(table: str, days: int = 30) -> int: """Purge records deleted more than X days ago.""" - cutoff = (datetime.utcnow() - timedelta(days=days)).isoformat() + cutoff = (utcnow() - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S") return await execute( f"DELETE FROM {table} WHERE deleted_at IS NOT NULL AND deleted_at < ?", (cutoff,) )