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/src/basic_memory/utils.py b/src/basic_memory/utils.py index 67714e4b..f003cc21 100644 --- a/src/basic_memory/utils.py +++ b/src/basic_memory/utils.py @@ -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. 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 diff --git a/tests/repository/test_repository.py b/tests/repository/test_repository.py index 79dfb503..afd0c7cf 100644 --- a/tests/repository/test_repository.py +++ b/tests/repository/test_repository.py @@ -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) )