answer.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. # SPDX-License-Identifier: AGPL-3.0-or-later
  2. """
  3. Typification of the *answer* results. Results of this type are rendered in
  4. the :origin:`answers.html <searx/templates/simple/elements/answers.html>`
  5. template.
  6. ----
  7. .. autoclass:: BaseAnswer
  8. :members:
  9. :show-inheritance:
  10. .. autoclass:: Answer
  11. :members:
  12. :show-inheritance:
  13. .. autoclass:: Translations
  14. :members:
  15. :show-inheritance:
  16. .. autoclass:: WeatherAnswer
  17. :members:
  18. :show-inheritance:
  19. .. autoclass:: AnswerSet
  20. :members:
  21. :show-inheritance:
  22. """
  23. # pylint: disable=too-few-public-methods
  24. from __future__ import annotations
  25. __all__ = ["AnswerSet", "Answer", "Translations", "WeatherAnswer"]
  26. from flask_babel import gettext
  27. import msgspec
  28. from searx import weather
  29. from ._base import Result
  30. class BaseAnswer(Result, kw_only=True):
  31. """Base class of all answer types. It is not intended to build instances of
  32. this class (aka *abstract*)."""
  33. class AnswerSet:
  34. """Aggregator for :py:obj:`BaseAnswer` items in a result container."""
  35. def __init__(self):
  36. self._answerlist = []
  37. def __len__(self):
  38. return len(self._answerlist)
  39. def __bool__(self):
  40. return bool(self._answerlist)
  41. def add(self, answer: BaseAnswer) -> None:
  42. a_hash = hash(answer)
  43. for i in self._answerlist:
  44. if hash(i) == a_hash:
  45. return
  46. self._answerlist.append(answer)
  47. def __iter__(self):
  48. """Sort items in this set and iterate over the items."""
  49. self._answerlist.sort(key=lambda answer: answer.template)
  50. yield from self._answerlist
  51. def __contains__(self, answer: BaseAnswer) -> bool:
  52. a_hash = hash(answer)
  53. for i in self._answerlist:
  54. if hash(i) == a_hash:
  55. return True
  56. return False
  57. class Answer(BaseAnswer, kw_only=True):
  58. """Simple answer type where the *answer* is a simple string with an optional
  59. :py:obj:`url field <Result.url>` field to link a resource (article, map, ..)
  60. related to the answer."""
  61. template: str = "answer/legacy.html"
  62. answer: str
  63. """Text of the answer."""
  64. def __hash__(self):
  65. """The hash value of field *answer* is the hash value of the
  66. :py:obj:`Answer` object. :py:obj:`Answer <Result.__eq__>` objects are
  67. equal, when the hash values of both objects are equal."""
  68. return hash(self.answer)
  69. class Translations(BaseAnswer, kw_only=True):
  70. """Answer type with a list of translations.
  71. The items in the list of :py:obj:`Translations.translations` are of type
  72. :py:obj:`Translations.Item`:
  73. .. code:: python
  74. def response(resp):
  75. results = []
  76. ...
  77. foo_1 = Translations.Item(
  78. text="foobar",
  79. synonyms=["bar", "foo"],
  80. examples=["foo and bar are placeholders"],
  81. )
  82. foo_url="https://www.deepl.com/de/translator#en/de/foo"
  83. ...
  84. Translations(results=results, translations=[foo], url=foo_url)
  85. """
  86. template: str = "answer/translations.html"
  87. """The template in :origin:`answer/translations.html
  88. <searx/templates/simple/answer/translations.html>`"""
  89. translations: list[Translations.Item]
  90. """List of translations."""
  91. def __post_init__(self):
  92. if not self.translations:
  93. raise ValueError("Translation does not have an item in the list translations")
  94. class Item(msgspec.Struct, kw_only=True):
  95. """A single element of the translations / a translation. A translation
  96. consists of at least a mandatory ``text`` property (the translation) ,
  97. optional properties such as *definitions*, *synonyms* and *examples* are
  98. possible."""
  99. text: str
  100. """Translated text."""
  101. transliteration: str = ""
  102. """Transliteration_ of the requested translation.
  103. .. _Transliteration: https://en.wikipedia.org/wiki/Transliteration
  104. """
  105. examples: list[str] = []
  106. """List of examples for the requested translation."""
  107. definitions: list[str] = []
  108. """List of definitions for the requested translation."""
  109. synonyms: list[str] = []
  110. """List of synonyms for the requested translation."""
  111. class WeatherAnswer(BaseAnswer, kw_only=True):
  112. """Answer type for weather data."""
  113. template: str = "answer/weather.html"
  114. """The template is located at :origin:`answer/weather.html
  115. <searx/templates/simple/answer/weather.html>`"""
  116. current: WeatherAnswer.Item
  117. """Current weather at ``location``."""
  118. forecasts: list[WeatherAnswer.Item] = []
  119. """Weather forecasts for ``location``."""
  120. service: str = ""
  121. """Weather service from which this information was provided."""
  122. class Item(msgspec.Struct, kw_only=True):
  123. """Weather parameters valid for a specific point in time."""
  124. location: weather.GeoLocation
  125. """The geo-location the weather data is from (e.g. `Berlin, Germany`)."""
  126. temperature: weather.Temperature
  127. """Air temperature at 2m above the ground."""
  128. condition: weather.WeatherConditionType
  129. """Standardized designations that summarize the weather situation
  130. (e.g. ``light sleet showers and thunder``)."""
  131. # optional fields
  132. datetime: weather.DateTime | None = None
  133. """Time of the forecast - not needed for the current weather."""
  134. summary: str | None = None
  135. """One-liner about the weather forecast / current weather conditions.
  136. If unset, a summary is build up from temperature and current weather
  137. conditions.
  138. """
  139. feels_like: weather.Temperature | None = None
  140. """Apparent temperature, the temperature equivalent perceived by
  141. humans, caused by the combined effects of air temperature, relative
  142. humidity and wind speed. The measure is most commonly applied to the
  143. perceived outdoor temperature.
  144. """
  145. pressure: weather.Pressure | None = None
  146. """Air pressure at sea level (e.g. 1030 hPa) """
  147. humidity: weather.RelativeHumidity | None = None
  148. """Amount of relative humidity in the air at 2m above the ground. The
  149. unit is ``%``, e.g. 60%)
  150. """
  151. wind_from: weather.Compass
  152. """The directon which moves towards / direction the wind is coming from."""
  153. wind_speed: weather.WindSpeed | None = None
  154. """Speed of wind / wind speed at 10m above the ground (10 min average)."""
  155. cloud_cover: int | None = None
  156. """Amount of sky covered by clouds / total cloud cover for all heights
  157. (cloudiness, unit: %)"""
  158. # attributes: dict[str, str | int] = {}
  159. # """Key-Value dict of additional typeless weather attributes."""
  160. def __post_init__(self):
  161. if not self.summary:
  162. self.summary = gettext("{location}: {temperature}, {condition}").format(
  163. location=self.location,
  164. temperature=self.temperature,
  165. condition=gettext(self.condition.capitalize()),
  166. )
  167. @property
  168. def url(self) -> str | None:
  169. """Determines a `data URL`_ with a symbol for the weather
  170. conditions. If no symbol can be assigned, ``None`` is returned.
  171. .. _data URL:
  172. https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data
  173. """
  174. return weather.symbol_url(self.condition)