.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
settings_loader.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Implementations for loading configurations from YAML files. This essentially
3includes the configuration of the (:ref:`SearXNG appl <searxng settings.yml>`)
4server. The default configuration for the application server is loaded from the
5:origin:`DEFAULT_SETTINGS_FILE <searx/settings.yml>`. This default
6configuration can be completely replaced or :ref:`customized individually
7<use_default_settings.yml>` and the ``SEARXNG_SETTINGS_PATH`` environment
8variable can be used to set the location from which the local customizations are
9to be loaded. The rules used for this can be found in the
10:py:obj:`get_user_cfg_folder` function.
11
12- By default, local configurations are expected in folder ``/etc/searxng`` from
13 where applications can load them with the :py:obj:`get_yaml_cfg` function.
14
15- By default, customized :ref:`SearXNG appl <searxng settings.yml>` settings are
16 expected in a file named ``settings.yml``.
17
18"""
19
20from __future__ import annotations
21
22import os.path
23from collections.abc import Mapping
24from itertools import filterfalse
25from pathlib import Path
26
27import yaml
28
29from searx.exceptions import SearxSettingsException
30
31searx_dir = os.path.abspath(os.path.dirname(__file__))
32
33SETTINGS_YAML = Path("settings.yml")
34DEFAULT_SETTINGS_FILE = Path(searx_dir) / SETTINGS_YAML
35"""The :origin:`searx/settings.yml` file with all the default settings."""
36
37
38def load_yaml(file_name: str | Path):
39 """Load YAML config from a file."""
40 try:
41 with open(file_name, 'r', encoding='utf-8') as settings_yaml:
42 return yaml.safe_load(settings_yaml) or {}
43 except IOError as e:
44 raise SearxSettingsException(e, str(file_name)) from e
45 except yaml.YAMLError as e:
46 raise SearxSettingsException(e, str(file_name)) from e
47
48
49def get_yaml_cfg(file_name: str | Path) -> dict:
50 """Shortcut to load a YAML config from a file, located in the
51
52 - :py:obj:`get_user_cfg_folder` or
53 - in the ``searx`` folder of the SearXNG installation
54 """
55
56 folder = get_user_cfg_folder() or Path(searx_dir)
57 fname = folder / file_name
58 if not fname.is_file():
59 raise FileNotFoundError(f"File {fname} does not exist!")
60
61 return load_yaml(fname)
62
63
64def get_user_cfg_folder() -> Path | None:
65 """Returns folder where the local configurations are located.
66
67 1. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a
68 folder (e.g. ``/etc/mysxng/``), all local configurations are expected in
69 this folder. The settings of the :ref:`SearXNG appl <searxng
70 settings.yml>` then expected in ``settings.yml``
71 (e.g. ``/etc/mysxng/settings.yml``).
72
73 2. If the ``SEARXNG_SETTINGS_PATH`` environment is set and points to a file
74 (e.g. ``/etc/mysxng/myinstance.yml``), this file contains the settings of
75 the :ref:`SearXNG appl <searxng settings.yml>` and the folder
76 (e.g. ``/etc/mysxng/``) is used for all other configurations.
77
78 This type (``SEARXNG_SETTINGS_PATH`` points to a file) is suitable for
79 use cases in which different profiles of the :ref:`SearXNG appl <searxng
80 settings.yml>` are to be managed, such as in test scenarios.
81
82 3. If folder ``/etc/searxng`` exists, it is used.
83
84 In case none of the above path exists, ``None`` is returned. In case of
85 environment ``SEARXNG_SETTINGS_PATH`` is set, but the (folder or file) does
86 not exists, a :py:obj:`EnvironmentError` is raised.
87
88 """
89
90 folder = None
91 settings_path = os.environ.get("SEARXNG_SETTINGS_PATH")
92
93 # Disable default /etc/searxng is intended exclusively for internal testing purposes
94 # and is therefore not documented!
95 disable_etc = os.environ.get('SEARXNG_DISABLE_ETC_SETTINGS', '').lower() in ('1', 'true')
96
97 if settings_path:
98 # rule 1. and 2.
99 settings_path = Path(settings_path)
100 if settings_path.is_dir():
101 folder = settings_path
102 elif settings_path.is_file():
103 folder = settings_path.parent
104 else:
105 raise EnvironmentError(1, f"{settings_path} not exists!", settings_path)
106
107 if not folder and not disable_etc:
108 # default: rule 3.
109 folder = Path("/etc/searxng")
110 if not folder.is_dir():
111 folder = None
112
113 return folder
114
115
116def update_dict(default_dict, user_dict):
117 for k, v in user_dict.items():
118 if isinstance(v, Mapping):
119 default_dict[k] = update_dict(default_dict.get(k, {}), v)
120 else:
121 default_dict[k] = v
122 return default_dict
123
124
125def update_settings(default_settings: dict, user_settings: dict):
126 # pylint: disable=too-many-branches
127
128 # merge everything except the engines
129 for k, v in user_settings.items():
130 if k not in ('use_default_settings', 'engines'):
131 if k in default_settings and isinstance(v, Mapping):
132 update_dict(default_settings[k], v)
133 else:
134 default_settings[k] = v
135
136 categories_as_tabs = user_settings.get('categories_as_tabs')
137 if categories_as_tabs:
138 default_settings['categories_as_tabs'] = categories_as_tabs
139
140 # parse the engines
141 remove_engines = None
142 keep_only_engines = None
143 use_default_settings = user_settings.get('use_default_settings')
144 if isinstance(use_default_settings, dict):
145 remove_engines = use_default_settings.get('engines', {}).get('remove')
146 keep_only_engines = use_default_settings.get('engines', {}).get('keep_only')
147
148 if 'engines' in user_settings or remove_engines is not None or keep_only_engines is not None:
149 engines = default_settings['engines']
150
151 # parse "use_default_settings.engines.remove"
152 if remove_engines is not None:
153 engines = list(filterfalse(lambda engine: (engine.get('name')) in remove_engines, engines))
154
155 # parse "use_default_settings.engines.keep_only"
156 if keep_only_engines is not None:
157 engines = list(filter(lambda engine: (engine.get('name')) in keep_only_engines, engines))
158
159 # parse "engines"
160 user_engines = user_settings.get('engines')
161 if user_engines:
162 engines_dict = dict((definition['name'], definition) for definition in engines)
163 for user_engine in user_engines:
164 default_engine = engines_dict.get(user_engine['name'])
165 if default_engine:
166 update_dict(default_engine, user_engine)
167 else:
168 engines.append(user_engine)
169
170 # store the result
171 default_settings['engines'] = engines
172
173 return default_settings
174
175
176def is_use_default_settings(user_settings):
177
178 use_default_settings = user_settings.get('use_default_settings')
179 if use_default_settings is True:
180 return True
181 if isinstance(use_default_settings, dict):
182 return True
183 if use_default_settings is False or use_default_settings is None:
184 return False
185 raise ValueError('Invalid value for use_default_settings')
186
187
188def load_settings(load_user_settings=True) -> tuple[dict, str]:
189 """Function for loading the settings of the SearXNG application
190 (:ref:`settings.yml <searxng settings.yml>`)."""
191
192 msg = f"load the default settings from {DEFAULT_SETTINGS_FILE}"
193 cfg = load_yaml(DEFAULT_SETTINGS_FILE)
194 cfg_folder = get_user_cfg_folder()
195
196 if not load_user_settings or not cfg_folder:
197 return cfg, msg
198
199 settings_yml = os.environ.get("SEARXNG_SETTINGS_PATH")
200 if settings_yml and Path(settings_yml).is_file():
201 # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a file
202 settings_yml = Path(settings_yml).name
203 else:
204 # see get_user_cfg_folder() --> SEARXNG_SETTINGS_PATH points to a folder
205 settings_yml = SETTINGS_YAML
206
207 cfg_file = cfg_folder / settings_yml
208 if not cfg_file.exists():
209 return cfg, msg
210
211 msg = f"load the user settings from {cfg_file}"
212 user_cfg = load_yaml(cfg_file)
213
214 if is_use_default_settings(user_cfg):
215 # the user settings are merged with the default configuration
216 msg = f"merge the default settings ( {DEFAULT_SETTINGS_FILE} ) and the user settings ( {cfg_file} )"
217 update_settings(cfg, user_cfg)
218 else:
219 cfg = user_cfg
220
221 return cfg, msg
Path|None get_user_cfg_folder()
is_use_default_settings(user_settings)
dict get_yaml_cfg(str|Path file_name)
tuple[dict, str] load_settings(load_user_settings=True)
update_settings(dict default_settings, dict user_settings)
load_yaml(str|Path file_name)
update_dict(default_dict, user_dict)