From 51424b3ab7dc4cba912feb30c0562810e436abc4 Mon Sep 17 00:00:00 2001 From: Jason Raitz Date: Tue, 6 Oct 2026 14:59:18 -0400 Subject: [PATCH 1/5] AP-863 aligning volume mounts - also adds empty filelist tests --- README.md | 7 +++++++ docker-compose.yml | 8 ++------ quiabo/tasks.py | 10 ++++++++++ test/unit/test_tasks.py | 35 +++++++++++++++++++++++++++++------ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f81087b..6919b0a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ 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. ```bash +# in order to run quiabo with mokelumne you need a symlink to mokelumne's tmp directory +# assuming mokelumne is installed in this project's parent... +ln -s ../mokelumne/tmp tmp + +# 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..192ecd6 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 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/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..e492004 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,7 +153,7 @@ 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 From 654173e6c6e52337a4cd4b07ff40a61b71ff7a18 Mon Sep 17 00:00:00 2001 From: Jason Raitz Date: Tue, 6 Oct 2026 15:00:27 -0400 Subject: [PATCH 2/5] remove old files directory --- .gitignore | 1 - files/.keep | 0 2 files changed, 1 deletion(-) delete mode 100644 files/.keep 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/files/.keep b/files/.keep deleted file mode 100644 index e69de29..0000000 From 61e28d730610841a8fca64c6e5c50ac8b23c794a Mon Sep 17 00:00:00 2001 From: Jason Raitz Date: Tue, 6 Oct 2026 15:07:23 -0400 Subject: [PATCH 3/5] fix test --- test/unit/test_tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/unit/test_tasks.py b/test/unit/test_tasks.py index e492004..e388b14 100644 --- a/test/unit/test_tasks.py +++ b/test/unit/test_tasks.py @@ -157,15 +157,16 @@ def test_run_tesseract_job_wraps_tesseract_errors(monkeypatch, tmp_path): 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)) From 01d6879061b1dfbd15fe9e683b6a09c114b30af6 Mon Sep 17 00:00:00 2001 From: Jason Raitz Date: Thu, 8 Oct 2026 11:26:21 -0400 Subject: [PATCH 4/5] mnt change --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 192ecd6..0d9a3e5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,7 +45,7 @@ services: - 8000:8000 volumes: - ./:/app - - ./tmp:/srv + - ./tmp:/srv:ro worker: build: From ce6386709e5d12c32dbbaa965569ffdc3cbc0233 Mon Sep 17 00:00:00 2001 From: Jason Raitz <145712254+jason-raitz@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:41:43 -0400 Subject: [PATCH 5/5] Update README.md Co-authored-by: Anna Wilcox --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6919b0a..42e68af 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ 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 -# in order to run quiabo with mokelumne you need a symlink to mokelumne's tmp directory # assuming mokelumne is installed in this project's parent... ln -s ../mokelumne/tmp tmp -# IF you do not have mokelumne installed locally, make an empty tmp directory +# 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