mirror of
https://github.com/searxng/searxng.git
synced 2026-07-27 10:21:23 +00:00
Compare commits
4 Commits
0f3ef5da59
...
06e4f4f758
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06e4f4f758 | ||
|
|
748b521ac6 | ||
|
|
e16b6cb148 | ||
|
|
41e0f2abf0 |
@@ -7,7 +7,6 @@ Developer documentation
|
||||
|
||||
quickstart
|
||||
commits
|
||||
rtm_asdf
|
||||
contribution_guide
|
||||
extended_types
|
||||
engines/index
|
||||
|
||||
@@ -23,6 +23,24 @@ Don't hesitate, just clone SearXNG's sources and start hacking right now ..
|
||||
|
||||
git clone https://github.com/searxng/searxng.git searxng
|
||||
|
||||
For the developer environment, `mise en place`_ is recommended:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ curl https://mise.run | sh
|
||||
|
||||
The tools required for a developer environment are provided via `mise.toml`_,
|
||||
trust the project:
|
||||
|
||||
.. code:: sh
|
||||
|
||||
$ mise trust
|
||||
|
||||
.. _mise.toml:
|
||||
https://github.com/searxng/searxng/blob/master/mise.toml
|
||||
.. _mise en place:
|
||||
https://mise.jdx.dev/getting-started.html
|
||||
|
||||
Here is how a minimal workflow looks like:
|
||||
|
||||
1. *start* hacking
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
==================
|
||||
Runtime Management
|
||||
==================
|
||||
|
||||
The runtimes are managed with asdf and are activated in this project via the
|
||||
`.tool-versions <.tool-versions>`_. If you have not yet installed asdf_, then
|
||||
chapter :ref:`introduce asdf` may be of help to you.
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
:local:
|
||||
:backlinks: entry
|
||||
|
||||
|
||||
Get started
|
||||
===========
|
||||
|
||||
If you have asdf installed you can install the runtimes of this project by:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ cd /path/to/searxng
|
||||
$ asdf install # will install runtimes listed in .tool-versions
|
||||
...
|
||||
|
||||
Manage Versions
|
||||
===============
|
||||
|
||||
If you want to perform a ``test`` with special runtime versions of nodejs,
|
||||
python or shellcheck, you can patch the ``.tool-versions``:
|
||||
|
||||
.. code:: diff
|
||||
|
||||
--- a/.tool-versions
|
||||
+++ b/.tool-versions
|
||||
@@ -1,2 +1,2 @@
|
||||
-python 3.12.0
|
||||
-shellcheck 0.9.0
|
||||
+python 3.11.6
|
||||
+shellcheck 0.8.0
|
||||
|
||||
To install use ``asdf install`` again. If the runtime tools have changed, any
|
||||
existing (nodejs and python) environments should be cleaned up with a ``make
|
||||
clean``.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ asdf install
|
||||
...
|
||||
$ make clean test
|
||||
|
||||
|
||||
.. _introduce asdf:
|
||||
|
||||
Introduce asdf
|
||||
==============
|
||||
|
||||
To `download asdf`_ and `install asdf`_:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch <version>
|
||||
$ echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
|
||||
$ echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc
|
||||
|
||||
Start a new shell and try to `install plugins`_:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ asdf plugin-list-all | grep -E '(golang|python|nodejs|shellcheck).git'
|
||||
golang https://github.com/asdf-community/asdf-golang.git
|
||||
nodejs https://github.com/asdf-vm/asdf-nodejs.git
|
||||
python https://github.com/danhper/asdf-python.git
|
||||
shellcheck https://github.com/luizm/asdf-shellcheck.git
|
||||
|
||||
$ asdf plugin add golang https://github.com/asdf-community/asdf-golang.git
|
||||
$ asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
|
||||
$ asdf plugin add python https://github.com/danhper/asdf-python.git
|
||||
$ asdf plugin add shellcheck https://github.com/luizm/asdf-shellcheck.git
|
||||
|
||||
Each plugin has dependencies, to compile runtimes visit the URLs from above and
|
||||
look out for the dependencies you need to install on your OS, on Debian for the
|
||||
runtimes listed above you will need:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ sudo apt update
|
||||
$ sudo apt install \
|
||||
dirmngr gpg curl gawk coreutils build-essential libssl-dev zlib1g-dev \
|
||||
libbz2-dev libreadline-dev libsqlite3-dev \
|
||||
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
|
||||
|
||||
With dependencies installed you can install/compile runtimes:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ asdf install golang latest
|
||||
$ asdf install nodejs latest
|
||||
$ asdf install python latest
|
||||
$ asdf install shellcheck latest
|
||||
|
||||
Python will be compiled and will take a while.
|
||||
|
||||
In the repository the version is defined in `.tool-versions`_. Outside the
|
||||
repository, its recommended that the runtime should use the versions of the OS
|
||||
(`Fallback to System Version`_) / if not already done register the system
|
||||
versions global:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ cd /
|
||||
$ asdf global golang system
|
||||
$ asdf global nodejs system
|
||||
$ asdf global python system
|
||||
$ asdf global shellcheck system
|
||||
|
||||
.. _asdf: https://asdf-vm.com/
|
||||
.. _download asdf: https://asdf-vm.com/guide/getting-started.html#_2-download-asdf
|
||||
.. _install asdf: https://asdf-vm.com/guide/getting-started.html#_3-install-asdf
|
||||
.. _install plugins: https://asdf-vm.com/guide/getting-started.html#install-the-plugin
|
||||
.. _Fallback to System Version: https://asdf-vm.com/manage/versions.html#fallback-to-system-version
|
||||
@@ -11,4 +11,3 @@ developers.
|
||||
:maxdepth: 2
|
||||
|
||||
update
|
||||
standalone_searx.py
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
.. _standalone_searx.py:
|
||||
|
||||
=====================================
|
||||
``searxng_extra/standalone_searx.py``
|
||||
=====================================
|
||||
|
||||
.. automodule:: searxng_extra.standalone_searx
|
||||
:members:
|
||||
@@ -76,12 +76,12 @@ def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]):
|
||||
|
||||
return EngineResults.types.WeatherAnswer.Item(
|
||||
location=location,
|
||||
temperature=weather.Temperature(unit="°C", value=data['temperature']),
|
||||
temperature=weather.Temperature(val=data['temperature'], unit="°C"),
|
||||
condition=WEATHERKIT_TO_CONDITION[data["conditionCode"]],
|
||||
feels_like=weather.Temperature(unit="°C", value=data['temperatureApparent']),
|
||||
feels_like=weather.Temperature(val=data['temperatureApparent'], unit="°C"),
|
||||
wind_from=weather.Compass(data["windDirection"]),
|
||||
wind_speed=weather.WindSpeed(data["windSpeed"], unit="mi/h"),
|
||||
pressure=weather.Pressure(data["pressure"], unit="hPa"),
|
||||
wind_speed=weather.WindSpeed(val=data["windSpeed"], unit="mi/h"),
|
||||
pressure=weather.Pressure(val=data["pressure"], unit="hPa"),
|
||||
humidity=weather.RelativeHumidity(data["humidity"] * 100),
|
||||
cloud_cover=data["cloudCover"] * 100,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Open Meteo (weather)"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
|
||||
@@ -106,16 +108,16 @@ WMO_TO_CONDITION: dict[int, weather.WeatherConditionType] = {
|
||||
}
|
||||
|
||||
|
||||
def _weather_data(location: weather.GeoLocation, data: dict):
|
||||
def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]):
|
||||
|
||||
return WeatherAnswer.Item(
|
||||
location=location,
|
||||
temperature=weather.Temperature(unit="°C", value=data["temperature_2m"]),
|
||||
temperature=weather.Temperature(val=data["temperature_2m"], unit="°C"),
|
||||
condition=WMO_TO_CONDITION[data["weather_code"]],
|
||||
feels_like=weather.Temperature(unit="°C", value=data["apparent_temperature"]),
|
||||
feels_like=weather.Temperature(val=data["apparent_temperature"], unit="°C"),
|
||||
wind_from=weather.Compass(data["wind_direction_10m"]),
|
||||
wind_speed=weather.WindSpeed(data["wind_speed_10m"], unit="km/h"),
|
||||
pressure=weather.Pressure(data["pressure_msl"], unit="hPa"),
|
||||
wind_speed=weather.WindSpeed(val=data["wind_speed_10m"], unit="km/h"),
|
||||
pressure=weather.Pressure(val=data["pressure_msl"], unit="hPa"),
|
||||
humidity=weather.RelativeHumidity(data["relative_humidity_2m"]),
|
||||
cloud_cover=data["cloud_cover"],
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ def construct_body(result):
|
||||
)
|
||||
thumbnail = pdbe_preview_url.format(pdb_id=result['pdb_id'])
|
||||
except KeyError:
|
||||
content = None
|
||||
content = ""
|
||||
thumbnail = None
|
||||
|
||||
# construct url for preview image
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""wttr.in (weather forecast service)"""
|
||||
|
||||
import typing as t
|
||||
|
||||
from urllib.parse import quote
|
||||
from datetime import datetime
|
||||
|
||||
@@ -80,19 +82,19 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
def _weather_data(location: weather.GeoLocation, data: dict):
|
||||
def _weather_data(location: weather.GeoLocation, data: dict[str, t.Any]):
|
||||
# the naming between different data objects is inconsitent, thus temp_C and
|
||||
# tempC are possible
|
||||
tempC: float = data.get("temp_C") or data.get("tempC") # type: ignore
|
||||
|
||||
return WeatherAnswer.Item(
|
||||
location=location,
|
||||
temperature=weather.Temperature(unit="°C", value=tempC),
|
||||
temperature=weather.Temperature(val=tempC, unit="°C"),
|
||||
condition=WWO_TO_CONDITION[data["weatherCode"]],
|
||||
feels_like=weather.Temperature(unit="°C", value=data["FeelsLikeC"]),
|
||||
feels_like=weather.Temperature(val=data["FeelsLikeC"], unit="°C"),
|
||||
wind_from=weather.Compass(int(data["winddirDegree"])),
|
||||
wind_speed=weather.WindSpeed(data["windspeedKmph"], unit="km/h"),
|
||||
pressure=weather.Pressure(data["pressure"], unit="hPa"),
|
||||
wind_speed=weather.WindSpeed(val=data["windspeedKmph"], unit="km/h"),
|
||||
pressure=weather.Pressure(val=data["pressure"], unit="hPa"),
|
||||
humidity=weather.RelativeHumidity(data["humidity"]),
|
||||
cloud_cover=data["cloudcover"],
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=too-few-public-methods,missing-module-docstring
|
||||
|
||||
|
||||
__all__ = ["PluginInfo", "Plugin", "PluginCfg", "PluginStorage"]
|
||||
|
||||
import abc
|
||||
@@ -9,16 +8,17 @@ import importlib
|
||||
import inspect
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
from collections.abc import Sequence
|
||||
|
||||
import typing as t
|
||||
from collections.abc import Generator
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
from searx.result_types import Result
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if t.TYPE_CHECKING:
|
||||
from searx.search import SearchWithPlugins
|
||||
from searx.result_types import Result, EngineResults, LegacyResult # pyright: ignore[reportPrivateLocalImportUsage]
|
||||
import flask
|
||||
|
||||
log: logging.Logger = logging.getLogger("searx.plugins")
|
||||
@@ -42,7 +42,7 @@ class PluginInfo:
|
||||
description: str
|
||||
"""Short description of the *answerer*."""
|
||||
|
||||
preference_section: typing.Literal["general", "ui", "privacy", "query"] | None = "general"
|
||||
preference_section: t.Literal["general", "ui", "privacy", "query"] | None = "general"
|
||||
"""Section (tab/group) in the preferences where this plugin is shown to the
|
||||
user.
|
||||
|
||||
@@ -71,7 +71,7 @@ class Plugin(abc.ABC):
|
||||
id: str = ""
|
||||
"""The ID (suffix) in the HTML form."""
|
||||
|
||||
active: typing.ClassVar[bool]
|
||||
active: t.ClassVar[bool]
|
||||
"""Plugin is enabled/disabled by default (:py:obj:`PluginCfg.active`)."""
|
||||
|
||||
keywords: list[str] = []
|
||||
@@ -109,7 +109,7 @@ class Plugin(abc.ABC):
|
||||
raise ValueError(f"plugin ID {self.id} contains invalid character (use lowercase ASCII)")
|
||||
|
||||
if not getattr(self, "log", None):
|
||||
pkg_name = inspect.getmodule(self.__class__).__package__ # type: ignore
|
||||
pkg_name = inspect.getmodule(self.__class__).__package__ # pyright: ignore[reportOptionalMemberAccess]
|
||||
self.log = logging.getLogger(f"{pkg_name}.{self.id}")
|
||||
|
||||
def __hash__(self) -> int:
|
||||
@@ -120,7 +120,7 @@ class Plugin(abc.ABC):
|
||||
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other: typing.Any):
|
||||
def __eq__(self, other: t.Any):
|
||||
"""py:obj:`Plugin` objects are equal if the hash values of the two
|
||||
objects are equal."""
|
||||
|
||||
@@ -146,7 +146,7 @@ class Plugin(abc.ABC):
|
||||
"""
|
||||
return True
|
||||
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: "Result") -> bool:
|
||||
"""Runs for each result of each engine and returns a boolean:
|
||||
|
||||
- ``True`` to keep the result
|
||||
@@ -166,7 +166,9 @@ class Plugin(abc.ABC):
|
||||
"""
|
||||
return True
|
||||
|
||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | Sequence[Result]:
|
||||
def post_search(
|
||||
self, request: SXNG_Request, search: "SearchWithPlugins"
|
||||
) -> "None | list[Result | LegacyResult] | EngineResults":
|
||||
"""Runs AFTER the search request. Can return a list of
|
||||
:py:obj:`Result <searx.result_types._base.Result>` objects to be added to the
|
||||
final result list."""
|
||||
@@ -196,7 +198,7 @@ class PluginStorage:
|
||||
def __init__(self):
|
||||
self.plugin_list = set()
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Generator[Plugin]:
|
||||
yield from self.plugin_list
|
||||
|
||||
def __len__(self):
|
||||
@@ -207,7 +209,7 @@ class PluginStorage:
|
||||
|
||||
return [p.info for p in self.plugin_list]
|
||||
|
||||
def load_settings(self, cfg: dict[str, dict[str, typing.Any]]):
|
||||
def load_settings(self, cfg: dict[str, dict[str, t.Any]]):
|
||||
"""Load plugins configured in SearXNG's settings :ref:`settings
|
||||
plugins`."""
|
||||
|
||||
@@ -262,7 +264,7 @@ class PluginStorage:
|
||||
break
|
||||
return ret
|
||||
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: "Result") -> bool:
|
||||
|
||||
ret = True
|
||||
for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]:
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
import typing as t
|
||||
|
||||
import datetime
|
||||
|
||||
from flask_babel import gettext # type: ignore
|
||||
from flask_babel import gettext
|
||||
from searx.result_types import EngineResults
|
||||
from searx.weather import DateTime, GeoLocation
|
||||
|
||||
@@ -53,13 +52,13 @@ class SXNGPlugin(Plugin):
|
||||
search_term = " ".join(query_parts).strip()
|
||||
|
||||
if not search_term:
|
||||
date_time = DateTime(time=datetime.datetime.now())
|
||||
date_time = DateTime(datetime.datetime.now())
|
||||
results.add(results.types.Answer(answer=date_time.l10n()))
|
||||
return results
|
||||
|
||||
geo = GeoLocation.by_query(search_term=search_term)
|
||||
if geo:
|
||||
date_time = DateTime(time=datetime.datetime.now(tz=geo.zoneinfo))
|
||||
date_time = DateTime(datetime.datetime.now(tz=geo.zoneinfo))
|
||||
tz_name = geo.timezone.replace('_', ' ')
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
|
||||
@@ -357,12 +357,12 @@ def merge_two_infoboxes(origin: LegacyResult, other: LegacyResult):
|
||||
def merge_two_main_results(origin: MainResult | LegacyResult, other: MainResult | LegacyResult):
|
||||
"""Merges the values from ``other`` into ``origin``."""
|
||||
|
||||
if len(other.content) > len(origin.content):
|
||||
if len(other.content or "") > len(origin.content or ""):
|
||||
# use content with more text
|
||||
origin.content = other.content
|
||||
|
||||
# use title with more text
|
||||
if len(other.title) > len(origin.title):
|
||||
if len(other.title or "") > len(origin.title or ""):
|
||||
origin.title = other.title
|
||||
|
||||
# merge all result's parameters not found in origin
|
||||
|
||||
195
searx/weather.py
195
searx/weather.py
@@ -14,11 +14,10 @@ __all__ = [
|
||||
"GeoLocation",
|
||||
]
|
||||
|
||||
import typing
|
||||
import typing as t
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import dataclasses
|
||||
import zoneinfo
|
||||
|
||||
from urllib.parse import quote_plus
|
||||
@@ -27,7 +26,8 @@ import babel
|
||||
import babel.numbers
|
||||
import babel.dates
|
||||
import babel.languages
|
||||
import flask_babel
|
||||
import flask_babel # pyright: ignore[reportMissingTypeStubs]
|
||||
import msgspec
|
||||
|
||||
from searx import network
|
||||
from searx.cache import ExpireCache, ExpireCacheCfg
|
||||
@@ -120,8 +120,7 @@ def symbol_url(condition: "WeatherConditionType") -> str | None:
|
||||
return data_url
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class GeoLocation:
|
||||
class GeoLocation(msgspec.Struct, kw_only=True):
|
||||
"""Minimal implementation of Geocoding."""
|
||||
|
||||
# The type definition was based on the properties from the geocoding API of
|
||||
@@ -176,6 +175,8 @@ class GeoLocation:
|
||||
|
||||
ctx = "weather_geolocation_by_query"
|
||||
cache = get_WEATHER_DATA_CACHE()
|
||||
# {'name': 'Berlin', 'latitude': 52.52437, 'longitude': 13.41053,
|
||||
# 'elevation': 74.0, 'country_code': 'DE', 'timezone': 'Europe/Berlin'}
|
||||
geo_props = cache.get(search_term, ctx=ctx)
|
||||
|
||||
if not geo_props:
|
||||
@@ -194,15 +195,14 @@ class GeoLocation:
|
||||
if not results:
|
||||
raise ValueError(f"unknown geo location: '{search_term}'")
|
||||
location = results[0]
|
||||
return {field.name: location[field.name] for field in dataclasses.fields(cls)}
|
||||
return {field_name: location[field_name] for field_name in cls.__struct_fields__}
|
||||
|
||||
|
||||
DateTimeFormats = typing.Literal["full", "long", "medium", "short"]
|
||||
DateTimeLocaleTypes = typing.Literal["UI"]
|
||||
DateTimeFormats = t.Literal["full", "long", "medium", "short"]
|
||||
DateTimeLocaleTypes = t.Literal["UI"]
|
||||
|
||||
|
||||
@typing.final
|
||||
class DateTime:
|
||||
class DateTime(msgspec.Struct):
|
||||
"""Class to represent date & time. Essentially, it is a wrapper that
|
||||
conveniently combines :py:obj:`datetime.datetime` and
|
||||
:py:obj:`babel.dates.format_datetime`. A conversion of time zones is not
|
||||
@@ -216,8 +216,7 @@ class DateTime:
|
||||
as the value for the ``locale``.
|
||||
"""
|
||||
|
||||
def __init__(self, time: datetime.datetime):
|
||||
self.datetime = time
|
||||
datetime: datetime.datetime
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
@@ -252,36 +251,35 @@ class DateTime:
|
||||
return babel.dates.format_date(self.datetime, format=fmt, locale=locale)
|
||||
|
||||
|
||||
@typing.final
|
||||
class Temperature:
|
||||
TemperatureUnits: t.TypeAlias = t.Literal["°C", "°F", "K"]
|
||||
|
||||
|
||||
class Temperature(msgspec.Struct, kw_only=True):
|
||||
"""Class for converting temperature units and for string representation of
|
||||
measured values."""
|
||||
|
||||
si_name = "Q11579"
|
||||
val: float
|
||||
unit: TemperatureUnits
|
||||
|
||||
Units = typing.Literal["°C", "°F", "K"]
|
||||
"""Supported temperature units."""
|
||||
si_name: t.ClassVar[str] = "Q11579"
|
||||
units: t.ClassVar[list[str]] = list(t.get_args(TemperatureUnits))
|
||||
|
||||
units = list(typing.get_args(Units))
|
||||
|
||||
def __init__(self, value: float, unit: Units):
|
||||
if unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {unit}")
|
||||
self.si: float = convert_to_si( # pylint: disable=invalid-name
|
||||
si_name=self.si_name,
|
||||
symbol=unit,
|
||||
value=value,
|
||||
)
|
||||
def __post_init__(self):
|
||||
if self.unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {self.unit}")
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
|
||||
def value(self, unit: Units) -> float:
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
|
||||
def value(self, unit: TemperatureUnits) -> float:
|
||||
if unit == self.unit:
|
||||
return self.val
|
||||
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val)
|
||||
|
||||
def l10n(
|
||||
self,
|
||||
unit: Units | None = None,
|
||||
unit: TemperatureUnits | None = None,
|
||||
locale: babel.Locale | GeoLocation | None = None,
|
||||
template: str = "{value} {unit}",
|
||||
num_pattern: str = "#,##0",
|
||||
@@ -320,33 +318,35 @@ class Temperature:
|
||||
return template.format(value=val_str, unit=unit)
|
||||
|
||||
|
||||
@typing.final
|
||||
class Pressure:
|
||||
PressureUnits: t.TypeAlias = t.Literal["Pa", "hPa", "cm Hg", "bar"]
|
||||
|
||||
|
||||
class Pressure(msgspec.Struct, kw_only=True):
|
||||
"""Class for converting pressure units and for string representation of
|
||||
measured values."""
|
||||
|
||||
si_name = "Q44395"
|
||||
val: float
|
||||
unit: PressureUnits
|
||||
|
||||
Units = typing.Literal["Pa", "hPa", "cm Hg", "bar"]
|
||||
"""Supported units."""
|
||||
si_name: t.ClassVar[str] = "Q44395"
|
||||
units: t.ClassVar[list[str]] = list(t.get_args(PressureUnits))
|
||||
|
||||
units = list(typing.get_args(Units))
|
||||
|
||||
def __init__(self, value: float, unit: Units):
|
||||
if unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {unit}")
|
||||
# pylint: disable=invalid-name
|
||||
self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value)
|
||||
def __post_init__(self):
|
||||
if self.unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {self.unit}")
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
|
||||
def value(self, unit: Units) -> float:
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
|
||||
def value(self, unit: PressureUnits) -> float:
|
||||
if unit == self.unit:
|
||||
return self.val
|
||||
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val)
|
||||
|
||||
def l10n(
|
||||
self,
|
||||
unit: Units | None = None,
|
||||
unit: PressureUnits | None = None,
|
||||
locale: babel.Locale | GeoLocation | None = None,
|
||||
template: str = "{value} {unit}",
|
||||
num_pattern: str = "#,##0",
|
||||
@@ -363,8 +363,10 @@ class Pressure:
|
||||
return template.format(value=val_str, unit=unit)
|
||||
|
||||
|
||||
@typing.final
|
||||
class WindSpeed:
|
||||
WindSpeedUnits: t.TypeAlias = t.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"]
|
||||
|
||||
|
||||
class WindSpeed(msgspec.Struct, kw_only=True):
|
||||
"""Class for converting speed or velocity units and for string
|
||||
representation of measured values.
|
||||
|
||||
@@ -375,28 +377,28 @@ class WindSpeed:
|
||||
(55.6 m/s)
|
||||
"""
|
||||
|
||||
si_name = "Q182429"
|
||||
val: float
|
||||
unit: WindSpeedUnits
|
||||
|
||||
Units = typing.Literal["m/s", "km/h", "kn", "mph", "mi/h", "Bft"]
|
||||
"""Supported units."""
|
||||
si_name: t.ClassVar[str] = "Q182429"
|
||||
units: t.ClassVar[list[str]] = list(t.get_args(WindSpeedUnits))
|
||||
|
||||
units = list(typing.get_args(Units))
|
||||
|
||||
def __init__(self, value: float, unit: Units):
|
||||
if unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {unit}")
|
||||
# pylint: disable=invalid-name
|
||||
self.si: float = convert_to_si(si_name=self.si_name, symbol=unit, value=value)
|
||||
def __post_init__(self):
|
||||
if self.unit not in self.units:
|
||||
raise ValueError(f"invalid unit: {self.unit}")
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
|
||||
def value(self, unit: Units) -> float:
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=self.si)
|
||||
def value(self, unit: WindSpeedUnits) -> float:
|
||||
if unit == self.unit:
|
||||
return self.val
|
||||
si_val = convert_to_si(si_name=self.si_name, symbol=self.unit, value=self.val)
|
||||
return convert_from_si(si_name=self.si_name, symbol=unit, value=si_val)
|
||||
|
||||
def l10n(
|
||||
self,
|
||||
unit: Units | None = None,
|
||||
unit: WindSpeedUnits | None = None,
|
||||
locale: babel.Locale | GeoLocation | None = None,
|
||||
template: str = "{value} {unit}",
|
||||
num_pattern: str = "#,##0",
|
||||
@@ -413,23 +415,23 @@ class WindSpeed:
|
||||
return template.format(value=val_str, unit=unit)
|
||||
|
||||
|
||||
@typing.final
|
||||
class RelativeHumidity:
|
||||
RelativeHumidityUnits: t.TypeAlias = t.Literal["%"]
|
||||
|
||||
|
||||
class RelativeHumidity(msgspec.Struct):
|
||||
"""Amount of relative humidity in the air. The unit is ``%``"""
|
||||
|
||||
Units = typing.Literal["%"]
|
||||
"""Supported unit."""
|
||||
val: float
|
||||
|
||||
units = list(typing.get_args(Units))
|
||||
|
||||
def __init__(self, humidity: float):
|
||||
self.humidity = humidity
|
||||
# there exists only one unit (%) --> set "%" as the final value (constant)
|
||||
unit: t.ClassVar["t.Final[RelativeHumidityUnits]"] = "%"
|
||||
units: t.ClassVar[list[str]] = list(t.get_args(RelativeHumidityUnits))
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
|
||||
def value(self) -> float:
|
||||
return self.humidity
|
||||
return self.val
|
||||
|
||||
def l10n(
|
||||
self,
|
||||
@@ -447,45 +449,50 @@ class RelativeHumidity:
|
||||
return template.format(value=val_str, unit=unit)
|
||||
|
||||
|
||||
@typing.final
|
||||
class Compass:
|
||||
CompassPoint: t.TypeAlias = t.Literal[
|
||||
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
|
||||
]
|
||||
"""Compass point type definition"""
|
||||
|
||||
CompassUnits: t.TypeAlias = t.Literal["°", "Point"]
|
||||
|
||||
|
||||
class Compass(msgspec.Struct):
|
||||
"""Class for converting compass points and azimuth values (360°)"""
|
||||
|
||||
Units = typing.Literal["°", "Point"]
|
||||
val: "float | int | CompassPoint"
|
||||
unit: CompassUnits = "°"
|
||||
|
||||
Point = typing.Literal[
|
||||
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"
|
||||
]
|
||||
"""Compass point type definition"""
|
||||
|
||||
TURN = 360.0
|
||||
TURN: t.ClassVar[float] = 360.0
|
||||
"""Full turn (360°)"""
|
||||
|
||||
POINTS = list(typing.get_args(Point))
|
||||
POINTS: t.ClassVar[list[CompassPoint]] = list(t.get_args(CompassPoint))
|
||||
"""Compass points."""
|
||||
|
||||
RANGE = TURN / len(POINTS)
|
||||
RANGE: t.ClassVar[float] = TURN / len(POINTS)
|
||||
"""Angle sector of a compass point"""
|
||||
|
||||
def __init__(self, azimuth: float | int | Point):
|
||||
if isinstance(azimuth, str):
|
||||
if azimuth not in self.POINTS:
|
||||
raise ValueError(f"Invalid compass point: {azimuth}")
|
||||
azimuth = self.POINTS.index(azimuth) * self.RANGE
|
||||
self.azimuth = azimuth % self.TURN
|
||||
def __post_init__(self):
|
||||
if isinstance(self.val, str):
|
||||
if self.val not in self.POINTS:
|
||||
raise ValueError(f"Invalid compass point: {self.val}")
|
||||
self.val = self.POINTS.index(self.val) * self.RANGE
|
||||
|
||||
self.val = self.val % self.TURN
|
||||
self.unit = "°"
|
||||
|
||||
def __str__(self):
|
||||
return self.l10n()
|
||||
|
||||
def value(self, unit: Units):
|
||||
if unit == "Point":
|
||||
return self.point(self.azimuth)
|
||||
def value(self, unit: CompassUnits):
|
||||
if unit == "Point" and isinstance(self.val, float):
|
||||
return self.point(self.val)
|
||||
if unit == "°":
|
||||
return self.azimuth
|
||||
return self.val
|
||||
raise ValueError(f"unknown unit: {unit}")
|
||||
|
||||
@classmethod
|
||||
def point(cls, azimuth: float | int) -> Point:
|
||||
def point(cls, azimuth: float | int) -> CompassPoint:
|
||||
"""Returns the compass point to an azimuth value."""
|
||||
azimuth = azimuth % cls.TURN
|
||||
# The angle sector of a compass point starts 1/2 sector range before
|
||||
@@ -496,7 +503,7 @@ class Compass:
|
||||
|
||||
def l10n(
|
||||
self,
|
||||
unit: Units = "Point",
|
||||
unit: CompassUnits = "Point",
|
||||
locale: babel.Locale | GeoLocation | None = None,
|
||||
template: str = "{value}{unit}",
|
||||
num_pattern: str = "#,##0",
|
||||
@@ -514,7 +521,7 @@ class Compass:
|
||||
return template.format(value=val_str, unit=unit)
|
||||
|
||||
|
||||
WeatherConditionType = typing.Literal[
|
||||
WeatherConditionType = t.Literal[
|
||||
# The capitalized string goes into to i18n/l10n (en: "Clear sky" -> de: "wolkenloser Himmel")
|
||||
"clear sky",
|
||||
"partly cloudy",
|
||||
@@ -632,7 +639,7 @@ YR_WEATHER_SYMBOL_MAP = {
|
||||
if __name__ == "__main__":
|
||||
|
||||
# test: fetch all symbols of the type catalog ..
|
||||
for c in typing.get_args(WeatherConditionType):
|
||||
for c in t.get_args(WeatherConditionType):
|
||||
symbol_url(condition=c)
|
||||
|
||||
_cache = get_WEATHER_DATA_CACHE()
|
||||
|
||||
@@ -16,6 +16,7 @@ from typing import Iterable, List, Tuple, TYPE_CHECKING
|
||||
from io import StringIO
|
||||
from codecs import getincrementalencoder
|
||||
|
||||
import msgspec
|
||||
from flask_babel import gettext, format_date # type: ignore
|
||||
|
||||
from searx import logger, get_setting
|
||||
@@ -147,6 +148,8 @@ def write_csv_response(csv: CSVWriter, rc: "ResultContainer") -> None: # pylint
|
||||
|
||||
class JSONEncoder(json.JSONEncoder): # pylint: disable=missing-class-docstring
|
||||
def default(self, o):
|
||||
if isinstance(o, msgspec.Struct):
|
||||
return msgspec.to_builtins(o)
|
||||
if isinstance(o, datetime):
|
||||
return o.isoformat()
|
||||
if isinstance(o, timedelta):
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Script to run SearXNG from terminal.
|
||||
|
||||
DON'T USE THIS SCRIPT!!
|
||||
|
||||
.. danger::
|
||||
|
||||
Be warned, using the ``standalone_searx.py`` won't give you privacy!
|
||||
|
||||
On the contrary, this script behaves like a SearXNG server: your IP is
|
||||
exposed and tracked by all active engines (google, bing, qwant, ... ), with
|
||||
every query!
|
||||
|
||||
.. note::
|
||||
|
||||
This is an old and grumpy hack / SearXNG is a Flask application with
|
||||
client/server structure, which can't be turned into a command line tool the
|
||||
way it was done here.
|
||||
|
||||
Getting categories without initiate the engine will only return `['general']`
|
||||
|
||||
>>> import searx.engines
|
||||
... list(searx.engines.categories.keys())
|
||||
['general']
|
||||
>>> import searx.search
|
||||
... searx.search.initialize()
|
||||
... list(searx.engines.categories.keys())
|
||||
['general', 'it', 'science', 'images', 'news', 'videos', 'music', 'files', 'social media', 'map']
|
||||
|
||||
Example to use this script:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ python3 searxng_extra/standalone_searx.py rain
|
||||
|
||||
""" # pylint: disable=line-too-long
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from json import dumps
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import searx
|
||||
import searx.preferences
|
||||
import searx.query
|
||||
import searx.search
|
||||
import searx.search.models
|
||||
import searx.webadapter
|
||||
from searx.search.processors import PROCESSORS
|
||||
|
||||
EngineCategoriesVar = Optional[List[str]]
|
||||
|
||||
|
||||
def get_search_query(
|
||||
args: argparse.Namespace, engine_categories: EngineCategoriesVar = None
|
||||
) -> searx.search.models.SearchQuery:
|
||||
"""Get search results for the query"""
|
||||
if engine_categories is None:
|
||||
engine_categories = list(searx.engines.categories.keys())
|
||||
try:
|
||||
category = args.category.decode('utf-8')
|
||||
except AttributeError:
|
||||
category = args.category
|
||||
form = {
|
||||
"q": args.query,
|
||||
"categories": category,
|
||||
"pageno": str(args.pageno),
|
||||
"language": args.lang,
|
||||
"time_range": args.timerange,
|
||||
}
|
||||
preferences = searx.preferences.Preferences(['simple'], engine_categories, searx.engines.engines, [])
|
||||
preferences.key_value_settings['safesearch'].parse(args.safesearch)
|
||||
|
||||
search_query = searx.webadapter.get_search_query_from_webapp(preferences, form)[0]
|
||||
return search_query
|
||||
|
||||
|
||||
def no_parsed_url(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Remove parsed url from dict."""
|
||||
for result in results:
|
||||
del result['parsed_url']
|
||||
return results
|
||||
|
||||
|
||||
def json_serial(obj: Any) -> Any:
|
||||
"""JSON serializer for objects not serializable by default json code.
|
||||
|
||||
:raise TypeError: raised when **obj** is not serializable
|
||||
"""
|
||||
if isinstance(obj, datetime):
|
||||
serial = obj.isoformat()
|
||||
return serial
|
||||
if isinstance(obj, bytes):
|
||||
return obj.decode('utf8')
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
raise TypeError("Type ({}) not serializable".format(type(obj)))
|
||||
|
||||
|
||||
def to_dict(search_query: searx.search.models.SearchQuery) -> Dict[str, Any]:
|
||||
"""Get result from parsed arguments."""
|
||||
result_container = searx.search.Search(search_query).search()
|
||||
result_container_json = {
|
||||
"search": {
|
||||
"q": search_query.query,
|
||||
"pageno": search_query.pageno,
|
||||
"lang": search_query.lang,
|
||||
"safesearch": search_query.safesearch,
|
||||
"timerange": search_query.time_range,
|
||||
},
|
||||
"results": no_parsed_url(result_container.get_ordered_results()),
|
||||
"infoboxes": result_container.infoboxes,
|
||||
"suggestions": list(result_container.suggestions),
|
||||
"answers": list(result_container.answers),
|
||||
"paging": result_container.paging,
|
||||
"number_of_results": result_container.number_of_results,
|
||||
}
|
||||
return result_container_json
|
||||
|
||||
|
||||
def parse_argument(
|
||||
args: Optional[List[str]] = None, category_choices: EngineCategoriesVar = None
|
||||
) -> argparse.Namespace:
|
||||
"""Parse command line.
|
||||
|
||||
:raise SystemExit: Query argument required on `args`
|
||||
|
||||
Examples:
|
||||
|
||||
>>> import importlib
|
||||
... # load module
|
||||
... spec = importlib.util.spec_from_file_location(
|
||||
... 'utils.standalone_searx', 'utils/standalone_searx.py')
|
||||
... sas = importlib.util.module_from_spec(spec)
|
||||
... spec.loader.exec_module(sas)
|
||||
... sas.parse_argument()
|
||||
usage: ptipython [-h] [--category [{general}]] [--lang [LANG]] [--pageno [PAGENO]] [--safesearch [{0,1,2}]] [--timerange [{day,week,month,year}]]
|
||||
query
|
||||
SystemExit: 2
|
||||
>>> sas.parse_argument(['rain'])
|
||||
Namespace(category='general', lang='all', pageno=1, query='rain', safesearch='0', timerange=None)
|
||||
""" # noqa: E501
|
||||
if not category_choices:
|
||||
category_choices = list(searx.engines.categories.keys())
|
||||
parser = argparse.ArgumentParser(description='Standalone searx.')
|
||||
parser.add_argument('query', type=str, help='Text query')
|
||||
parser.add_argument(
|
||||
'--category', type=str, nargs='?', choices=category_choices, default='general', help='Search category'
|
||||
)
|
||||
parser.add_argument('--lang', type=str, nargs='?', default='all', help='Search language')
|
||||
parser.add_argument('--pageno', type=int, nargs='?', default=1, help='Page number starting from 1')
|
||||
parser.add_argument(
|
||||
'--safesearch',
|
||||
type=str,
|
||||
nargs='?',
|
||||
choices=['0', '1', '2'],
|
||||
default='0',
|
||||
help='Safe content filter from none to strict',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timerange', type=str, nargs='?', choices=['day', 'week', 'month', 'year'], help='Filter by time range'
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
settings_engines = searx.settings['engines']
|
||||
searx.search.load_engines(settings_engines)
|
||||
engine_cs = list(searx.engines.categories.keys())
|
||||
prog_args = parse_argument(category_choices=engine_cs)
|
||||
searx.search.initialize_network(settings_engines, searx.settings['outgoing'])
|
||||
searx.search.check_network_configuration()
|
||||
searx.search.initialize_metrics([engine['name'] for engine in settings_engines])
|
||||
PROCESSORS.init(settings_engines)
|
||||
search_q = get_search_query(prog_args, engine_categories=engine_cs)
|
||||
res_dict = to_dict(search_q)
|
||||
sys.stdout.write(dumps(res_dict, sort_keys=True, indent=4, ensure_ascii=False, default=json_serial))
|
||||
Reference in New Issue
Block a user