from collective.transmute import _types as t
from copy import deepcopy
from dynaconf import Dynaconf
from dynaconf import Validator
from dynaconf.base import Settings
from pathlib import Path
SETTINGS_FILE = "transmute.toml"
[docs]
def _as_set(value: list) -> set:
"""
Cast a list value to a set.
Parameters
----------
value : list
The list to cast.
Returns
-------
set
The set containing the elements of the list.
Example
-------
.. code-block:: pycon
>>> _as_set([1, 2, 2])
{1, 2}
"""
value = value if value else []
return set(value)
[docs]
def _as_tuple(value: list) -> tuple:
"""
Cast a list value to a tuple.
Parameters
----------
value : list
The list to cast.
Returns
-------
tuple
The tuple containing the elements of the list.
Example
-------
.. code-block:: pycon
>>> _as_tuple([1, 2, 3])
(1, 2, 3)
"""
value = value if value else []
return tuple(value)
_SET_SETTINGS = {"cast": _as_set, "default": set()}
_TUPLE_SETTINGS = {"cast": _as_tuple, "default": ()}
_VALIDATORS: dict[str, list[dict]] = {
"pipeline.steps": [{"len_min": 1}, _TUPLE_SETTINGS],
"pipeline.prepare_steps": [{"len_min": 0}, _TUPLE_SETTINGS],
"pipeline.report_steps": [{"len_min": 1}, _TUPLE_SETTINGS],
"pipeline.do_not_add_drop": [
_TUPLE_SETTINGS,
],
"principals.remove": [
_TUPLE_SETTINGS,
],
"default_pages.keys_from_parent": [
_TUPLE_SETTINGS,
],
"review_state.filter.allowed": [
_TUPLE_SETTINGS,
],
"paths.filter.allowed": [
_SET_SETTINGS,
],
"paths.filter.drop": [
_SET_SETTINGS,
],
"paths.export_prefixes": [
_TUPLE_SETTINGS,
],
"images.to_preview_image_link": [
_TUPLE_SETTINGS,
],
"sanitize.drop_keys": [
_TUPLE_SETTINGS,
],
"sanitize.block_keys": [
_TUPLE_SETTINGS,
],
}
[docs]
def settings_validators() -> tuple[Validator, ...]:
"""
Return a tuple of ``dynaconf.Validator`` objects for settings validation.
Returns
-------
tuple[Validator, ...]
Validators for the settings structure and types.
Example
-------
.. code-block:: pycon
>>> validators = settings_validators()
>>> isinstance(validators[0], Validator)
True
"""
validators = []
for key, checks in _VALIDATORS.items():
for kwargs in checks:
validators.append(Validator(key, **kwargs))
return tuple(validators)
[docs]
def _find_config_path(settings: Settings | Dynaconf) -> Path:
"""
Return the absolute path to the transmute.toml config file.
Parameters
----------
settings : Settings or Dynaconf
The dynaconf settings object.
Returns
-------
Path
The resolved path to the config file.
"""
settings_path = Path(settings.find_file(SETTINGS_FILE))
return settings_path.resolve()
[docs]
def parse_default() -> dict:
"""
Parse and return the default transmute settings from ``default.toml``.
Returns
-------
dict
The default settings as a dictionary.
"""
validators = settings_validators()
cwd_path = Path(__file__).parent
settings_file = cwd_path / "default.toml"
settings = Dynaconf(
settings_files=[settings_file],
merge_enabled=False,
validators=validators,
)
return settings.as_dict()
[docs]
def parse_config(cwd_path: Path) -> dict:
"""
Parse and return the transmute config settings from ``transmute.toml``.
Parameters
----------
cwd_path : Path
The current working directory.
Returns
-------
dict
The config settings as a dictionary.
"""
settings = Dynaconf(
envvar_prefix="TRANSMUTE",
root_path=cwd_path,
settings_files=[SETTINGS_FILE],
merge_enabled=False,
)
filepath = _find_config_path(settings)
if filepath.is_dir():
raise FileNotFoundError(
f"Transmute settings file '{SETTINGS_FILE}' not found in {cwd_path}."
)
settings.config.filepath = str(filepath)
return settings.as_dict()
[docs]
def _merge_dicts(defaults: dict, settings: dict) -> dict:
"""
Merge two dictionaries, with settings overriding defaults.
If a key exists in both and values are dicts, they are merged recursively.
Otherwise, the value from settings overwrites the value from defaults.
Parameters
----------
defaults : dict
The default dictionary.
settings : dict
The settings dictionary.
Returns
-------
dict
The merged dictionary.
"""
result = defaults.copy()
for key, value in settings.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = _merge_dicts(result[key], value)
else:
result[key] = value
return result
[docs]
def _update_value(data: dict, key: str, cast) -> dict:
"""
Update a nested value in a dictionary using dot notation and apply a cast function.
Parameters
----------
data : dict
The dictionary to update.
key : str
The dot-separated key path.
cast : callable
The function to cast the value.
Returns
-------
dict
The updated dictionary.
"""
parts = key.split(".")
value = data
parent = data
for part in parts:
parent = value
value = value[part]
parent[part] = cast(value)
return data
[docs]
def _merge_defaults(defaults: dict, settings: dict) -> dict:
"""
Merge default settings with user settings and apply validators.
Parameters
----------
defaults : dict
The default settings dictionary.
settings : dict
The user settings dictionary.
Returns
-------
dict
The merged and validated settings dictionary.
"""
merged = {k.lower(): v for k, v in defaults.items()}
settings = {k.lower(): v for k, v in settings.items()}
for key, value in settings.items():
if isinstance(value, dict) and key in merged:
merged[key] = _merge_dicts(merged[key], value)
else:
merged[key] = value
# Store the raw data to be used with cli settings command.
raw_data = deepcopy(merged)
raw_data["config"].pop("filepath", None)
merged["_raw_data"] = raw_data
# Apply validators to the merged settings.
for key, checks in _VALIDATORS.items():
for kwargs in checks:
if cast := kwargs.get("cast"):
merged = _update_value(merged, key, cast)
return merged
[docs]
def get_settings(cwd_path: Path | None = None) -> t.TransmuteSettings:
"""
Get the transmute settings, merging defaults and user config.
Parameters
----------
cwd_path : Path or None, optional
The current working directory. If ``None``, uses ``Path.cwd()``.
Returns
-------
TransmuteSettings
The settings object for ``collective.transmute``.
Example
-------
.. code-block:: pycon
>>> from pathlib import Path
>>> settings = get_settings(Path("/project"))
>>> print(settings.config)
"""
cwd_path = Path.cwd() if cwd_path is None else cwd_path
defaults = parse_default()
raw_settings = parse_config(cwd_path)
payload = _merge_defaults(defaults, raw_settings)
payload["config"]["prepare_data_location"] = Path(
payload["config"]["prepare_data_location"]
)
payload["config"]["reports_location"] = Path(payload["config"]["reports_location"])
data = t.TransmuteSettings(**payload)
return data
[docs]
def get_default_settings() -> t.TransmuteSettings:
"""
Return the default settings used by ``collective.transmute``.
Returns
-------
TransmuteSettings
The default settings object for ``collective.transmute``.
Example
-------
.. code-block:: pycon
>>> settings = get_default_settings()
>>> print(settings.config)
"""
defaults = _merge_defaults(parse_default(), {})
data = t.TransmuteSettings(**defaults)
return data