Source code for searx.settings_loader

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Implementations for loading configurations from YAML files.  This essentially
includes the configuration of the (:ref:`SearXNG appl <searxng settings.yml>`)
server. The default configuration for the application server is loaded from the
:origin:`DEFAULT_SETTINGS_FILE <searx/settings.yml>`.  This default
configuration can be completely replaced or :ref:`customized individually
<use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environment
variable can be used to set the location from which the local customizations are
to be loaded. The rules used for this can be found in the
:py:obj:`get_user_cfg_folder` function.

- By default, local configurations are expected in folder ``/etc/searxng`` from
  where applications can load them with the :py:obj:`get_yaml_cfg` function.

- By default, customized :ref:`SearXNG appl <searxng settings.yml>` settings are
  expected in a file named ``settings.yml``.

"""

from __future__ import annotations

import os.path
from collections.abc import Mapping
from itertools import filterfalse
from pathlib import Path

import yaml

from searx.exceptions import SearxSettingsException

searx_dir = os.path.abspath(os.path.dirname(__file__))

SETTINGS_YAML = Path("settings.yml")
DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML
"""The :origin:`searx/settings.yml` file with all the default settings."""


[docs] def load_yaml(file_name: str | Path): """Load YAML config from a file.""" try: with open(file_name, 'r', encoding='utf-8') as settings_yaml: return yaml.safe_load(settings_yaml) or {} except IOError as e: raise SearxSettingsException(e, str(file_name)) from e except yaml.YAMLError as e: raise SearxSettingsException(e, str(file_name)) from e
[docs] def get_yaml_cfg(file_name: str | Path) -> dict: """Shortcut to load a YAML config from a file, located in the - :py:obj:`get_user_cfg_folder` or - in the ``searx`` folder of the SearXNG installation """ folder = get_user_cfg_folder() or Path(searx_dir) fname = folder / file_name if not fname.is_file(): raise FileNotFoundError(f"File {fname} does not exist!") return load_yaml(fname)
[docs] def get_user_cfg_folder() -> Path | None: """Returns folder where the local configurations are located. 1. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a folder (e.g. ``/etc/mysxng/``), all local configurations are expected in this folder. The settings of the :ref:`SearXNG appl <searxng settings.yml>` then expected in ``settings.yml`` (e.g. ``/etc/mysxng/settings.yml``). 2. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a file (e.g. ``/etc/mysxng/myinstance.yml``), this file contains the settings of the :ref:`SearXNG appl <searxng settings.yml>` and the folder (e.g. ``/etc/mysxng/``) is used for all other configurations. This type (``SEARXNG_SETTINGS_PATH`` points to a file) is suitable for use cases in which different profiles of the :ref:`SearXNG appl <searxng settings.yml>` are to be managed, such as in test scenarios. 3. If folder ``/etc/searxng`` exists, it is used. In case none of the above path exists, ``None`` is returned. In case of environment ``SEARXNG_SETTINGS_PATH`` is set, but the (folder or file) does not exists, a :py:obj:`EnvironmentError` is raised. """ folder = None settings_path = os.environ.get("SEARXNG_SETTINGS_PATH") # Disable default /etc/searxng is intended exclusively for internal testing purposes # and is therefore not documented! disable_etc = os.environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true') if settings_path: # rule 1. and 2. settings_path = Path(settings_path) if settings_path.is_dir(): folder = settings_path elif settings_path.is_file(): folder = settings_path.parent else: raise EnvironmentError(1, f"{settings_path} not exists!", settings_path) if not folder and not disable_etc: # default: rule 3. folder = Path("/etc/searxng") if not folder.is_dir(): folder = None return folder
def update_dict(default_dict, user_dict): for k, v in user_dict.items(): if isinstance(v, Mapping): default_dict[k] = update_dict(default_dict.get(k, {}), v) else: default_dict[k] = v return default_dict def update_settings(default_settings: dict, user_settings: dict): # pylint: disable=too-many-branches # merge everything except the engines for k, v in user_settings.items(): if k not in ('use_default_settings', 'engines'): if k in default_settings and isinstance(v, Mapping): update_dict(default_settings[k], v) else: default_settings[k] = v categories_as_tabs = user_settings.get('categories_as_tabs') if categories_as_tabs: default_settings['categories_as_tabs'] = categories_as_tabs # parse the engines remove_engines = None keep_only_engines = None use_default_settings = user_settings.get('use_default_settings') if isinstance(use_default_settings, dict): remove_engines = use_default_settings.get('engines', {}).get('remove') keep_only_engines = use_default_settings.get('engines', {}).get('keep_only') if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None: engines = default_settings['engines'] # parse "use_default_settings.engines.remove" if remove_engines is not None: engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines)) # parse "use_default_settings.engines.keep_only" if keep_only_engines is not None: engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines)) # parse "engines" user_engines = user_settings.get('engines') if user_engines: engines_dict = dict((definition['name'], definition) for definition in engines) for user_engine in user_engines: default_engine = engines_dict.get(user_engine['name']) if default_engine: update_dict(default_engine, user_engine) else: engines.append(user_engine) # store the result default_settings['engines'] = engines return default_settings def is_use_default_settings(user_settings): use_default_settings = user_settings.get('use_default_settings') if use_default_settings is True: return True if isinstance(use_default_settings, dict): return True if use_default_settings is False or use_default_settings is None: return False raise ValueError('Invalid value for use_default_settings')
[docs] def load_settings(load_user_settings=True) -> tuple[dict, str]: """Function for loading the settings of the SearXNG application (:ref:`settings.yml <searxng settings.yml>`).""" msg = f"load the default settings from {DEFAULT_SETTINGS_FILE}" cfg = load_yaml(DEFAULT_SETTINGS_FILE) cfg_folder = get_user_cfg_folder() if not load_user_settings or not cfg_folder: return cfg, msg settings_yml = os.environ.get("SEARXNG_SETTINGS_PATH") if settings_yml and Path(settings_yml).is_file(): # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a file settings_yml = Path(settings_yml).name else: # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a folder settings_yml = SETTINGS_YAML cfg_file = cfg_folder / settings_yml if not cfg_file.exists(): return cfg, msg msg = f"load the user settings from {cfg_file}" user_cfg = load_yaml(cfg_file) if is_use_default_settings(user_cfg): # the user settings are merged with the default configuration msg = f"merge the default settings ( {DEFAULT_SETTINGS_FILE} ) and the user settings ( {cfg_file} )" update_settings(cfg, user_cfg) else: cfg = user_cfg return cfg, msg