Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b4dbd8e
Add mock config server fixture
jacob720 Dec 9, 2025
bf08ced
Use config server to read beamlineParmaeters
jacob720 Dec 9, 2025
91fc322
Remove tests for removed functions
jacob720 Dec 9, 2025
386b521
Convert test beamlineParameters files
jacob720 Dec 9, 2025
acd5805
Fix test
jacob720 Dec 9, 2025
28b7823
Fix lint
jacob720 Dec 9, 2025
84f0836
Fix
jacob720 Jan 12, 2026
afd4fbc
Require latest daq-config-server
jacob720 Jan 12, 2026
2d92790
PR comments WIP
jacob720 Jan 20, 2026
e3f6fad
Parameterise config server URL
jacob720 Jan 23, 2026
3dbb881
Refactor get_beamline_parameters
jacob720 Jan 23, 2026
9517b6a
Update lockfile
jacob720 Jan 23, 2026
bc858b2
Use server deployed on i03 beamline cluster for i03 config
jacob720 Jan 29, 2026
9529dc1
typo
jacob720 Feb 4, 2026
4aac691
Merge branch 'main' into mx_bluesky_1504_migrate_beamline_parameters_…
jacob720 Feb 4, 2026
cba50ab
Merge branch 'main' into mx_bluesky_1504_migrate_beamline_parameters_…
jacob720 Feb 5, 2026
eaa20a2
WIP
jacob720 Feb 11, 2026
0d29dbe
Reset cache between tests
jacob720 Feb 11, 2026
feae3b6
Merge branch 'main' into mx_bluesky_1504_migrate_beamline_parameters_…
jacob720 Feb 11, 2026
d4fda7b
Fix
jacob720 Feb 11, 2026
a947f60
Fix lint
jacob720 Feb 11, 2026
d5bd077
Fix lint
jacob720 Feb 12, 2026
4b4a3f1
Merge branch 'main' into mx_bluesky_1504_migrate_beamline_parameters_…
jacob720 Feb 13, 2026
357a6c9
PR comments and coverage
jacob720 Feb 13, 2026
286a17c
Lint
jacob720 Feb 13, 2026
3d05eb2
Merge branch 'main' into mx_bluesky_1504_migrate_beamline_parameters_…
jacob720 Feb 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/dodal/beamlines/i03.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def daq_configuration_path() -> str:

@devices.factory()
def aperture_scatterguard() -> ApertureScatterguard:
params = get_beamline_parameters()
params = get_beamline_parameters(BL)
return ApertureScatterguard(
aperture_prefix=f"{PREFIX.beamline_prefix}-MO-MAPT-01:",
scatterguard_prefix=f"{PREFIX.beamline_prefix}-MO-SCAT-01:",
Expand All @@ -116,7 +116,7 @@ def attenuator() -> BinaryFilterAttenuator:
def beamstop() -> Beamstop:
return Beamstop(
prefix=f"{PREFIX.beamline_prefix}-MO-BS-01:",
beamline_parameters=get_beamline_parameters(),
beamline_parameters=get_beamline_parameters(BL),
)


Expand Down Expand Up @@ -346,7 +346,7 @@ def scintillator(aperture_scatterguard: ApertureScatterguard) -> Scintillator:
return Scintillator(
f"{PREFIX.beamline_prefix}-MO-SCIN-01:",
Reference(aperture_scatterguard),
get_beamline_parameters(),
get_beamline_parameters(BL),
)


Expand Down
6 changes: 3 additions & 3 deletions src/dodal/beamlines/i04.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def ipin() -> IPin:
def beamstop() -> Beamstop:
return Beamstop(
f"{PREFIX.beamline_prefix}-MO-BS-01:",
beamline_parameters=get_beamline_parameters(),
beamline_parameters=get_beamline_parameters(BL),
)


Expand Down Expand Up @@ -148,7 +148,7 @@ def backlight() -> Backlight:

@devices.factory()
def aperture_scatterguard() -> ApertureScatterguard:
params = get_beamline_parameters()
params = get_beamline_parameters(BL)
return ApertureScatterguard(
aperture_prefix=f"{PREFIX.beamline_prefix}-MO-MAPT-01:",
scatterguard_prefix=f"{PREFIX.beamline_prefix}-MO-SCAT-01:",
Expand Down Expand Up @@ -281,7 +281,7 @@ def scintillator(aperture_scatterguard: ApertureScatterguard) -> Scintillator:
return Scintillator(
f"{PREFIX.beamline_prefix}-MO-SCIN-01:",
Reference(aperture_scatterguard),
get_beamline_parameters(),
get_beamline_parameters(BL),
)


Expand Down
79 changes: 15 additions & 64 deletions src/dodal/common/beamlines/beamline_parameters.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,26 @@
import ast
from typing import Any, cast
from typing import Any

from dodal.log import LOGGER
from dodal.utils import get_beamline_name

BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
from dodal.common.beamlines.config_client import get_config_client

BEAMLINE_PARAMETER_PATHS = {
"i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
"i04": "/dls_sw/i04/software/daq_configuration/domain/beamlineParameters",
}


class GDABeamlineParameters:
params: dict[str, Any]

def __init__(self, params: dict[str, Any]):
self.params = params

def __repr__(self) -> str:
return repr(self.params)

def __getitem__(self, item: str):
return self.params[item]

@classmethod
def from_lines(cls, file_name: str, config_lines: list[str]):
config_lines_nocomments = [line.split("#", 1)[0] for line in config_lines]
config_lines_sep_key_and_value = [
# XXX removes all whitespace instead of just trim
line.translate(str.maketrans("", "", " \n\t\r")).split("=")
for line in config_lines_nocomments
]
config_pairs: list[tuple[str, Any]] = [
cast(tuple[str, Any], param)
for param in config_lines_sep_key_and_value
if len(param) == 2
]
for i, (param, value) in enumerate(config_pairs):
try:
# BEAMLINE_PARAMETER_KEYWORDS effectively raw string but whitespace removed
if value not in BEAMLINE_PARAMETER_KEYWORDS:
config_pairs[i] = (
param,
cls.parse_value(value),
)
except Exception as e:
LOGGER.warning(f"Unable to parse {file_name} line {i}: {e}")

return cls(params=dict(config_pairs))

@classmethod
def from_file(cls, path: str):
with open(path) as f:
config_lines = f.readlines()
return cls.from_lines(path, config_lines)

@classmethod
def parse_value(cls, value: str):
return ast.literal_eval(value.replace("Yes", "True").replace("No", "False"))
def get_beamline_parameters(beamline: str) -> dict[str, Any]:
"""Loads the beamline parameters for a specified beamline from the config server.

Args:
beamline (str): The beamline for which beamline parameters will be retrieved.

def get_beamline_parameters(beamline_param_path: str | None = None):
"""Loads the beamline parameters from the specified path, or according to the
environment variable if none is given.
Returns:
dict[str, Any]: Dict of beamline parameters.
"""
if not beamline_param_path:
beamline_name = get_beamline_name("i03")
beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline_name)
if beamline_param_path is None:
raise KeyError(
"No beamline parameter path found, maybe 'BEAMLINE' environment variable is not set!"
)
return GDABeamlineParameters.from_file(beamline_param_path)
beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline)
if beamline_param_path is None:
raise KeyError(
"No beamline parameter path found, maybe 'BEAMLINE' environment variable is not set!"
)
config_client = get_config_client(beamline)
return config_client.get_file_contents(beamline_param_path, dict)
16 changes: 16 additions & 0 deletions src/dodal/common/beamlines/config_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from functools import cache

from daq_config_server.client import ConfigServer

BEAMLINE_CONFIG_SERVER_ENDPOINTS = {
"i03": "https://i03-daq-config.diamond.ac.uk",
"i04": "https://daq-config.diamond.ac.uk",
}


@cache
def get_config_client(beamline: str) -> ConfigServer:
url = BEAMLINE_CONFIG_SERVER_ENDPOINTS.get(
beamline, "https://daq-config.diamond.ac.uk"
)
return ConfigServer(url=url)
8 changes: 4 additions & 4 deletions src/dodal/devices/aperturescatterguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from math import inf
from typing import Any

from bluesky.protocols import Preparable
from ophyd_async.core import (
Expand All @@ -14,7 +15,6 @@
)
from pydantic import BaseModel, Field

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
from dodal.devices.aperture import Aperture
from dodal.devices.motors import XYStage

Expand Down Expand Up @@ -65,7 +65,7 @@ def values(self) -> tuple[float, float, float, float, float]:

@staticmethod
def tolerances_from_gda_params(
params: GDABeamlineParameters,
params: dict[str, Any],
) -> AperturePosition:
return AperturePosition(
aperture_x=params["miniap_x_tolerance"],
Expand All @@ -79,7 +79,7 @@ def tolerances_from_gda_params(
def from_gda_params(
name: _GDAParamApertureValue,
diameter: float,
params: GDABeamlineParameters,
params: dict[str, Any],
) -> AperturePosition:
return AperturePosition(
aperture_x=params[f"miniap_x_{name.value}"],
Expand Down Expand Up @@ -109,7 +109,7 @@ def __str__(self):


def load_positions_from_beamline_parameters(
params: GDABeamlineParameters,
params: dict[str, Any],
) -> dict[ApertureValue, AperturePosition]:
return {
ApertureValue.OUT_OF_BEAM: AperturePosition.from_gda_params(
Expand Down
7 changes: 4 additions & 3 deletions src/dodal/devices/beamlines/i03/undulator_dcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dodal.devices.beamlines.i03.dcm import DCM
from dodal.devices.undulator import UndulatorInKeV
from dodal.log import LOGGER
from dodal.utils import get_beamline_name

ENERGY_TIMEOUT_S: float = 30.0

Expand Down Expand Up @@ -47,9 +48,9 @@ def __init__(
)
# I03 configures the DCM Perp as a side effect of applying this fixed value to the DCM Offset after an energy change
# Nb this parameter is misleadingly named to confuse you
self.dcm_fixed_offset_mm = get_beamline_parameters(
daq_configuration_path + "/domain/beamlineParameters"
)["DCM_Perp_Offset_FIXED"]
self.dcm_fixed_offset_mm = get_beamline_parameters(get_beamline_name())[
"DCM_Perp_Offset_FIXED"
]

super().__init__(name)

Expand Down
5 changes: 2 additions & 3 deletions src/dodal/devices/mx_phase1/beamstop.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
from math import isclose
from typing import Any

from ophyd_async.core import (
StandardReadable,
Expand All @@ -8,8 +9,6 @@
)
from ophyd_async.epics.motor import Motor

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters

_BEAMSTOP_OUT_DELTA_Y_MM = -2


Expand Down Expand Up @@ -46,7 +45,7 @@ class Beamstop(StandardReadable):
def __init__(
self,
prefix: str,
beamline_parameters: GDABeamlineParameters,
beamline_parameters: dict[str, Any],
name: str = "",
):
with self.add_children_as_readables():
Expand Down
4 changes: 2 additions & 2 deletions src/dodal/devices/scintillator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from math import isclose
from typing import Any

from ophyd_async.core import Reference, StandardReadable, StrictEnum, derived_signal_rw
from ophyd_async.epics.motor import Motor

from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue


Expand All @@ -29,7 +29,7 @@ def __init__(
self,
prefix: str,
aperture_scatterguard: Reference[ApertureScatterguard],
beamline_parameters: GDABeamlineParameters,
beamline_parameters: dict[str, Any],
name: str = "",
):
with self.add_children_as_readables():
Expand Down
5 changes: 3 additions & 2 deletions src/dodal/plan_stubs/check_topup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from dodal.devices.synchrotron import Synchrotron, SynchrotronMode
from dodal.log import LOGGER
from dodal.utils import get_beamline_name

ALLOWED_MODES = [SynchrotronMode.USER, SynchrotronMode.SPECIAL]
DECAY_MODE_COUNTDOWN = -1 # Value of the start_countdown PV when in decay mode
Expand Down Expand Up @@ -133,5 +134,5 @@ def check_topup_and_wait_if_necessary(


def _load_topup_configuration_from_properties_file() -> dict[str, Any]:
params = get_beamline_parameters()
return params.params
params = get_beamline_parameters(get_beamline_name("i03"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this practice that we have in the code of inserting arbitrary default beamline names into random bits of code. By removing the default from get_beamline_parameters() this has just spread it around more.

I think it would be better if we never assumed it anywhere in production code and instead took it from the BEAMLINE environment variable in a single function which we can patch out for unit tests.

Copy link
Contributor Author

@jacob720 jacob720 Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically remove the 'default' argument from get_beamline_name()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a quick go at doing this and it's looking like a big change, and I'm not 100% sure of the consequences, so I think we should pull it out into a new issue. For now, I'll make the default argument optional so that we don't have to arbitrarily decide on defaults.

return params
38 changes: 38 additions & 0 deletions src/dodal/testing/fixtures/config_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
from pathlib import Path
from typing import TypeVar
from unittest.mock import patch

import pytest
from daq_config_server.models import ConfigModel

T = TypeVar("T", str, dict, ConfigModel)


def fake_config_server_get_file_contents(
filepath: str | Path,
desired_return_type: type[T] = str,
reset_cached_result: bool = True,
) -> T:
filepath = Path(filepath)
# Minimal logic required for unit tests
with filepath.open("r") as f:
contents = f.read()
if desired_return_type is str:
return contents # type: ignore
elif desired_return_type is dict:
return json.loads(contents)
elif issubclass(desired_return_type, ConfigModel):
return desired_return_type.model_validate(json.loads(contents))
raise ValueError("Invalid return type requested")


@pytest.fixture(autouse=True)
def mock_config_server():
# Don't actually talk to central service during unit tests, and reset caches between test

with patch(
"daq_config_server.client.ConfigServer.get_file_contents",
side_effect=fake_config_server_get_file_contents,
):
yield
7 changes: 5 additions & 2 deletions src/dodal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@
AnyDeviceFactory: TypeAlias = V1DeviceFactory | V2DeviceFactory


def get_beamline_name(default: str) -> str:
return environ.get("BEAMLINE") or default
def get_beamline_name(default: str | None = None) -> str:
beamline_name = environ.get("BEAMLINE") or default
if beamline_name is None:
raise ValueError("Set BEAMLINE environment variable or provide default.")
return beamline_name


def is_test_mode() -> bool:
Expand Down
Loading