Implement patching base labthings configuration files

This commit is contained in:
Julian Stirling 2025-10-26 19:19:12 +00:00
parent 9e9764e196
commit c7e4160a08
2 changed files with 79 additions and 1 deletions

View file

@ -6,10 +6,13 @@ from typing import Optional, Callable, Any
from functools import wraps from functools import wraps
from copy import copy from copy import copy
import logging import logging
from argparse import Namespace
import labthings_fastapi as lt import labthings_fastapi as lt
import uvicorn import uvicorn
from uvicorn.main import Server from uvicorn.main import Server
from openflexure_microscope_server.utilities import load_patched_config
from .serve_static_files import add_static_files from .serve_static_files import add_static_files
from .legacy_api import add_v2_endpoints from .legacy_api import add_v2_endpoints
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
@ -81,7 +84,7 @@ def serve_from_cli(argv: Optional[list[str]] = None) -> None:
config = None config = None
server = None server = None
try: try:
config = lt.cli.config_from_args(args) config = _full_config_from_args(args)
log_folder = config.get("log_folder", "./openflexure/logs") log_folder = config.get("log_folder", "./openflexure/logs")
scans_folder = _get_scans_dir(config) scans_folder = _get_scans_dir(config)
server = lt.cli.server_from_config(config) server = lt.cli.server_from_config(config)
@ -128,3 +131,16 @@ def serve_from_cli(argv: Optional[list[str]] = None) -> None:
) )
else: else:
raise e raise e
def _full_config_from_args(args: Namespace) -> dict:
"""Load configuration from LabThings args allowing patching.
This provides similar functionarlity to lt.cli.config_from_args except allows the
configuration file to specify a base config, and optionally patches.
"""
# If no config file specified let LabThings handle it.
if not args.config:
return lt.cli.config_from_args(args)
return load_patched_config(args.config)

View file

@ -20,6 +20,7 @@ import logging
from importlib.metadata import version from importlib.metadata import version
import tomllib import tomllib
from functools import wraps from functools import wraps
import json
from pydantic import BaseModel from pydantic import BaseModel
import numpy as np import numpy as np
@ -430,3 +431,64 @@ def merge_patch(
# Else, use the patch value # Else, use the patch value
result[key] = patch_val result[key] = patch_val
return result return result
def load_patched_config(config_path: str) -> dict:
"""Load a json configuration file, if it a patch, return the full config.
Load a json file. If it does not contains the "base_config_file" key then return
the data as read.
If the configuration specifies a "base_config_file" but no "patch" then return the
data from reading base_config_file as json.
If the configuration specifies a "base_config_file" and "patch" then apply the
patch to the data in base_config_file and return the result. The patching method
is merge_patch()
:param config_path: The path of the configuration file.
:return: The contents of the configuration file after loading and patching the
specified base configuration file if applicable.
"""
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
except IOError as e:
raise type(e)(f"Couldn't load configuration file {config_path}") from e
# If base_config_file not specified then this is a normal LabThings config file
# Just return it.
if "base_config_file" not in config:
return config
config_dir = os.path.dirname(os.path.abspath(config_path))
base_conf_path = config["base_config_file"]
base_conf_path = resolve_path_from_dir(base_conf_path, config_dir)
try:
with open(base_conf_path, "r", encoding="utf-8") as f:
base_config = json.load(f)
except IOError as e:
raise type(e)(f"Couldn't load configuration file {base_conf_path}") from e
if "patch" in config:
return merge_patch(base_config, config["patch"], enforce_dict=True)
return base_config
def resolve_path_from_dir(path: str, directory: str) -> str:
"""Convert a relative or abs path specified in one dir to the working dir.
This also expands any environment variables.
:param path: The specified path (could be absolute or relative)
:param directory: The directory from which the path was specified
:return: The normalised path.
"""
path = os.path.expandvars(os.path.expanduser(path))
if not os.path.isabs(path):
path = os.path.join(directory, path)
return os.path.normpath(path)