Coverage for src / local_deep_research / research_library / utils / __init__.py: 23%
58 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
1"""Shared utility functions for the Research Library."""
3import hashlib
4import os
5import subprocess
6import sys
7from pathlib import Path
9from flask import jsonify
10from loguru import logger
12from ...config.paths import get_library_directory
15def get_url_hash(url: str) -> str:
16 """
17 Generate a SHA256 hash of a URL.
19 Args:
20 url: The URL to hash
22 Returns:
23 The SHA256 hash of the URL
24 """
25 return hashlib.sha256(url.lower().encode()).hexdigest()
28def get_library_storage_path(username: str) -> Path:
29 """
30 Get the storage path for a user's library.
32 Uses the settings system which respects environment variable overrides:
33 - research_library.storage_path: Base path for library storage
34 - research_library.shared_library: If true, all users share the same directory
36 Args:
37 username: The username
39 Returns:
40 Path to the library storage directory
41 """
42 from ...utilities.db_utils import get_settings_manager
44 settings = get_settings_manager()
46 # Get the base path from settings (uses centralized path, respects LDR_DATA_DIR)
47 base_path = Path(
48 settings.get_setting(
49 "research_library.storage_path",
50 str(get_library_directory()),
51 )
52 ).expanduser()
54 # Check if shared library mode is enabled
55 shared_library = settings.get_setting(
56 "research_library.shared_library", False
57 )
59 if shared_library:
60 # Shared mode: all users use the same directory
61 base_path.mkdir(parents=True, exist_ok=True)
62 return base_path
63 else:
64 # Default: user isolation with subdirectories
65 user_dir = base_path / username
66 user_dir.mkdir(parents=True, exist_ok=True)
67 return user_dir
70def open_file_location(file_path: str) -> bool:
71 """
72 Open the file location in the system file manager.
74 Args:
75 file_path: Path to the file
77 Returns:
78 True if successful, False otherwise
79 """
80 try:
81 folder = str(Path(file_path).parent)
82 if sys.platform == "win32":
83 os.startfile(folder)
84 elif sys.platform == "darwin": # macOS
85 result = subprocess.run(
86 ["open", folder], capture_output=True, text=True
87 )
88 if result.returncode != 0:
89 logger.error(f"Failed to open folder on macOS: {result.stderr}")
90 return False
91 else: # Linux
92 result = subprocess.run(
93 ["xdg-open", folder], capture_output=True, text=True
94 )
95 if result.returncode != 0:
96 logger.error(f"Failed to open folder on Linux: {result.stderr}")
97 return False
98 return True
99 except Exception:
100 logger.exception("Failed to open file location")
101 return False
104def get_relative_library_path(absolute_path: str, username: str) -> str:
105 """
106 Get the relative path from the library root.
108 Args:
109 absolute_path: The absolute file path
110 username: The username
112 Returns:
113 The relative path from the library root
114 """
115 try:
116 library_root = get_library_storage_path(username)
117 return str(Path(absolute_path).relative_to(library_root))
118 except ValueError:
119 # Path is not relative to library root
120 return Path(absolute_path).name
123def get_absolute_library_path(relative_path: str, username: str) -> Path:
124 """
125 Get the absolute path from a relative library path.
127 Args:
128 relative_path: The relative path from library root
129 username: The username
131 Returns:
132 The absolute path
133 """
134 library_root = get_library_storage_path(username)
135 return library_root / relative_path
138def get_absolute_path_from_settings(relative_path: str) -> Path:
139 """
140 Get absolute path using settings manager for library root.
142 Args:
143 relative_path: The relative path from library root
145 Returns:
146 The absolute path
147 """
148 from ...utilities.db_utils import get_settings_manager
150 settings = get_settings_manager()
151 library_root = Path(
152 settings.get_setting(
153 "research_library.storage_path",
154 str(get_library_directory()),
155 )
156 ).expanduser()
158 if not relative_path:
159 return library_root
161 return library_root / relative_path
164def handle_api_error(operation: str, error: Exception, status_code: int = 500):
165 """
166 Handle API errors consistently - log internally, return generic message to user.
168 This prevents information exposure by logging full error details internally
169 while returning a generic message to the user.
171 Args:
172 operation: Description of the operation that failed (for logging)
173 error: The exception that occurred
174 status_code: HTTP status code to return (default: 500)
176 Returns:
177 Flask JSON response tuple (response, status_code)
178 """
179 # Log the full error internally with stack trace
180 logger.exception(f"Error during {operation}")
182 # Return generic message to user (no internal details exposed)
183 return jsonify(
184 {
185 "success": False,
186 "error": "An internal error occurred. Please try again or contact support.",
187 }
188 ), status_code