Skip to content

Commit

Permalink
Refactor configuration management.
Browse files Browse the repository at this point in the history
  • Loading branch information
ebertin committed Oct 4, 2024
1 parent 5eee624 commit af5f627
Show file tree
Hide file tree
Showing 7 changed files with 746 additions and 99 deletions.
48 changes: 24 additions & 24 deletions src/visiomatic/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from .. import package

from . import config
from .config import config_filename, image_filename, settings

from .tiled import (
colordict,
Expand All @@ -37,7 +37,7 @@
from .cache import LRUCache, LRUSharedRWCache

# True with multiple workers (multiprocessing).
shared = config.settings["workers"] > 1 and not config.settings["reload"]
shared = settings["workers"] > 1 and not settings["reload"]

# Prepare the RegExps; note that those with non-capturing groups do not
# work with Rust
Expand Down Expand Up @@ -78,39 +78,39 @@ def create_app() -> FastAPI:

worker_id = getpid()

banner_template = config.settings["banner_template"]
base_template = config.settings["base_template"]
template_dir = path.abspath(config.settings["template_dir"])
cache_dir = path.abspath(config.settings["cache_dir"])
client_dir = path.abspath(config.settings["client_dir"])
data_dir = path.abspath(config.settings["data_dir"])
extra_dir = path.abspath(config.settings["extra_dir"])
doc_dir = config.settings["doc_dir"]
doc_path = config.settings["doc_path"]
userdoc_url = config.settings["userdoc_url"]
api_path = config.settings["api_path"]
brightness = config.settings["brightness"]
contrast = config.settings["contrast"]
color_saturation = config.settings["color_saturation"]
gamma = config.settings["gamma"]
quality = config.settings["quality"]
tile_size = config.settings["tile_size"]
image_argname = config.image_filename
banner_template = settings["banner_template"]
base_template = settings["base_template"]
template_dir = path.abspath(settings["template_dir"])
cache_dir = path.abspath(settings["cache_dir"])
client_dir = path.abspath(settings["client_dir"])
data_dir = path.abspath(settings["data_dir"])
extra_dir = path.abspath(settings["extra_dir"])
doc_dir = settings["doc_dir"]
doc_path = settings["doc_path"]
userdoc_url = settings["userdoc_url"]
api_path = settings["api_path"]
brightness = settings["brightness"]
contrast = settings["contrast"]
color_saturation = settings["color_saturation"]
gamma = settings["gamma"]
quality = settings["quality"]
tile_size = settings["tile_size"]
image_argname = image_filename

logger = logging.getLogger("uvicorn.error")

# Provide an endpoint for the user's manual (if it exists)
if config.config_filename:
logger.info(f"Configuration read from {config.config_filename}.")
if config_filename:
logger.info(f"Configuration read from {config_filename}.")
else:
logger.warning(
f"Configuration file not found: {config.config_filename}!"
f"Configuration file not found: {config_filename}!"
)
# Get shared lock dictionary if processing in parallel
cache = LRUSharedRWCache(
pickledTiled,
name=f"{package.title}.{getppid()}",
maxsize=config.settings["max_cache_image_count"],
maxsize=settings["max_cache_image_count"],
removecall=delTiled,
shared=shared,
logger=logger
Expand Down
57 changes: 57 additions & 0 deletions src/visiomatic/server/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Configure application.
"""
# Copyright CEA/CFHT/CNRS/UParisSaclay
# Licensed under the MIT licence

from sys import exit, modules
from typing import Any

from .config import Config
from .settings import AppSettings

# Initialize global dictionary
# Set up settings by instantiating a configuration object
config = Config(AppSettings())
config_filename = None
settings = config.flat_dict()


def override(key: str, value: Any) -> Any:
"""
Convenience function that returns the input value unless it is None,
in which case the settings value from key is returned.
Examples
--------
```python
from .config import override
print(f"{override('a_setting', 10)}")
#> 10
print(f"{override('a_setting', None)}")
#> 3
Parameters
----------
key: str
Key to settings value.
value: Any
Input value.
Returns
-------
value: Any
Input value or settings value.
"""
return settings[key] if value is None else value



if 'sphinx' not in modules and 'pytest' not in modules:
config_filename = config.config_filename
image_filename = config.image_filename

Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
# Copyright CEA/CFHT/CNRS/UParisSaclay
# Licensed under the MIT licence

from argparse import ArgumentParser, SUPPRESS
from configparser import ConfigParser
from os import makedirs, path
from pathlib import Path
from pprint import pprint
from sys import exit, modules
from time import localtime, strftime
from typing import Tuple
from argparse import ArgumentParser, SUPPRESS
from configparser import ConfigParser

from astropy import units as u #type: ignore[import-untyped]
from pydantic import ValidationError

from .. import package
from ... import package
from .quantity import str_to_quantity_array
from .settings import AppSettings


Expand All @@ -23,16 +27,20 @@ class Config(object):
Settings are stored as Pydantic fields.
"""
def __init__(self, args: bool=True, config_file: str=package.config_file):
def __init__(
self,
settings: AppSettings,
args: bool=True,
config_file: str=package.config_file):

self.settings = AppSettings()
self.settings = settings
self.groups = tuple(self.settings.dict().keys())
self.image_filename = None
self.config_filename = config_file

# Skip argument parsing and stuff if Sphinx or PyTest are involved
# Skip argument parsing if Sphinx or PyTest are involved
if 'sphinx' in modules or 'pytest' in modules:
return
args = False
# Parse command line
if args:
args_dict = self.parse_args()
Expand All @@ -46,6 +54,12 @@ def __init__(self, args: bool=True, config_file: str=package.config_file):
self.save_config(self.config_filename)
exit(0)
self.config_filename = args_dict['config']
image_filename = args_dict['file']
if Path(image_filename).exists():
self.image_filename = image_filename
else:
exit(f"*Error*: {image_filename} not found!")


# Parse configuration file
if Path(self.config_filename).exists():
Expand All @@ -56,14 +70,10 @@ def __init__(self, args: bool=True, config_file: str=package.config_file):
# Update settings from the command line (overriding config file values)
if args:
self.update_from_dict(args_dict)

image_filename = args_dict['file']
if Path(image_filename).exists():
self.image_filename = image_filename
else:
exit(f"*Error*: {image_filename} not found!")


if args_dict['show_config']:
pprint(self.flat_dict())


def grouped_dict(self) -> dict:
"""
Return a dictionary of all settings, organized in groups.
Expand Down Expand Up @@ -134,72 +144,92 @@ def parse_args(self) -> dict:
gdict: dict
Dictionary of all settings, organized in groups.
"""
config = ArgumentParser(
parser = ArgumentParser(
description=f"{package.title} v{package.version} : {package.summary}"
)
# Add options not relevant to configuration itself
config.add_argument(
parser.add_argument(
"-V", "--version",
default=False,
help="Return the version of the package and exit",
action='store_true'
)
config.add_argument(
parser.add_argument(
"-c", "--config",
type=str, default=package.config_file,
help=f"Configuration filename (default={package.config_file})",
metavar="FILE"
)
config.add_argument(
parser.add_argument(
"-s", "--save_config",
default=False,
help=f"Save a default {package.title} configuration file and exit",
action='store_true'
)
config.add_argument(
parser.add_argument(
"-S", "--show_config",
default=False,
help=f"Print the actual {package.title} configuration settings",
action='store_true'
)
parser.add_argument(
"file",
default="",
type=str,
help="FITS image filename",
nargs="?"
)
for group in self.groups:
args_group = config.add_argument_group(group.title())
settings = getattr(self.settings, group).schema()['properties']
args_group = parser.add_argument_group(group.title())
groupsettings = getattr(self.settings, group)
settings = groupsettings.schema()['properties']
defaults = groupsettings.dict()
for setting in settings:
props = settings[setting]
arg = ["-" + props['short'], "--" + setting] \
if props.get('short') else ["--" + setting]
default = props['default']
if props['type']=='boolean':
default = defaults[setting]
# Booleans don't have units
help = props.get('description', "")
if props.get('type', 'unit')=='boolean':
args_group.add_argument(
*arg,
default=SUPPRESS,
help=props['description'],
help=props.get('description', ""),
action='store_true'
)
elif props['type']=='array':
elif props.get('type', 'unit')=='array':
deftype = type(default[0])
args_group.add_argument(
*arg,
default=SUPPRESS,
type=lambda s: tuple([deftype(val) for val in s.split(',')]),
help=f"{props['description']} (default={props['default']})"
type=(lambda s: tuple([int(val) for val in s.split(',')]))
if deftype==int
else (lambda s: tuple([float(val) for val in s.split(',')])),
help=f"{help} (default={default})"
)
elif isinstance(default, u.Quantity):
args_group.add_argument(
*arg,
default=SUPPRESS,
type=u.Quantity if default.isscalar else str_to_quantity_array,
help=f"{help} (default={default})"
)
else:
args_group.add_argument(
*arg,
default=SUPPRESS,
type=type(default),
help=f"{props['description']} (default={props['default']})"
help=f"{help} (default={default})"
)
# Generate dictionary of args grouped by section
fdict = vars(config.parse_args())
fdict = vars(parser.parse_args())
gdict = {}
# Command-line specific arguments
gdict['version'] = fdict['version']
gdict['config'] = fdict['config']
gdict['save_config'] = fdict['save_config']
gdict['show_config'] = fdict['show_config']
gdict['file'] = fdict['file']
for group in self.groups:
gdict[group] = {}
Expand Down Expand Up @@ -237,12 +267,15 @@ def parse_config(self, filename: str) -> dict:
settings = getattr(self.settings, group).dict()
for setting in settings:
if (value := config.get(group, setting, fallback=None)) is not None:
stype = type(settings[setting])
default = settings[setting]
stype = type(default)
gdictg[setting] = tuple(
type(settings[setting][i])(val.strip()) \
for i, val in enumerate(value[1:-1].split(','))
) if stype == tuple \
else value.lower() in ("yes", "true", "t", "1") if stype == bool \
else str_to_quantity_array(value) if \
isinstance(default, u.Quantity) and not default.isscalar \
else stype(value)
return gdict

Expand Down Expand Up @@ -305,13 +338,3 @@ def update_from_dict(self, settings_dict) -> None:
print(other_exception)
exit(1)

# Initialize global dictionary
# Set up settings by instantiating a configuration object
config = Config()
config_filename = None
image_filename = None
settings = config.flat_dict()
if 'sphinx' not in modules and 'pytest' not in modules:
config_filename = config.config_filename
image_filename = config.image_filename

Loading

0 comments on commit af5f627

Please sign in to comment.