diff --git a/.gitignore b/.gitignore index 857e48d..f263384 100644 --- a/.gitignore +++ b/.gitignore @@ -220,5 +220,4 @@ __marimo__/ # other stuff artifacts/* uv.lock -files/* .DS_Store diff --git a/README.md b/README.md index f81087b..42e68af 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,15 @@ Python dependencies are declared in `pyproject.toml`. Spin up the application using Docker Compose. There are number of dependencies (Postgres and Redis) as well as Flask/Celery app components (`app`, `worker`, and optionially `flower`). Redis serves as the Celery broker (source of jobs) and Postgres is the Celery results backend. +In order to run Quiabo on the same system as [Mokelumne](https://github.com/BerkeleyLibrary/mokelumne) for development or integration testing, you need to symlink the `tmp` directory to Mokelumne's. Otherwise, you will need to create an empty `tmp` directory before starting Quiabo. + ```bash +# assuming mokelumne is installed in this project's parent... +ln -s ../mokelumne/tmp tmp + +# or, if you do not have mokelumne installed locally, make an empty tmp directory: +# mkdir tmp + # Build the Docker image for app, worker, and flower docker compose build diff --git a/docker-compose.yml b/docker-compose.yml index 1261195..0d9a3e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,8 +23,6 @@ services: interval: 10s retries: 5 start_period: 5s - ports: - - 5432:5432 restart: always volumes: - postgres-db-volume:/var/lib/postgresql/data @@ -47,7 +45,7 @@ services: - 8000:8000 volumes: - ./:/app - - ./files:/srv/files:ro + - ./tmp:/srv:ro worker: build: @@ -67,7 +65,7 @@ services: command: celery -A quiabo.celery_app worker --loglevel INFO volumes: - ./:/app - - ./files:/srv/files + - ./tmp:/srv flower: build: @@ -102,8 +100,6 @@ services: retries: 50 start_period: 30s image: redis:8 - ports: - - 6379:6379 restart: always volumes: diff --git a/files/.keep b/files/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/quiabo/tasks.py b/quiabo/tasks.py index 1720762..b9820cb 100644 --- a/quiabo/tasks.py +++ b/quiabo/tasks.py @@ -13,6 +13,16 @@ def run_tesseract_job(self, filelist: str, languages: list[str], output: str) -> dict: """Run Tesseract over the files listed in filelist and write output PDF.""" + # is filelist a valid file with at least one line? If not, raise an error. + filelist_path = Path(filelist) + if not filelist_path.is_file(): + raise FileNotFoundError( + f"File list {filelist} does not exist or is not a file." + ) + with filelist_path.open("r", encoding="utf-8") as f: + if not any(line.strip() for line in f): + raise ValueError(f"File list {filelist} is empty.") + output = output.removesuffix(".pdf") output_dir = Path(output).parent diff --git a/test/unit/test_tasks.py b/test/unit/test_tasks.py index 86d7dfd..e388b14 100644 --- a/test/unit/test_tasks.py +++ b/test/unit/test_tasks.py @@ -33,10 +33,27 @@ def test_shared_task_delay_delegates_to_apply_async(monkeypatch): ) +def test_run_tesseract_job_raises_error_for_empty_filelist(tmp_path): + """Verify that the task rejects an empty file list.""" + filelist = tmp_path / "empty.txt" + filelist.touch() + + with pytest.raises(ValueError, match="is empty"): + run_tesseract_job.run(str(filelist), ["eng"], "output") + + +def test_run_tesseract_job_raises_error_for_missing_filelist(tmp_path): + """Verify that the task rejects a missing file list.""" + filelist = tmp_path / "missing.txt" + + with pytest.raises(FileNotFoundError, match="does not exist"): + run_tesseract_job.run(str(filelist), ["eng"], "output") + + def test_run_tesseract_job_updates_state_and_returns_digest(monkeypatch, tmp_path): """Verify that the task runs Tesseract and returns the PDF digest.""" filelist = tmp_path / "files.txt" - filelist.touch() + filelist.write_text("image1.tif\nimage2.tif\n", encoding="utf-8") output = tmp_path / "output" pdf_contents = b"generated PDF" @@ -82,6 +99,8 @@ def test_run_tesseract_job_removes_pdf_suffix(monkeypatch, tmp_path): run = Mock() output = tmp_path / "output.pdf" output_base = str(output.with_suffix("")) + filelist = tmp_path / "files.txt" + filelist.write_text("image1.tif\nimage2.tif\n", encoding="utf-8") def write_output(**kwargs): Path(f"{kwargs['output_filename_base']}.pdf").touch() @@ -90,18 +109,18 @@ def write_output(**kwargs): monkeypatch.setattr(run_tesseract_job, "update_state", update_state) monkeypatch.setattr("quiabo.tasks.run_tesseract", run) - result = run_tesseract_job.run("files.txt", ["eng"], str(output)) + result = run_tesseract_job.run(str(filelist), ["eng"], str(output)) update_state.assert_called_once_with( state="STARTED", meta={ - "filelist": "files.txt", + "filelist": str(filelist), "languages": ["eng"], "output": output_base, }, ) run.assert_called_once_with( - input_filename="files.txt", + input_filename=str(filelist), output_filename_base=output_base, extension="pdf", lang="eng", @@ -115,14 +134,18 @@ def write_output(**kwargs): def test_run_tesseract_job_requires_existing_output_directory(tmp_path): """Verify that the task rejects a missing output directory.""" output = tmp_path / "missing" / "output" + filelist = tmp_path / "files.txt" + filelist.write_text("image1.tif\nimage2.tif\n", encoding="utf-8") with pytest.raises(FileNotFoundError, match="does not exist"): - run_tesseract_job.run("files.txt", ["eng"], str(output)) + run_tesseract_job.run(str(filelist), ["eng"], str(output)) def test_run_tesseract_job_wraps_tesseract_errors(monkeypatch, tmp_path): """Verify that Tesseract errors are wrapped with task context.""" output = tmp_path / "output" + filelist = tmp_path / "files.txt" + filelist.write_text("image1.tif\nimage2.tif\n", encoding="utf-8") error = RuntimeError("Tesseract failed") run = Mock(side_effect=error) monkeypatch.setattr("quiabo.tasks.run_tesseract", run) @@ -130,19 +153,20 @@ def test_run_tesseract_job_wraps_tesseract_errors(monkeypatch, tmp_path): with pytest.raises( RuntimeError, match="Error running Tesseract: Tesseract failed" ) as exc_info: - run_tesseract_job.run("files.txt", ["eng"], str(output)) + run_tesseract_job.run(str(filelist), ["eng"], str(output)) assert exc_info.value.__cause__ is error + def test_run_tesseract_job_fails_when_language_unavailable(monkeypatch, tmp_path): """Verify that the task fails when Tesseract cannot load a language.""" output = tmp_path / "output" + filelist = tmp_path / "files.txt" + filelist.write_text("image1.tif\nimage2.tif\n", encoding="utf-8") error = TesseractError(1, "Failed loading language 'swe'") run = Mock(side_effect=error) monkeypatch.setattr("quiabo.tasks.run_tesseract", run) - with pytest.raises( - RuntimeError, match="Failed loading language 'swe'" - ): - run_tesseract_job.run("files.txt", ["swe"], str(output)) + with pytest.raises(RuntimeError, match="Failed loading language 'swe'"): + run_tesseract_job.run(str(filelist), ["swe"], str(output))