Repository: MannLabs/alphakraken
Pull request #4: Disentangle paths
Author: login: mschwoer
Labels: name: code-review
Created: 2025-04-23T09:14:28Z | Merged: 2025-04-23T12:30:50Z

Move (almost) all path information to the yaml file.


Diff:
diff --git a/airflow_src/dags/impl/handler_impl.py b/airflow_src/dags/impl/handler_impl.py
index 7af38e41..d7ff2ac2 100644
--- a/airflow_src/dags/impl/handler_impl.py
+++ b/airflow_src/dags/impl/handler_impl.py
@@ -23,10 +23,10 @@
 )
 from common.settings import (
     get_instrument_settings,
+    get_path,
 )
 from common.utils import (
     get_airflow_variable,
-    get_env_variable,
     get_xcom,
     trigger_dag_run,
 )
@@ -38,7 +38,7 @@
 from shared.keys import (
     ALLOWED_CHARACTERS_IN_RAW_FILE_NAME,
     DDA_FLAG_IN_RAW_FILE_NAME,
-    EnvVars,
+    Locations,
 )
 
 
@@ -70,9 +70,7 @@ def copy_raw_file(ti: TaskInstance, **kwargs) -> None:
 
     file_info = _get_file_info(copied_files)
 
-    pool_base_path = Path(get_env_variable(EnvVars.POOL_BASE_PATH))
-    backup_pool_folder = get_env_variable(EnvVars.BACKUP_POOL_FOLDER)
-    backup_base_path = pool_base_path / backup_pool_folder
+    backup_base_path = get_path(Locations.BACKUP)
 
     # a bit hacky to get the file size once again, but it's a cheap operation and avoids complicate logic
     # TODO: in rare cases (manual intervention) this could yield to inconsistencies, change this!
diff --git a/airflow_src/dags/impl/processor_impl.py b/airflow_src/dags/impl/processor_impl.py
index 02aba4c2..0bcf6264 100644
--- a/airflow_src/dags/impl/processor_impl.py
+++ b/airflow_src/dags/impl/processor_impl.py
@@ -25,10 +25,9 @@
     get_internal_output_path_for_raw_file,
     get_output_folder_rel_path,
 )
-from common.settings import get_fallback_project_id
+from common.settings import get_fallback_project_id, get_path
 from common.utils import (
     get_airflow_variable,
-    get_env_variable,
     get_xcom,
     put_xcom,
 )
@@ -46,7 +45,7 @@
     update_raw_file,
 )
 from shared.db.models import RawFile, RawFileStatus, get_created_at_year_month
-from shared.keys import EnvVars
+from shared.keys import Locations
 
 
 def _get_project_id_or_fallback(project_id: str | None, instrument_id: str) -> str:
@@ -78,30 +77,18 @@ def prepare_quanting(ti: TaskInstance, **kwargs) -> None:
                 f"No active settings found for project id '{project_id_or_fallback}'. Please add settings in the WebApp."
             )
 
-    pool_base_path = Path(get_env_variable(EnvVars.POOL_BASE_PATH))
-
     # get raw file path
-    backup_pool_folder = get_env_variable(EnvVars.BACKUP_POOL_FOLDER)
+    backup_base_path = get_path(Locations.BACKUP)
     year_month_subfolder = get_created_at_year_month(raw_file)
     raw_file_path = (
-        pool_base_path
-        / backup_pool_folder
-        / instrument_id
-        / year_month_subfolder
-        / raw_file_id
+        backup_base_path / instrument_id / year_month_subfolder / raw_file_id
     )
 
     # get settings and output_path
-    quanting_pool_folder = get_env_variable(EnvVars.QUANTING_POOL_FOLDER)
-
-    settings_path = (
-        pool_base_path / quanting_pool_folder / "settings" / project_id_or_fallback
-    )
+    settings_path = get_path(Locations.SETTINGS) / project_id_or_fallback
 
-    output_path = (
-        pool_base_path
-        / quanting_pool_folder
-        / get_output_folder_rel_path(raw_file, project_id_or_fallback)
+    output_path = get_path(Locations.OUTPUT) / get_output_folder_rel_path(
+        raw_file, project_id_or_fallback
     )
 
     quanting_env = {
diff --git a/airflow_src/plugins/common/constants.py b/airflow_src/plugins/common/constants.py
index a3fddd42..d20f668a 100644
--- a/airflow_src/plugins/common/constants.py
+++ b/airflow_src/plugins/common/constants.py
@@ -7,7 +7,7 @@
 CLUSTER_SSH_CONNECTION_TIMEOUT = 60
 CLUSTER_SSH_COMMAND_TIMEOUT = 60
 
-# these are relative to the SLURM_BASE_DIR environment variable
+# these are relative to the locations.general.slurm_base_path variable
 CLUSTER_JOB_SCRIPT_NAME = "submit_job.sh"
 CLUSTER_WORKING_DIR_NAME = "jobs"
 
diff --git a/airflow_src/plugins/common/paths.py b/airflow_src/plugins/common/paths.py
index c22bc6d7..c81dcbc2 100644
--- a/airflow_src/plugins/common/paths.py
+++ b/airflow_src/plugins/common/paths.py
@@ -37,21 +37,20 @@ def get_internal_backup_path_for_instrument(
 
 
 def get_output_folder_rel_path(raw_file: RawFile, project_id_or_fallback: str) -> Path:
-    """Get the relative path of the output directory for given raw file name.
+    """Get the path of the output directory for given raw file name relative to the `output` folder.
 
     Only if the raw_file has no project defined, we use a month-specific subfolder
     This is to avoid having too many files in the fallback output folders.
 
     E.g.
-        output/<project_id_or_fallback>/2024_07/out_RAW-FILE-1.raw in case raw_file has no project ID
-        output/<project_id_or_fallback>/out_RAW-FILE-1.raw in case raw_file has a project ID
+        <project_id_or_fallback>/2024_07/out_RAW-FILE-1.raw in case raw_file has no project ID
+        <project_id_or_fallback>/out_RAW-FILE-1.raw in case raw_file has a project ID
     """
     optional_sub_folder = (
         get_created_at_year_month(raw_file) if raw_file.project_id is None else ""
     )
     return (
-        Path(InternalPaths.OUTPUT)
-        / project_id_or_fallback
+        Path(project_id_or_fallback)
         / optional_sub_folder
         / f"{OUTPUT_FOLDER_PREFIX}{raw_file.id}"
     )
@@ -66,6 +65,8 @@ def get_internal_output_path_for_raw_file(
     raw_file: RawFile, project_id_or_fallback: str
 ) -> Path:
     """Get absolute internal output path for the given raw file name."""
-    return Path(InternalPaths.MOUNTS_PATH) / get_output_folder_rel_path(
-        raw_file, project_id_or_fallback
+    return (
+        Path(InternalPaths.MOUNTS_PATH)
+        / InternalPaths.OUTPUT
+        / get_output_folder_rel_path(raw_file, project_id_or_fallback)
     )
diff --git a/airflow_src/plugins/common/settings.py b/airflow_src/plugins/common/settings.py
index 64c3cd93..587012ec 100644
--- a/airflow_src/plugins/common/settings.py
+++ b/airflow_src/plugins/common/settings.py
@@ -110,6 +110,18 @@ def _load_alphakraken_yaml(env_name: str) -> dict[str, dict[str, Any]]:
 _INSTRUMENTS = _SETTINGS["instruments"].copy()
 
 
+def get_path(path_key: str) -> Path:
+    """Get a certain path from the yaml settings."""
+    path = _SETTINGS.get("locations", {}).get(path_key, {}).get("absolute_path")
+
+    if path is None:
+        raise KeyError(
+            f"Key `{path_key}` or `{path_key}.absolute_path` not found in alphakraken.yaml."
+        )
+
+    return Path(path)
+
+
 def get_instrument_ids() -> list[str]:
     """Get all IDs for all instruments."""
     return list(_INSTRUMENTS.keys())
diff --git a/airflow_src/plugins/jobs/job_handler.py b/airflow_src/plugins/jobs/job_handler.py
index 5f07d431..b08c78bb 100644
--- a/airflow_src/plugins/jobs/job_handler.py
+++ b/airflow_src/plugins/jobs/job_handler.py
@@ -7,15 +7,13 @@
 import abc
 import logging
 from datetime import datetime
-from pathlib import Path
 
 from airflow.exceptions import AirflowFailException
 from common.constants import CLUSTER_JOB_SCRIPT_NAME, CLUSTER_WORKING_DIR_NAME
-from common.settings import _SETTINGS
-from common.utils import get_env_variable
+from common.settings import _SETTINGS, get_path
 from sensors.ssh_utils import ssh_execute
 
-from shared.keys import EnvVars
+from shared.keys import Locations
 
 # TODO: move to settings, introduce constants
 ENGINE: str = _SETTINGS.get("general", {}).get("job_engine", {}).get("type", "slurm")
@@ -72,7 +70,7 @@ class SlurmSSHJobHandler(JobHandler):
     def __init__(self):
         """Initialize the Slurm job handler."""
         super().__init__()
-        cluster_base_dir = Path(get_env_variable(EnvVars.SLURM_BASE_DIR))
+        cluster_base_dir = get_path(Locations.SLURM)
         self._cluster_working_dir_path = cluster_base_dir / CLUSTER_WORKING_DIR_NAME
         self._cluster_job_script_path = cluster_base_dir / CLUSTER_JOB_SCRIPT_NAME
 
diff --git a/airflow_src/plugins/sensors/ssh_utils.py b/airflow_src/plugins/sensors/ssh_utils.py
index 6dd99125..1fc04daa 100644
--- a/airflow_src/plugins/sensors/ssh_utils.py
+++ b/airflow_src/plugins/sensors/ssh_utils.py
@@ -35,7 +35,8 @@ def ssh_execute(
     call_count = 0
     while exit_status != 0 or agg_stdout in [b"", b"\n"]:
         if call_count >= max_tries:
-            raise AirflowFailException(f"Too many calls to ssh_execute: {command=}")
+            logging.error(f"Execution of SSH command failed too often: {command=}")
+            raise AirflowFailException("Execution of SSH command failed too often.")
 
         sleep(30 * call_count)  # no sleep in the first iteration
         call_count += 1
diff --git a/airflow_src/tests/common/test_paths.py b/airflow_src/tests/common/test_paths.py
index f7e3c220..bc0b0e9f 100644
--- a/airflow_src/tests/common/test_paths.py
+++ b/airflow_src/tests/common/test_paths.py
@@ -22,7 +22,7 @@ def test_get_output_folder_rel_path_no_fallback() -> None:
     # when
     result = get_output_folder_rel_path(mock_raw_file, "some_project_id")
 
-    assert result == Path("output/some_project_id/out_some_file.raw")
+    assert result == Path("some_project_id/out_some_file.raw")
 
 
 def test_get_output_folder_rel_path_fallback() -> None:
@@ -37,4 +37,4 @@ def test_get_output_folder_rel_path_fallback() -> None:
     # when
     result = get_output_folder_rel_path(mock_raw_file, "some_fallback_id")
 
-    assert result == Path("output/some_fallback_id/1970_01/out_some_file.raw")
+    assert result == Path("some_fallback_id/1970_01/out_some_file.raw")
diff --git a/airflow_src/tests/common/test_settings.py b/airflow_src/tests/common/test_settings.py
index 53e90eea..deba42a4 100644
--- a/airflow_src/tests/common/test_settings.py
+++ b/airflow_src/tests/common/test_settings.py
@@ -1,9 +1,10 @@
 """Tests for the settings module."""
 
+from pathlib import Path
 from unittest.mock import MagicMock, mock_open, patch
 
 import pytest
-from common.settings import _load_alphakraken_yaml, get_instrument_settings
+from common.settings import _load_alphakraken_yaml, get_instrument_settings, get_path
 
 
 @patch("common.settings.Path")
@@ -82,6 +83,41 @@ def test_get_instrument_settings_raises_key_error_for_non_existing_key() -> None
         get_instrument_settings("instrument1", "key2")
 
 
+def test_get_path_returns_setting_for_existing_instrument_and_key() -> None:
+    """Test that correct path is returned."""
+    with (
+        patch(
+            "common.settings._SETTINGS",
+            {"locations": {"backup": {"absolute_path": "some_path"}}},
+        ),
+    ):
+        assert get_path("backup") == Path("some_path")
+
+
+def test_get_path_returns_setting_raises_key_error_for_non_exisiting_key_1() -> None:
+    """Test that a KeyError is raised if the key does not exist."""
+    with (
+        patch(
+            "common.settings._SETTINGS",
+            {"locations": {"backup": {"absolute_path": "some_path"}}},
+        ),
+        pytest.raises(KeyError),
+    ):
+        get_path("Xbackup")
+
+
+def test_get_path_returns_setting_raises_key_error_for_non_exisiting_key_2() -> None:
+    """Test that a KeyError is raised if the key does not exist."""
+    with (
+        patch(
+            "common.settings._SETTINGS",
+            {"locations": {"backup": {"Xabsolute_path": "some_path"}}},
+        ),
+        pytest.raises(KeyError),
+    ):
+        get_path("backup")
+
+
 def test_get_instrument_settings_raises_key_error_for_non_existing_instrument() -> None:
     """Test that a KeyError is raised if the instrument does not exist in the instrument settings."""
     with (
diff --git a/airflow_src/tests/dags/impl/test_handler_impl.py b/airflow_src/tests/dags/impl/test_handler_impl.py
index 261af0b7..de375686 100644
--- a/airflow_src/tests/dags/impl/test_handler_impl.py
+++ b/airflow_src/tests/dags/impl/test_handler_impl.py
@@ -1,6 +1,5 @@
 """Unit tests for handler_impl.py."""
 
-import os
 from pathlib import Path
 from unittest.mock import MagicMock, Mock, call, patch
 
@@ -18,20 +17,18 @@
 from shared.db.models import RawFileStatus
 
 
-@patch.dict(
-    os.environ,
-    {"POOL_BASE_PATH": "/path/to/pool", "BACKUP_POOL_FOLDER": "some_backup_folder"},
-)
 @patch("dags.impl.handler_impl.get_raw_file_by_id")
+@patch("dags.impl.handler_impl.get_path")
 @patch("dags.impl.handler_impl.copy_file")
 @patch("dags.impl.handler_impl.RawFileWrapperFactory")
 @patch("dags.impl.handler_impl.get_file_size")
 @patch("dags.impl.handler_impl.update_raw_file")
-def test_copy_raw_file_calls_update_with_correct_args(
+def test_copy_raw_file_calls_update_with_correct_args(  # noqa: PLR0913
     mock_update_raw_file: MagicMock,
     mock_get_file_size: MagicMock,
     mock_raw_file_wrapper_factory: MagicMock,
     mock_copy_file: MagicMock,
+    mock_get_path_setting: MagicMock,
     mock_get_raw_file_by_id: MagicMock,
 ) -> None:
     """Test copy_raw_file calls update with correct arguments."""
@@ -42,6 +39,8 @@ def test_copy_raw_file_calls_update_with_correct_args(
     mock_raw_file = MagicMock()
     mock_get_raw_file_by_id.return_value = mock_raw_file
 
+    mock_get_path_setting.return_value = "some_backup_folder"
+
     mock_get_file_size.return_value = 1000
     mock_raw_file_wrapper_factory.create_write_wrapper.return_value.get_files_to_copy.return_value = {
         Path("/path/to/instrument/test_file.raw"): Path(
@@ -75,18 +74,16 @@ def test_copy_raw_file_calls_update_with_correct_args(
                         "some_hash",
                     )
                 },
-                backup_base_path="/path/to/pool/some_backup_folder",
+                backup_base_path="some_backup_folder",
             ),
         ]
     )
     mock_get_file_size.assert_called_once_with(mock_file_path_to_calculate_size, -1)
+    mock_get_path_setting.assert_called_once_with("backup")
 
 
-@patch.dict(
-    os.environ,
-    {"POOL_BASE_PATH": "/path/to/pool", "BACKUP_POOL_FOLDER": "some_backup_folder"},
-)
 @patch("dags.impl.handler_impl.get_raw_file_by_id")
+@patch("dags.impl.handler_impl.get_path")
 @patch("dags.impl.handler_impl.copy_file")
 @patch("dags.impl.handler_impl.RawFileWrapperFactory")
 @patch("dags.impl.handler_impl.get_airflow_variable", return_value="test_file.raw")
@@ -98,6 +95,7 @@ def test_copy_raw_file_calls_update_with_correct_args_overwrite(  # noqa: PLR091
     mock_get_airflow_variable: MagicMock,
     mock_raw_file_wrapper_factory: MagicMock,
     mock_copy_file: MagicMock,
+    mock_get_path: MagicMock,
     mock_get_raw_file_by_id: MagicMock,
 ) -> None:
     """Test copy_raw_file calls update with correct arguments in case overwrite is requested."""
@@ -109,6 +107,8 @@ def test_copy_raw_file_calls_update_with_correct_args_overwrite(  # noqa: PLR091
     mock_raw_file.id = "test_file.raw"
     mock_get_raw_file_by_id.return_value = mock_raw_file
 
+    mock_get_path.return_value = "some_backup_folder"
+
     mock_raw_file_wrapper_factory.create_write_wrapper.return_value.get_files_to_copy.return_value = {
         Path("/path/to/instrument/test_file.raw"): Path(
             "/opt/airflow/mounts/backup/test_file.raw"
diff --git a/airflow_src/tests/dags/impl/test_processor_impl.py b/airflow_src/tests/dags/impl/test_processor_impl.py
index 24f2d90e..3791455f 100644
--- a/airflow_src/tests/dags/impl/test_processor_impl.py
+++ b/airflow_src/tests/dags/impl/test_processor_impl.py
@@ -1,6 +1,5 @@
 """Tests for the processor_impl module."""
 
-import os
 from datetime import datetime
 from pathlib import Path
 from unittest.mock import MagicMock, call, mock_open, patch
@@ -59,15 +58,8 @@ def test_get_project_id_for_raw_file_fallback_bruker() -> None:
 
 
 @patch.dict(_INSTRUMENTS, {"instrument1": {}})
-@patch.dict(
-    os.environ,
-    {
-        "POOL_BASE_PATH": "/pool/path/to",
-        "QUANTING_POOL_FOLDER": "some_quanting_pool_folder",
-        "BACKUP_POOL_FOLDER": "some_backup_pool_folder",
-    },
-)
 @patch("dags.impl.processor_impl.get_raw_file_by_id")
+@patch("dags.impl.processor_impl.get_path")
 @patch("dags.impl.processor_impl.put_xcom")
 @patch("dags.impl.processor_impl._get_project_id_or_fallback")
 @patch("dags.impl.processor_impl.get_settings_for_project")
@@ -75,6 +67,7 @@ def test_prepare_quanting(
     mock_get_settings: MagicMock,
     mock_get_project_id_for_raw_file: MagicMock,
     mock_put_xcom: MagicMock,
+    mock_get_path: MagicMock,
     mock_get_raw_file_by_id: MagicMock,
 ) -> None:
     """Test that prepare_quanting makes the expected calls."""
@@ -85,7 +78,11 @@ def test_prepare_quanting(
         project_id="some_project_id",
     )
     mock_get_raw_file_by_id.return_value = mock_raw_file
-
+    mock_get_path.side_effect = [
+        Path("/some_backup_base_path"),
+        Path("/some_quanting_settings_path"),
+        Path("/some_quanting_output_path"),
+    ]
     mock_get_project_id_for_raw_file.return_value = "some_project_id"
     mock_get_settings.return_value = MagicMock(
         speclib_file_name="some_speclib_file_name",
@@ -113,9 +110,9 @@ def test_prepare_quanting(
 
     # when you adapt something here, don't forget to adapt also the submit_job.sh script
     expected_quanting_env = {
-        "RAW_FILE_PATH": "/pool/path/to/some_backup_pool_folder/instrument1/1970_01/test_file.raw",
-        "SETTINGS_PATH": "/pool/path/to/some_quanting_pool_folder/settings/some_project_id",
-        "OUTPUT_PATH": "/pool/path/to/some_quanting_pool_folder/output/some_project_id/out_test_file.raw",
+        "RAW_FILE_PATH": "/some_backup_base_path/instrument1/1970_01/test_file.raw",
+        "SETTINGS_PATH": "/some_quanting_settings_path/some_project_id",
+        "OUTPUT_PATH": "/some_quanting_output_path/some_project_id/out_test_file.raw",
         "SPECLIB_FILE_NAME": "some_speclib_file_name",
         "FASTA_FILE_NAME": "some_fasta_file_name",
         "CONFIG_FILE_NAME": "some_config_file_name",
@@ -132,6 +129,7 @@ def test_prepare_quanting(
         ]
     )
     mock_get_raw_file_by_id.assert_called_once_with("test_file.raw")
+    mock_get_path.assert_has_calls([call("backup"), call("settings"), call("output")])
 
 
 @patch("dags.impl.processor_impl.get_raw_file_by_id")
diff --git a/airflow_src/tests/plugins/jobs/test_job_handler.py b/airflow_src/tests/plugins/jobs/test_job_handler.py
index 835eb4d2..c7064609 100644
--- a/airflow_src/tests/plugins/jobs/test_job_handler.py
+++ b/airflow_src/tests/plugins/jobs/test_job_handler.py
@@ -1,6 +1,6 @@
 """Tests for the job_handler module."""
 
-import os
+from pathlib import Path
 from unittest.mock import MagicMock, patch
 
 import pytest
@@ -8,39 +8,48 @@
 from jobs.job_handler import SlurmSSHJobHandler
 
 
-@patch.dict(os.environ, {"SLURM_BASE_DIR": "/path/to/slurm_base_dir"})
+@patch("jobs.job_handler.get_path")
 @patch("jobs.job_handler.ssh_execute")
-def test_start_job_returns_valid_job_id(mock_ssh_execute: MagicMock) -> None:
+def test_start_job_returns_valid_job_id(
+    mock_ssh_execute: MagicMock, mock_get_path: MagicMock
+) -> None:
     """Test that start_job returns a valid job ID."""
+    mock_get_path.return_value = Path("/path/to/slurm_base_path")
     mock_ssh_execute.return_value = "12345"
     # when
     job_id = SlurmSSHJobHandler().start_job({"ENV_VAR": "value"}, "2024_07")
     assert job_id == "12345"
     expected_command = (
         "export ENV_VAR=value\n"
-        "mkdir -p /path/to/slurm_base_dir/jobs/2024_07\n"
-        "cd /path/to/slurm_base_dir/jobs/2024_07\n"
-        "cat /path/to/slurm_base_dir/submit_job.sh\n"
-        "JID=$(sbatch /path/to/slurm_base_dir/submit_job.sh)\n"
+        "mkdir -p /path/to/slurm_base_path/jobs/2024_07\n"
+        "cd /path/to/slurm_base_path/jobs/2024_07\n"
+        "cat /path/to/slurm_base_path/submit_job.sh\n"
+        "JID=$(sbatch /path/to/slurm_base_path/submit_job.sh)\n"
         "echo ${JID##* }"
     )
     mock_ssh_execute.assert_called_once_with(expected_command)
 
 
-@patch.dict(os.environ, {"SLURM_BASE_DIR": "/path/to/slurm_base_dir"})
+@patch("jobs.job_handler.get_path")
 @patch("jobs.job_handler.ssh_execute")
-def test_start_job_handles_invalid_job_id(mock_ssh_execute: MagicMock) -> None:
+def test_start_job_handles_invalid_job_id(
+    mock_ssh_execute: MagicMock, mock_get_path: MagicMock
+) -> None:
     """Test that start_job raises an exception when the job ID is invalid."""
+    mock_get_path.return_value = Path("/path/to/slurm_base_path")
     mock_ssh_execute.return_value = "invalid_id"
 
     with pytest.raises(AirflowFailException, match="Job submission failed."):
         SlurmSSHJobHandler().start_job({"ENV_VAR": "value"}, "2024_07")
 
 
-@patch.dict(os.environ, {"SLURM_BASE_DIR": "/path/to/slurm_base_dir"})
+@patch("jobs.job_handler.get_path")
 @patch("jobs.job_handler.ssh_execute")
-def test_get_job_status_returns_correctly(mock_ssh_execute: MagicMock) -> None:
+def test_get_job_status_returns_correctly(
+    mock_ssh_execute: MagicMock, mock_get_path: MagicMock
+) -> None:
     """Test that get_job_status returns the correct status."""
+    mock_get_path.return_value = Path("/path/to/slurm_base_path")
     mock_ssh_execute.return_value = "COMPLETED"
 
     job_status = SlurmSSHJobHandler().get_job_status("12345")
@@ -51,12 +60,13 @@ def test_get_job_status_returns_correctly(mock_ssh_execute: MagicMock) -> None:
     mock_ssh_execute.assert_called_once_with(expected_command)
 
 
-@patch.dict(os.environ, {"SLURM_BASE_DIR": "/path/to/slurm_base_dir"})
+@patch("jobs.job_handler.get_path")
 @patch("jobs.job_handler.ssh_execute")
 def test_get_job_result_returns_correct_job_status_and_time_elapsed(
-    mock_ssh_execute: MagicMock,
+    mock_ssh_execute: MagicMock, mock_get_path: MagicMock
 ) -> None:
     """Test that get_job_result returns the correct job status and time elapsed."""
+    mock_get_path.return_value = Path("/path/to/slurm_base_path")
     mock_ssh_execute.return_value = "00:08:42\nCOMPLETED"
 
     # when
@@ -66,7 +76,7 @@ def test_get_job_result_returns_correct_job_status_and_time_elapsed(
     expected_command = (
         "TIME_ELAPSED=$(sacct --format=Elapsed -j 12345 | tail -n 1); echo $TIME_ELAPSED\n"
         "sacct -l -j 12345\n"
-        "cat /path/to/slurm_base_dir/jobs/*/slurm-12345.out\n"
+        "cat /path/to/slurm_base_path/jobs/*/slurm-12345.out\n"
         "ST=$(sacct -j 12345 -o State | awk 'FNR == 3 {print $1}')\n"
         "echo $ST"
     )
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 6075509e..85074fda 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -55,10 +55,6 @@ x-airflow-common: &airflow-common
     MONGO_USER: ${MONGO_USER:?error}
     MONGO_PASSWORD: ${MONGO_PASSWORD:?error}
     MONGO_PORT: ${MONGO_PORT:?error}
-    POOL_BASE_PATH: ${POOL_BASE_PATH:?error}
-    BACKUP_POOL_FOLDER: ${BACKUP_POOL_FOLDER:?error}
-    QUANTING_POOL_FOLDER: ${QUANTING_POOL_FOLDER:?error}
-    SLURM_BASE_DIR: ${SLURM_BASE_DIR:?error}
     SLACK_WEBHOOK_URL: ${SLACK_WEBHOOK_URL:?error}
     WEBAPP_URL: ${WEBAPP_URL:?error}
   volumes:
diff --git a/docs/deployment.md b/docs/deployment.md
index bf7029f3..db516f5a 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -155,7 +155,7 @@ In case you want to set up a URL redirect from one PC to one or multiple others,
 ```bash
 mkdir ~/slurm
 ```
-and set the `SLURM_BASE_DIR` environmental variable in `envs/${ENV}.env` to this value.
+and set the `locations.slurm.absolute_path` key in `envs/alphakraken.${ENV}.yaml` to this value.
 3. Copy the cluster run script `submit_job.sh` to `~/slurm` and adapt the `partition` (and optionally `nodelist`) directives.
 Make sure to update also this file when deploying a new version of the AlphaKraken.
 
@@ -163,20 +163,22 @@ Make sure to update also this file when deploying a new version of the AlphaKrak
 
 ### General note on how Kraken gets to know the data
 
-Each worker needs two 'views' on the pool backup and output data.
+Each worker needs two 'views' on the raw and output data.
 
-The first view enables read/write access,
+The first view ("worker PC view") enables read/write access,
 by mounting on the Kraken host PC a specific (network) folder (e.g. `\\pool-backup\pool-backup` or `\\pool-output\pool-output`)
 using `cifs` mounts (wrapped by `mount.sh`)
 to a target folder and then mapping this target folder to a worker container
-in `docker-compose.yaml`.
+in `docker-compose.yaml`, such that it can be accessed in a unified manner from within the containers (cf. `InternalPaths`).
 
-The second view is the location of the data as seen from the cluster
+The second view ("cluster view") is the location of the data on the shared filesystem as seen from the Slurm cluster
 (e.g. `/fs/pool/pool-backup` or `/fs/pool/pool-output`),
 which is required to set the paths for the cluster jobs correctly.
 
 For instruments, only the first type of view is required, as the cluster does not access the instruments directly.
 
+All paths are configured in the `locations` section of the `envs/alphakraken.${ENV}.yaml` file (see comments in `alphakraken.local.yaml`
+for details).
 
 ### Set up pool bind mounts
 The workers need bind mounts set up to the pool filesystems for backup and reading AlphaDIA output data.
@@ -191,9 +193,8 @@ sudo apt install cifs-utils
 
 1. Create folders `settings`, `output`, and `airflow_logs` in the desired pool location(s), e.g. under `/fs/pool/pool-alphakraken`.
 
-2. Make sure the variables `MOUNTS_PATH`, `BACKUP_POOL_FOLDER` `QUANTING_POOL_FOLDER`,
-in the `envs/${ENV}.env` file are set to the desired values.
-Set them also in the `envs/alphakraken.${ENV}.yaml` file (section: `locations`).
+2. Make sure the variable `MOUNTS_PATH` in the `envs/${ENV}.env` file is set correctly.
+Set them also in the `envs/alphakraken.${ENV}.yaml` file (`locations.general.mounts_path`).
 
 3. Mount the backup, output and logs folder. You will be asked for passwords.
 ```bash
@@ -256,7 +257,7 @@ The following files need to be edited to customize your deployment:
 - `envs/.env-airflow`: set the current user as the user within the airflow containers
 - `envs/.env-mongo`: set the MongoDB root password
 - `envs/${ENV}.env`: set the environment variables for the basic wiring of components
-- `envs/alphakraken.${ENV}.yaml`: add a configuration for each instrument
+- `envs/alphakraken.${ENV}.yaml`: set up the paths and add a configuration for each instrument
 - `docker-compose.yaml`: add a worker for each instrument
 - `airflow_src/plugins/cluster_scripts/submit_job.sh` (cluster-local copy): configure partition and nodelist
 
diff --git a/envs/alphakraken.local.yaml b/envs/alphakraken.local.yaml
index 684834e1..f27c0a12 100644
--- a/envs/alphakraken.local.yaml
+++ b/envs/alphakraken.local.yaml
@@ -5,7 +5,7 @@ instruments:
     # the following is required for mounting only (read by mount.sh):
     username: user
     mount_src: //0.0.0.0/test1/
-    mount_target: instruments/test1
+    mount_target: instruments/test1 # DO NOT CHANGE THE 'instruments' part -> InternalPaths.INSTRUMENTS
     # skip_quanting: true  # optional, default: false
     # min_free_space_gb: 100  # optional, default: taken from Airflow variable MIN_FREE_SPACE_GB. Set to -1 to exclude from file removing.
     # file_move_delay_m: 60  # optional, default: 15. Set to -1 to disable file moving.
@@ -15,22 +15,35 @@ instruments:
     type: sciex
 
 locations:
-  # the following is required for mounting only (read by mount.sh):
   general:
-    # cf. also the variable MOUNTS_PATH .env/production.env (sandbox.env)
     mounts_path: /home/kraken-user/alphakraken/mounts
+  # e.g.:
+  #  xxx:
+  #    username: user  # username for mounting
+  #    mount_src: //mount_src/xxx  # source path for mounting ("worker pc view" part 1)
+  #    mount_target: xxx # DO NOT CHANGE! this is related to the file structure within the containers -> cf. InternalPaths ("worker pc view" part 2)
+  #    absolute_path: /fs/pool/pool-0/alphakraken_sandbox/xxx  # this is the absolute path of this folder on the shared file system ("cluster view")
   backup:
     username: user
     mount_src: //mount_src/backup
-    mount_target: backup
+    mount_target: backup # DO NOT CHANGE! relative to mounts_path -> InternalPaths.BACKUP
+    absolute_path: /fs/pool/pool-0/alphakraken_sandbox/backup
+  settings:
+    # no need for mount information (yet)
+    absolute_path: /fs/pool/pool-0/alphakraken_sandbox/settings
+  slurm:
+    # location of slurm submit script and destination for slurm logs
+    # no need for mount information (yet)
+    absolute_path: /fs/pool/pool-0/alphakraken_local/slurm
   output:
     username: user
     mount_src: //mount_src/output
-    mount_target: output
+    mount_target: output # DO NOT CHANGE! relative to mounts_path -> InternalPaths.OUTPUT
+    absolute_path: /fs/pool/pool-0/alphakraken_sandbox/output
   logs:
     username: user
     mount_src: //mount_src/airflow_logs
-    mount_target: airflow_logs
+    mount_target: airflow_logs # DO NOT CHANGE! relative to mounts_path ->/opt/airflow/logs
 
 general:
   job_engine:
diff --git a/envs/alphakraken.production.yaml b/envs/alphakraken.production.yaml
index 1803c0e8..404fdba5 100644
--- a/envs/alphakraken.production.yaml
+++ b/envs/alphakraken.production.yaml
@@ -1,24 +1,30 @@
 # Configuration file for alphakraken: instruments and mounts (environment: production)
+# see `alphakraken.local.yaml` for a commented version
 instruments:
   test1:
-    type: thermo # Needs to match one of the values defined in common.keys.InstrumentTypes (thermo, bruker, sciex).
-    # the following is required for mounting only (read by mount.sh):
+    type: thermo
     username: user
     mount_src: //0.0.0.0/test1/
     mount_target: instruments/test1
 locations:
-  # cf. also the PATHS section in .env/production.env
   general:
     mounts_path: /home/kraken-user/alphakraken/production/mounts
   backup:
     username: kraken-write
     mount_src: //samba-pool-1/pool-1
     mount_target: backup
+    absolute_path: /fs/pool-1/backup
+  settings:
+    absolute_path: /fs/pool-2/settings
+  slurm:
+    absolute_path: /fs/pool-2/slurm
   output:
     username: kraken-write
     mount_src: //samba-pool-2/pool-2/output
     mount_target: output
+    absolute_path: /fs/pool-2/output
   logs:
     username: kraken-write
     mount_src: //samba-pool-2/pool-2/airflow_logs
     mount_target: airflow_logs
+    absolute_path: /fs/pool-2/airflow_logs
diff --git a/envs/alphakraken.sandbox.yaml b/envs/alphakraken.sandbox.yaml
index e2b76708..51a0f4a0 100644
--- a/envs/alphakraken.sandbox.yaml
+++ b/envs/alphakraken.sandbox.yaml
@@ -1,25 +1,30 @@
 # Configuration file for alphakraken: instruments and mounts (environment: sandbox)
+# see `alphakraken.local.yaml` for a commented version
 instruments:
   test1:
-    type: thermo # Needs to match one of the values defined in common.keys.InstrumentTypes (thermo, bruker, sciex).
-    # the following is required for mounting only (read by mount.sh):
+    type: thermo
     username: user
     mount_src: //0.0.0.0/test1/
     mount_target: instruments/test1
 locations:
-  # cf. also the PATHS section in .env/sandbox.env
   general:
-    # cf. also the variable MOUNTS_PATH .env/production.env (sandbox.env)
     mounts_path: /home/kraken-user/alphakraken/sandbox/mounts
   backup:
     username: kraken-write
     mount_src: //samba-pool-0/pool-0/alphakraken_sandbox/backup
     mount_target: backup
+    absolute_path: /fs/pool-0/alphakraken_sandbox/backup
+  settings:
+    absolute_path: /fs/pool-0/alphakraken_sandbox/settings
+  slurm:
+    absolute_path: /fs/pool-0/alphakraken_sandbox/slurm
   output:
     username: kraken-write
     mount_src: //samba-pool-0/pool-0/alphakraken_sandbox/output
     mount_target: output
+    absolute_path: /fs/pool-0/alphakraken_sandbox/output
   logs:
     username: kraken-write
     mount_src: //samba-pool-0/pool-0/alphakraken_sandbox/airflow_logs
     mount_target: airflow_logs
+    absolute_path: /fs/pool-0/alphakraken_sandbox/airflow_logs
diff --git a/envs/local.env b/envs/local.env
index 90f47a60..b4b15e87 100644
--- a/envs/local.env
+++ b/envs/local.env
@@ -33,11 +33,5 @@ SLACK_WEBHOOK_URL=https://hooks.slack.com/services/<something>
 
 WEBAPP_URL=http://localhost:8501
 
+# all mounts (backup pool folders, results pool folders, instruments) need to be available in MOUNTS_PATH, cf. Readme
 MOUNTS_PATH=./local_test
-
-# the following are required only if you want to feed the cluster from local:
-POOL_BASE_PATH=/fs/pool
-QUANTING_POOL_FOLDER=pool-0/alphakraken_local
-BACKUP_POOL_FOLDER=pool-0/alphakraken_local
-
-SLURM_BASE_DIR=/fs/pool/pool-0/alphakraken_local/slurm
diff --git a/envs/production.env b/envs/production.env
index 2a881e20..1a916be5 100644
--- a/envs/production.env
+++ b/envs/production.env
@@ -33,22 +33,5 @@ SLACK_WEBHOOK_URL=https://hooks.slack.com/services/<something>
 
 WEBAPP_URL=<webapp_url>
 
-############################# PATHS #############################
-# the following paths need also to be set in alphakraken.production.yaml->locations
-
 # all mounts (backup pool folders, results pool folders, instruments) need to be available in MOUNTS_PATH, cf. Readme
 MOUNTS_PATH=/home/kraken-user/alphakraken/production/mounts
-
-# location of BACKUP_POOL_FOLDER & QUANTING_POOL_FOLDER
-POOL_BASE_PATH=/fs/pool
-
-# this is the folder where backed up files will be read from by the slurm script (relative to /fs/pool/)
-BACKUP_POOL_FOLDER=pool-1
-# subfolders: /test1, /test2, /test3, ...
-
-# this is the folder where settings are read and output will be written to by the slurm script (relative to /fs/pool/)
-QUANTING_POOL_FOLDER=pool-2
-# subfolders: /output, /settings, /airflow_logs
-
-# location of slurm submit script and destination for slurm logs
-SLURM_BASE_DIR=/fs/pool/pool-2/slurm
diff --git a/envs/sandbox.env b/envs/sandbox.env
index 6080472f..fd7f7fa2 100644
--- a/envs/sandbox.env
+++ b/envs/sandbox.env
@@ -33,22 +33,5 @@ SLACK_WEBHOOK_URL=https://hooks.slack.com/services/<something>
 
 WEBAPP_URL=<webapp_url>
 
-############################# PATHS #############################
-# the following paths need also to be set in alphakraken.sandbox.yaml->locations
-
 # all mounts (backup pool folders, results pool folders, instruments) need to be available in MOUNTS_PATH, cf. Readme
 MOUNTS_PATH=/home/kraken-user/alphakraken/sandbox/mounts
-
-# location of BACKUP_POOL_FOLDER & QUANTING_POOL_FOLDER
-POOL_BASE_PATH=/fs/pool
-
-# this is the folder where backed up files will be read from by the slurm script (relative to /fs/pool/)
-BACKUP_POOL_FOLDER=pool-0/alphakraken_sandbox/backup
-# subfolders: /test1, /test2, /test3, ...
-
-# this is the folder where settings are read and output will be written to by the slurm script (relative to /fs/pool/)
-QUANTING_POOL_FOLDER=pool-0/alphakraken_sandbox
-# subfolders: /output, /settings, airflow_logs
-
-# location of slurm submit script and destination for slurm logs
-SLURM_BASE_DIR=/fs/pool/pool-2/slurm
diff --git a/shared/keys.py b/shared/keys.py
index c661f85a..1ed7ef5a 100644
--- a/shared/keys.py
+++ b/shared/keys.py
@@ -12,13 +12,16 @@ class EnvVars:
     MONGO_USER = "MONGO_USER"
     MONGO_PASSWORD = "MONGO_PASSWORD"  # noqa: S105  #Possible hardcoded password
 
-    POOL_BASE_PATH = "POOL_BASE_PATH"
-    BACKUP_POOL_FOLDER = "BACKUP_POOL_FOLDER"
-    QUANTING_POOL_FOLDER = "QUANTING_POOL_FOLDER"
+    SLACK_WEBHOOK_URL = "SLACK_WEBHOOK_URL"  # TODO: move to yaml
 
-    SLURM_BASE_DIR = "SLURM_BASE_DIR"
 
-    SLACK_WEBHOOK_URL = "SLACK_WEBHOOK_URL"
+class Locations:
+    """Keys for accessing paths in the yaml config."""
+
+    BACKUP = "backup"
+    SETTINGS = "settings"
+    OUTPUT = "output"
+    SLURM = "slurm"
 
 
 ALLOWED_CHARACTERS_IN_RAW_FILE_NAME = r"[^a-zA-Z0-9\-_+.]"
diff --git a/webapp/home.py b/webapp/home.py
index 9170cf5a..db709b91 100644
--- a/webapp/home.py
+++ b/webapp/home.py
@@ -4,7 +4,7 @@
 
 import streamlit as st
 from service.components import show_sandbox_message
-from service.utils import APP_URL, _log, display_info_message
+from service.utils import APP_URL, _log, display_info_message, quanting_output_path
 
 from shared.keys import ALLOWED_CHARACTERS_IN_RAW_FILE_NAME, EnvVars
 
@@ -12,8 +12,6 @@
 
 st.set_page_config(page_title="AlphaKraken: home", layout="wide")
 
-quanting_pool_folder = os.environ.get(EnvVars.QUANTING_POOL_FOLDER)
-
 """
 # Welcome to AlphaKraken!
 """
@@ -79,7 +77,7 @@
 Q: Where to I find the AlphaDIA output files?
 
 A:  The output files associated for a given raw file are stored at
-   `/fs/pool/{quanting_pool_folder}/output/<project id>/out_<raw file name>/`
+   `{quanting_output_path}/<project id>/out_<raw file name>/`
 
 
 Q: A lot of jobs are stuck in status "quanting" or "queued_for_quanting".
diff --git a/webapp/pages/projects.py b/webapp/pages/projects.py
index be0d8db6..958353bb 100644
--- a/webapp/pages/projects.py
+++ b/webapp/pages/projects.py
@@ -1,6 +1,5 @@
 """Page allowing Project mgmt."""
 
-import os
 import re
 
 import pandas as pd
@@ -12,11 +11,11 @@
     SessionStateKeys,
     _log,
     empty_to_none,
+    quanting_output_path,
     show_feedback_in_sidebar,
 )
 
 from shared.db.interface import add_project
-from shared.keys import EnvVars
 
 _log(f"loading {__file__}")
 
@@ -41,8 +40,6 @@
 projects_db = get_project_data()
 projects_df = df_from_db_data(projects_db)
 
-quanting_pool_folder = os.environ.get(EnvVars.QUANTING_POOL_FOLDER)
-
 # ########################################### DISPLAY
 
 st.warning("This page should be edited only by AlphaKraken admin users!", icon="⚠️")
@@ -56,7 +53,7 @@ def display_projects(
     filtered_df, *_ = show_filter(projects_df, st_display=st_display)
     st_display.table(filtered_df)
     st_display.markdown(
-        "Output files are stored at `/fs/pool/{quanting_pool_folder}/output/<project id>/out_<raw file name>/`"
+        f"Output files are stored at `{quanting_output_path}/<project id>/out_<raw file name>/`"
     )
 
 
diff --git a/webapp/pages/settings.py b/webapp/pages/settings.py
index 0775c372..1a175000 100644
--- a/webapp/pages/settings.py
+++ b/webapp/pages/settings.py
@@ -1,7 +1,5 @@
 """Simple data overview."""
 
-import os
-
 # ruff: noqa: TRY301 # Abstract `raise` to an inner function
 import pandas as pd
 import streamlit as st
@@ -16,12 +14,12 @@
     SessionStateKeys,
     _log,
     empty_to_none,
+    quanting_settings_path,
     show_feedback_in_sidebar,
 )
 
 from shared.db.interface import add_settings
 from shared.db.models import ProjectStatus
-from shared.keys import EnvVars
 
 _log(f"loading {__file__}")
 # ########################################### PAGE HEADER
@@ -43,7 +41,6 @@
 projects_db = get_project_data()
 settings_df = df_from_db_data(settings_db)
 
-quanting_pool_folder = os.environ.get(EnvVars.QUANTING_POOL_FOLDER)
 
 # ########################################### DISPLAY
 
@@ -76,7 +73,7 @@ def display_settings(
 
     st_display.markdown(
         "The files associated with the settings of a given project are stored at "
-        f"`/fs/pool/{quanting_pool_folder}/settings/<project id>/`"
+        f"`{quanting_settings_path}/<project id>/`"
     )
 
 
@@ -179,7 +176,7 @@ def display_settings(
         st.markdown("### Step 3/3: Upload files to pool folder")
         st.markdown(
             "Make sure you have uploaded all the files correctly to "
-            f"`/fs/pool/{quanting_pool_folder}/settings/{project_id}/`"
+            f"`{quanting_settings_path}/{project_id}/`"
         )
         upload_checkbox = st.checkbox(
             "I have uploaded the above files to this folder.", value=False
diff --git a/webapp/service/utils.py b/webapp/service/utils.py
index 4109ce5b..50ec6f9f 100644
--- a/webapp/service/utils.py
+++ b/webapp/service/utils.py
@@ -23,6 +23,10 @@
 DISABLE_WRITE = False
 
 
+quanting_settings_path = "n_a"  # TODO: os.environ.get(EnvVars.QUANTING_SETTINGS_PATH)
+quanting_output_path = "n_a"  # TODO: os.environ.get(EnvVars.QUANTING_OUTPUT_PATH)
+
+
 class QueryParams:
     """Query parameters for streamlit pages."""
 


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors path management by disentangling configuration keys from environment variables into a centralized YAML‐based system. Key changes include replacing hardcoded environment variables with new Locations keys, updating YAML configuration files to include absolute paths, and aligning tests and job handler code with the revised settings.

### Reviewed Changes

Copilot reviewed 21 out of 24 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                        | Description                                                         |
| ------------------------------------------- | ------------------------------------------------------------------- |
| shared/keys.py                              | Removed several environment variable keys and introduced Locations. |
| envs/*.yaml                                 | Added absolute_path keys and reorganized location settings.         |
| docs/deployment.md                          | Updated documentation to reference new Locations key for slurm paths. |
| docker-compose.yaml                         | Removed deprecated environment variable mappings.                 |
| airflow_src/tests/**/*.py and plugins files | Updated tests and code to use get_path and the new Locations structure. |
</details>


<details>
<summary>Files not reviewed (3)</summary>

* **envs/local.env**: Language not supported
* **envs/production.env**: Language not supported
* **envs/sandbox.env**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**docs/deployment.md:158**
* The documentation refers to `locations.general.slurm_base_path` while the YAML and code use the key `slurm` under locations. Consider updating the docs to reflect the new naming scheme.
```
and set the `locations.general.slurm_base_path`variable in `envs/alphakraken.${ENV}.yaml` to this value.
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T09:32:48Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T09:39:57Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T09:39:59Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T09:40:01Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T09:40:03Z
user: login: github-actions[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T09:40:10Z

Review comments:
diff_hunk: @@ -155,7 +155,7 @@ In case you want to set up a URL redirect from one PC to one or multiple others,
 ```bash
 mkdir ~/slurm
 ```
-and set the `SLURM_BASE_DIR` environmental variable in `envs/${ENV}.env` to this value.
+and set the `locations.general.slurm_base_path`variable in `envs/alphakraken.${ENV}.yaml` to this value.
 3. Copy the cluster run script `submit_job.sh` to `~/slurm` and adapt the `partition` (and optionally `nodelist`) directives.
path: docs/deployment.md
user: login: Copilot
body: There appears to be a missing space between `locations.general.slurm_base_path` and 'variable'. Consider inserting a space for improved readability.
```suggestion
and set the `locations.general.slurm_base_path` variable in `envs/alphakraken.${ENV}.yaml` to this value.
```
created_at: 2025-04-23T09:32:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -110,6 +110,18 @@ def _load_alphakraken_yaml(env_name: str) -> dict[str, dict[str, Any]]:
 _INSTRUMENTS = _SETTINGS["instruments"].copy()
path: airflow_src/plugins/common/settings.py
user: login: github-actions[bot]
body: The error messages in the exception cases weren't clear enough about what was missing. The modified version provides more specific error messages that indicate exactly which key is missing and where it should be located in the config file.

```python
def get_path(path_key: str) -> Path:
    """Get a certain path from the yaml settings."""
    if path_key not in _SETTINGS.get("locations", {}):
        raise KeyError(f"Key `{path_key}` not found in the locations section of alphakraken.yaml.")
        
    path = _SETTINGS["locations"][path_key].get("absolute_path")
    if path is None:
        raise KeyError(f"Key `absolute_path` not found for location `{path_key}` in alphakraken.yaml.")

    return Path(path)
```
created_at: 2025-04-23T09:39:57Z
author_association: NONE
diff_hunk: @@ -37,21 +37,20 @@ def get_internal_backup_path_for_instrument(
 
 
path: airflow_src/plugins/common/paths.py
user: login: github-actions[bot]
body: The docstring for `get_output_folder_rel_path` should be updated to match its actual functionality. Since the method no longer includes 'output/' in the returned path, the docstring examples should be updated to reflect this change. This will avoid confusion for developers.

```python
def get_output_folder_rel_path(raw_file: RawFile, project_id_or_fallback: str) -> Path:
    """Get the path of the output directory for given raw file name relative to the `output` folder.

    Only if the raw_file has no project defined, we use a month-specific subfolder
    This is to avoid having too many files in the fallback output folders.

    E.g.
        <project_id_or_fallback>/2024_07/out_RAW-FILE-1.raw in case raw_file has no project ID
        <project_id_or_fallback>/out_RAW-FILE-1.raw in case raw_file has a project ID
    """
    optional_sub_folder = (
        get_created_at_year_month(raw_file) if raw_file.project_id is None else ""
    )
    return (
        Path(project_id_or_fallback)
        / optional_sub_folder
        / f"{OUTPUT_FOLDER_PREFIX}{raw_file.id}"
    )
```
created_at: 2025-04-23T09:39:59Z
author_association: NONE
diff_hunk: @@ -66,6 +65,8 @@ def get_internal_output_path_for_raw_file(
     raw_file: RawFile, project_id_or_fallback: str
 ) -> Path:
path: airflow_src/plugins/common/paths.py
user: login: github-actions[bot]
body: The implementation is slightly inefficient as it calls `get_output_folder_rel_path` and then constructs the path. A cleaner approach would be to directly use the result from `get_output_folder_rel_path`. This makes the code more maintainable and explicit about how paths are constructed.

```python
def get_internal_output_path_for_raw_file(
    raw_file: RawFile, project_id_or_fallback: str
) -> Path:
    """Get absolute internal output path for the given raw file name."""
    output_rel_path = get_output_folder_rel_path(raw_file, project_id_or_fallback)
    return Path(InternalPaths.MOUNTS_PATH) / InternalPaths.OUTPUT / output_rel_path
```
created_at: 2025-04-23T09:40:01Z
author_association: NONE
diff_hunk: @@ -23,6 +23,10 @@
 DISABLE_WRITE = False
path: webapp/service/utils.py
user: login: github-actions[bot]
body: The 'n_a' placeholder values for paths aren't user-friendly. It would be better to provide more realistic placeholder values or add a clearer TODO comment explaining how these values should be populated in the future. The current TODOs are too vague.

```python
DISABLE_WRITE = False


# These values should be determined based on settings
# TODO: Load these from configuration
quanting_settings_path = "/fs/pool/settings"  # Placeholder until proper config is implemented
quanting_output_path = "/fs/pool/output"  # Placeholder until proper config is implemented
```
created_at: 2025-04-23T09:40:03Z
author_association: NONE
diff_hunk: @@ -78,30 +77,18 @@ def prepare_quanting(ti: TaskInstance, **kwargs) -> None:
                 f"No active settings found for project id '{project_id_or_fallback}'. Please add settings in the WebApp."
path: airflow_src/dags/impl/processor_impl.py
user: login: github-actions[bot]
body: The previous implementation had variable declarations and error handling separated by too much code. This reorganization keeps error handling logic together, making it easier to understand the control flow and maintain the code.

```python
def prepare_quanting(ti: TaskInstance, **kwargs) -> None:
    """Prepare the environmental variables for the quanting job."""
    raw_file_id = kwargs[DagContext.PARAMS][DagParams.RAW_FILE_ID]
    instrument_id = kwargs[OpArgs.INSTRUMENT_ID]

    raw_file = get_raw_file_by_id(raw_file_id)

    project_id_or_fallback = _get_project_id_or_fallback(
        raw_file.project_id, instrument_id
    )
    try:
        settings = get_settings_for_project(project_id_or_fallback)
    except DoesNotExist as e:
        raise AirflowFailException(
            f"No project found with id '{project_id_or_fallback}'. Please add a project and settings in the WebApp."
        ) from e
    else:
        if settings is None:
            raise AirflowFailException(
                f"No active settings found for project id '{project_id_or_fallback}'. Please add settings in the WebApp."
            )
```
created_at: 2025-04-23T09:40:10Z
author_association: NONE

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-23T09:33:19Z
body: The following feedback could not be added to specific lines, but still contains valuable information:

```
I've carefully reviewed the changes and analyzed the code patches. This pull request implements a significant refactoring of the configuration handling in AlphaKraken, specifically around how paths are managed.

[{
  "change_id": 1,
  "comment": "The patch refactors how paths are handled in AlphaKraken. Instead of using environment variables directly, paths are now defined in a YAML configuration file (alphakraken.*.yaml) and accessed through a new `get_path()` function. This is a good architectural change that centralizes configuration and reduces the reliance on environment variables. The implementation uses better path management with pathlib.Path throughout the codebase."
}]

[{
  "change_id": 2,
  "file_name": "./airflow_src/dags/impl/handler_impl.py",
  "start_line": 70,
  "end_line": 71,
  "proposed_code": "    backup_base_path = get_path(Locations.BACKUP)",
  "comment": "Use the new centralized path management function rather than constructing paths manually from environment variables. This simplifies the code and makes it more maintainable."
}]

[{
  "change_id": 3,
  "file_name": "./airflow_src/plugins/common/settings.py",
  "start_line": 113,
  "end_line": 123,
  "proposed_code": "def get_path(path_key: str) -> Path:\n    \"\"\"Get a certain path from the yaml settings.\"\"\"\n    path = _SETTINGS.get(\"locations\", {}).get(path_key, {}).get(\"absolute_path\")\n\n    if path is None:\n        raise KeyError(\n            f\"Key `{path_key}` or `{path_key}.absolute_path` not found in alphakraken.yaml.\"\n        )\n\n    return Path(path)",
  "comment": "The implementation of `get_path()` should handle error cases more gracefully. Consider returning an optional PathLike object or using a default path value when the key is not found in the configuration."
}]

[{
  "change_id": 4,
  "file_name": "./airflow_src/dags/impl/processor_impl.py",
  "start_line": 77,
  "end_line": 91,
  "proposed_code": "    # get raw file path\n    backup_base_path = get_path(Locations.BACKUP)\n    year_month_subfolder = get_created_at_year_month(raw_file)\n    raw_file_path = (\n        backup_base_path / instrument_id / year_month_subfolder / raw_file_id\n    )\n\n    # get settings and output_path\n    settings_path = get_path(Locations.SETTINGS) / project_id_or_fallback\n\n    output_path = get_path(Locations.OUTPUT) / get_output_folder_rel_path(\n        raw_file, project_id_or_fallback\n    )",
  "comment": "The path construction is now more consistent using the `get_path()` function. Consider creating helper functions for common path patterns to further reduce duplicated code."
}]

[{
  "change_id": 5,
  "file_name": "./airflow_src/plugins/common/paths.py",
  "start_line": 39,
  "end_line": 50,
  "proposed_code": "def get_output_folder_rel_path(raw_file: RawFile, project_id_or_fallback: str) -> Path:\n    \"\"\"Get the path of the output directory for given raw file name relative to the `output` folder.\n\n    Only if the raw_file has no project defined, we use a month-specific subfolder\n    This is to avoid having too many files in the fallback output folders.\n\n    E.g.\n        <project_id_or_fallback>/2024_07/out_RAW-FILE-1.raw in case raw_file has no project ID\n        <project_id_or_fallback>/out_RAW-FILE-1.raw in case raw_file has a project ID\n    \"\"\"\n    optional_sub_folder = (\n        get_created_at_year_month(raw_file) if raw_file.project_id is None else \"\"\n    )\n    return (\n        Path(project_id_or_fallback)\n        / optional_sub_folder\n        / f\"{OUTPUT_FOLDER_PREFIX}{raw_file.id}\"\n    )",
  "comment": "The function now correctly returns paths relative to the output folder rather than including the 'output' directory in the path, which aligns with its updated docstring and intended usage pattern."
}]

[{
  "change_id": 6,
  "file_name": "./airflow_src/plugins/jobs/job_handler.py",
  "start_line": 71,
  "end_line": 75,
  "proposed_code": "    def __init__(self):\n        \"\"\"Initialize the Slurm job handler.\"\"\"\n        super().__init__()\n        cluster_base_dir = get_path(Locations.SLURM)\n        self._cluster_working_dir_path = cluster_base_dir / CLUSTER_WORKING_DIR_NAME\n        self._cluster_job_script_path = cluster_base_dir / CLUSTER_JOB_SCRIPT_NAME",
  "comment": "Using the centralized `get_path()` function for the SLURM directory path is consistent with the new approach for path management."
}]

[{
  "change_id": 7,
  "file_name": "./shared/keys.py",
  "start_line": 12,
  "end_line": 24,
  "proposed_code": "    MONGO_USER = \"MONGO_USER\"\n    MONGO_PASSWORD = \"MONGO_PASSWORD\"  # noqa: S105  #Possible hardcoded password\n\n    SLACK_WEBHOOK_URL = \"SLACK_WEBHOOK_URL\"  # TODO: move to yaml\n\n\nclass Locations:\n    \"\"\"Keys for accessing paths in the yaml config.\"\"\"\n\n    BACKUP = \"backup\"\n    SETTINGS = \"settings\"\n    OUTPUT = \"output\"\n    SLURM = \"slurm\"",
  "comment": "The new `Locations` class improves code organization by centralizing the path location keys in one place. Consider also adding class constants for any subdirectories that are used consistently across the codebase."
}]

[{
  "change_id": 8,
  "file_name": "./webapp/service/utils.py",
  "start_line": 26,
  "end_line": 27,
  "proposed_code": "quanting_settings_path = \"n_a\"  # TODO: implement using get_path(Locations.SETTINGS)\nquanting_output_path = \"n_a\"  # TODO: implement using get_path(Locations.OUTPUT)",
  "comment": "The TODO comments should provide clearer guidance on how to properly implement these placeholders. They should use the new `get_path()` function with the appropriate location keys."
}]
```
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T09:33:20Z
body: Number of tokens: input_tokens=62205 output_tokens=1793 max_tokens=4096
review_instructions=''
config={}
thinking: ```
[]
```
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T09:40:12Z
body: The path management refactoring is a significant improvement to the codebase. It centralizes path configuration in the YAML files and removes environment variable dependencies. However, there are a few areas that need attention:

1. Testing should thoroughly verify path construction and error handling in all scenarios, especially edge cases like missing configuration entries.

2. The webapp still has hardcoded path placeholders that need to be connected to the same configuration system used by the Airflow components.

3. The documentation has been updated to explain the new approach, but more examples would help users understand how to properly configure paths.

4. Consider adding runtime validation to verify that configured paths exist and are accessible when the system starts.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-23T09:40:13Z
body: Number of tokens: input_tokens=62840 output_tokens=1757 max_tokens=4096
review_instructions=''
config={}
thinking: ```
[]
```
author_association: NONE