Coverage for src/local_deep_research/web/services/resource_service.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-06 15:42 +0000

1from datetime import datetime, UTC 

2 

3from loguru import logger 

4 

5from ...database.models import ResearchResource 

6from ...database.session_context import get_user_db_session 

7 

8 

9def get_resources_for_research(research_id, username): 

10 """ 

11 Retrieve resources associated with a specific research project. 

12 

13 Args: 

14 research_id (str): The UUID of the research. 

15 username (str): The authenticated caller — required so the lookup 

16 opens that user's encrypted database explicitly. Without it, 

17 ``get_user_db_session()`` falls back to a contextvar read 

18 (or a cross-session password) which can silently return data 

19 from the wrong user's DB if context isn't set. 

20 

21 Returns: 

22 list: List of resource objects for the research. 

23 """ 

24 try: 

25 with get_user_db_session(username) as db_session: 

26 # Query to get resources for the research 

27 resources_list = ( 

28 db_session.query(ResearchResource) 

29 .filter_by(research_id=research_id) 

30 .order_by(ResearchResource.id.asc()) 

31 .all() 

32 ) 

33 

34 resources = [] 

35 for resource in resources_list: 

36 resources.append( 

37 { 

38 "id": resource.id, 

39 "research_id": resource.research_id, 

40 "title": resource.title, 

41 "url": resource.url, 

42 "content_preview": resource.content_preview, 

43 "source_type": resource.source_type, 

44 "metadata": resource.resource_metadata or {}, 

45 } 

46 ) 

47 

48 return resources 

49 

50 except Exception: 

51 logger.exception("Error retrieving resources for research") 

52 raise 

53 

54 

55def add_resource( 

56 research_id, 

57 title, 

58 url, 

59 content_preview=None, 

60 source_type="web", 

61 metadata=None, 

62 username=None, 

63): 

64 """ 

65 Add a new resource to the research_resources table 

66 

67 Args: 

68 research_id (str): The UUID of the research 

69 title (str): The title of the resource 

70 url (str): The URL of the resource 

71 content_preview (str, optional): A preview of the content 

72 source_type (str, optional): The type of source 

73 metadata (dict, optional): Additional metadata 

74 username (str): The authenticated caller — REQUIRED. Under FastAPI 

75 get_user_db_session needs it explicitly; without it the open 

76 raises DatabaseSessionError and this always 500s (the sibling 

77 get_resources_for_research was fixed the same way). 

78 

79 Returns: 

80 ResearchResource: The created resource object 

81 """ 

82 try: 

83 with get_user_db_session(username) as db_session: 

84 # NOTE: ResearchResource has no `accessed_at` column, and 

85 # `created_at` (String, nullable=False) has no server default — 

86 # so passing accessed_at raised TypeError and omitting created_at 

87 # would violate NOT NULL. Match the working insert in 

88 # research_sources_service.ResearchResource(...): set created_at 

89 # to an ISO string, drop accessed_at. (This function 500'd on 

90 # every call on both this branch and main until now.) 

91 resource = ResearchResource( 

92 research_id=research_id, 

93 title=title, 

94 url=url, 

95 content_preview=content_preview, 

96 source_type=source_type, 

97 resource_metadata=metadata, 

98 created_at=datetime.now(UTC).isoformat(), 

99 ) 

100 

101 db_session.add(resource) 

102 db_session.commit() 

103 

104 return resource 

105 

106 except Exception: 

107 logger.exception("Error adding resource") 

108 raise 

109 

110 

111def delete_resource(resource_id, username=None): 

112 """ 

113 Delete a resource from the database 

114 

115 Args: 

116 resource_id (int): The ID of the resource to delete 

117 username (str): The authenticated caller — REQUIRED (see add_resource; 

118 without it get_user_db_session raises and this always 500s). 

119 

120 Returns: 

121 bool: True if deletion was successful 

122 

123 Raises: 

124 ValueError: If resource not found 

125 """ 

126 try: 

127 with get_user_db_session(username) as db_session: 

128 # Find the resource 

129 resource = ( 

130 db_session.query(ResearchResource) 

131 .filter_by(id=resource_id) 

132 .first() 

133 ) 

134 

135 if not resource: 

136 raise ValueError(f"Resource with ID {resource_id} not found") # noqa: TRY301 — re-raised by except ValueError 

137 

138 db_session.delete(resource) 

139 db_session.commit() 

140 

141 logger.info(f"Deleted resource {resource_id}") 

142 return True 

143 

144 except ValueError: 

145 raise 

146 except Exception: 

147 logger.exception("Error deleting resource") 

148 raise