diff --git a/quantmind/preprocess/format/pdf.py b/quantmind/preprocess/format/pdf.py index f78ab47..cc623a1 100644 --- a/quantmind/preprocess/format/pdf.py +++ b/quantmind/preprocess/format/pdf.py @@ -2,6 +2,7 @@ import asyncio import hashlib +import os import tempfile from dataclasses import dataclass from importlib.metadata import version @@ -72,10 +73,21 @@ def _write_artifacts( screenshots_dir.mkdir(exist_ok=True) image_dir = artifact_dir / "images" image_dir.mkdir(exist_ok=True) - with tempfile.NamedTemporaryFile(suffix=".pdf") as source: + # On Windows, NamedTemporaryFile keeps an exclusive handle on the file, + # which blocks the native parser from opening the path. Use delete=False, + # close the handle before handing the path to the parser, then remove the + # file afterwards so no temp file is left behind. + source = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) + try: source.write(pdf_bytes) source.flush() + source.close() screenshots = parser.screenshot(source.name) + finally: + try: + os.unlink(source.name) + except OSError: + pass screenshot_paths: dict[int, str] = {} for screenshot in screenshots: path = screenshots_dir / f"page_{screenshot.page_num}.png" diff --git a/tests/library/test_example_bundle.py b/tests/library/test_example_bundle.py index 2d92efd..a8098ed 100644 --- a/tests/library/test_example_bundle.py +++ b/tests/library/test_example_bundle.py @@ -146,6 +146,7 @@ async def test_prebuilt_bundle_searches_and_matches_source_json(self): {("text-embedding-3-small", 1536)}, ) + db.close() provider = _QueryEmbeddingProvider() library = await LocalKnowledgeLibrary.open( _DATABASE_PATH, diff --git a/tests/library/test_local.py b/tests/library/test_local.py index a0a506c..3fab7ec 100644 --- a/tests/library/test_local.py +++ b/tests/library/test_local.py @@ -258,6 +258,7 @@ async def test_tree_root_and_non_root_nodes_use_exact_grain_and_filters( ).fetchone()[0], 3, ) + db.close() hits = await library.search( SemanticQuery( text=query_text, @@ -551,6 +552,8 @@ async def test_delete_removes_canonical_root_and_nodes_transactionally( 0, ) + db.close() + async def test_stale_canonical_get_fails_but_delete_can_recover(self): item = _news("Stale canonical") provider = _FakeEmbeddingProvider() @@ -568,6 +571,7 @@ async def test_stale_canonical_get_fails_but_delete_can_recover(self): "WHERE item_id = ?", (str(item.id),), ) + db.close() with self.assertRaisesRegex( RuntimeError, "Stale canonical knowledge" ): @@ -595,6 +599,7 @@ async def test_orphan_and_missing_derived_data_are_reported_as_stale(self): "DELETE FROM knowledge_items WHERE item_id = ?", (str(item.id),) ) + db.close() reopened_provider = _FakeEmbeddingProvider() library = await LocalKnowledgeLibrary.open( self.db_path, @@ -630,6 +635,7 @@ async def test_corrupt_canonical_tree_node_fails_rehydration(self): """, (str(paper.id), str(methods_id)), ) + db.close() with self.assertRaisesRegex( RuntimeError, "node.*content hash mismatch" ): @@ -656,6 +662,7 @@ async def test_corrupt_vector_and_query_dimension_mismatch_fail_clearly( (b"bad", str(item.id)), ) + db.close() library = await LocalKnowledgeLibrary.open( self.db_path, embedding_model="fake-2d", diff --git a/tests/library/test_paper.py b/tests/library/test_paper.py index d73f1bb..8e5ce1f 100644 --- a/tests/library/test_paper.py +++ b/tests/library/test_paper.py @@ -130,6 +130,8 @@ async def test_put_persists_explicit_source_artifact_and_projection_layers( all("embedding" not in payload for payload in payloads) ) + db.close() + async def test_reopen_round_trip_reuses_vectors_and_resolves_hits( self, ) -> None: @@ -285,6 +287,8 @@ async def test_multiple_chunk_and_summary_versions_coexist(self) -> None: 8, ) + db.close() + async def test_required_projection_failure_is_atomic(self) -> None: library = await LocalKnowledgeLibrary.open( self.db_path, @@ -315,6 +319,8 @@ async def test_required_projection_failure_is_atomic(self) -> None: 0, ) + db.close() + async def test_rehydrate_rejects_asset_metadata_drift(self) -> None: result = build_paper_result() library = await LocalKnowledgeLibrary.open( @@ -332,6 +338,7 @@ async def test_rehydrate_rejects_asset_metadata_drift(self) -> None: ("application/tampered",), ) + db.close() library = await LocalKnowledgeLibrary.open( self.db_path, embedding_model="fake-2d", @@ -361,6 +368,7 @@ async def test_rehydrate_rejects_missing_summary_lineage(self) -> None: (str(result.global_summary.id),), ) + db.close() library = await LocalKnowledgeLibrary.open( self.db_path, embedding_model="fake-2d", @@ -395,6 +403,7 @@ async def test_search_rejects_projection_text_drift(self) -> None: (tampered, hashlib.sha256(tampered.encode()).hexdigest()), ) + db.close() library = await LocalKnowledgeLibrary.open( self.db_path, embedding_model="fake-2d", diff --git a/tests/library/test_structure.py b/tests/library/test_structure.py index 5dbb79f..785324f 100644 --- a/tests/library/test_structure.py +++ b/tests/library/test_structure.py @@ -100,6 +100,7 @@ async def test_put_reopen_get_and_idempotency_without_source_or_chunks( ).fetchone()[0] self.assertEqual(target_count, 0) + db.close() reopened = await self._open(_FakeEmbeddingProvider()) try: restored = await reopened.get_artifact(tree.id) @@ -212,6 +213,8 @@ async def test_put_rejects_a_tampered_tree_and_writes_nothing(self) -> None: 0, ) + db.close() + async def test_put_paper_structure_tree_rejects_a_tree_for_another_source( self, ) -> None: @@ -236,6 +239,8 @@ async def test_put_paper_structure_tree_rejects_a_tree_for_another_source( 0, ) + db.close() + async def test_rehydrate_fails_closed_on_member_metadata_drift( self, ) -> None: @@ -254,6 +259,7 @@ async def test_rehydrate_fails_closed_on_member_metadata_drift( (str(tree.id), str(node.node_id)), ) + db.close() reopened = await self._open(_FakeEmbeddingProvider()) try: with self.assertRaisesRegex( @@ -307,6 +313,7 @@ async def test_version_three_migrates_to_source_free_artifacts( """ ) + db.close() library = await LocalKnowledgeLibrary.open( path, embedding_model="fake-2d", @@ -360,6 +367,8 @@ async def test_version_three_migrates_to_source_free_artifacts( 0, ) + db.close() + if __name__ == "__main__": unittest.main()