From d4d6414bc41dd57153c569869e06eb8d5c0eb3af Mon Sep 17 00:00:00 2001 From: Lawrence Akka Date: Wed, 12 Aug 2026 15:20:11 +0100 Subject: [PATCH] Similar fix for haiku.rag.ingester, and formatting - Add regression tests - Update Changelog --- CHANGELOG.md | 2 ++ haiku_rag_slim/haiku/rag/ingester/cli.py | 17 ++++++----- tests/ingester/test_cli.py | 36 ++++++++++++++++++------ tests/store/test_engine.py | 3 +- tests/store/test_v0_40_0_migration.py | 3 +- tests/test_cli.py | 18 ++++++++++++ 6 files changed, 59 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa48ed23..4752b636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed - `cross-encoder` reranking no longer ties the scores of strongly-relevant candidates, which left their order to the sort. Scores remain 0-1. +- haiku-rag CLI startup no longer imports `lancedb`, `pyarrow` and `pydantic_ai`. +- `haiku.rag.store` no longer re-exports `Store`; import it from `haiku.rag.store.engine`. ## [0.73.0] - 2026-08-06 diff --git a/haiku_rag_slim/haiku/rag/ingester/cli.py b/haiku_rag_slim/haiku/rag/ingester/cli.py index 21d67355..0caf90eb 100644 --- a/haiku_rag_slim/haiku/rag/ingester/cli.py +++ b/haiku_rag_slim/haiku/rag/ingester/cli.py @@ -4,6 +4,7 @@ from collections.abc import Iterator from contextlib import contextmanager from datetime import UTC, datetime from pathlib import Path +from typing import TYPE_CHECKING import typer import yaml @@ -28,11 +29,6 @@ from haiku.rag.config import ( # noqa: E402 load_yaml_config, set_config, ) -from haiku.rag.ingester.app import ( # noqa: E402 - BatchProgress, - BatchProgressCallback, - IngesterApp, -) from haiku.rag.ingester.batch import BatchManifest # noqa: E402 from haiku.rag.ingester.queue.migrations import open_queue # noqa: E402 from haiku.rag.logging import configure_cli_logging # noqa: E402 @@ -41,6 +37,9 @@ from haiku.rag.store.exceptions import ( # noqa: E402 ReadOnlyError, ) +if TYPE_CHECKING: + from haiku.rag.ingester.app import BatchProgress, BatchProgressCallback + _cli = typer.Typer( name="haiku-ingester", no_args_is_help=True, @@ -167,7 +166,7 @@ def _write_manifest(manifest: BatchManifest, path: Path) -> None: @contextmanager def _batch_progress( description: str, -) -> Iterator[BatchProgressCallback | None]: # pragma: no cover +) -> "Iterator[BatchProgressCallback | None]": # pragma: no cover console = Console(file=sys.stdout) if not console.is_terminal: yield None @@ -184,7 +183,7 @@ def _batch_progress( ) task_id = None - def _update(snapshot: BatchProgress) -> None: + def _update(snapshot: "BatchProgress") -> None: nonlocal task_id task_description = ( f"{description} ({snapshot.succeeded} ok, {snapshot.dead} dead)" @@ -244,6 +243,8 @@ def serve( ) -> None: """Run the production ingester: pollers + workers (and the HTTP API unless --no-api is set). Blocks until SIGINT/SIGTERM.""" + from haiku.rag.ingester.app import IngesterApp + app_config = get_config() if host is not None: app_config.ingester.api.host = host @@ -309,6 +310,8 @@ async def _run_batch( output: Path | None = None, manifest_path: Path | None = None, ) -> None: + from haiku.rag.ingester.app import IngesterApp + db = _resolve_db_path(app_config, db_path) app = IngesterApp(config=app_config, db_path=db) if dry_run: diff --git a/tests/ingester/test_cli.py b/tests/ingester/test_cli.py index 385456ab..c32e3c48 100644 --- a/tests/ingester/test_cli.py +++ b/tests/ingester/test_cli.py @@ -1,6 +1,8 @@ """haiku-ingester CLI: exercises every subcommand via CliRunner with IngesterApp / open_queue patched out so no real ingestion runs.""" +import subprocess +import sys from contextlib import contextmanager from datetime import UTC, datetime from unittest.mock import AsyncMock, MagicMock @@ -24,6 +26,22 @@ from haiku.rag.ingester.queue.models import JobOp runner = CliRunner() +def test_importing_ingester_cli_does_not_load_lancedb(): + """Test that lancedb is not imported automatically by the cli. Doing so is + expensive. Must be run in a subprocess because lancedb might be imported by + other tests in the same session.""" + result = subprocess.run( + [ + sys.executable, + "-c", + "import haiku.rag.ingester.cli, sys; assert 'lancedb' not in sys.modules", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + # --- helpers --- @@ -41,7 +59,7 @@ def _config_with_queue(queue: QueueConfig): def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock: fake = AsyncMock() fake.run_batch.return_value = report - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) return fake @@ -78,14 +96,14 @@ def _manifest() -> BatchManifest: def _fake_dry_run_app(report: BatchDryRunReport, monkeypatch) -> AsyncMock: fake = AsyncMock() fake.run_batch_dry_run.return_value = report - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) return fake def _fake_manifest_app(report: BatchReport, monkeypatch) -> AsyncMock: fake = AsyncMock() fake.run_batch_from_manifest.return_value = report - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) return fake @@ -111,7 +129,7 @@ def test_run_batch_passes_progress_callback_when_enabled(monkeypatch): return BatchReport(succeeded=1, dead=0) fake.run_batch.side_effect = _run_batch - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) monkeypatch.setattr( "haiku.rag.ingester.cli._batch_progress", lambda _: _progress_context(lambda snapshot: None), @@ -227,7 +245,7 @@ def test_run_batch_manifest_passes_progress_callback(monkeypatch, tmp_path): return BatchReport(succeeded=1, dead=0) fake.run_batch_from_manifest.side_effect = _run_manifest - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) monkeypatch.setattr( "haiku.rag.ingester.cli._batch_progress", lambda _: _progress_context(lambda snapshot: None), @@ -256,7 +274,7 @@ def test_run_batch_manifest_reports_validation_error(monkeypatch, tmp_path): _write_manifest(manifest_path) fake = AsyncMock() fake.run_batch_from_manifest.side_effect = ValueError("bad manifest") - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) result = runner.invoke(cli, ["run-batch", "--manifest", str(manifest_path)]) @@ -322,7 +340,7 @@ def test_resolve_queue_config_keeps_dburi_when_path_override_present(tmp_path): def test_serve_invokes_app(monkeypatch): fake = AsyncMock() - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake) result = runner.invoke(cli, ["serve", "--db", "x.lancedb", "--no-api"]) @@ -338,7 +356,7 @@ def test_serve_passes_host_and_port(monkeypatch): captured.update(kwargs) return fake - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture) result = runner.invoke( cli, ["serve", "--db", "x.lancedb", "--host", "0.0.0.0", "--port", "9999"] @@ -357,7 +375,7 @@ def test_serve_passes_root_path(monkeypatch): captured.update(kwargs) return fake - monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture) + monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture) result = runner.invoke( cli, ["serve", "--db", "x.lancedb", "--root-path", "/ingester/"] diff --git a/tests/store/test_engine.py b/tests/store/test_engine.py index a8ff3d2a..89d3ef65 100644 --- a/tests/store/test_engine.py +++ b/tests/store/test_engine.py @@ -1,7 +1,6 @@ import pytest -from haiku.rag.store.engine import Store -from haiku.rag.store.engine import get_database_stats +from haiku.rag.store.engine import Store, get_database_stats class TestGetDatabaseStats: diff --git a/tests/store/test_v0_40_0_migration.py b/tests/store/test_v0_40_0_migration.py index f5547558..84e13605 100644 --- a/tests/store/test_v0_40_0_migration.py +++ b/tests/store/test_v0_40_0_migration.py @@ -8,9 +8,8 @@ introduced by later migrations (``picture_data`` in v0.45.0, import pytest -from haiku.rag.store.engine import Store from haiku.rag.store.compression import compress_docling_split -from haiku.rag.store.engine import DocumentRecord +from haiku.rag.store.engine import DocumentRecord, Store from haiku.rag.store.upgrades.v0_40_0 import _apply_populate_document_items diff --git a/tests/test_cli.py b/tests/test_cli.py index 994df249..4e00cbb2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,5 @@ +import subprocess +import sys from unittest.mock import patch import pytest @@ -12,6 +14,22 @@ from haiku.rag.store.exceptions import MigrationRequiredError runner = CliRunner() +def test_importing_cli_does_not_load_lancedb(): + """Test that lancedb is not imported automatically by the cli. Doing so is + expensive. Must be run in a subprocess because lancedb might be imported by + other tests in the same session.""" + result = subprocess.run( + [ + sys.executable, + "-c", + "import haiku.rag.cli, sys; assert 'lancedb' not in sys.modules", + ], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + + class TestParseMetaOptions: def test_empty_input(self): assert _parse_meta_options(None) == {}