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
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1from datetime import datetime, UTC
3from loguru import logger
5from ...database.models import ResearchResource
6from ...database.session_context import get_user_db_session
9def get_resources_for_research(research_id, username):
10 """
11 Retrieve resources associated with a specific research project.
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.
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 )
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 )
48 return resources
50 except Exception:
51 logger.exception("Error retrieving resources for research")
52 raise
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
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).
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 )
101 db_session.add(resource)
102 db_session.commit()
104 return resource
106 except Exception:
107 logger.exception("Error adding resource")
108 raise
111def delete_resource(resource_id, username=None):
112 """
113 Delete a resource from the database
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).
120 Returns:
121 bool: True if deletion was successful
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 )
135 if not resource:
136 raise ValueError(f"Resource with ID {resource_id} not found") # noqa: TRY301 — re-raised by except ValueError
138 db_session.delete(resource)
139 db_session.commit()
141 logger.info(f"Deleted resource {resource_id}")
142 return True
144 except ValueError:
145 raise
146 except Exception:
147 logger.exception("Error deleting resource")
148 raise