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

1"""Shared utility functions for the Research Library.""" 

2 

3import hashlib 

4import os 

5import subprocess 

6import sys 

7from pathlib import Path 

8 

9from flask import jsonify 

10from loguru import logger 

11 

12from ...config.paths import get_library_directory 

13 

14 

15def get_url_hash(url: str) -> str: 

16 """ 

17 Generate a SHA256 hash of a URL. 

18 

19 Args: 

20 url: The URL to hash 

21 

22 Returns: 

23 The SHA256 hash of the URL 

24 """ 

25 return hashlib.sha256(url.lower().encode()).hexdigest() 

26 

27 

28def get_library_storage_path(username: str) -> Path: 

29 """ 

30 Get the storage path for a user's library. 

31 

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 

35 

36 Args: 

37 username: The username 

38 

39 Returns: 

40 Path to the library storage directory 

41 """ 

42 from ...utilities.db_utils import get_settings_manager 

43 

44 settings = get_settings_manager() 

45 

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() 

53 

54 # Check if shared library mode is enabled 

55 shared_library = settings.get_setting( 

56 "research_library.shared_library", False 

57 ) 

58 

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 

68 

69 

70def open_file_location(file_path: str) -> bool: 

71 """ 

72 Open the file location in the system file manager. 

73 

74 Args: 

75 file_path: Path to the file 

76 

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 

102 

103 

104def get_relative_library_path(absolute_path: str, username: str) -> str: 

105 """ 

106 Get the relative path from the library root. 

107 

108 Args: 

109 absolute_path: The absolute file path 

110 username: The username 

111 

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 

121 

122 

123def get_absolute_library_path(relative_path: str, username: str) -> Path: 

124 """ 

125 Get the absolute path from a relative library path. 

126 

127 Args: 

128 relative_path: The relative path from library root 

129 username: The username 

130 

131 Returns: 

132 The absolute path 

133 """ 

134 library_root = get_library_storage_path(username) 

135 return library_root / relative_path 

136 

137 

138def get_absolute_path_from_settings(relative_path: str) -> Path: 

139 """ 

140 Get absolute path using settings manager for library root. 

141 

142 Args: 

143 relative_path: The relative path from library root 

144 

145 Returns: 

146 The absolute path 

147 """ 

148 from ...utilities.db_utils import get_settings_manager 

149 

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() 

157 

158 if not relative_path: 

159 return library_root 

160 

161 return library_root / relative_path 

162 

163 

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. 

167 

168 This prevents information exposure by logging full error details internally 

169 while returning a generic message to the user. 

170 

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) 

175 

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}") 

181 

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