.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
traits.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2"""Engine's traits are fetched from the origin engines and stored in a JSON file
3in the *data folder*. Most often traits are languages and region codes and
4their mapping from SearXNG's representation to the representation in the origin
5search engine. For new traits new properties can be added to the class
6:py:class:`EngineTraits`.
7
8To load traits from the persistence :py:obj:`EngineTraitsMap.from_data` can be
9used.
10"""
11
12from __future__ import annotations
13import json
14import dataclasses
15import types
16from typing import Dict, Literal, Iterable, Union, Callable, Optional, TYPE_CHECKING
17
18from searx import locales
19from searx.data import data_dir, ENGINE_TRAITS
20
21if TYPE_CHECKING:
22 from . import Engine
23
24
25class EngineTraitsEncoder(json.JSONEncoder):
26 """Encodes :class:`EngineTraits` to a serializable object, see
27 :class:`json.JSONEncoder`."""
28
29 def default(self, o):
30 """Return dictionary of a :class:`EngineTraits` object."""
31 if isinstance(o, EngineTraits):
32 return o.__dict__
33 return super().default(o)
34
35
36@dataclasses.dataclass
38 """The class is intended to be instantiated for each engine."""
39
40 regions: Dict[str, str] = dataclasses.field(default_factory=dict)
41 """Maps SearXNG's internal representation of a region to the one of the engine.
42
43 SearXNG's internal representation can be parsed by babel and the value is
44 send to the engine:
45
46 .. code:: python
47
48 regions ={
49 'fr-BE' : <engine's region name>,
50 }
51
52 for key, egnine_region regions.items():
53 searxng_region = babel.Locale.parse(key, sep='-')
54 ...
55 """
56
57 languages: Dict[str, str] = dataclasses.field(default_factory=dict)
58 """Maps SearXNG's internal representation of a language to the one of the engine.
59
60 SearXNG's internal representation can be parsed by babel and the value is
61 send to the engine:
62
63 .. code:: python
64
65 languages = {
66 'ca' : <engine's language name>,
67 }
68
69 for key, egnine_lang in languages.items():
70 searxng_lang = babel.Locale.parse(key)
71 ...
72 """
73
74 all_locale: Optional[str] = None
75 """To which locale value SearXNG's ``all`` language is mapped (shown a "Default
76 language").
77 """
78
79 data_type: Literal['traits_v1'] = 'traits_v1'
80 """Data type, default is 'traits_v1'.
81 """
82
83 custom: Dict[str, Union[Dict[str, Dict], Iterable[str]]] = dataclasses.field(default_factory=dict)
84 """A place to store engine's custom traits, not related to the SearXNG core.
85 """
86
87 def get_language(self, searxng_locale: str, default=None):
88 """Return engine's language string that *best fits* to SearXNG's locale.
89
90 :param searxng_locale: SearXNG's internal representation of locale
91 selected by the user.
92
93 :param default: engine's default language
94
95 The *best fits* rules are implemented in
96 :py:obj:`searx.locales.get_engine_locale`. Except for the special value ``all``
97 which is determined from :py:obj:`EngineTraits.all_locale`.
98 """
99 if searxng_locale == 'all' and self.all_locale is not None:
100 return self.all_locale
101 return locales.get_engine_locale(searxng_locale, self.languages, default=default)
102
103 def get_region(self, searxng_locale: str, default=None):
104 """Return engine's region string that best fits to SearXNG's locale.
105
106 :param searxng_locale: SearXNG's internal representation of locale
107 selected by the user.
108
109 :param default: engine's default region
110
111 The *best fits* rules are implemented in
112 :py:obj:`searx.locales.get_engine_locale`. Except for the special value ``all``
113 which is determined from :py:obj:`EngineTraits.all_locale`.
114 """
115 if searxng_locale == 'all' and self.all_locale is not None:
116 return self.all_locale
117 return locales.get_engine_locale(searxng_locale, self.regions, default=default)
118
119 def is_locale_supported(self, searxng_locale: str) -> bool:
120 """A *locale* (SearXNG's internal representation) is considered to be
121 supported by the engine if the *region* or the *language* is supported
122 by the engine.
123
124 For verification the functions :py:func:`EngineTraits.get_region` and
125 :py:func:`EngineTraits.get_language` are used.
126 """
127 if self.data_typedata_type == 'traits_v1':
128 return bool(self.get_region(searxng_locale) or self.get_language(searxng_locale))
129
130 raise TypeError('engine traits of type %s is unknown' % self.data_typedata_type)
131
132 def copy(self):
133 """Create a copy of the dataclass object."""
134 return EngineTraits(**dataclasses.asdict(self))
135
136 @classmethod
137 def fetch_traits(cls, engine: Engine) -> Union['EngineTraits', None]:
138 """Call a function ``fetch_traits(engine_traits)`` from engines namespace to fetch
139 and set properties from the origin engine in the object ``engine_traits``. If
140 function does not exists, ``None`` is returned.
141 """
142
143 fetch_traits = getattr(engine, 'fetch_traits', None)
144 engine_traits = None
145
146 if fetch_traits:
147 engine_traits = cls()
148 fetch_traits(engine_traits)
149 return engine_traits
150
151 def set_traits(self, engine: Engine):
152 """Set traits from self object in a :py:obj:`.Engine` namespace.
153
154 :param engine: engine instance build by :py:func:`searx.engines.load_engine`
155 """
156
157 if self.data_typedata_type == 'traits_v1':
158 self._set_traits_v1(engine)
159 else:
160 raise TypeError('engine traits of type %s is unknown' % self.data_typedata_type)
161
162 def _set_traits_v1(self, engine: Engine):
163 # For an engine, when there is `language: ...` in the YAML settings the engine
164 # does support only this one language (region)::
165 #
166 # - name: google italian
167 # engine: google
168 # language: it
169 # region: it-IT # type: ignore
170
171 traits = self.copy()
172
173 _msg = "settings.yml - engine: '%s' / %s: '%s' not supported"
174
175 languages = traits.languages
176 if hasattr(engine, 'language'):
177 if engine.language not in languages:
178 raise ValueError(_msg % (engine.name, 'language', engine.language))
179 traits.languages = {engine.language: languages[engine.language]}
180
181 regions = traits.regions
182 if hasattr(engine, 'region'):
183 if engine.region not in regions:
184 raise ValueError(_msg % (engine.name, 'region', engine.region))
185 traits.regions = {engine.region: regions[engine.region]}
186
187 engine.language_support = bool(traits.languages or traits.regions)
188
189 # set the copied & modified traits in engine's namespace
190 engine.traits = traits
191
192
193class EngineTraitsMap(Dict[str, EngineTraits]):
194 """A python dictionary to map :class:`EngineTraits` by engine name."""
195
196 ENGINE_TRAITS_FILE = (data_dir / 'engine_traits.json').resolve()
197 """File with persistence of the :py:obj:`EngineTraitsMap`."""
198
199 def save_data(self):
200 """Store EngineTraitsMap in in file :py:obj:`self.ENGINE_TRAITS_FILE`"""
201 with open(self.ENGINE_TRAITS_FILE, 'w', encoding='utf-8') as f:
202 json.dump(self, f, indent=2, sort_keys=True, cls=EngineTraitsEncoder)
203
204 @classmethod
205 def from_data(cls) -> 'EngineTraitsMap':
206 """Instantiate :class:`EngineTraitsMap` object from :py:obj:`ENGINE_TRAITS`"""
207 obj = cls()
208 for k, v in ENGINE_TRAITS.items():
209 obj[k] = EngineTraits(**v)
210 return obj
211
212 @classmethod
213 def fetch_traits(cls, log: Callable) -> 'EngineTraitsMap':
214 from searx import engines # pylint: disable=cyclic-import, import-outside-toplevel
215
216 names = list(engines.engines)
217 names.sort()
218 obj = cls()
219
220 for engine_name in names:
221 engine = engines.engines[engine_name]
222
223 traits = EngineTraits.fetch_traits(engine)
224 if traits is not None:
225 log("%-20s: SearXNG languages --> %s " % (engine_name, len(traits.languages)))
226 log("%-20s: SearXNG regions --> %s" % (engine_name, len(traits.regions)))
227 obj[engine_name] = traits
228
229 return obj
230
231 def set_traits(self, engine: Engine | types.ModuleType):
232 """Set traits in a :py:obj:`Engine` namespace.
233
234 :param engine: engine instance build by :py:func:`searx.engines.load_engine`
235 """
236
237 engine_traits = EngineTraits(data_type='traits_v1')
238 if engine.name in self.keys():
239 engine_traits = self[engine.name]
240
241 elif engine.engine in self.keys():
242 # The key of the dictionary traits_map is the *engine name*
243 # configured in settings.xml. When multiple engines are configured
244 # in settings.yml to use the same origin engine (python module)
245 # these additional engines can use the languages from the origin
246 # engine. For this use the configured ``engine: ...`` from
247 # settings.yml
248 engine_traits = self[engine.engine]
249
250 engine_traits.set_traits(engine)
'EngineTraitsMap' fetch_traits(cls, Callable log)
Definition traits.py:213
'EngineTraitsMap' from_data(cls)
Definition traits.py:205
set_traits(self, Engine|types.ModuleType engine)
Definition traits.py:231
_set_traits_v1(self, Engine engine)
Definition traits.py:162
get_region(self, str searxng_locale, default=None)
Definition traits.py:103
bool is_locale_supported(self, str searxng_locale)
Definition traits.py:119
get_language(self, str searxng_locale, default=None)
Definition traits.py:87
Union[ 'EngineTraits', None] fetch_traits(cls, Engine engine)
Definition traits.py:137
set_traits(self, Engine engine)
Definition traits.py:151