Coverage for src / local_deep_research / research_library / deletion / routes / delete_routes.py: 95%

160 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-14 23:55 +0000

1""" 

2Delete API Routes 

3 

4Provides endpoints for delete operations: 

5- Delete document 

6- Delete document blob only 

7- Delete documents in bulk 

8- Delete blobs in bulk 

9- Remove document from collection 

10- Delete collection 

11- Delete collection index only 

12""" 

13 

14from flask import Blueprint, jsonify, request, session 

15 

16 

17from ....web.auth.decorators import login_required 

18from ...utils import handle_api_error 

19from ..services.document_deletion import DocumentDeletionService 

20from ..services.collection_deletion import CollectionDeletionService 

21from ..services.bulk_deletion import BulkDeletionService 

22 

23 

24delete_bp = Blueprint("delete", __name__, url_prefix="/library/api") 

25# NOTE: Routes use session["username"] (not .get()) intentionally. 

26# @login_required guarantees the key exists; direct access fails fast 

27# if the decorator is ever removed. 

28 

29 

30class _ValidationError(ValueError): 

31 """Raised when request validation fails.""" 

32 

33 def __init__(self, message: str): 

34 super().__init__(message) 

35 self.message = message 

36 

37 

38def _validate_document_ids() -> list: 

39 """Extract and validate document_ids from the JSON request body. 

40 

41 Returns: 

42 list: The validated document IDs. 

43 

44 Raises: 

45 _ValidationError: If the request body is missing or invalid. 

46 """ 

47 data = request.get_json() 

48 if not data or "document_ids" not in data: 

49 raise _ValidationError("document_ids required in request body") 

50 

51 document_ids = data["document_ids"] 

52 if not isinstance(document_ids, list) or not document_ids: 

53 raise _ValidationError("document_ids must be a non-empty list") 

54 

55 return document_ids 

56 

57 

58# ============================================================================= 

59# Document Delete Endpoints 

60# ============================================================================= 

61 

62 

63@delete_bp.route("/document/<string:document_id>", methods=["DELETE"]) 

64@login_required 

65def delete_document(document_id): 

66 """ 

67 Delete a document and all related data. 

68 

69 Tooltip: "Permanently delete this document, including PDF and text content. 

70 This cannot be undone." 

71 

72 Returns: 

73 JSON with deletion details including chunks deleted, blob size freed 

74 """ 

75 try: 

76 username = session["username"] 

77 service = DocumentDeletionService(username) 

78 result = service.delete_document(document_id) 

79 

80 if result.get("deleted"): 

81 return jsonify({"success": True, **result}) 

82 return jsonify({"success": False, **result}), 404 

83 

84 except Exception as e: 

85 return handle_api_error("deleting document", e) 

86 

87 

88@delete_bp.route("/document/<string:document_id>/blob", methods=["DELETE"]) 

89@login_required 

90def delete_document_blob(document_id): 

91 """ 

92 Delete PDF binary but keep document metadata and text content. 

93 

94 Tooltip: "Remove the PDF file to save space. Text content will be 

95 preserved for searching." 

96 

97 Returns: 

98 JSON with bytes freed 

99 """ 

100 try: 

101 username = session["username"] 

102 service = DocumentDeletionService(username) 

103 result = service.delete_blob_only(document_id) 

104 

105 if result.get("deleted"): 

106 return jsonify({"success": True, **result}) 

107 error_code = ( 

108 404 if "not found" in result.get("error", "").lower() else 400 

109 ) 

110 return jsonify({"success": False, **result}), error_code 

111 

112 except Exception as e: 

113 return handle_api_error("deleting document blob", e) 

114 

115 

116@delete_bp.route("/document/<string:document_id>/preview", methods=["GET"]) 

117@login_required 

118def get_document_deletion_preview(document_id): 

119 """ 

120 Get a preview of what will be deleted. 

121 

122 Returns information about the document to help user confirm deletion. 

123 """ 

124 try: 

125 username = session["username"] 

126 service = DocumentDeletionService(username) 

127 result = service.get_deletion_preview(document_id) 

128 

129 if result.get("found"): 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true

130 return jsonify({"success": True, **result}) 

131 return jsonify({"success": False, "error": "Document not found"}), 404 

132 

133 except Exception as e: 

134 return handle_api_error("getting document preview", e) 

135 

136 

137# ============================================================================= 

138# Collection Document Endpoints 

139# ============================================================================= 

140 

141 

142@delete_bp.route( 

143 "/collection/<string:collection_id>/document/<string:document_id>", 

144 methods=["DELETE"], 

145) 

146@login_required 

147def remove_document_from_collection(collection_id, document_id): 

148 """ 

149 Remove document from a collection. 

150 

151 If the document is not in any other collection, it will be deleted. 

152 

153 Tooltip: "Remove from this collection. If not in any other collection, 

154 the document will be deleted." 

155 

156 Returns: 

157 JSON with unlink status and whether document was deleted 

158 """ 

159 try: 

160 username = session["username"] 

161 service = DocumentDeletionService(username) 

162 result = service.remove_from_collection(document_id, collection_id) 

163 

164 if result.get("unlinked"): 

165 return jsonify({"success": True, **result}) 

166 return jsonify({"success": False, **result}), 404 

167 

168 except Exception as e: 

169 return handle_api_error("removing document from collection", e) 

170 

171 

172# ============================================================================= 

173# Collection Delete Endpoints 

174# ============================================================================= 

175 

176 

177@delete_bp.route("/collections/<string:collection_id>", methods=["DELETE"]) 

178@login_required 

179def delete_collection(collection_id): 

180 """ 

181 Delete a collection and clean up all related data. 

182 

183 Deletes the collection, its RAG index, chunks, and any orphaned 

184 documents (documents not in any other collection). 

185 

186 Returns: 

187 JSON with deletion details including orphaned documents deleted 

188 """ 

189 try: 

190 username = session["username"] 

191 service = CollectionDeletionService(username) 

192 result = service.delete_collection( 

193 collection_id, delete_orphaned_documents=True 

194 ) 

195 

196 if result.get("deleted"): 

197 return jsonify({"success": True, **result}) 

198 error = result.get("error", "Unknown error") 

199 status_code = 404 if "not found" in error.lower() else 400 

200 return jsonify({"success": False, **result}), status_code 

201 

202 except Exception as e: 

203 return handle_api_error("deleting collection", e) 

204 

205 

206@delete_bp.route( 

207 "/collections/<string:collection_id>/index", methods=["DELETE"] 

208) 

209@login_required 

210def delete_collection_index(collection_id): 

211 """ 

212 Delete only the RAG index for a collection, keeping the collection itself. 

213 

214 Useful for rebuilding an index from scratch. 

215 

216 Returns: 

217 JSON with deletion details 

218 """ 

219 try: 

220 username = session["username"] 

221 service = CollectionDeletionService(username) 

222 result = service.delete_collection_index_only(collection_id) 

223 

224 if result.get("deleted"): 

225 return jsonify({"success": True, **result}) 

226 return jsonify({"success": False, **result}), 404 

227 

228 except Exception as e: 

229 return handle_api_error("deleting collection index", e) 

230 

231 

232@delete_bp.route("/collections/<string:collection_id>/preview", methods=["GET"]) 

233@login_required 

234def get_collection_deletion_preview(collection_id): 

235 """ 

236 Get a preview of what will be deleted. 

237 

238 Returns information about the collection to help user confirm deletion. 

239 """ 

240 try: 

241 username = session["username"] 

242 service = CollectionDeletionService(username) 

243 result = service.get_deletion_preview(collection_id) 

244 

245 if result.get("found"): 245 ↛ 246line 245 didn't jump to line 246 because the condition on line 245 was never true

246 return jsonify({"success": True, **result}) 

247 return jsonify({"success": False, "error": "Collection not found"}), 404 

248 

249 except Exception as e: 

250 return handle_api_error("getting collection preview", e) 

251 

252 

253# ============================================================================= 

254# Bulk Delete Endpoints 

255# ============================================================================= 

256 

257 

258@delete_bp.route("/documents/bulk", methods=["DELETE"]) 

259@login_required 

260def delete_documents_bulk(): 

261 """ 

262 Delete multiple documents at once. 

263 

264 Tooltip: "Permanently delete all selected documents and their associated data." 

265 

266 Request body: 

267 {"document_ids": ["id1", "id2", ...]} 

268 

269 Returns: 

270 JSON with bulk deletion results 

271 """ 

272 try: 

273 document_ids = _validate_document_ids() 

274 username = session["username"] 

275 service = BulkDeletionService(username) 

276 result = service.delete_documents(document_ids) 

277 

278 return jsonify({"success": True, **result}) 

279 

280 except _ValidationError as e: 

281 return jsonify({"success": False, "error": e.message}), 400 

282 except Exception as e: 

283 return handle_api_error("bulk deleting documents", e) 

284 

285 

286@delete_bp.route("/documents/blobs", methods=["DELETE"]) 

287@login_required 

288def delete_documents_blobs_bulk(): 

289 """ 

290 Delete PDF binaries for multiple documents. 

291 

292 Tooltip: "Remove PDF files from selected documents to free up database space. 

293 Text content is preserved." 

294 

295 Request body: 

296 {"document_ids": ["id1", "id2", ...]} 

297 

298 Returns: 

299 JSON with bulk blob deletion results 

300 """ 

301 try: 

302 document_ids = _validate_document_ids() 

303 username = session["username"] 

304 service = BulkDeletionService(username) 

305 result = service.delete_blobs(document_ids) 

306 

307 return jsonify({"success": True, **result}) 

308 

309 except _ValidationError as e: 

310 return jsonify({"success": False, "error": e.message}), 400 

311 except Exception as e: 

312 return handle_api_error("bulk deleting blobs", e) 

313 

314 

315@delete_bp.route( 

316 "/collection/<string:collection_id>/documents/bulk", methods=["DELETE"] 

317) 

318@login_required 

319def remove_documents_from_collection_bulk(collection_id): 

320 """ 

321 Remove multiple documents from a collection. 

322 

323 Documents that are not in any other collection will be deleted. 

324 

325 Request body: 

326 {"document_ids": ["id1", "id2", ...]} 

327 

328 Returns: 

329 JSON with bulk removal results 

330 """ 

331 try: 

332 document_ids = _validate_document_ids() 

333 username = session["username"] 

334 service = BulkDeletionService(username) 

335 result = service.remove_documents_from_collection( 

336 document_ids, collection_id 

337 ) 

338 

339 return jsonify({"success": True, **result}) 

340 

341 except _ValidationError as e: 

342 return jsonify({"success": False, "error": e.message}), 400 

343 except Exception as e: 

344 return handle_api_error("bulk removing documents from collection", e) 

345 

346 

347@delete_bp.route("/documents/preview", methods=["POST"]) 

348@login_required 

349def get_bulk_deletion_preview(): 

350 """ 

351 Get a preview of what will be affected by a bulk operation. 

352 

353 Request body: 

354 { 

355 "document_ids": ["id1", "id2", ...], 

356 "operation": "delete" or "delete_blobs" 

357 } 

358 

359 Returns: 

360 JSON with preview information 

361 """ 

362 try: 

363 document_ids = _validate_document_ids() 

364 data = request.get_json() 

365 operation = data.get("operation", "delete") 

366 

367 username = session["username"] 

368 service = BulkDeletionService(username) 

369 result = service.get_bulk_preview(document_ids, operation) 

370 

371 return jsonify({"success": True, **result}) 

372 

373 except _ValidationError as e: 

374 return jsonify({"success": False, "error": e.message}), 400 

375 except Exception as e: 

376 return handle_api_error("getting bulk preview", e)