diff --git a/quiabo/jobs.py b/quiabo/jobs.py index 8af4310..9b5ed67 100644 --- a/quiabo/jobs.py +++ b/quiabo/jobs.py @@ -1,8 +1,9 @@ """Routes for submitting and managing OCR jobs.""" import traceback - from pathlib import Path + +from celery.result import AsyncResult from flask import Blueprint, request from quiabo.tasks import run_tesseract_job @@ -51,6 +52,26 @@ def create_job(): "job_status": f"/jobs/{celery_result.id}", }, 202 +@bp.get("/") +def get_job(job_id: str): + """Get the status of an OCR job.""" + celery_result = AsyncResult(job_id) + + response = { + "id": job_id, + "status": celery_result.state, + } + + if celery_result.state == "SUCCESS": + response["result"] = celery_result.result + response["date_done"] = celery_result.date_done.isoformat() + + if celery_result.state == "FAILURE": + response["result"] = str(celery_result.result) + response["traceback"] = celery_result.traceback + + return response, 200 + def _error_response(error: Exception, status_code: int): """Build error response.""" diff --git a/test/unit/test_jobs.py b/test/unit/test_jobs.py index 8e3d5f5..eb2abf9 100644 --- a/test/unit/test_jobs.py +++ b/test/unit/test_jobs.py @@ -1,4 +1,6 @@ """Tests for the jobs endpoints.""" +from datetime import datetime +from types import SimpleNamespace from unittest.mock import Mock @@ -169,3 +171,78 @@ def test_create_job_requires_filelist_to_be_file(client, tmp_path): assert response.json["status"] == "ERROR" assert "does not exist or is not a file" in response.json["result"] assert "traceback" in response.json + +def test_get_job_route_exists(client): + """GET /jobs/ is a valid route.""" + response = client.get("/jobs/test-job-id") + + assert response.status_code != 404 + +def test_get_job_returns_celery_status(client, monkeypatch): + """GET /jobs/ returns job's Celery status.""" + fake_result = SimpleNamespace(state="STARTED") + + monkeypatch.setattr( + "quiabo.jobs.AsyncResult", + lambda job_id: fake_result, + ) + + response = client.get("/jobs/test-job-id") + + assert response.status_code == 200 + assert response.json == { + "id": "test-job-id", + "status": "STARTED", + } + +def test_get_successful_job_returns_result(client, monkeypatch): + """GET a successful job returns its result and completion date.""" + fake_result = SimpleNamespace( + state="SUCCESS", + result={ + "output_path": "/app/files/test.pdf", + "sha256": "abc123", + }, + date_done=datetime(2026, 9, 28, 16, 0, 0), + ) + + monkeypatch.setattr( + "quiabo.jobs.AsyncResult", + lambda job_id: fake_result, + ) + + response = client.get("/jobs/test-job-id") + + assert response.status_code == 200 + assert response.json == { + "id": "test-job-id", + "status": "SUCCESS", + "result": { + "output_path": "/app/files/test.pdf", + "sha256": "abc123", + }, + "date_done": "2026-09-28T16:00:00", + } + +def test_get_failed_job_returns_error(client, monkeypatch): + """GET a failed job returns its error and traceback.""" + fake_result = SimpleNamespace( + state="FAILURE", + result="Tesseract failed", + traceback="Traceback: something went horribly wrong", + ) + + monkeypatch.setattr( + "quiabo.jobs.AsyncResult", + lambda job_id: fake_result, + ) + + response = client.get("/jobs/test-job-id") + + assert response.status_code == 200 + assert response.json == { + "id": "test-job-id", + "status": "FAILURE", + "result": "Tesseract failed", + "traceback": "Traceback: something went horribly wrong", + }