b71cd3fcbb
new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-environment-setup-in-conftest.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-geocode.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-route_metrics.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/add-logging-to-tracking-simulator.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/extend-sqlite-tuning-in-database.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/fix-route-handling-in-routing.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/handle-api-response-errors-in-routing.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/refactor-database-path-handling-in-database.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-fcm-message-construction-in-notifications.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-role-check-in-ws.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/logs/refs/heads/codex/update-user-seed-in-database.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-environment-setup-in-conftest.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-geocode.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-route_metrics.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/add-logging-to-tracking-simulator.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/extend-sqlite-tuning-in-database.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/fix-route-handling-in-routing.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/handle-api-response-errors-in-routing.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/refactor-database-path-handling-in-database.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-fcm-message-construction-in-notifications.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-role-check-in-ws.py new file: apps/gitea/data/git/repositories/beatzaplenty/limo-booking-app.git/refs/heads/codex/update-user-seed-in-database.py renamed: gitea/docker-compose.yml -> apps/gitea/docker-compose.yml new file: apps/gramps/docker-compose.yml renamed: nextcloud/Dockerfile -> apps/nextcloud/Dockerfile new file: apps/nextcloud/docker-compose.yml renamed: passbolt/Dockerfile -> apps/passbolt/Dockerfile renamed: passbolt/docker-compose.yml -> apps/passbolt/docker-compose.yml renamed: searxng/Dockerfile -> apps/searxng/Dockerfile renamed: searxng/docker-compose.yml -> apps/searxng/docker-compose.yml renamed: searxng/dockerfiles/docker-entrypoint.sh -> apps/searxng/dockerfiles/docker-entrypoint.sh renamed: searxng/docs/conf.py -> apps/searxng/docs/conf.py renamed: searxng/docs/user/.gitignore -> apps/searxng/docs/user/.gitignore renamed: searxng/examples/basic_engine.py -> apps/searxng/examples/basic_engine.py renamed: searxng/searx/__init__.py -> apps/searxng/searx/__init__.py renamed: searxng/searx/answerers/__init__.py -> apps/searxng/searx/answerers/__init__.py renamed: searxng/searx/answerers/random/answerer.py -> apps/searxng/searx/answerers/random/answerer.py renamed: searxng/searx/answerers/statistics/answerer.py -> apps/searxng/searx/answerers/statistics/answerer.py renamed: searxng/searx/autocomplete.py -> apps/searxng/searx/autocomplete.py renamed: searxng/searx/babel_extract.py -> apps/searxng/searx/babel_extract.py renamed: searxng/searx/botdetection/__init__.py -> apps/searxng/searx/botdetection/__init__.py renamed: searxng/searx/botdetection/_helpers.py -> apps/searxng/searx/botdetection/_helpers.py renamed: searxng/searx/botdetection/http_accept.py -> apps/searxng/searx/botdetection/http_accept.py renamed: searxng/searx/botdetection/http_accept_encoding.py -> apps/searxng/searx/botdetection/http_accept_encoding.py renamed: searxng/searx/botdetection/http_accept_language.py -> apps/searxng/searx/botdetection/http_accept_language.py renamed: searxng/searx/botdetection/http_connection.py -> apps/searxng/searx/botdetection/http_connection.py renamed: searxng/searx/botdetection/http_user_agent.py -> apps/searxng/searx/botdetection/http_user_agent.py renamed: searxng/searx/botdetection/ip_limit.py -> apps/searxng/searx/botdetection/ip_limit.py renamed: searxng/searx/botdetection/ip_lists.py -> apps/searxng/searx/botdetection/ip_lists.py renamed: searxng/searx/botdetection/limiter.py -> apps/searxng/searx/botdetection/limiter.py renamed: searxng/searx/botdetection/link_token.py -> apps/searxng/searx/botdetection/link_token.py renamed: searxng/searx/compat.py -> apps/searxng/searx/compat.py renamed: searxng/searx/data/__init__.py -> apps/searxng/searx/data/__init__.py renamed: searxng/searx/enginelib/__init__.py -> apps/searxng/searx/enginelib/__init__.py renamed: searxng/searx/enginelib/traits.py -> apps/searxng/searx/enginelib/traits.py renamed: searxng/searx/engines/1337x.py -> apps/searxng/searx/engines/1337x.py renamed: searxng/searx/engines/9gag.py -> apps/searxng/searx/engines/9gag.py renamed: searxng/searx/engines/__init__.py -> apps/searxng/searx/engines/__init__.py renamed: searxng/searx/engines/ahmia.py -> apps/searxng/searx/engines/ahmia.py renamed: searxng/searx/engines/annas_archive.py -> apps/searxng/searx/engines/annas_archive.py renamed: searxng/searx/engines/apkmirror.py -> apps/searxng/searx/engines/apkmirror.py renamed: searxng/searx/engines/apple_app_store.py -> apps/searxng/searx/engines/apple_app_store.py renamed: searxng/searx/engines/apple_maps.py -> apps/searxng/searx/engines/apple_maps.py renamed: searxng/searx/engines/archlinux.py -> apps/searxng/searx/engines/archlinux.py renamed: searxng/searx/engines/artic.py -> apps/searxng/searx/engines/artic.py renamed: searxng/searx/engines/arxiv.py -> apps/searxng/searx/engines/arxiv.py renamed: searxng/searx/engines/bandcamp.py -> apps/searxng/searx/engines/bandcamp.py renamed: searxng/searx/engines/base.py -> apps/searxng/searx/engines/base.py renamed: searxng/searx/engines/bing.py -> apps/searxng/searx/engines/bing.py renamed: searxng/searx/engines/bing_images.py -> apps/searxng/searx/engines/bing_images.py renamed: searxng/searx/engines/bing_news.py -> apps/searxng/searx/engines/bing_news.py renamed: searxng/searx/engines/bing_videos.py -> apps/searxng/searx/engines/bing_videos.py renamed: searxng/searx/engines/brave.py -> apps/searxng/searx/engines/brave.py renamed: searxng/searx/engines/bt4g.py -> apps/searxng/searx/engines/bt4g.py renamed: searxng/searx/engines/btdigg.py -> apps/searxng/searx/engines/btdigg.py renamed: searxng/searx/engines/command.py -> apps/searxng/searx/engines/command.py renamed: searxng/searx/engines/core.py -> apps/searxng/searx/engines/core.py renamed: searxng/searx/engines/crossref.py -> apps/searxng/searx/engines/crossref.py renamed: searxng/searx/engines/currency_convert.py -> apps/searxng/searx/engines/currency_convert.py renamed: searxng/searx/engines/dailymotion.py -> apps/searxng/searx/engines/dailymotion.py renamed: searxng/searx/engines/deepl.py -> apps/searxng/searx/engines/deepl.py renamed: searxng/searx/engines/deezer.py -> apps/searxng/searx/engines/deezer.py renamed: searxng/searx/engines/demo_offline.py -> apps/searxng/searx/engines/demo_offline.py renamed: searxng/searx/engines/demo_online.py -> apps/searxng/searx/engines/demo_online.py renamed: searxng/searx/engines/deviantart.py -> apps/searxng/searx/engines/deviantart.py renamed: searxng/searx/engines/dictzone.py -> apps/searxng/searx/engines/dictzone.py renamed: searxng/searx/engines/digbt.py -> apps/searxng/searx/engines/digbt.py renamed: searxng/searx/engines/docker_hub.py -> apps/searxng/searx/engines/docker_hub.py renamed: searxng/searx/engines/doku.py -> apps/searxng/searx/engines/doku.py renamed: searxng/searx/engines/duckduckgo.py -> apps/searxng/searx/engines/duckduckgo.py renamed: searxng/searx/engines/duckduckgo_definitions.py -> apps/searxng/searx/engines/duckduckgo_definitions.py renamed: searxng/searx/engines/duckduckgo_images.py -> apps/searxng/searx/engines/duckduckgo_images.py renamed: searxng/searx/engines/duckduckgo_weather.py -> apps/searxng/searx/engines/duckduckgo_weather.py renamed: searxng/searx/engines/duden.py -> apps/searxng/searx/engines/duden.py renamed: searxng/searx/engines/dummy-offline.py -> apps/searxng/searx/engines/dummy-offline.py renamed: searxng/searx/engines/dummy.py -> apps/searxng/searx/engines/dummy.py renamed: searxng/searx/engines/ebay.py -> apps/searxng/searx/engines/ebay.py renamed: searxng/searx/engines/elasticsearch.py -> apps/searxng/searx/engines/elasticsearch.py renamed: searxng/searx/engines/emojipedia.py -> apps/searxng/searx/engines/emojipedia.py renamed: searxng/searx/engines/fdroid.py -> apps/searxng/searx/engines/fdroid.py renamed: searxng/searx/engines/flickr.py -> apps/searxng/searx/engines/flickr.py renamed: searxng/searx/engines/flickr_noapi.py -> apps/searxng/searx/engines/flickr_noapi.py renamed: searxng/searx/engines/framalibre.py -> apps/searxng/searx/engines/framalibre.py renamed: searxng/searx/engines/freesound.py -> apps/searxng/searx/engines/freesound.py renamed: searxng/searx/engines/frinkiac.py -> apps/searxng/searx/engines/frinkiac.py renamed: searxng/searx/engines/genius.py -> apps/searxng/searx/engines/genius.py renamed: searxng/searx/engines/gentoo.py -> apps/searxng/searx/engines/gentoo.py renamed: searxng/searx/engines/github.py -> apps/searxng/searx/engines/github.py renamed: searxng/searx/engines/google.py -> apps/searxng/searx/engines/google.py renamed: searxng/searx/engines/google_images.py -> apps/searxng/searx/engines/google_images.py renamed: searxng/searx/engines/google_news.py -> apps/searxng/searx/engines/google_news.py renamed: searxng/searx/engines/google_play.py -> apps/searxng/searx/engines/google_play.py renamed: searxng/searx/engines/google_scholar.py -> apps/searxng/searx/engines/google_scholar.py renamed: searxng/searx/engines/google_videos.py -> apps/searxng/searx/engines/google_videos.py renamed: searxng/searx/engines/imdb.py -> apps/searxng/searx/engines/imdb.py renamed: searxng/searx/engines/ina.py -> apps/searxng/searx/engines/ina.py renamed: searxng/searx/engines/invidious.py -> apps/searxng/searx/engines/invidious.py renamed: searxng/searx/engines/jisho.py -> apps/searxng/searx/engines/jisho.py renamed: searxng/searx/engines/json_engine.py -> apps/searxng/searx/engines/json_engine.py renamed: searxng/searx/engines/kickass.py -> apps/searxng/searx/engines/kickass.py renamed: searxng/searx/engines/lemmy.py -> apps/searxng/searx/engines/lemmy.py renamed: searxng/searx/engines/lingva.py -> apps/searxng/searx/engines/lingva.py renamed: searxng/searx/engines/loc.py -> apps/searxng/searx/engines/loc.py renamed: searxng/searx/engines/mediathekviewweb.py -> apps/searxng/searx/engines/mediathekviewweb.py renamed: searxng/searx/engines/mediawiki.py -> apps/searxng/searx/engines/mediawiki.py renamed: searxng/searx/engines/meilisearch.py -> apps/searxng/searx/engines/meilisearch.py renamed: searxng/searx/engines/metacpan.py -> apps/searxng/searx/engines/metacpan.py renamed: searxng/searx/engines/mixcloud.py -> apps/searxng/searx/engines/mixcloud.py renamed: searxng/searx/engines/mongodb.py -> apps/searxng/searx/engines/mongodb.py renamed: searxng/searx/engines/mysql_server.py -> apps/searxng/searx/engines/mysql_server.py renamed: searxng/searx/engines/nyaa.py -> apps/searxng/searx/engines/nyaa.py renamed: searxng/searx/engines/opensemantic.py -> apps/searxng/searx/engines/opensemantic.py renamed: searxng/searx/engines/openstreetmap.py -> apps/searxng/searx/engines/openstreetmap.py renamed: searxng/searx/engines/openverse.py -> apps/searxng/searx/engines/openverse.py renamed: searxng/searx/engines/pdbe.py -> apps/searxng/searx/engines/pdbe.py renamed: searxng/searx/engines/peertube.py -> apps/searxng/searx/engines/peertube.py renamed: searxng/searx/engines/photon.py -> apps/searxng/searx/engines/photon.py renamed: searxng/searx/engines/piped.py -> apps/searxng/searx/engines/piped.py renamed: searxng/searx/engines/piratebay.py -> apps/searxng/searx/engines/piratebay.py renamed: searxng/searx/engines/postgresql.py -> apps/searxng/searx/engines/postgresql.py renamed: searxng/searx/engines/pubmed.py -> apps/searxng/searx/engines/pubmed.py renamed: searxng/searx/engines/qwant.py -> apps/searxng/searx/engines/qwant.py renamed: searxng/searx/engines/recoll.py -> apps/searxng/searx/engines/recoll.py renamed: searxng/searx/engines/reddit.py -> apps/searxng/searx/engines/reddit.py renamed: searxng/searx/engines/redis_server.py -> apps/searxng/searx/engines/redis_server.py renamed: searxng/searx/engines/rumble.py -> apps/searxng/searx/engines/rumble.py renamed: searxng/searx/engines/scanr_structures.py -> apps/searxng/searx/engines/scanr_structures.py renamed: searxng/searx/engines/searchcode_code.py -> apps/searxng/searx/engines/searchcode_code.py renamed: searxng/searx/engines/searx_engine.py -> apps/searxng/searx/engines/searx_engine.py renamed: searxng/searx/engines/semantic_scholar.py -> apps/searxng/searx/engines/semantic_scholar.py renamed: searxng/searx/engines/sepiasearch.py -> apps/searxng/searx/engines/sepiasearch.py renamed: searxng/searx/engines/seznam.py -> apps/searxng/searx/engines/seznam.py renamed: searxng/searx/engines/sjp.py -> apps/searxng/searx/engines/sjp.py renamed: searxng/searx/engines/solidtorrents.py -> apps/searxng/searx/engines/solidtorrents.py renamed: searxng/searx/engines/solr.py -> apps/searxng/searx/engines/solr.py renamed: searxng/searx/engines/soundcloud.py -> apps/searxng/searx/engines/soundcloud.py renamed: searxng/searx/engines/spotify.py -> apps/searxng/searx/engines/spotify.py renamed: searxng/searx/engines/springer.py -> apps/searxng/searx/engines/springer.py renamed: searxng/searx/engines/sqlite.py -> apps/searxng/searx/engines/sqlite.py renamed: searxng/searx/engines/stackexchange.py -> apps/searxng/searx/engines/stackexchange.py renamed: searxng/searx/engines/startpage.py -> apps/searxng/searx/engines/startpage.py renamed: searxng/searx/engines/tagesschau.py -> apps/searxng/searx/engines/tagesschau.py renamed: searxng/searx/engines/tineye.py -> apps/searxng/searx/engines/tineye.py renamed: searxng/searx/engines/tokyotoshokan.py -> apps/searxng/searx/engines/tokyotoshokan.py renamed: searxng/searx/engines/torznab.py -> apps/searxng/searx/engines/torznab.py renamed: searxng/searx/engines/translated.py -> apps/searxng/searx/engines/translated.py renamed: searxng/searx/engines/twitter.py -> apps/searxng/searx/engines/twitter.py renamed: searxng/searx/engines/unsplash.py -> apps/searxng/searx/engines/unsplash.py renamed: searxng/searx/engines/vimeo.py -> apps/searxng/searx/engines/vimeo.py renamed: searxng/searx/engines/wikidata.py -> apps/searxng/searx/engines/wikidata.py renamed: searxng/searx/engines/wikipedia.py -> apps/searxng/searx/engines/wikipedia.py renamed: searxng/searx/engines/wolframalpha_api.py -> apps/searxng/searx/engines/wolframalpha_api.py renamed: searxng/searx/engines/wolframalpha_noapi.py -> apps/searxng/searx/engines/wolframalpha_noapi.py renamed: searxng/searx/engines/wordnik.py -> apps/searxng/searx/engines/wordnik.py renamed: searxng/searx/engines/wttr.py -> apps/searxng/searx/engines/wttr.py renamed: searxng/searx/engines/www1x.py -> apps/searxng/searx/engines/www1x.py renamed: searxng/searx/engines/xpath.py -> apps/searxng/searx/engines/xpath.py renamed: searxng/searx/engines/yacy.py -> apps/searxng/searx/engines/yacy.py renamed: searxng/searx/engines/yahoo.py -> apps/searxng/searx/engines/yahoo.py renamed: searxng/searx/engines/yahoo_news.py -> apps/searxng/searx/engines/yahoo_news.py renamed: searxng/searx/engines/youtube_api.py -> apps/searxng/searx/engines/youtube_api.py renamed: searxng/searx/engines/youtube_noapi.py -> apps/searxng/searx/engines/youtube_noapi.py renamed: searxng/searx/engines/zlibrary.py -> apps/searxng/searx/engines/zlibrary.py renamed: searxng/searx/exceptions.py -> apps/searxng/searx/exceptions.py renamed: searxng/searx/external_bang.py -> apps/searxng/searx/external_bang.py renamed: searxng/searx/external_urls.py -> apps/searxng/searx/external_urls.py renamed: searxng/searx/flaskfix.py -> apps/searxng/searx/flaskfix.py renamed: searxng/searx/infopage/__init__.py -> apps/searxng/searx/infopage/__init__.py renamed: searxng/searx/locales.py -> apps/searxng/searx/locales.py renamed: searxng/searx/metrics/__init__.py -> apps/searxng/searx/metrics/__init__.py renamed: searxng/searx/metrics/error_recorder.py -> apps/searxng/searx/metrics/error_recorder.py renamed: searxng/searx/metrics/models.py -> apps/searxng/searx/metrics/models.py renamed: searxng/searx/network/__init__.py -> apps/searxng/searx/network/__init__.py renamed: searxng/searx/network/client.py -> apps/searxng/searx/network/client.py renamed: searxng/searx/network/network.py -> apps/searxng/searx/network/network.py renamed: searxng/searx/network/raise_for_httperror.py -> apps/searxng/searx/network/raise_for_httperror.py renamed: searxng/searx/plugins/__init__.py -> apps/searxng/searx/plugins/__init__.py renamed: searxng/searx/plugins/ahmia_filter.py -> apps/searxng/searx/plugins/ahmia_filter.py renamed: searxng/searx/plugins/hash_plugin.py -> apps/searxng/searx/plugins/hash_plugin.py renamed: searxng/searx/plugins/hostname_replace.py -> apps/searxng/searx/plugins/hostname_replace.py renamed: searxng/searx/plugins/limiter.py -> apps/searxng/searx/plugins/limiter.py renamed: searxng/searx/plugins/oa_doi_rewrite.py -> apps/searxng/searx/plugins/oa_doi_rewrite.py renamed: searxng/searx/plugins/search_on_category_select.py -> apps/searxng/searx/plugins/search_on_category_select.py renamed: searxng/searx/plugins/self_info.py -> apps/searxng/searx/plugins/self_info.py renamed: searxng/searx/plugins/tor_check.py -> apps/searxng/searx/plugins/tor_check.py renamed: searxng/searx/plugins/tracker_url_remover.py -> apps/searxng/searx/plugins/tracker_url_remover.py renamed: searxng/searx/plugins/vim_hotkeys.py -> apps/searxng/searx/plugins/vim_hotkeys.py renamed: searxng/searx/preferences.py -> apps/searxng/searx/preferences.py renamed: searxng/searx/query.py -> apps/searxng/searx/query.py renamed: searxng/searx/redisdb.py -> apps/searxng/searx/redisdb.py renamed: searxng/searx/redislib.py -> apps/searxng/searx/redislib.py renamed: searxng/searx/results.py -> apps/searxng/searx/results.py renamed: searxng/searx/search/__init__.py -> apps/searxng/searx/search/__init__.py renamed: searxng/searx/search/checker/__init__.py -> apps/searxng/searx/search/checker/__init__.py renamed: searxng/searx/search/checker/__main__.py -> apps/searxng/searx/search/checker/__main__.py renamed: searxng/searx/search/checker/background.py -> apps/searxng/searx/search/checker/background.py renamed: searxng/searx/search/checker/impl.py -> apps/searxng/searx/search/checker/impl.py renamed: searxng/searx/search/checker/scheduler.py -> apps/searxng/searx/search/checker/scheduler.py renamed: searxng/searx/search/models.py -> apps/searxng/searx/search/models.py renamed: searxng/searx/search/processors/__init__.py -> apps/searxng/searx/search/processors/__init__.py renamed: searxng/searx/search/processors/abstract.py -> apps/searxng/searx/search/processors/abstract.py renamed: searxng/searx/search/processors/offline.py -> apps/searxng/searx/search/processors/offline.py renamed: searxng/searx/search/processors/online.py -> apps/searxng/searx/search/processors/online.py renamed: searxng/searx/search/processors/online_currency.py -> apps/searxng/searx/search/processors/online_currency.py renamed: searxng/searx/search/processors/online_dictionary.py -> apps/searxng/searx/search/processors/online_dictionary.py renamed: searxng/searx/search/processors/online_url_search.py -> apps/searxng/searx/search/processors/online_url_search.py renamed: searxng/searx/settings.yml -> apps/searxng/searx/settings.yml renamed: searxng/searx/settings_defaults.py -> apps/searxng/searx/settings_defaults.py renamed: searxng/searx/settings_loader.py -> apps/searxng/searx/settings_loader.py renamed: searxng/searx/static/plugins/external_plugins/.gitignore -> apps/searxng/searx/static/plugins/external_plugins/.gitignore renamed: searxng/searx/static/themes/simple/.gitattributes -> apps/searxng/searx/static/themes/simple/.gitattributes renamed: searxng/searx/static/themes/simple/.gitignore -> apps/searxng/searx/static/themes/simple/.gitignore renamed: searxng/searx/sxng_locales.py -> apps/searxng/searx/sxng_locales.py renamed: searxng/searx/tools/__init__.py -> apps/searxng/searx/tools/__init__.py renamed: searxng/searx/tools/config.py -> apps/searxng/searx/tools/config.py renamed: searxng/searx/unixthreadname.py -> apps/searxng/searx/unixthreadname.py renamed: searxng/searx/utils.py -> apps/searxng/searx/utils.py renamed: searxng/searx/version.py -> apps/searxng/searx/version.py renamed: searxng/searx/webadapter.py -> apps/searxng/searx/webadapter.py renamed: searxng/searx/webapp.py -> apps/searxng/searx/webapp.py renamed: searxng/searx/webutils.py -> apps/searxng/searx/webutils.py renamed: searxng/searxng_extra/__init__.py -> apps/searxng/searxng_extra/__init__.py renamed: searxng/searxng_extra/standalone_searx.py -> apps/searxng/searxng_extra/standalone_searx.py renamed: searxng/searxng_extra/update/__init__.py -> apps/searxng/searxng_extra/update/__init__.py renamed: searxng/searxng_extra/update/update_ahmia_blacklist.py -> apps/searxng/searxng_extra/update/update_ahmia_blacklist.py renamed: searxng/searxng_extra/update/update_currencies.py -> apps/searxng/searxng_extra/update/update_currencies.py renamed: searxng/searxng_extra/update/update_engine_descriptions.py -> apps/searxng/searxng_extra/update/update_engine_descriptions.py renamed: searxng/searxng_extra/update/update_engine_traits.py -> apps/searxng/searxng_extra/update/update_engine_traits.py renamed: searxng/searxng_extra/update/update_external_bangs.py -> apps/searxng/searxng_extra/update/update_external_bangs.py renamed: searxng/searxng_extra/update/update_firefox_version.py -> apps/searxng/searxng_extra/update/update_firefox_version.py renamed: searxng/searxng_extra/update/update_osm_keys_tags.py -> apps/searxng/searxng_extra/update/update_osm_keys_tags.py renamed: searxng/searxng_extra/update/update_pygments.py -> apps/searxng/searxng_extra/update/update_pygments.py renamed: searxng/searxng_extra/update/update_wikidata_units.py -> apps/searxng/searxng_extra/update/update_wikidata_units.py renamed: searxng/setup.py -> apps/searxng/setup.py renamed: searxng/tests/__init__.py -> apps/searxng/tests/__init__.py renamed: searxng/tests/robot/__init__.py -> apps/searxng/tests/robot/__init__.py renamed: searxng/tests/robot/__main__.py -> apps/searxng/tests/robot/__main__.py renamed: searxng/tests/robot/settings_robot.yml -> apps/searxng/tests/robot/settings_robot.yml renamed: searxng/tests/robot/test_webapp.py -> apps/searxng/tests/robot/test_webapp.py renamed: searxng/tests/unit/__init__.py -> apps/searxng/tests/unit/__init__.py renamed: searxng/tests/unit/engines/test_command.py -> apps/searxng/tests/unit/engines/test_command.py renamed: searxng/tests/unit/engines/test_xpath.py -> apps/searxng/tests/unit/engines/test_xpath.py renamed: searxng/tests/unit/network/__init__.py -> apps/searxng/tests/unit/network/__init__.py renamed: searxng/tests/unit/network/test_network.py -> apps/searxng/tests/unit/network/test_network.py renamed: searxng/tests/unit/settings/empty_settings.yml -> apps/searxng/tests/unit/settings/empty_settings.yml renamed: searxng/tests/unit/settings/syntaxerror_settings.yml -> apps/searxng/tests/unit/settings/syntaxerror_settings.yml renamed: searxng/tests/unit/settings/test_settings.yml -> apps/searxng/tests/unit/settings/test_settings.yml renamed: searxng/tests/unit/settings/user_settings.yml -> apps/searxng/tests/unit/settings/user_settings.yml renamed: searxng/tests/unit/settings/user_settings_keep_only.yml -> apps/searxng/tests/unit/settings/user_settings_keep_only.yml renamed: searxng/tests/unit/settings/user_settings_remove.yml -> apps/searxng/tests/unit/settings/user_settings_remove.yml renamed: searxng/tests/unit/settings/user_settings_remove2.yml -> apps/searxng/tests/unit/settings/user_settings_remove2.yml renamed: searxng/tests/unit/settings/user_settings_simple.yml -> apps/searxng/tests/unit/settings/user_settings_simple.yml renamed: searxng/tests/unit/test_answerers.py -> apps/searxng/tests/unit/test_answerers.py renamed: searxng/tests/unit/test_engines_init.py -> apps/searxng/tests/unit/test_engines_init.py renamed: searxng/tests/unit/test_exceptions.py -> apps/searxng/tests/unit/test_exceptions.py renamed: searxng/tests/unit/test_external_bangs.py -> apps/searxng/tests/unit/test_external_bangs.py renamed: searxng/tests/unit/test_locales.py -> apps/searxng/tests/unit/test_locales.py renamed: searxng/tests/unit/test_plugins.py -> apps/searxng/tests/unit/test_plugins.py renamed: searxng/tests/unit/test_preferences.py -> apps/searxng/tests/unit/test_preferences.py renamed: searxng/tests/unit/test_query.py -> apps/searxng/tests/unit/test_query.py renamed: searxng/tests/unit/test_results.py -> apps/searxng/tests/unit/test_results.py renamed: searxng/tests/unit/test_search.py -> apps/searxng/tests/unit/test_search.py renamed: searxng/tests/unit/test_settings_loader.py -> apps/searxng/tests/unit/test_settings_loader.py renamed: searxng/tests/unit/test_utils.py -> apps/searxng/tests/unit/test_utils.py renamed: searxng/tests/unit/test_webadapter.py -> apps/searxng/tests/unit/test_webadapter.py renamed: searxng/tests/unit/test_webapp.py -> apps/searxng/tests/unit/test_webapp.py renamed: searxng/tests/unit/test_webutils.py -> apps/searxng/tests/unit/test_webutils.py renamed: searxng/utils/build_env.py -> apps/searxng/utils/build_env.py renamed: searxng/utils/filtron.sh -> apps/searxng/utils/filtron.sh renamed: searxng/utils/lib.sh -> apps/searxng/utils/lib.sh renamed: searxng/utils/lib_go.sh -> apps/searxng/utils/lib_go.sh renamed: searxng/utils/lib_nvm.sh -> apps/searxng/utils/lib_nvm.sh renamed: searxng/utils/lib_redis.sh -> apps/searxng/utils/lib_redis.sh renamed: searxng/utils/lib_sxng_data.sh -> apps/searxng/utils/lib_sxng_data.sh renamed: searxng/utils/lib_sxng_node.sh -> apps/searxng/utils/lib_sxng_node.sh renamed: searxng/utils/lib_sxng_static.sh -> apps/searxng/utils/lib_sxng_static.sh renamed: searxng/utils/lib_sxng_test.sh -> apps/searxng/utils/lib_sxng_test.sh renamed: searxng/utils/lib_sxng_themes.sh -> apps/searxng/utils/lib_sxng_themes.sh renamed: searxng/utils/lib_sxng_weblate.sh -> apps/searxng/utils/lib_sxng_weblate.sh renamed: searxng/utils/lxc.sh -> apps/searxng/utils/lxc.sh renamed: searxng/utils/morty.sh -> apps/searxng/utils/morty.sh renamed: searxng/utils/searx.sh -> apps/searxng/utils/searx.sh renamed: searxng/utils/searxng.sh -> apps/searxng/utils/searxng.sh renamed: searxng/utils/searxng_check.py -> apps/searxng/utils/searxng_check.py renamed: searxng/utils/templates/etc/searxng/settings.yml -> apps/searxng/utils/templates/etc/searxng/settings.yml new file: apps/shift-recorder new file: apps/stockfill new file: core/authelia/configuration.yml new file: core/authelia/users_database.yml new file: core/crowdsec/Dockerfile new file: core/crowdsec/data/detect.yaml new file: core/docker-compose.yml new file: core/test/Dockerfile new file: core/test/docker-compose.yml new file: core/test/exporter.py new file: core/traefik/data/dynamic.yaml renamed: traefik/data/plugins.yaml -> core/traefik/data/plugins.yaml new file: core/traefik/dynamic.yml new file: core/traefik/traefik.yml new file: default-network.yml new file: monitoring/docker-exporter/Dockerfile new file: monitoring/docker-exporter/exporter.py new file: monitoring/gotify/docker-compose.yml new file: monitoring/gotify/docker-health-to-gotify.sh new file: monitoring/grafana/docker-compose.yml new file: monitoring/node-red/Dockerfile new file: monitoring/node-red/data/test-container.sh new file: monitoring/node-red/docker-compose.yml new file: monitoring/portainer/docker-compose.yml new file: monitoring/prometheus/docker-compose.yml new file: monitoring/prometheus/prometheus.yml new file: monitoring/prometheus/rules/alerts.yml new file: monitoring/uptime-kuma/docker-compose.yml deleted: nextcloud/docker-compose.yml new file: services-up.sh deleted: traefik/docker-compose.yml deleted: traefik/traefik.Dockerfile modified: update-containers.py modified: update-containers.sh modified: apps/shift-recorder (modified content) modified: apps/stockfill (modified content)
624 lines
21 KiB
Python
Executable File
624 lines
21 KiB
Python
Executable File
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
# lint: pylint
|
|
# pyright: basic
|
|
"""Utility functions for the engines
|
|
|
|
"""
|
|
import re
|
|
import importlib
|
|
import importlib.util
|
|
import types
|
|
|
|
from typing import Optional, Union, Any, Set, List, Dict, MutableMapping, Tuple, Callable
|
|
from numbers import Number
|
|
from os.path import splitext, join
|
|
from random import choice
|
|
from html.parser import HTMLParser
|
|
from urllib.parse import urljoin, urlparse
|
|
|
|
from lxml import html
|
|
from lxml.etree import ElementBase, XPath, XPathError, XPathSyntaxError, _ElementStringResult, _ElementUnicodeResult
|
|
|
|
from searx import settings
|
|
from searx.data import USER_AGENTS, data_dir
|
|
from searx.version import VERSION_TAG
|
|
from searx.sxng_locales import sxng_locales
|
|
from searx.exceptions import SearxXPathSyntaxException, SearxEngineXPathException
|
|
from searx import logger
|
|
|
|
|
|
logger = logger.getChild('utils')
|
|
|
|
XPathSpecType = Union[str, XPath]
|
|
|
|
_BLOCKED_TAGS = ('script', 'style')
|
|
|
|
_ECMA_UNESCAPE4_RE = re.compile(r'%u([0-9a-fA-F]{4})', re.UNICODE)
|
|
_ECMA_UNESCAPE2_RE = re.compile(r'%([0-9a-fA-F]{2})', re.UNICODE)
|
|
|
|
_STORAGE_UNIT_VALUE: Dict[str, int] = {
|
|
'TB': 1024 * 1024 * 1024 * 1024,
|
|
'GB': 1024 * 1024 * 1024,
|
|
'MB': 1024 * 1024,
|
|
'TiB': 1000 * 1000 * 1000 * 1000,
|
|
'MiB': 1000 * 1000,
|
|
'KiB': 1000,
|
|
}
|
|
|
|
_XPATH_CACHE: Dict[str, XPath] = {}
|
|
_LANG_TO_LC_CACHE: Dict[str, Dict[str, str]] = {}
|
|
|
|
_FASTTEXT_MODEL: Optional["fasttext.FastText._FastText"] = None
|
|
"""fasttext model to predict laguage of a search term"""
|
|
|
|
SEARCH_LANGUAGE_CODES = frozenset([searxng_locale[0].split('-')[0] for searxng_locale in sxng_locales])
|
|
"""Languages supported by most searxng engines (:py:obj:`searx.sxng_locales.sxng_locales`)."""
|
|
|
|
|
|
class _NotSetClass: # pylint: disable=too-few-public-methods
|
|
"""Internal class for this module, do not create instance of this class.
|
|
Replace the None value, allow explicitly pass None as a function argument"""
|
|
|
|
|
|
_NOTSET = _NotSetClass()
|
|
|
|
|
|
def searx_useragent() -> str:
|
|
"""Return the searx User Agent"""
|
|
return 'searx/{searx_version} {suffix}'.format(
|
|
searx_version=VERSION_TAG, suffix=settings['outgoing']['useragent_suffix']
|
|
).strip()
|
|
|
|
|
|
def gen_useragent(os_string: Optional[str] = None) -> str:
|
|
"""Return a random browser User Agent
|
|
|
|
See searx/data/useragents.json
|
|
"""
|
|
return USER_AGENTS['ua'].format(os=os_string or choice(USER_AGENTS['os']), version=choice(USER_AGENTS['versions']))
|
|
|
|
|
|
class _HTMLTextExtractorException(Exception):
|
|
"""Internal exception raised when the HTML is invalid"""
|
|
|
|
|
|
class _HTMLTextExtractor(HTMLParser): # pylint: disable=W0223 # (see https://bugs.python.org/issue31844)
|
|
"""Internal class to extract text from HTML"""
|
|
|
|
def __init__(self):
|
|
HTMLParser.__init__(self)
|
|
self.result = []
|
|
self.tags = []
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
self.tags.append(tag)
|
|
if tag == 'br':
|
|
self.result.append(' ')
|
|
|
|
def handle_endtag(self, tag):
|
|
if not self.tags:
|
|
return
|
|
|
|
if tag != self.tags[-1]:
|
|
raise _HTMLTextExtractorException()
|
|
|
|
self.tags.pop()
|
|
|
|
def is_valid_tag(self):
|
|
return not self.tags or self.tags[-1] not in _BLOCKED_TAGS
|
|
|
|
def handle_data(self, data):
|
|
if not self.is_valid_tag():
|
|
return
|
|
self.result.append(data)
|
|
|
|
def handle_charref(self, name):
|
|
if not self.is_valid_tag():
|
|
return
|
|
if name[0] in ('x', 'X'):
|
|
codepoint = int(name[1:], 16)
|
|
else:
|
|
codepoint = int(name)
|
|
self.result.append(chr(codepoint))
|
|
|
|
def handle_entityref(self, name):
|
|
if not self.is_valid_tag():
|
|
return
|
|
# codepoint = htmlentitydefs.name2codepoint[name]
|
|
# self.result.append(chr(codepoint))
|
|
self.result.append(name)
|
|
|
|
def get_text(self):
|
|
return ''.join(self.result).strip()
|
|
|
|
|
|
def html_to_text(html_str: str) -> str:
|
|
"""Extract text from a HTML string
|
|
|
|
Args:
|
|
* html_str (str): string HTML
|
|
|
|
Returns:
|
|
* str: extracted text
|
|
|
|
Examples:
|
|
>>> html_to_text('Example <span id="42">#2</span>')
|
|
'Example #2'
|
|
|
|
>>> html_to_text('<style>.span { color: red; }</style><span>Example</span>')
|
|
'Example'
|
|
"""
|
|
html_str = html_str.replace('\n', ' ').replace('\r', ' ')
|
|
html_str = ' '.join(html_str.split())
|
|
s = _HTMLTextExtractor()
|
|
try:
|
|
s.feed(html_str)
|
|
except _HTMLTextExtractorException:
|
|
logger.debug("HTMLTextExtractor: invalid HTML\n%s", html_str)
|
|
return s.get_text()
|
|
|
|
|
|
def extract_text(xpath_results, allow_none: bool = False) -> Optional[str]:
|
|
"""Extract text from a lxml result
|
|
|
|
* if xpath_results is list, extract the text from each result and concat the list
|
|
* if xpath_results is a xml element, extract all the text node from it
|
|
( text_content() method from lxml )
|
|
* if xpath_results is a string element, then it's already done
|
|
"""
|
|
if isinstance(xpath_results, list):
|
|
# it's list of result : concat everything using recursive call
|
|
result = ''
|
|
for e in xpath_results:
|
|
result = result + (extract_text(e) or '')
|
|
return result.strip()
|
|
if isinstance(xpath_results, ElementBase):
|
|
# it's a element
|
|
text: str = html.tostring(xpath_results, encoding='unicode', method='text', with_tail=False)
|
|
text = text.strip().replace('\n', ' ')
|
|
return ' '.join(text.split())
|
|
if isinstance(xpath_results, (_ElementStringResult, _ElementUnicodeResult, str, Number, bool)):
|
|
return str(xpath_results)
|
|
if xpath_results is None and allow_none:
|
|
return None
|
|
if xpath_results is None and not allow_none:
|
|
raise ValueError('extract_text(None, allow_none=False)')
|
|
raise ValueError('unsupported type')
|
|
|
|
|
|
def normalize_url(url: str, base_url: str) -> str:
|
|
"""Normalize URL: add protocol, join URL with base_url, add trailing slash if there is no path
|
|
|
|
Args:
|
|
* url (str): Relative URL
|
|
* base_url (str): Base URL, it must be an absolute URL.
|
|
|
|
Example:
|
|
>>> normalize_url('https://example.com', 'http://example.com/')
|
|
'https://example.com/'
|
|
>>> normalize_url('//example.com', 'http://example.com/')
|
|
'http://example.com/'
|
|
>>> normalize_url('//example.com', 'https://example.com/')
|
|
'https://example.com/'
|
|
>>> normalize_url('/path?a=1', 'https://example.com')
|
|
'https://example.com/path?a=1'
|
|
>>> normalize_url('', 'https://example.com')
|
|
'https://example.com/'
|
|
>>> normalize_url('/test', '/path')
|
|
raise ValueError
|
|
|
|
Raises:
|
|
* lxml.etree.ParserError
|
|
|
|
Returns:
|
|
* str: normalized URL
|
|
"""
|
|
if url.startswith('//'):
|
|
# add http or https to this kind of url //example.com/
|
|
parsed_search_url = urlparse(base_url)
|
|
url = '{0}:{1}'.format(parsed_search_url.scheme or 'http', url)
|
|
elif url.startswith('/'):
|
|
# fix relative url to the search engine
|
|
url = urljoin(base_url, url)
|
|
|
|
# fix relative urls that fall through the crack
|
|
if '://' not in url:
|
|
url = urljoin(base_url, url)
|
|
|
|
parsed_url = urlparse(url)
|
|
|
|
# add a / at this end of the url if there is no path
|
|
if not parsed_url.netloc:
|
|
raise ValueError('Cannot parse url')
|
|
if not parsed_url.path:
|
|
url += '/'
|
|
|
|
return url
|
|
|
|
|
|
def extract_url(xpath_results, base_url) -> str:
|
|
"""Extract and normalize URL from lxml Element
|
|
|
|
Args:
|
|
* xpath_results (Union[List[html.HtmlElement], html.HtmlElement]): lxml Element(s)
|
|
* base_url (str): Base URL
|
|
|
|
Example:
|
|
>>> def f(s, search_url):
|
|
>>> return searx.utils.extract_url(html.fromstring(s), search_url)
|
|
>>> f('<span id="42">https://example.com</span>', 'http://example.com/')
|
|
'https://example.com/'
|
|
>>> f('https://example.com', 'http://example.com/')
|
|
'https://example.com/'
|
|
>>> f('//example.com', 'http://example.com/')
|
|
'http://example.com/'
|
|
>>> f('//example.com', 'https://example.com/')
|
|
'https://example.com/'
|
|
>>> f('/path?a=1', 'https://example.com')
|
|
'https://example.com/path?a=1'
|
|
>>> f('', 'https://example.com')
|
|
raise lxml.etree.ParserError
|
|
>>> searx.utils.extract_url([], 'https://example.com')
|
|
raise ValueError
|
|
|
|
Raises:
|
|
* ValueError
|
|
* lxml.etree.ParserError
|
|
|
|
Returns:
|
|
* str: normalized URL
|
|
"""
|
|
if xpath_results == []:
|
|
raise ValueError('Empty url resultset')
|
|
|
|
url = extract_text(xpath_results)
|
|
if url:
|
|
return normalize_url(url, base_url)
|
|
raise ValueError('URL not found')
|
|
|
|
|
|
def dict_subset(dictionary: MutableMapping, properties: Set[str]) -> Dict:
|
|
"""Extract a subset of a dict
|
|
|
|
Examples:
|
|
>>> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'C'])
|
|
{'A': 'a', 'C': 'c'}
|
|
>>> >> dict_subset({'A': 'a', 'B': 'b', 'C': 'c'}, ['A', 'D'])
|
|
{'A': 'a'}
|
|
"""
|
|
return {k: dictionary[k] for k in properties if k in dictionary}
|
|
|
|
|
|
def get_torrent_size(filesize: str, filesize_multiplier: str) -> Optional[int]:
|
|
"""
|
|
|
|
Args:
|
|
* filesize (str): size
|
|
* filesize_multiplier (str): TB, GB, .... TiB, GiB...
|
|
|
|
Returns:
|
|
* int: number of bytes
|
|
|
|
Example:
|
|
>>> get_torrent_size('5', 'GB')
|
|
5368709120
|
|
>>> get_torrent_size('3.14', 'MiB')
|
|
3140000
|
|
"""
|
|
try:
|
|
multiplier = _STORAGE_UNIT_VALUE.get(filesize_multiplier, 1)
|
|
return int(float(filesize) * multiplier)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def convert_str_to_int(number_str: str) -> int:
|
|
"""Convert number_str to int or 0 if number_str is not a number."""
|
|
if number_str.isdigit():
|
|
return int(number_str)
|
|
return 0
|
|
|
|
|
|
def int_or_zero(num: Union[List[str], str]) -> int:
|
|
"""Convert num to int or 0. num can be either a str or a list.
|
|
If num is a list, the first element is converted to int (or return 0 if the list is empty).
|
|
If num is a str, see convert_str_to_int
|
|
"""
|
|
if isinstance(num, list):
|
|
if len(num) < 1:
|
|
return 0
|
|
num = num[0]
|
|
return convert_str_to_int(num)
|
|
|
|
|
|
def is_valid_lang(lang) -> Optional[Tuple[bool, str, str]]:
|
|
"""Return language code and name if lang describe a language.
|
|
|
|
Examples:
|
|
>>> is_valid_lang('zz')
|
|
None
|
|
>>> is_valid_lang('uk')
|
|
(True, 'uk', 'ukrainian')
|
|
>>> is_valid_lang(b'uk')
|
|
(True, 'uk', 'ukrainian')
|
|
>>> is_valid_lang('en')
|
|
(True, 'en', 'english')
|
|
>>> searx.utils.is_valid_lang('Español')
|
|
(True, 'es', 'spanish')
|
|
>>> searx.utils.is_valid_lang('Spanish')
|
|
(True, 'es', 'spanish')
|
|
"""
|
|
if isinstance(lang, bytes):
|
|
lang = lang.decode()
|
|
is_abbr = len(lang) == 2
|
|
lang = lang.lower()
|
|
if is_abbr:
|
|
for l in sxng_locales:
|
|
if l[0][:2] == lang:
|
|
return (True, l[0][:2], l[3].lower())
|
|
return None
|
|
for l in sxng_locales:
|
|
if l[1].lower() == lang or l[3].lower() == lang:
|
|
return (True, l[0][:2], l[3].lower())
|
|
return None
|
|
|
|
|
|
def load_module(filename: str, module_dir: str) -> types.ModuleType:
|
|
modname = splitext(filename)[0]
|
|
modpath = join(module_dir, filename)
|
|
# and https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
spec = importlib.util.spec_from_file_location(modname, modpath)
|
|
if not spec:
|
|
raise ValueError(f"Error loading '{modpath}' module")
|
|
module = importlib.util.module_from_spec(spec)
|
|
if not spec.loader:
|
|
raise ValueError(f"Error loading '{modpath}' module")
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def to_string(obj: Any) -> str:
|
|
"""Convert obj to its string representation."""
|
|
if isinstance(obj, str):
|
|
return obj
|
|
if hasattr(obj, '__str__'):
|
|
return str(obj)
|
|
return repr(obj)
|
|
|
|
|
|
def ecma_unescape(string: str) -> str:
|
|
"""Python implementation of the unescape javascript function
|
|
|
|
https://www.ecma-international.org/ecma-262/6.0/#sec-unescape-string
|
|
https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/unescape
|
|
|
|
Examples:
|
|
>>> ecma_unescape('%u5409')
|
|
'吉'
|
|
>>> ecma_unescape('%20')
|
|
' '
|
|
>>> ecma_unescape('%F3')
|
|
'ó'
|
|
"""
|
|
# "%u5409" becomes "吉"
|
|
string = _ECMA_UNESCAPE4_RE.sub(lambda e: chr(int(e.group(1), 16)), string)
|
|
# "%20" becomes " ", "%F3" becomes "ó"
|
|
string = _ECMA_UNESCAPE2_RE.sub(lambda e: chr(int(e.group(1), 16)), string)
|
|
return string
|
|
|
|
|
|
def get_string_replaces_function(replaces: Dict[str, str]) -> Callable[[str], str]:
|
|
rep = {re.escape(k): v for k, v in replaces.items()}
|
|
pattern = re.compile("|".join(rep.keys()))
|
|
|
|
def func(text):
|
|
return pattern.sub(lambda m: rep[re.escape(m.group(0))], text)
|
|
|
|
return func
|
|
|
|
|
|
def get_engine_from_settings(name: str) -> Dict:
|
|
"""Return engine configuration from settings.yml of a given engine name"""
|
|
|
|
if 'engines' not in settings:
|
|
return {}
|
|
|
|
for engine in settings['engines']:
|
|
if 'name' not in engine:
|
|
continue
|
|
if name == engine['name']:
|
|
return engine
|
|
|
|
return {}
|
|
|
|
|
|
def get_xpath(xpath_spec: XPathSpecType) -> XPath:
|
|
"""Return cached compiled XPath
|
|
|
|
There is no thread lock.
|
|
Worst case scenario, xpath_str is compiled more than one time.
|
|
|
|
Args:
|
|
* xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
|
|
|
|
Returns:
|
|
* result (bool, float, list, str): Results.
|
|
|
|
Raises:
|
|
* TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
|
|
* SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
|
|
"""
|
|
if isinstance(xpath_spec, str):
|
|
result = _XPATH_CACHE.get(xpath_spec, None)
|
|
if result is None:
|
|
try:
|
|
result = XPath(xpath_spec)
|
|
except XPathSyntaxError as e:
|
|
raise SearxXPathSyntaxException(xpath_spec, str(e.msg)) from e
|
|
_XPATH_CACHE[xpath_spec] = result
|
|
return result
|
|
|
|
if isinstance(xpath_spec, XPath):
|
|
return xpath_spec
|
|
|
|
raise TypeError('xpath_spec must be either a str or a lxml.etree.XPath')
|
|
|
|
|
|
def eval_xpath(element: ElementBase, xpath_spec: XPathSpecType):
|
|
"""Equivalent of element.xpath(xpath_str) but compile xpath_str once for all.
|
|
See https://lxml.de/xpathxslt.html#xpath-return-values
|
|
|
|
Args:
|
|
* element (ElementBase): [description]
|
|
* xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
|
|
|
|
Returns:
|
|
* result (bool, float, list, str): Results.
|
|
|
|
Raises:
|
|
* TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
|
|
* SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
|
|
* SearxEngineXPathException: Raise when the XPath can't be evaluated.
|
|
"""
|
|
xpath = get_xpath(xpath_spec)
|
|
try:
|
|
return xpath(element)
|
|
except XPathError as e:
|
|
arg = ' '.join([str(i) for i in e.args])
|
|
raise SearxEngineXPathException(xpath_spec, arg) from e
|
|
|
|
|
|
def eval_xpath_list(element: ElementBase, xpath_spec: XPathSpecType, min_len: Optional[int] = None):
|
|
"""Same as eval_xpath, check if the result is a list
|
|
|
|
Args:
|
|
* element (ElementBase): [description]
|
|
* xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath
|
|
* min_len (int, optional): [description]. Defaults to None.
|
|
|
|
Raises:
|
|
* TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
|
|
* SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
|
|
* SearxEngineXPathException: raise if the result is not a list
|
|
|
|
Returns:
|
|
* result (bool, float, list, str): Results.
|
|
"""
|
|
result = eval_xpath(element, xpath_spec)
|
|
if not isinstance(result, list):
|
|
raise SearxEngineXPathException(xpath_spec, 'the result is not a list')
|
|
if min_len is not None and min_len > len(result):
|
|
raise SearxEngineXPathException(xpath_spec, 'len(xpath_str) < ' + str(min_len))
|
|
return result
|
|
|
|
|
|
def eval_xpath_getindex(elements: ElementBase, xpath_spec: XPathSpecType, index: int, default=_NOTSET):
|
|
"""Call eval_xpath_list then get one element using the index parameter.
|
|
If the index does not exist, either aise an exception is default is not set,
|
|
other return the default value (can be None).
|
|
|
|
Args:
|
|
* elements (ElementBase): lxml element to apply the xpath.
|
|
* xpath_spec (str|lxml.etree.XPath): XPath as a str or lxml.etree.XPath.
|
|
* index (int): index to get
|
|
* default (Object, optional): Defaults if index doesn't exist.
|
|
|
|
Raises:
|
|
* TypeError: Raise when xpath_spec is neither a str nor a lxml.etree.XPath
|
|
* SearxXPathSyntaxException: Raise when there is a syntax error in the XPath
|
|
* SearxEngineXPathException: if the index is not found. Also see eval_xpath.
|
|
|
|
Returns:
|
|
* result (bool, float, list, str): Results.
|
|
"""
|
|
result = eval_xpath_list(elements, xpath_spec)
|
|
if -len(result) <= index < len(result):
|
|
return result[index]
|
|
if default == _NOTSET:
|
|
# raise an SearxEngineXPathException instead of IndexError
|
|
# to record xpath_spec
|
|
raise SearxEngineXPathException(xpath_spec, 'index ' + str(index) + ' not found')
|
|
return default
|
|
|
|
|
|
def _get_fasttext_model() -> "fasttext.FastText._FastText":
|
|
global _FASTTEXT_MODEL # pylint: disable=global-statement
|
|
if _FASTTEXT_MODEL is None:
|
|
import fasttext # pylint: disable=import-outside-toplevel
|
|
|
|
# Monkey patch: prevent fasttext from showing a (useless) warning when loading a model.
|
|
fasttext.FastText.eprint = lambda x: None
|
|
_FASTTEXT_MODEL = fasttext.load_model(str(data_dir / 'lid.176.ftz'))
|
|
return _FASTTEXT_MODEL
|
|
|
|
|
|
def detect_language(text: str, threshold: float = 0.3, only_search_languages: bool = False) -> Optional[str]:
|
|
"""Detect the language of the ``text`` parameter.
|
|
|
|
:param str text: The string whose language is to be detected.
|
|
|
|
:param float threshold: Threshold filters the returned labels by a threshold
|
|
on probability. A choice of 0.3 will return labels with at least 0.3
|
|
probability.
|
|
|
|
:param bool only_search_languages: If ``True``, returns only supported
|
|
SearXNG search languages. see :py:obj:`searx.languages`
|
|
|
|
:rtype: str, None
|
|
:returns:
|
|
The detected language code or ``None``. See below.
|
|
|
|
:raises ValueError: If ``text`` is not a string.
|
|
|
|
The language detection is done by using `a fork`_ of the fastText_ library
|
|
(`python fasttext`_). fastText_ distributes the `language identification
|
|
model`_, for reference:
|
|
|
|
- `FastText.zip: Compressing text classification models`_
|
|
- `Bag of Tricks for Efficient Text Classification`_
|
|
|
|
The `language identification model`_ support the language codes
|
|
(ISO-639-3)::
|
|
|
|
af als am an ar arz as ast av az azb ba bar bcl be bg bh bn bo bpy br bs
|
|
bxr ca cbk ce ceb ckb co cs cv cy da de diq dsb dty dv el eml en eo es
|
|
et eu fa fi fr frr fy ga gd gl gn gom gu gv he hi hif hr hsb ht hu hy ia
|
|
id ie ilo io is it ja jbo jv ka kk km kn ko krc ku kv kw ky la lb lez li
|
|
lmo lo lrc lt lv mai mg mhr min mk ml mn mr mrj ms mt mwl my myv mzn nah
|
|
nap nds ne new nl nn no oc or os pa pam pfl pl pms pnb ps pt qu rm ro ru
|
|
rue sa sah sc scn sco sd sh si sk sl so sq sr su sv sw ta te tg th tk tl
|
|
tr tt tyv ug uk ur uz vec vep vi vls vo wa war wuu xal xmf yi yo yue zh
|
|
|
|
By using ``only_search_languages=True`` the `language identification model`_
|
|
is harmonized with the SearXNG's language (locale) model. General
|
|
conditions of SearXNG's locale model are:
|
|
|
|
a. SearXNG's locale of a query is passed to the
|
|
:py:obj:`searx.locales.get_engine_locale` to get a language and/or region
|
|
code that is used by an engine.
|
|
|
|
b. Most of SearXNG's engines do not support all the languages from `language
|
|
identification model`_ and there is also a discrepancy in the ISO-639-3
|
|
(fastext) and ISO-639-2 (SearXNG)handling. Further more, in SearXNG the
|
|
locales like ``zh-TH`` (``zh-CN``) are mapped to ``zh_Hant``
|
|
(``zh_Hans``) while the `language identification model`_ reduce both to
|
|
``zh``.
|
|
|
|
.. _a fork: https://github.com/searxng/fasttext-predict
|
|
.. _fastText: https://fasttext.cc/
|
|
.. _python fasttext: https://pypi.org/project/fasttext/
|
|
.. _language identification model: https://fasttext.cc/docs/en/language-identification.html
|
|
.. _Bag of Tricks for Efficient Text Classification: https://arxiv.org/abs/1607.01759
|
|
.. _`FastText.zip: Compressing text classification models`: https://arxiv.org/abs/1612.03651
|
|
|
|
"""
|
|
if not isinstance(text, str):
|
|
raise ValueError('text must a str')
|
|
r = _get_fasttext_model().predict(text.replace('\n', ' '), k=1, threshold=threshold)
|
|
if isinstance(r, tuple) and len(r) == 2 and len(r[0]) > 0 and len(r[1]) > 0:
|
|
language = r[0][0].split('__label__')[1]
|
|
if only_search_languages and language not in SEARCH_LANGUAGE_CODES:
|
|
return None
|
|
return language
|
|
return None
|