Skip to content

Commit bea726d

Browse files
authored
feat: Add configurable datafile template (#1269)
Detector file name defaults to `{instrument}-{scan_id}-{device_name}` when using the numtracker backed path provider but can now be configured by setting `detector_file_template` in the `numtracker` section of the config.
1 parent 641a4ac commit bea726d

File tree

6 files changed

+41
-15
lines changed

6 files changed

+41
-15
lines changed

helm/blueapi/config_schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,11 @@
173173
"minLength": 1,
174174
"title": "Url",
175175
"type": "string"
176+
},
177+
"detector_file_template": {
178+
"default": "{instrument}-{scan_id}-{device_name}",
179+
"title": "Detector File Template",
180+
"type": "string"
176181
}
177182
},
178183
"title": "NumtrackerConfig",

helm/blueapi/values.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,11 @@
654654
"title": "NumtrackerConfig",
655655
"type": "object",
656656
"properties": {
657+
"detector_file_template": {
658+
"title": "Detector File Template",
659+
"default": "{instrument}-{scan_id}-{device_name}",
660+
"type": "string"
661+
},
657662
"url": {
658663
"title": "Url",
659664
"default": "http://localhost:8406/graphql",

src/blueapi/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def id_token_signing_alg_values_supported(self) -> list[str]:
221221

222222
class NumtrackerConfig(BlueapiBaseModel):
223223
url: HttpUrl = HttpUrl("http://localhost:8406/graphql")
224+
detector_file_template: str = "{instrument}-{scan_id}-{device_name}"
224225

225226

226227
class ApplicationConfig(BlueapiBaseModel):

src/blueapi/core/context.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,35 +109,37 @@ def __post_init__(self, configuration: ApplicationConfig | None):
109109
if not configuration:
110110
return
111111

112-
if configuration.numtracker is not None:
112+
if (nt_conf := configuration.numtracker) is not None:
113113
if configuration.env.metadata is not None:
114-
self.numtracker = NumtrackerClient(url=configuration.numtracker.url)
114+
self.numtracker = NumtrackerClient(url=nt_conf.url)
115115
else:
116116
raise InvalidConfigError(
117117
"Numtracker url has been configured, but there is no instrument or"
118118
" instrument_session in the environment metadata"
119119
)
120120

121-
if self.numtracker is not None:
122-
numtracker = self.numtracker
123-
124121
path_provider = StartDocumentPathProvider()
125122
set_path_provider(path_provider)
123+
126124
self.run_engine.subscribe(path_provider.run_start, "start")
127125
self.run_engine.subscribe(path_provider.run_stop, "stop")
128126

127+
# local reference so it's available in _update_scan_num
128+
numtracker = self.numtracker
129+
129130
def _update_scan_num(md: dict[str, Any]) -> int:
130131
scan = numtracker.create_scan(
131132
md["instrument_session"], md["instrument"]
132133
)
133134
md["data_session_directory"] = str(scan.scan.directory.path)
135+
md["detector_file_template"] = nt_conf.detector_file_template
134136
md["scan_file"] = scan.scan.scan_file
135137
return scan.scan.scan_number
136138

137139
self.run_engine.scan_id_source = _update_scan_num
138140

139141
self.with_config(configuration.env)
140-
if self.numtracker and not isinstance(
142+
if configuration.numtracker and not isinstance(
141143
get_path_provider(), StartDocumentPathProvider
142144
):
143145
raise InvalidConfigError(

src/blueapi/utils/path_provider.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,13 @@
33
from event_model import RunStart, RunStop
44
from ophyd_async.core import PathInfo, PathProvider
55

6-
DEFAULT_TEMPLATE = "{instrument}-{scan_id}-{device_name}"
7-
86

97
class StartDocumentPathProvider(PathProvider):
108
"""A PathProvider that sources from metadata in a RunStart document.
119
12-
This uses metadata from a RunStart document to determine file names and data session
13-
directories. The file naming defaults to "{instrument}-{scan_id}-{device_name}}", so
14-
the file name is incremented by scan number. A template can be included in the
15-
StartDocument to allow for custom naming conventions.
10+
This uses metadata from a RunStart document to determine file names and
11+
data session directories. A template can be included in the StartDocument
12+
to allow for custom naming conventions.
1613
1714
"""
1815

@@ -38,7 +35,7 @@ def __call__(self, device_name: str | None = None) -> PathInfo:
3835
3936
The default template for file naming is: "{instrument}-{scan_id}-{device_name}"
4037
however, this can be changed by providing a template in the start document. For
41-
example: "template": "custom-{device_name}-{scan_id}".
38+
example: "detector_file_template": "custom-{device_name}-{scan_id}".
4239
4340
If you do not provide a data_session_directory it will default to "/tmp".
4441
"""
@@ -47,7 +44,9 @@ def __call__(self, device_name: str | None = None) -> PathInfo:
4744
"Start document not found. This call must be made inside a run."
4845
)
4946
else:
50-
template = self._docs[-1].get("data_file_path_template", DEFAULT_TEMPLATE)
47+
template = self._docs[-1].get("detector_file_template")
48+
if not template:
49+
raise ValueError("detector_file_template must be set in metadata")
5150
sub_path = template.format_map(
5251
self._docs[-1] | {"device_name": device_name}
5352
)

tests/unit_tests/utils/test_path_provider.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def start_doc_default_template() -> dict:
1818
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
1919
"data_session": "ab123",
2020
"instrument": "p01",
21+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
2122
"data_session_directory": "/p01/ab123",
2223
"scan_id": 22,
2324
"plan_type": "generator",
@@ -61,7 +62,7 @@ def start_doc_custom_template() -> dict:
6162
"instrument": "p01",
6263
"data_session_directory": "/p01/ab123",
6364
"scan_id": 22,
64-
"data_file_path_template": "{instrument}-{scan_id}-{device_name}-custom",
65+
"detector_file_template": "{instrument}-{scan_id}-{device_name}-custom",
6566
"plan_type": "generator",
6667
"plan_name": "count",
6768
"detectors": ["det"],
@@ -99,6 +100,7 @@ def start_doc_missing_instrument() -> dict:
99100
"uid": "27c48d2f-d8c6-4ac0-8146-fedf467ce11f",
100101
"time": 1741264729.96875,
101102
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
103+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
102104
"data_session": "ab123",
103105
"data_session_directory": "/p01/ab123",
104106
"scan_id": 22,
@@ -137,6 +139,7 @@ def start_doc_missing_scan_id() -> dict:
137139
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
138140
"data_session": "ab123",
139141
"instrument": "p01",
142+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
140143
"data_session_directory": "/p01/ab123",
141144
"plan_type": "generator",
142145
"plan_name": "count",
@@ -171,6 +174,7 @@ def start_doc_default_data_session_directory() -> dict:
171174
"uid": "27c48d2f-d8c6-4ac0-8146-fedf467ce11f",
172175
"time": 1741264729.96875,
173176
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
177+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
174178
"data_session": "ab123",
175179
"instrument": "p01",
176180
"scan_id": 22,
@@ -240,6 +244,7 @@ def start_doc_1() -> dict:
240244
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
241245
"data_session": "ab123",
242246
"instrument": "p01",
247+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
243248
"data_session_directory": "/p01/ab123",
244249
"scan_id": 50,
245250
"plan_type": "generator",
@@ -279,6 +284,7 @@ def start_doc_2() -> dict:
279284
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
280285
"data_session": "ab123",
281286
"instrument": "p02",
287+
"detector_file_template": "{instrument}-{scan_id}-{device_name}",
282288
"data_session_directory": "/p02/ab123",
283289
"scan_id": 51,
284290
"plan_type": "generator",
@@ -378,3 +384,11 @@ def test_start_document_path_provider_run_stop_called_out_of_order_raises(
378384
"This is not supported. If you need to do this speak to core DAQ.",
379385
):
380386
pp.run_stop(name="stop", stop_document=stop_doc_1)
387+
388+
389+
def test_error_if_template_missing(start_doc_1: RunStart):
390+
pp = StartDocumentPathProvider()
391+
start_doc_1.pop("detector_file_template")
392+
pp.run_start("start", start_doc_1)
393+
with pytest.raises(ValueError, match="detector_file_template"):
394+
pp("foo")

0 commit comments

Comments
 (0)