Coverage for src / local_deep_research / utilities / db_utils.py: 85%
78 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1import functools
2from typing import Any, Callable, Dict
4from cachetools import LRUCache
5from flask import (
6 g,
7 has_app_context,
8 has_request_context,
9 session as flask_session,
10)
11from loguru import logger
12from sqlalchemy.orm import Session
14from ..settings.env_registry import use_fallback_llm
16from ..config.paths import get_data_directory
17from ..database.encrypted_db import db_manager
18from .threading_utils import thread_specific_cache
20# Database paths using new centralized configuration
21DATA_DIR = get_data_directory()
22# DB_PATH removed - use per-user encrypted databases instead
25@thread_specific_cache(cache=LRUCache(maxsize=10))
26def get_db_session(
27 _namespace: str = "", username: str | None = None
28) -> Session:
29 """
30 Get database session - uses encrypted per-user database if authenticated.
32 Args:
33 _namespace: This can be specified to an arbitrary string in order to
34 force the caching mechanism to create separate settings even in
35 the same thread. Usually it does not need to be specified.
36 username: Optional username for thread context (e.g., background research threads).
37 If not provided, will try to get from Flask context.
39 Returns:
40 The database session for the current user/context.
41 """
42 # CRITICAL: Detect if we're in a background thread and raise an error
43 # This helps identify code that's trying to access the database from threads
44 import threading
46 # Check if we're in a background thread (not in Flask request context)
47 # We check for request context specifically because app context might exist
48 # during startup but we still shouldn't access the database from background threads
49 thread_name = threading.current_thread().name
51 # Allow MainThread during startup, but not other threads
52 if not has_app_context() and thread_name != "MainThread":
53 thread_id = threading.get_ident()
54 raise RuntimeError(
55 f"Database access attempted from background thread '{thread_name}' (ID: {thread_id}). "
56 f"Database access from threads is not allowed due to SQLite thread safety constraints. "
57 f"Use settings_snapshot or pass all required data to the thread at creation time."
58 )
60 # If username is explicitly provided (e.g., from background thread)
61 if username:
62 user_session = db_manager.get_session(username)
63 if user_session:
64 return user_session
65 raise RuntimeError(f"No database found for user {username}")
67 # Otherwise, check Flask request context
68 try:
69 # Check if we have a database session in Flask's g object
70 if hasattr(g, "db_session") and g.db_session: 70 ↛ 74line 70 didn't jump to line 74 because the condition on line 70 was always true
71 return g.db_session
73 # Check if we have a username in the Flask session
74 username = flask_session.get("username")
75 if username:
76 user_session = db_manager.get_session(username)
77 if user_session:
78 return user_session
79 except Exception:
80 # Error accessing Flask context
81 pass
83 # No shared database - return None to allow SettingsManager to work without DB
84 logger.warning(
85 "get_db_session() is deprecated. Use get_user_db_session() from database.session_context"
86 )
87 return None
90def get_settings_manager(
91 db_session: Session | None = None, username: str | None = None
92):
93 """
94 Get the settings manager for the current context.
96 Args:
97 db_session: Optional database session
98 username: Optional username for caching (required for SettingsManager)
100 Returns:
101 The appropriate settings manager instance.
102 """
103 # If db_session not provided, try to get one
104 if db_session is None and username is None and has_request_context():
105 username = flask_session.get("username")
107 if db_session is None:
108 try:
109 db_session = get_db_session(username=username)
110 except RuntimeError:
111 # No authenticated user - settings manager will use defaults
112 db_session = None
113 username = "anonymous"
115 # Import here to avoid circular imports
116 from ..settings import SettingsManager
118 # Always use regular SettingsManager (now with built-in simple caching)
119 return SettingsManager(db_session)
122def no_db_settings(func: Callable[..., Any]) -> Callable[..., Any]:
123 """
124 Decorator that runs the wrapped function with the settings database
125 completely disabled. This will prevent the function from accidentally
126 reading settings from the DB. Settings can only be read from environment
127 variables or the defaults file.
129 Args:
130 func: The function to wrap.
132 Returns:
133 The wrapped function.
135 """
137 @functools.wraps(func)
138 def wrapper(*args, **kwargs):
139 # Temporarily disable DB access in the settings manager.
140 manager = get_settings_manager()
141 db_session = manager.db_session
142 manager.db_session = None
144 try:
145 return func(*args, **kwargs)
146 finally:
147 # Restore the original database session.
148 manager.db_session = db_session
150 return wrapper
153def get_setting_from_db_main_thread(
154 key: str, default_value: Any | None = None, username: str | None = None
155) -> str | Dict[str, Any] | None:
156 """
157 Get a setting from the database with fallback to default value
159 Args:
160 key: The setting key.
161 default_value: If the setting is not found, it will return this instead.
162 username: Optional username for thread context (e.g., background research threads).
164 Returns:
165 The setting value.
167 """
168 # In fallback LLM mode, always return default values without database access
169 if use_fallback_llm():
170 logger.debug(
171 f"Using default value for {key} in fallback LLM environment"
172 )
173 return default_value
175 # CRITICAL: Detect if we're in a background thread and raise an error
176 import threading
178 # Check if we're in a background thread
179 thread_name = threading.current_thread().name
181 # Allow MainThread during startup, but not other threads
182 if not has_app_context() and thread_name != "MainThread":
183 thread_id = threading.get_ident()
184 raise RuntimeError(
185 f"get_db_setting('{key}') called from background thread '{thread_name}' (ID: {thread_id}). "
186 f"Database access from threads is not allowed. Use settings_snapshot or thread-local settings context."
187 )
189 try:
190 # Use the new session context to ensure proper database access
191 from ..database.session_context import get_user_db_session
193 try:
194 with get_user_db_session(username) as db_session:
195 if db_session: 195 ↛ 197line 195 didn't jump to line 197 because the condition on line 195 was never true
196 # Use the unified settings manager
197 settings_manager = get_settings_manager(
198 db_session, username
199 )
200 return settings_manager.get_setting(
201 key, default=default_value
202 )
203 except Exception:
204 # If we can't get a session, fall back to default
205 pass
207 except Exception:
208 logger.exception(f"Error getting setting {key} from database")
210 logger.warning(f"Could not read setting '{key}' from the database.")
211 return default_value