fix: replace deprecated datetime.utcnow() with timezone-aware datetime.now(timezone.utc)

Addresses Python 3.13 deprecation warnings by updating all datetime.utcnow() usage:

- Replace datetime.utcnow() with datetime.now(timezone.utc) across 5 files
- Use lambda functions for SQLAlchemy model defaults to ensure proper evaluation timing  
- Add timezone imports where needed
- Enhance logging configuration with warning filters for user environment
- Preserve deprecation warnings for developers in dev/test environments

Fixes deprecation warnings that appeared when running commands like `bm project add`.
Code is now ready for Python 3.15 when datetime.utcnow() will be removed.

Fixes #210

Co-authored-by: Drew Cain <groksrc@users.noreply.github.com>
This commit is contained in:
claude[bot]
2025-07-03 12:48:20 +00:00
committed by GitHub
parent 23ddf1918c
commit dc89ca13e5
6 changed files with 41 additions and 23 deletions
+8 -8
View File
@@ -1,7 +1,7 @@
"""OAuth authentication provider for Basic Memory MCP server."""
import secrets
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Dict, Optional
import jwt
@@ -92,7 +92,7 @@ class BasicMemoryOAuthProvider(
self.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
code=auth_code,
scopes=params.scopes or [],
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
expires_at=(datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp(),
client_id=client.client_id,
code_challenge=params.code_challenge,
redirect_uri=params.redirect_uri,
@@ -119,7 +119,7 @@ class BasicMemoryOAuthProvider(
if code and code.client_id == client.client_id:
# Check if expired
if datetime.utcnow().timestamp() > code.expires_at:
if datetime.now(timezone.utc).timestamp() > code.expires_at:
del self.authorization_codes[authorization_code]
return None
return code
@@ -135,7 +135,7 @@ class BasicMemoryOAuthProvider(
refresh_token = secrets.token_urlsafe(32)
# Store tokens
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
self.access_tokens[access_token] = BasicMemoryAccessToken(
token=access_token,
@@ -187,7 +187,7 @@ class BasicMemoryOAuthProvider(
new_refresh_token = secrets.token_urlsafe(32)
# Store new tokens
expires_at = (datetime.utcnow() + timedelta(hours=1)).timestamp()
expires_at = (datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()
self.access_tokens[new_access_token] = BasicMemoryAccessToken(
token=new_access_token,
@@ -220,7 +220,7 @@ class BasicMemoryOAuthProvider(
if access_token:
# Check if expired
if access_token.expires_at and datetime.utcnow().timestamp() > access_token.expires_at:
if access_token.expires_at and datetime.now(timezone.utc).timestamp() > access_token.expires_at:
logger.debug("Token found in memory but expired, removing")
del self.access_tokens[token]
return None
@@ -262,8 +262,8 @@ class BasicMemoryOAuthProvider(
"iss": self.issuer_url,
"sub": client_id,
"aud": "basic-memory",
"exp": datetime.utcnow() + timedelta(hours=1),
"iat": datetime.utcnow(),
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"iat": datetime.now(timezone.utc),
"scopes": scopes,
}
@@ -3,7 +3,7 @@
import os
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
import httpx
@@ -123,7 +123,7 @@ class SupabaseOAuthProvider(
self.pending_auth_codes[state] = SupabaseAuthorizationCode(
code=state,
scopes=params.scopes or [],
expires_at=(datetime.utcnow() + timedelta(minutes=10)).timestamp(),
expires_at=(datetime.now(timezone.utc) + timedelta(minutes=10)).timestamp(),
client_id=client.client_id,
code_challenge=params.code_challenge,
redirect_uri=params.redirect_uri,
@@ -218,7 +218,7 @@ class SupabaseOAuthProvider(
if code and code.client_id == client.client_id:
# Check expiration
if datetime.utcnow().timestamp() > code.expires_at:
if datetime.now(timezone.utc).timestamp() > code.expires_at:
del self.pending_auth_codes[authorization_code]
return None
return code
@@ -453,8 +453,8 @@ class SupabaseOAuthProvider(
"email": email,
"scopes": scopes,
"supabase_token": supabase_access_token[:10] + "...", # Reference only
"exp": datetime.utcnow() + timedelta(hours=1),
"iat": datetime.utcnow(),
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
"iat": datetime.now(timezone.utc),
}
# Use Supabase JWT secret if available
+3 -3
View File
@@ -1,6 +1,6 @@
"""Project model for Basic Memory."""
from datetime import datetime
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import (
@@ -52,9 +52,9 @@ class Project(Base):
is_default: Mapped[Optional[bool]] = mapped_column(Boolean, default=None, nullable=True)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
)
# Define relationships to entities, observations, and relations
+18
View File
@@ -6,6 +6,7 @@ import logging
import re
import sys
import unicodedata
import warnings
from pathlib import Path
from typing import Optional, Protocol, Union, runtime_checkable, List, Any
@@ -179,6 +180,23 @@ def setup_logging(
for logger_name, level in noisy_loggers.items():
logging.getLogger(logger_name).setLevel(level)
# Filter deprecation warnings for user environment to provide clean UX
# Keep them visible in dev/test environments for developers
if env == "user":
# Suppress SQLAlchemy deprecation warnings that appear in user-facing commands
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
module="sqlalchemy",
message=".*datetime\\.datetime\\.utcnow.*"
)
# Also suppress any other datetime.utcnow deprecation warnings
warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
message=".*datetime\\.datetime\\.utcnow.*"
)
def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
"""Parse tags from various input formats into a consistent list.
+4 -4
View File
@@ -1,7 +1,7 @@
"""Tests for OAuth authentication provider."""
import pytest
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from mcp.server.auth.provider import AuthorizationParams
from mcp.shared.auth import OAuthClientInformationFull
from pydantic import AnyHttpUrl
@@ -185,7 +185,7 @@ class TestBasicMemoryOAuthProvider:
token=token_str,
client_id=client.client_id,
scopes=["read", "write"],
expires_at=int((datetime.utcnow() + timedelta(hours=1)).timestamp()),
expires_at=int((datetime.now(timezone.utc) + timedelta(hours=1)).timestamp()),
)
provider.access_tokens[token_str] = access_token
@@ -226,7 +226,7 @@ class TestBasicMemoryOAuthProvider:
provider.authorization_codes[auth_code] = BasicMemoryAuthorizationCode(
code=auth_code,
scopes=["read"],
expires_at=(datetime.utcnow() - timedelta(minutes=1)).timestamp(),
expires_at=(datetime.now(timezone.utc) - timedelta(minutes=1)).timestamp(),
client_id=client.client_id,
code_challenge="challenge",
redirect_uri=AnyHttpUrl("http://localhost:3000/callback"),
@@ -288,7 +288,7 @@ class TestBasicMemoryOAuthProvider:
token=expired_token_str,
client_id="test-client",
scopes=["read"],
expires_at=int((datetime.utcnow() - timedelta(minutes=1)).timestamp()), # Expired
expires_at=int((datetime.now(timezone.utc) - timedelta(minutes=1)).timestamp()), # Expired
)
provider.access_tokens[expired_token_str] = expired_access_token
+3 -3
View File
@@ -1,6 +1,6 @@
"""Test repository implementation."""
from datetime import datetime
from datetime import datetime, timezone
import pytest
from sqlalchemy import String, DateTime
from sqlalchemy.orm import Mapped, mapped_column
@@ -17,9 +17,9 @@ class ModelTest(Base):
id: Mapped[str] = mapped_column(String(255), primary_key=True)
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(String(255), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
created_at: Mapped[datetime] = mapped_column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)
)