From b8c3f225601202e45bfbb0cb7961d3e445aab7df Mon Sep 17 00:00:00 2001 From: Dagonite Date: Thu, 30 Apr 2026 10:04:02 +0100 Subject: [PATCH 1/4] Add a new env var and base live data path on it --- plotting_service/routers/live_data.py | 5 +++-- plotting_service/services/live_data_service.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plotting_service/routers/live_data.py b/plotting_service/routers/live_data.py index 48cea6c..3cb1087 100644 --- a/plotting_service/routers/live_data.py +++ b/plotting_service/routers/live_data.py @@ -15,6 +15,7 @@ LiveDataRouter = APIRouter(prefix="/live") CEPH_DIR = os.environ.get("CEPH_DIR", "/ceph") +GENERIC_DIR = "GENERIC" if os.environ.get("PRODUCTION", "").lower() == "true" else "GENERIC-staging" stdout_handler = logging.StreamHandler(stream=sys.stdout) logging.basicConfig( @@ -42,7 +43,7 @@ async def get_live_data_files(instrument: str) -> list[str]: status_code=HTTPStatus.NOT_FOUND, detail=f"Live data directory for '{instrument}' not found" ) - safe_check_filepath(live_data_path, CEPH_DIR + "/GENERIC/livereduce") + safe_check_filepath(live_data_path, CEPH_DIR + f"/{GENERIC_DIR}/livereduce") files = [f.name for f in live_data_path.iterdir() if f.is_file()] return sorted(files) @@ -71,7 +72,7 @@ async def live_data(instrument: str, poll_interval: int = 2, keepalive_interval: status_code=HTTPStatus.NOT_FOUND, detail=f"Live data directory for '{instrument}' not found" ) - safe_check_filepath(live_data_path, CEPH_DIR + "/GENERIC/livereduce") + safe_check_filepath(live_data_path, CEPH_DIR + f"/{GENERIC_DIR}/livereduce") return StreamingResponse( generate_file_change_events(live_data_path, CEPH_DIR, instrument, keepalive_interval, poll_interval), diff --git a/plotting_service/services/live_data_service.py b/plotting_service/services/live_data_service.py index 6b7c9dd..b33d385 100644 --- a/plotting_service/services/live_data_service.py +++ b/plotting_service/services/live_data_service.py @@ -3,11 +3,14 @@ import asyncio import contextlib import logging +import os import typing from pathlib import Path logger = logging.getLogger(__name__) +PRODUCTION = os.environ.get("PRODUCTION", "False").lower() == "true" + def get_file_snapshot(directory: Path) -> dict[str, float]: """Get a snapshot of all files in a directory with their modification times. @@ -108,7 +111,8 @@ def get_live_data_directory(instrument: str, ceph_dir: str) -> Path | None: :return: Path to live data directory, or None if it doesn't exist """ instrument_upper = instrument.upper() - live_data_path = Path(ceph_dir) / "GENERIC" / "livereduce" / instrument_upper + generic_dir = "GENERIC" if PRODUCTION else "GENERIC-staging" + live_data_path = Path(ceph_dir) / generic_dir / "livereduce" / instrument_upper if not (live_data_path.exists() and live_data_path.is_dir()): return None From c1c16f22884311d654f767d8ef6e4b34c8746061 Mon Sep 17 00:00:00 2001 From: Dagonite Date: Thu, 30 Apr 2026 10:04:40 +0100 Subject: [PATCH 2/4] Add tests for live data service and router --- test/test_live_data.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 test/test_live_data.py diff --git a/test/test_live_data.py b/test/test_live_data.py new file mode 100644 index 0000000..13ab01d --- /dev/null +++ b/test/test_live_data.py @@ -0,0 +1,82 @@ +import importlib +from unittest import mock + +import pytest + +import plotting_service.routers.live_data as live_data_router +import plotting_service.services.live_data_service as live_data_service + + +def reload_live_data_modules(monkeypatch: pytest.MonkeyPatch, production: bool | None) -> tuple[object, object]: + if production is None: + monkeypatch.delenv("PRODUCTION", raising=False) + else: + monkeypatch.setenv("PRODUCTION", str(production).lower()) + + reloaded_service = importlib.reload(live_data_service) + reloaded_router = importlib.reload(live_data_router) + return reloaded_service, reloaded_router + + +def test_get_live_data_directory_uses_staging_path_when_production_is_unset(tmp_path, monkeypatch): + live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=None) + live_data_path = tmp_path / "GENERIC-staging" / "livereduce" / "LOQ" + live_data_path.mkdir(parents=True) + + assert live_data_service_module.get_live_data_directory("loq", str(tmp_path)) == live_data_path + + +def test_get_live_data_directory_uses_generic_path_when_production_is_true(tmp_path, monkeypatch): + live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=True) + live_data_path = tmp_path / "GENERIC" / "livereduce" / "LOQ" + live_data_path.mkdir(parents=True) + + assert live_data_service_module.get_live_data_directory("loq", str(tmp_path)) == live_data_path + + +def test_get_live_data_directory_returns_none_when_selected_path_is_missing(tmp_path, monkeypatch): + live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=False) + (tmp_path / "GENERIC" / "livereduce" / "LOQ").mkdir(parents=True) + + assert live_data_service_module.get_live_data_directory("loq", str(tmp_path)) is None + + +@pytest.mark.asyncio +async def test_get_live_data_files_validates_staging_base_path(tmp_path, monkeypatch): + _, live_data_router_module = reload_live_data_modules(monkeypatch, production=False) + live_data_path = tmp_path / "GENERIC-staging" / "livereduce" / "LOQ" + live_data_path.mkdir(parents=True) + (live_data_path / "second.txt").write_text("second") + (live_data_path / "first.txt").write_text("first") + + with ( + mock.patch.object(live_data_router_module, "CEPH_DIR", str(tmp_path)), + mock.patch.object(live_data_router_module, "safe_check_filepath") as safe_check_filepath, + ): + files = await live_data_router_module.get_live_data_files("loq") + + assert files == ["first.txt", "second.txt"] + safe_check_filepath.assert_called_once_with(live_data_path, str(tmp_path) + "/GENERIC-staging/livereduce") + + +@pytest.mark.asyncio +async def test_live_data_validates_production_base_path(tmp_path, monkeypatch): + _, live_data_router_module = reload_live_data_modules(monkeypatch, production=True) + live_data_path = tmp_path / "GENERIC" / "livereduce" / "LOQ" + live_data_path.mkdir(parents=True) + + with ( + mock.patch.object(live_data_router_module, "CEPH_DIR", str(tmp_path)), + mock.patch.object(live_data_router_module, "safe_check_filepath") as safe_check_filepath, + mock.patch.object( + live_data_router_module, + "generate_file_change_events", + return_value=iter([b"event: connected\ndata: {}\n\n"]), + ) as generate_file_change_events, + ): + response = await live_data_router_module.live_data("loq") + + safe_check_filepath.assert_called_once_with(live_data_path, str(tmp_path) + "/GENERIC/livereduce") + generate_file_change_events.assert_called_once_with(live_data_path, str(tmp_path), "loq", 30, 2) + assert response.media_type == "text/event-stream" + assert response.headers["X-Accel-Buffering"] == "no" From 0facf89da90eda75f40aa2789c468d19f0a29ef9 Mon Sep 17 00:00:00 2001 From: Dagonite Date: Thu, 30 Apr 2026 10:07:59 +0100 Subject: [PATCH 3/4] Add docstrings to live data tests --- test/test_live_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_live_data.py b/test/test_live_data.py index 13ab01d..db63804 100644 --- a/test/test_live_data.py +++ b/test/test_live_data.py @@ -19,6 +19,8 @@ def reload_live_data_modules(monkeypatch: pytest.MonkeyPatch, production: bool | def test_get_live_data_directory_uses_staging_path_when_production_is_unset(tmp_path, monkeypatch): + """Use the staging live-data directory when the PRODUCTION flag is + unset.""" live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=None) live_data_path = tmp_path / "GENERIC-staging" / "livereduce" / "LOQ" live_data_path.mkdir(parents=True) @@ -27,6 +29,8 @@ def test_get_live_data_directory_uses_staging_path_when_production_is_unset(tmp_ def test_get_live_data_directory_uses_generic_path_when_production_is_true(tmp_path, monkeypatch): + """Use the production live-data directory when the PRODUCTION flag is + true.""" live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=True) live_data_path = tmp_path / "GENERIC" / "livereduce" / "LOQ" live_data_path.mkdir(parents=True) @@ -35,6 +39,8 @@ def test_get_live_data_directory_uses_generic_path_when_production_is_true(tmp_p def test_get_live_data_directory_returns_none_when_selected_path_is_missing(tmp_path, monkeypatch): + """Return None when the environment-selected live-data directory is + absent.""" live_data_service_module, _ = reload_live_data_modules(monkeypatch, production=False) (tmp_path / "GENERIC" / "livereduce" / "LOQ").mkdir(parents=True) @@ -43,6 +49,7 @@ def test_get_live_data_directory_returns_none_when_selected_path_is_missing(tmp_ @pytest.mark.asyncio async def test_get_live_data_files_validates_staging_base_path(tmp_path, monkeypatch): + """Validate listed files against the staging live-data base path.""" _, live_data_router_module = reload_live_data_modules(monkeypatch, production=False) live_data_path = tmp_path / "GENERIC-staging" / "livereduce" / "LOQ" live_data_path.mkdir(parents=True) @@ -61,6 +68,7 @@ async def test_get_live_data_files_validates_staging_base_path(tmp_path, monkeyp @pytest.mark.asyncio async def test_live_data_validates_production_base_path(tmp_path, monkeypatch): + """Validate streamed live-data events against the production base path.""" _, live_data_router_module = reload_live_data_modules(monkeypatch, production=True) live_data_path = tmp_path / "GENERIC" / "livereduce" / "LOQ" live_data_path.mkdir(parents=True) From f04cc6bedf34e9dcf4f8e0416df565036d480e01 Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 30 Apr 2026 09:09:45 +0000 Subject: [PATCH 4/4] Formatting and linting commit --- test/test_live_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_live_data.py b/test/test_live_data.py index db63804..ce18982 100644 --- a/test/test_live_data.py +++ b/test/test_live_data.py @@ -4,7 +4,7 @@ import pytest import plotting_service.routers.live_data as live_data_router -import plotting_service.services.live_data_service as live_data_service +from plotting_service.services import live_data_service def reload_live_data_modules(monkeypatch: pytest.MonkeyPatch, production: bool | None) -> tuple[object, object]: