From e604455332ed6035fd0c92f42d7ed4d5014f2bc1 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 13:12:48 +0000 Subject: [PATCH] fix: replace deprecated datetime.utcnow() with timezone-aware alternatives Replace all datetime.utcnow() calls with datetime.now(timezone.utc) to fix Python 3.13 deprecation warnings: - Update SQLAlchemy model defaults in project.py with lambda functions - Fix JWT token generation in auth providers - Update test fixtures with timezone-aware datetime - Add timezone imports where needed Fixes Python 3.13 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 --- src/basic_memory/mcp/auth_provider.py | 16 ++++++++-------- src/basic_memory/mcp/supabase_auth_provider.py | 10 +++++----- src/basic_memory/models/project.py | 6 +++--- tests/mcp/test_auth_provider.py | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/basic_memory/mcp/auth_provider.py b/src/basic_memory/mcp/auth_provider.py index d7f1bac0..da5a94b4 100644 --- a/src/basic_memory/mcp/auth_provider.py +++ b/src/basic_memory/mcp/auth_provider.py @@ -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, } diff --git a/src/basic_memory/mcp/supabase_auth_provider.py b/src/basic_memory/mcp/supabase_auth_provider.py index 9771984a..e026446f 100644 --- a/src/basic_memory/mcp/supabase_auth_provider.py +++ b/src/basic_memory/mcp/supabase_auth_provider.py @@ -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 diff --git a/src/basic_memory/models/project.py b/src/basic_memory/models/project.py index 2bcf188b..5d6bd7c4 100644 --- a/src/basic_memory/models/project.py +++ b/src/basic_memory/models/project.py @@ -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 diff --git a/tests/mcp/test_auth_provider.py b/tests/mcp/test_auth_provider.py index a266395d..fae4c96c 100644 --- a/tests/mcp/test_auth_provider.py +++ b/tests/mcp/test_auth_provider.py @@ -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