refactor: add system_config and review docs

This commit is contained in:
2024-06-06 12:45:38 +02:00
parent 183152fac6
commit a481fdadfe
8 changed files with 148 additions and 52 deletions
+35
View File
@@ -12,6 +12,7 @@ import inspect
from typing import TYPE_CHECKING
import redis
from pydantic import BaseModel, Field, field_validator
from rich.console import Console
from rich.table import Table
@@ -39,6 +40,39 @@ ScanManager = lazy_import_from("bec_lib.scan_manager", ("ScanManager",))
Scans = lazy_import_from("bec_lib.scans", ("Scans",))
class SystemConfig(BaseModel):
"""System configuration model"""
file_suffix: str | None = Field(default=None)
file_directory: str | None = Field(default=None)
model_config: dict = {"validate_assignment": True}
@field_validator("file_suffix", "file_directory")
@staticmethod
def check_validity(value: str, field: Field) -> str:
"""Check the validity of the value
Args:
value (str): The value to check
field_name (str): The name of the field
Returns:
str: The value if it is valid
"""
if value is None:
return value
field_name = field.field_name
check_value = value.replace("_", "").replace("-", "")
if field_name == "file_directory":
value = value.strip("/")
check_value = check_value.replace("/", "")
if not check_value.isalnum() or not check_value.isascii():
raise ValueError(
f"{field_name} must only contain alphanumeric ASCII characters. Provided string is: {value}"
)
return value
class BECClient(BECService, UserScriptsMixin):
"""
The BECClient class is the main entry point for the BEC client and all derived classes.
@@ -82,6 +116,7 @@ class BECClient(BECService, UserScriptsMixin):
self.bl_checks = None
self._hli_funcs = {}
self.metadata = {}
self.system_config = SystemConfig()
self.callbacks = CallbackHandler()
self._parent = parent if parent is not None else self
self._initialized = True
+37 -31
View File
@@ -17,6 +17,7 @@ from typeguard import typechecked
from bec_lib import messages
from bec_lib.bec_errors import ScanAbortion
from bec_lib.client import SystemConfig
from bec_lib.device import DeviceBase
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
@@ -51,6 +52,8 @@ class ScanObject:
hide_report: bool = False,
metadata: dict = None,
monitored: list[str | DeviceBase] = None,
file_suffix: str = None,
file_directory: str = None,
**kwargs,
) -> ScanReport:
"""
@@ -76,28 +79,21 @@ class ScanObject:
# pylint: disable=protected-access
hide_report = hide_report or scans._hide_report
md = deepcopy(self.client.metadata)
key_parameter = ["sample_name", "file_suffix", "file_directory"]
for key in key_parameter:
if key not in md:
var = self.client.get_global_var(key)
if var is not None:
md[key] = var
user_metadata = deepcopy(self.client.metadata)
sys_config = self.client.system_config.model_copy(deep=True)
if file_suffix:
sys_config.file_suffix = file_suffix
if file_directory:
sys_config.file_directory = file_directory
if "sample_name" not in user_metadata:
var = self.client.get_global_var("sample_name")
if var is not None:
user_metadata["sample_name"] = var
if metadata is not None:
md.update(metadata)
if "file_suffix" in md:
suffix = md.get("file_suffix")
check_suffix = suffix.replace("_", "").replace("-", "")
if not check_suffix.isalnum() or not check_suffix.isascii():
raise ValueError("file_suffix must only contain alphanumeric ASCII characters.")
if "file_directory" in md:
directory = md.get("file_directory")
check_directory = directory.replace("/", "").replace("_", "").replace("-", "")
if not check_directory.isalnum() or not check_directory.isascii():
raise ValueError("file_directory must only contain alphanumeric ASCII characters.")
md["file_directory"] = directory.strip("/")
user_metadata.update(metadata)
if monitored is not None:
if not isinstance(monitored, list):
@@ -111,15 +107,17 @@ class ScanObject:
)
kwargs["monitored"] = monitored
sys_config = sys_config.model_dump()
# pylint: disable=protected-access
if scans._scan_group:
md["queue_group"] = scans._scan_group
sys_config["queue_group"] = scans._scan_group
if scans._scan_def_id:
md["scan_def_id"] = scans._scan_def_id
sys_config["scan_def_id"] = scans._scan_def_id
if scans._dataset_id_on_hold:
md["dataset_id_on_hold"] = scans._dataset_id_on_hold
sys_config["dataset_id_on_hold"] = scans._dataset_id_on_hold
kwargs["metadata"] = md
kwargs["user_metadata"] = user_metadata
kwargs["system_config"] = sys_config
request = Scans.prepare_scan_request(self.scan_name, self.scan_info, *args, **kwargs)
request_id = str(uuid.uuid4())
@@ -265,8 +263,9 @@ class Scans:
)
metadata = {}
if "metadata" in kwargs:
metadata = kwargs.pop("metadata")
metadata.update(kwargs["system_config"])
metadata["user_metadata"] = kwargs.pop("user_metadata", {})
params = {"args": Scans._parameter_bundler(args, bundle_size), "kwargs": kwargs}
# check the number of arg bundles against the number of required bundles
if bundle_size:
@@ -408,17 +407,20 @@ class DatasetIdOnHold(ContextDecorator):
queue.next_dataset_number += 1
class FileWriterData:
class FileWriter:
@typechecked
def __init__(self, fw_config: Dict[Literal["file_suffix", "file_directory"], str]) -> None:
def __init__(self, file_suffix: str = None, file_directory: str = None) -> None:
"""Context manager for updating metadata
Args:
fw_config (dict): Dictionary with metadata for the filewriter, can only have keys "file_suffix" and "file_directory"
"""
self.client = self._get_client()
self._fw_config = fw_config
self.system_config = self.client.system_config
self._orig_system_config = None
self._orig_metadata = None
self.file_suffix = file_suffix
self.file_directory = file_directory
def _get_client(self):
"""Get BEC client"""
@@ -427,12 +429,16 @@ class FileWriterData:
def __enter__(self):
"""Enter the context manager"""
self._orig_metadata = deepcopy(self.client.metadata)
self.client.metadata.update(self._fw_config)
self._orig_system_config = self.system_config.model_copy(deep=True)
self.system_config.file_suffix = self.file_suffix
self.system_config.file_directory = self.file_directory
return self
def exit(self, *exc):
def __exit__(self, *exc):
"""Exit the context manager"""
self.client.metadata = self._orig_metadata
self.system_config.file_suffix = self._orig_system_config.file_suffix
self.system_config.file_directory = self._orig_system_config.file_directory
class Metadata:
+22
View File
@@ -0,0 +1,22 @@
""" This module tests the bec_lib.client module. """
import pytest
from bec_lib.client import SystemConfig
def test_system_config():
"""Test the SystemConfig class."""
config = SystemConfig(file_suffix="suff", file_directory="dir")
assert config.file_suffix == "suff"
assert config.file_directory == "dir"
config = SystemConfig()
assert config.file_suffix is None
assert config.file_directory is None
config.file_suffix = "suff_-"
config.file_directory = "/dir_-/blabla"
assert config.file_suffix == "suff_-"
assert config.file_directory == "dir_-/blabla"
with pytest.raises(ValueError):
config = SystemConfig(file_suffix="@")
config = SystemConfig(file_directory="ä")
+21 -1
View File
@@ -3,7 +3,15 @@ from unittest import mock
import pytest
from bec_lib.device import DeviceBase
from bec_lib.scans import DatasetIdOnHold, HideReport, Metadata, ScanDef, ScanExport, ScanGroup
from bec_lib.scans import (
DatasetIdOnHold,
FileWriter,
HideReport,
Metadata,
ScanDef,
ScanExport,
ScanGroup,
)
# pylint: disable=no-member
# pylint: disable=missing-function-docstring
@@ -11,6 +19,18 @@ from bec_lib.scans import DatasetIdOnHold, HideReport, Metadata, ScanDef, ScanEx
# pylint: disable=protected-access
def test_filewriter_cm(bec_client_mock):
client = bec_client_mock
client.scans._file_writer = None
client.system_config.file_directory = None
client.system_config.file_suffix = None
with FileWriter(file_suffix="testsuffix", file_directory="testdirectory"):
assert client.system_config.file_directory == "testdirectory"
assert client.system_config.file_suffix == "testsuffix"
assert client.system_config.file_directory is None
assert client.system_config.file_suffix is None
def test_metadata_handler(bec_client_mock):
client = bec_client_mock
client.metadata = {"descr": "test", "uid": "12345"}
+9 -6
View File
@@ -88,7 +88,7 @@ def test_scan_object_file_suffix(scan_obj, dev):
step=0.5,
exp_time=0.1,
relative=False,
metadata={"file_suffix": "testsample"},
file_suffix="testsample",
)
assert scan_report.call_args.args[0].metadata["file_suffix"] == "testsample"
@@ -124,7 +124,7 @@ def test_scan_object_raises_on_non_ascii_chars(scan_obj, dev, file_suffix, file_
step=0.5,
exp_time=0.1,
relative=False,
metadata={"file_suffix": file_suffix},
file_suffix=file_suffix,
)
else:
scan_obj._run(
@@ -137,7 +137,7 @@ def test_scan_object_raises_on_non_ascii_chars(scan_obj, dev, file_suffix, file_
step=0.5,
exp_time=0.1,
relative=False,
metadata={"file_suffix": file_suffix},
file_suffix=file_suffix,
)
assert scan_report.call_args.args[0].metadata["file_suffix"] == file_suffix
@@ -168,7 +168,7 @@ def test_scan_object_raises_on_non_ascii_chars_dir(scan_obj, dev, file_dir, file
step=0.5,
exp_time=0.1,
relative=False,
metadata={"file_directory": file_dir},
file_directory=file_dir,
)
else:
scan_obj._run(
@@ -181,7 +181,7 @@ def test_scan_object_raises_on_non_ascii_chars_dir(scan_obj, dev, file_dir, file
step=0.5,
exp_time=0.1,
relative=False,
metadata={"file_directory": file_dir},
file_directory=file_dir,
)
assert scan_report.call_args.args[0].metadata["file_directory"] == file_dir.strip(
"/"
@@ -201,7 +201,10 @@ def test_scan_object_receives_sample_name(scan_obj, dev):
scan_obj.client, "get_global_var", side_effect=get_global_var_side_effect
):
scan_obj._run(dev.samx, -5, 5, dev.samy, -5, 5, step=0.5, exp_time=0.1, relative=False)
assert scan_report.call_args.args[0].metadata["sample_name"] == "test_sample"
assert (
scan_report.call_args.args[0].metadata["user_metadata"]["sample_name"]
== "test_sample"
)
def test_scan_object_receives_scan_group(scan_obj, dev):
+1
View File
@@ -59,6 +59,7 @@ myst_enable_extensions = [
autosummary_generate = True # Turn on sphinx.ext.autosummary
add_module_names = False # Remove namespaces from class/method signatures
autodoc_inherit_docstrings = True # If no docstring, inherit from base class
autodoc_mock_imports = ["pydantic"]
set_type_checking_flag = True # Enable 'expensive' imports for sphinx_autodoc_typehints
autoclass_content = "both" # Include both class docstring and __init__
+1 -1
View File
@@ -20,7 +20,7 @@ glossary/
***
````{grid} 2
:gutter: 6
:gutter: 5
```{grid-item-card}
:link: developer.getting_started
@@ -2,22 +2,31 @@
## File Writer
BECs file writer is a dedicated service that writes HDF5 files with Nexus-compatible metadata entries to disk. It also adds external links to files written by other services, such as data backends for large 2D detector data. The internal structure of the files can be adjusted to the beamlines needs using customizable plugins to comply with the desired [NeXus application definition](https://manual.nexusformat.org/classes/applications/index.html).
When the service starts, a `base_path` is configured, and all data can only be written to disk relative to this path. By default, the relative path follows the template `/data/S00000-S00999/S00001/S00001_master.h5` for scan number 1. To compile the appropriate path for secondary services, we provide the utility class [`bec_lib.file_utils.FileWriter`](/api_reference/_autosummary/bec_lib.file_utils.FileWriter.rst#bec_lib.file_utils.FileWriter) with the method [`compile_full_filename`](/api_reference/_autosummary/bec_lib.file_utils.FileWriter.rst#bec_lib.file_utils.FileWriter.compile_full_filename), which automatically prepares the correct filepath.
When the service starts, a **base_path** is configured, and all data can only be written to disk relative to this path. By default, the relative path follows the template `/data/S00000-S00999/S00001/S00001_master.h5` for scan number 1. To compile the appropriate path for secondary services, we provide the utility class [`bec_lib.file_utils.FileWriter`](/api_reference/_autosummary/bec_lib.file_utils.FileWriter.rst#bec_lib.file_utils.FileWriter) with the method [`compile_full_filename`](/api_reference/_autosummary/bec_lib.file_utils.FileWriter.rst#bec_lib.file_utils.FileWriter.compile_full_filename), which automatically prepares the correct filepath.
If secondary services within *ophyd_devices* need to be configured with the appropriate file path, we recommend using this function since it will ensure that all custom changes to the file name and directory will be properly compiled and returned.
### Changing the File Directory or Adding a Suffix
### Changing the file directory or adding a suffix
The relative filepath can be configured and adapted dynamically. We use the metadata provided within BEC to inform the file writer about these changes. For this purpose, we reserve the keys `file_suffix` and `file_directory` in the metadata for scans. We note that both variables must only contain *alphanumeric ASCII* characters.
To configure these variables, we offer three different options:
The relative filepath can be configured and adapted dynamically.
We provide the possibility to change the file directory or add a suffix to the file name through a **system_config**.
Keep in mind that the file_directory will always be considered relative to the **base_path**. Providing an absolute path by accident will be transformed into a relative path.
In addition, both file_suffix and file_directory may only contain alphanumeric ASCII characters and the following special characters: '-', '_' and '/'.
This will be automatically checked, and raise for an invalid input.
You may use one of the two options below:
1. **Handing the adjusted metadata directly to the scan command**: This method will only be considered for the given scan command but has the highest priority.
```python
scans.line_scan(dev.samx, -5, 5, steps=100, relativ=True, metadata={'file_suffix': 'sampleA', 'file_directory': 'study1337'})
```
1. **Changing the system_config**
2. **Adding the information to `bec.metadata` in the command line interface**: This information will be considered for all following scans, unless explicitly overridden or deleted by a scan command.
```python
bec.metadata.update({'file_suffix': 'sampleA', 'file_directory': 'study1337'})
```
The **system_config** is accessible via [`bec.system_config`](/api_reference/_autosummary/bec_lib.client.SystemConfig) and can be used to change file_suffix and file_directory. It is directly exposed to users via the client. Changing the *file_suffix* and *file_directory* will be considered for all following scans.
```python
bec.system_config.file_suffix = 'sampleA'
bec.system_config.file_directory = 'my_dir/my_setup'
```
Assuming the *basepath* to be `'/bec/data'` and a `scannr = 101`, the file writer now writes to the following filepath: `'/bec/data/my_dir/my_setup/S00101_master_sampleA.h5'`.
If you only provide *file_suffix*, but no additional *file_directory*, the filepath will be:`'/bec/data/S00000-S00999/S00101_sampleA/S00101_master_sampleA.h5'`.
3. **Using global variables [`bec.set_global_var`](/api_reference/_autosummary/bec_lib.client.BECClient.rst#bec_lib.client.BECClient.set_global_var)**: You can set `file_suffix` and `file_directory` as global variables within BEC, and they will be automatically considered. Note, this has the lowest priority compared to options 1 or 2.
2. **Adding additional arguments to a scan**
You can also add the *file_suffix* and *file_directory* as arguments to the scan command. This will only affect the current scan and will not be considered for following scans. Adding the arguments to the scan command has priority and will override the information provided in *system_config*.
```python
scans.line_scan(dev.samx, -5, 5, steps=1, relative=True, file_suffix='sampleA', file_directory='my_dir/my_setup')
```