modified: .gitignore

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)
This commit is contained in:
git
2026-03-31 19:59:49 +10:00
parent d5b6cb22cd
commit b71cd3fcbb
340 changed files with 2084 additions and 311 deletions
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
# (C) Copyright Contributors to the SearXNG project.
"""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.webadapter
EngineCategoriesVar = Optional[List[str]]
def get_search_query(
args: argparse.Namespace, engine_categories: EngineCategoriesVar = None
) -> searx.search.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.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])
searx.search.initialize_processors(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))
@@ -0,0 +1,37 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""This script saves `Ahmia's blacklist`_ for onion sites.
Output file: :origin:`searx/data/ahmia_blacklist.txt` (:origin:`CI Update data
... <.github/workflows/data-update.yml>`).
.. _Ahmia's blacklist: https://ahmia.fi/blacklist/
"""
# pylint: disable=use-dict-literal
from os.path import join
import requests
from searx import searx_dir
URL = 'https://ahmia.fi/blacklist/banned/'
def fetch_ahmia_blacklist():
resp = requests.get(URL, timeout=3.0)
if resp.status_code != 200:
# pylint: disable=broad-exception-raised
raise Exception("Error fetching Ahmia blacklist, HTTP code " + resp.status_code)
return resp.text.split()
def get_ahmia_blacklist_filename():
return join(join(searx_dir, "data"), "ahmia_blacklist.txt")
if __name__ == '__main__':
blacklist = fetch_ahmia_blacklist()
with open(get_ahmia_blacklist_filename(), "w", encoding='utf-8') as f:
f.write('\n'.join(blacklist))
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fetch currencies from :origin:`searx/engines/wikidata.py` engine.
Output file: :origin:`searx/data/currencies.json` (:origin:`CI Update data ...
<.github/workflows/data-update.yml>`).
"""
# pylint: disable=invalid-name
import re
import unicodedata
import json
# set path
from os.path import join
from searx import searx_dir
from searx.locales import LOCALE_NAMES, locales_initialize
from searx.engines import wikidata, set_loggers
set_loggers(wikidata, 'wikidata')
locales_initialize()
# ORDER BY (with all the query fields) is important to keep a deterministic result order
# so multiple invocation of this script doesn't change currencies.json
SARQL_REQUEST = """
SELECT DISTINCT ?iso4217 ?unit ?unicode ?label ?alias WHERE {
?item wdt:P498 ?iso4217; rdfs:label ?label.
OPTIONAL { ?item skos:altLabel ?alias FILTER (LANG (?alias) = LANG(?label)). }
OPTIONAL { ?item wdt:P5061 ?unit. }
OPTIONAL { ?item wdt:P489 ?symbol.
?symbol wdt:P487 ?unicode. }
MINUS { ?item wdt:P582 ?end_data . } # Ignore monney with an end date
MINUS { ?item wdt:P31/wdt:P279* wd:Q15893266 . } # Ignore "former entity" (obsolete currency)
FILTER(LANG(?label) IN (%LANGUAGES_SPARQL%)).
}
ORDER BY ?iso4217 ?unit ?unicode ?label ?alias
"""
# ORDER BY (with all the query fields) is important to keep a deterministic result order
# so multiple invocation of this script doesn't change currencies.json
SPARQL_WIKIPEDIA_NAMES_REQUEST = """
SELECT DISTINCT ?iso4217 ?article_name WHERE {
?item wdt:P498 ?iso4217 .
?article schema:about ?item ;
schema:name ?article_name ;
schema:isPartOf [ wikibase:wikiGroup "wikipedia" ]
MINUS { ?item wdt:P582 ?end_data . } # Ignore monney with an end date
MINUS { ?item wdt:P31/wdt:P279* wd:Q15893266 . } # Ignore "former entity" (obsolete currency)
FILTER(LANG(?article_name) IN (%LANGUAGES_SPARQL%)).
}
ORDER BY ?iso4217 ?article_name
"""
LANGUAGES = LOCALE_NAMES.keys()
LANGUAGES_SPARQL = ', '.join(set(map(lambda l: repr(l.split('_')[0]), LANGUAGES)))
def remove_accents(name):
return unicodedata.normalize('NFKD', name).lower()
def remove_extra(name):
for c in ('(', ':'):
if c in name:
name = name.split(c)[0].strip()
return name
def _normalize_name(name):
name = re.sub(' +', ' ', remove_accents(name.lower()).replace('-', ' '))
name = remove_extra(name)
return name
def add_currency_name(db, name, iso4217, normalize_name=True):
db_names = db['names']
if normalize_name:
name = _normalize_name(name)
iso4217_set = db_names.setdefault(name, [])
if iso4217 not in iso4217_set:
iso4217_set.insert(0, iso4217)
def add_currency_label(db, label, iso4217, language):
labels = db['iso4217'].setdefault(iso4217, {})
labels[language] = label
def wikidata_request_result_iterator(request):
result = wikidata.send_wikidata_query(request.replace('%LANGUAGES_SPARQL%', LANGUAGES_SPARQL))
if result is not None:
for r in result['results']['bindings']:
yield r
def fetch_db():
db = {
'names': {},
'iso4217': {},
}
for r in wikidata_request_result_iterator(SPARQL_WIKIPEDIA_NAMES_REQUEST):
iso4217 = r['iso4217']['value']
article_name = r['article_name']['value']
article_lang = r['article_name']['xml:lang']
add_currency_name(db, article_name, iso4217)
add_currency_label(db, article_name, iso4217, article_lang)
for r in wikidata_request_result_iterator(SARQL_REQUEST):
iso4217 = r['iso4217']['value']
if 'label' in r:
label = r['label']['value']
label_lang = r['label']['xml:lang']
add_currency_name(db, label, iso4217)
add_currency_label(db, label, iso4217, label_lang)
if 'alias' in r:
add_currency_name(db, r['alias']['value'], iso4217)
if 'unicode' in r:
add_currency_name(db, r['unicode']['value'], iso4217, normalize_name=False)
if 'unit' in r:
add_currency_name(db, r['unit']['value'], iso4217, normalize_name=False)
return db
def get_filename():
return join(join(searx_dir, "data"), "currencies.json")
def main():
db = fetch_db()
# static
add_currency_name(db, "euro", 'EUR')
add_currency_name(db, "euros", 'EUR')
add_currency_name(db, "dollar", 'USD')
add_currency_name(db, "dollars", 'USD')
add_currency_name(db, "peso", 'MXN')
add_currency_name(db, "pesos", 'MXN')
# reduce memory usage:
# replace lists with one item by the item. see
# searx.search.processors.online_currency.name_to_iso4217
for name in db['names']:
if len(db['names'][name]) == 1:
db['names'][name] = db['names'][name][0]
with open(get_filename(), 'w', encoding='utf8') as f:
json.dump(db, f, ensure_ascii=False, indent=4)
if __name__ == '__main__':
main()
@@ -0,0 +1,370 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fetch website description from websites and from
:origin:`searx/engines/wikidata.py` engine.
Output file: :origin:`searx/data/engine_descriptions.json`.
"""
# pylint: disable=invalid-name, global-statement
import json
from urllib.parse import urlparse
from os.path import join
from lxml.html import fromstring
from searx.engines import wikidata, set_loggers
from searx.utils import extract_text, searx_useragent
from searx.locales import LOCALE_NAMES, locales_initialize, match_locale
from searx import searx_dir
from searx.utils import gen_useragent, detect_language
import searx.search
import searx.network
set_loggers(wikidata, 'wikidata')
locales_initialize()
# you can run the query in https://query.wikidata.org
# replace %IDS% by Wikidata entities separated by spaces with the prefix wd:
# for example wd:Q182496 wd:Q1540899
# replace %LANGUAGES_SPARQL% by languages
SPARQL_WIKIPEDIA_ARTICLE = """
SELECT DISTINCT ?item ?name ?article ?lang
WHERE {
hint:Query hint:optimizer "None".
VALUES ?item { %IDS% }
?article schema:about ?item ;
schema:inLanguage ?lang ;
schema:name ?name ;
schema:isPartOf [ wikibase:wikiGroup "wikipedia" ] .
FILTER(?lang in (%LANGUAGES_SPARQL%)) .
FILTER (!CONTAINS(?name, ':')) .
}
ORDER BY ?item ?lang
"""
SPARQL_DESCRIPTION = """
SELECT DISTINCT ?item ?itemDescription
WHERE {
VALUES ?item { %IDS% }
?item schema:description ?itemDescription .
FILTER (lang(?itemDescription) in (%LANGUAGES_SPARQL%))
}
ORDER BY ?itemLang
"""
NOT_A_DESCRIPTION = [
'web site',
'site web',
'komputa serĉilo',
'interreta serĉilo',
'bilaketa motor',
'web search engine',
'wikimedia täpsustuslehekülg',
]
SKIP_ENGINE_SOURCE = [
# fmt: off
('gitlab', 'wikidata')
# descriptions are about wikipedia disambiguation pages
# fmt: on
]
WIKIPEDIA_LANGUAGES = {}
LANGUAGES_SPARQL = ''
IDS = None
WIKIPEDIA_LANGUAGE_VARIANTS = {'zh_Hant': 'zh-tw'}
descriptions = {}
wd_to_engine_name = {}
def normalize_description(description):
for c in [chr(c) for c in range(0, 31)]:
description = description.replace(c, ' ')
description = ' '.join(description.strip().split())
return description
def update_description(engine_name, lang, description, source, replace=True):
if not isinstance(description, str):
return
description = normalize_description(description)
if description.lower() == engine_name.lower():
return
if description.lower() in NOT_A_DESCRIPTION:
return
if (engine_name, source) in SKIP_ENGINE_SOURCE:
return
if ' ' not in description:
# skip unique word description (like "website")
return
if replace or lang not in descriptions[engine_name]:
descriptions[engine_name][lang] = [description, source]
def get_wikipedia_summary(wikipedia_url, searxng_locale):
# get the REST API URL from the HTML URL
# Headers
headers = {'User-Agent': searx_useragent()}
if searxng_locale in WIKIPEDIA_LANGUAGE_VARIANTS:
headers['Accept-Language'] = WIKIPEDIA_LANGUAGE_VARIANTS.get(searxng_locale)
# URL path : from HTML URL to REST API URL
parsed_url = urlparse(wikipedia_url)
# remove the /wiki/ prefix
article_name = parsed_url.path.split('/wiki/')[1]
# article_name is already encoded but not the / which is required for the REST API call
encoded_article_name = article_name.replace('/', '%2F')
path = '/api/rest_v1/page/summary/' + encoded_article_name
wikipedia_rest_url = parsed_url._replace(path=path).geturl()
try:
response = searx.network.get(wikipedia_rest_url, headers=headers, timeout=10)
response.raise_for_status()
except Exception as e: # pylint: disable=broad-except
print(" ", wikipedia_url, e)
return None
api_result = json.loads(response.text)
return api_result.get('extract')
def get_website_description(url, lang1, lang2=None):
headers = {
'User-Agent': gen_useragent(),
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'DNT': '1',
'Upgrade-Insecure-Requests': '1',
'Sec-GPC': '1',
'Cache-Control': 'max-age=0',
}
if lang1 is not None:
lang_list = [lang1]
if lang2 is not None:
lang_list.append(lang2)
headers['Accept-Language'] = f'{",".join(lang_list)};q=0.8'
try:
response = searx.network.get(url, headers=headers, timeout=10)
response.raise_for_status()
except Exception: # pylint: disable=broad-except
return (None, None)
try:
html = fromstring(response.text)
except ValueError:
html = fromstring(response.content)
description = extract_text(html.xpath('/html/head/meta[@name="description"]/@content'))
if not description:
description = extract_text(html.xpath('/html/head/meta[@property="og:description"]/@content'))
if not description:
description = extract_text(html.xpath('/html/head/title'))
lang = extract_text(html.xpath('/html/@lang'))
if lang is None and len(lang1) > 0:
lang = lang1
lang = detect_language(description) or lang or 'en'
lang = lang.split('_')[0]
lang = lang.split('-')[0]
return (lang, description)
def initialize():
global IDS, LANGUAGES_SPARQL
searx.search.initialize()
wikipedia_engine = searx.engines.engines['wikipedia']
locale2lang = {'nl-BE': 'nl'}
for sxng_ui_lang in LOCALE_NAMES:
sxng_ui_alias = locale2lang.get(sxng_ui_lang, sxng_ui_lang)
wiki_lang = None
if sxng_ui_alias in wikipedia_engine.traits.custom['WIKIPEDIA_LANGUAGES']:
wiki_lang = sxng_ui_alias
if not wiki_lang:
wiki_lang = wikipedia_engine.traits.get_language(sxng_ui_alias)
if not wiki_lang:
print(f"WIKIPEDIA_LANGUAGES missing {sxng_ui_lang}")
continue
WIKIPEDIA_LANGUAGES[sxng_ui_lang] = wiki_lang
LANGUAGES_SPARQL = ', '.join(f"'{l}'" for l in set(WIKIPEDIA_LANGUAGES.values()))
for engine_name, engine in searx.engines.engines.items():
descriptions[engine_name] = {}
wikidata_id = getattr(engine, "about", {}).get('wikidata_id')
if wikidata_id is not None:
wd_to_engine_name.setdefault(wikidata_id, set()).add(engine_name)
IDS = ' '.join(list(map(lambda wd_id: 'wd:' + wd_id, wd_to_engine_name.keys())))
def fetch_wikidata_descriptions():
print('Fetching wikidata descriptions')
searx.network.set_timeout_for_thread(60)
result = wikidata.send_wikidata_query(
SPARQL_DESCRIPTION.replace('%IDS%', IDS).replace('%LANGUAGES_SPARQL%', LANGUAGES_SPARQL)
)
if result is not None:
for binding in result['results']['bindings']:
wikidata_id = binding['item']['value'].replace('http://www.wikidata.org/entity/', '')
wikidata_lang = binding['itemDescription']['xml:lang']
desc = binding['itemDescription']['value']
for engine_name in wd_to_engine_name[wikidata_id]:
for searxng_locale in LOCALE_NAMES:
if WIKIPEDIA_LANGUAGES[searxng_locale] != wikidata_lang:
continue
print(
f" engine: {engine_name:20} / wikidata_lang: {wikidata_lang:5}",
f"/ len(wikidata_desc): {len(desc)}",
)
update_description(engine_name, searxng_locale, desc, 'wikidata')
def fetch_wikipedia_descriptions():
print('Fetching wikipedia descriptions')
result = wikidata.send_wikidata_query(
SPARQL_WIKIPEDIA_ARTICLE.replace('%IDS%', IDS).replace('%LANGUAGES_SPARQL%', LANGUAGES_SPARQL)
)
if result is not None:
for binding in result['results']['bindings']:
wikidata_id = binding['item']['value'].replace('http://www.wikidata.org/entity/', '')
wikidata_lang = binding['name']['xml:lang']
wikipedia_url = binding['article']['value'] # for example the URL https://de.wikipedia.org/wiki/PubMed
for engine_name in wd_to_engine_name[wikidata_id]:
for searxng_locale in LOCALE_NAMES:
if WIKIPEDIA_LANGUAGES[searxng_locale] != wikidata_lang:
continue
desc = get_wikipedia_summary(wikipedia_url, searxng_locale)
if not desc:
continue
print(
f" engine: {engine_name:20} / wikidata_lang: {wikidata_lang:5}",
f"/ len(wikipedia_desc): {len(desc)}",
)
update_description(engine_name, searxng_locale, desc, 'wikipedia')
def normalize_url(url):
url = url.replace('{language}', 'en')
url = urlparse(url)._replace(path='/', params='', query='', fragment='').geturl()
url = url.replace('https://api.', 'https://')
return url
def fetch_website_description(engine_name, website):
print(f"- fetch website descr: {engine_name} / {website}")
default_lang, default_description = get_website_description(website, None, None)
if default_lang is None or default_description is None:
# the front page can't be fetched: skip this engine
return
# to specify an order in where the most common languages are in front of the
# language list ..
languages = ['en', 'es', 'pt', 'ru', 'tr', 'fr']
languages = languages + [l for l in LOCALE_NAMES if l not in languages]
previous_matched_lang = None
previous_count = 0
for lang in languages:
if lang in descriptions[engine_name]:
continue
fetched_lang, desc = get_website_description(website, lang, WIKIPEDIA_LANGUAGES[lang])
if fetched_lang is None or desc is None:
continue
# check if desc changed with the different lang values
if fetched_lang == previous_matched_lang:
previous_count += 1
if previous_count == 6:
# the website has returned the same description for 6 different languages in Accept-Language header
# stop now
break
else:
previous_matched_lang = fetched_lang
previous_count = 0
# Don't trust in the value of fetched_lang, some websites return
# for some inappropriate values, by example bing-images::
#
# requested lang: zh-Hans-CN / fetched lang: ceb / desc: 查看根据您的兴趣量身定制的提要
#
# The lang ceb is "Cebuano" but the description is given in zh-Hans-CN
print(
f" engine: {engine_name:20} / requested lang:{lang:7}"
f" / fetched lang: {fetched_lang:7} / len(desc): {len(desc)}"
)
matched_lang = match_locale(fetched_lang, LOCALE_NAMES.keys(), fallback=lang)
update_description(engine_name, matched_lang, desc, website, replace=False)
def fetch_website_descriptions():
print('Fetching website descriptions')
for engine_name, engine in searx.engines.engines.items():
website = getattr(engine, "about", {}).get('website')
if website is None and hasattr(engine, "search_url"):
website = normalize_url(getattr(engine, "search_url"))
if website is None and hasattr(engine, "base_url"):
website = normalize_url(getattr(engine, "base_url"))
if website is not None:
fetch_website_description(engine_name, website)
def get_engine_descriptions_filename():
return join(join(searx_dir, "data"), "engine_descriptions.json")
def get_output():
"""
From descriptions[engine][language] = [description, source]
To
* output[language][engine] = description_and_source
* description_and_source can be:
* [description, source]
* description (if source = "wikipedia")
* [f"engine:lang", "ref"] (reference to another existing description)
"""
output = {locale: {} for locale in LOCALE_NAMES}
seen_descriptions = {}
for engine_name, lang_descriptions in descriptions.items():
for language, description in lang_descriptions.items():
if description[0] in seen_descriptions:
ref = seen_descriptions[description[0]]
description = [f'{ref[0]}:{ref[1]}', 'ref']
else:
seen_descriptions[description[0]] = (engine_name, language)
if description[1] == 'wikipedia':
description = description[0]
output.setdefault(language, {}).setdefault(engine_name, description)
return output
def main():
initialize()
fetch_wikidata_descriptions()
fetch_wikipedia_descriptions()
fetch_website_descriptions()
output = get_output()
with open(get_engine_descriptions_filename(), 'w', encoding='utf8') as f:
f.write(json.dumps(output, indent=1, separators=(',', ':'), ensure_ascii=False))
if __name__ == "__main__":
main()
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Update :py:obj:`searx.enginelib.traits.EngineTraitsMap` and :origin:`searx/languages.py`
:py:obj:`searx.enginelib.traits.EngineTraitsMap.ENGINE_TRAITS_FILE`:
Persistence of engines traits, fetched from the engines.
:origin:`searx/languages.py`
Is generated from intersecting each engine's supported traits.
The script :origin:`searxng_extra/update/update_engine_traits.py` is called in
the :origin:`CI Update data ... <.github/workflows/data-update.yml>`
"""
# pylint: disable=invalid-name
from unicodedata import lookup
from pathlib import Path
from pprint import pformat
import babel
from searx import settings, searx_dir
from searx import network
from searx.engines import load_engines
from searx.enginelib.traits import EngineTraitsMap
# Output files.
languages_file = Path(searx_dir) / 'sxng_locales.py'
languages_file_header = """\
# -*- coding: utf-8 -*-
'''List of SearXNG's locale codes.
This file is generated automatically by::
./manage pyenv.cmd searxng_extra/update/update_engine_traits.py
'''
sxng_locales = (
"""
languages_file_footer = """,
)
'''
A list of five-digit tuples:
0. SearXNG's internal locale tag (a language or region tag)
1. Name of the language (:py:obj:`babel.core.Locale.get_language_name`)
2. For region tags the name of the region (:py:obj:`babel.core.Locale.get_territory_name`).
Empty string for language tags.
3. English language name (from :py:obj:`babel.core.Locale.english_name`)
4. Unicode flag (emoji) that fits to SearXNG's internal region tag. Languages
are represented by a globe (\U0001F310)
.. code:: python
('en', 'English', '', 'English', '\U0001f310'),
('en-CA', 'English', 'Canada', 'English', '\U0001f1e8\U0001f1e6'),
('en-US', 'English', 'United States', 'English', '\U0001f1fa\U0001f1f8'),
..
('fr', 'Français', '', 'French', '\U0001f310'),
('fr-BE', 'Français', 'Belgique', 'French', '\U0001f1e7\U0001f1ea'),
('fr-CA', 'Français', 'Canada', 'French', '\U0001f1e8\U0001f1e6'),
:meta hide-value:
'''
"""
lang2emoji = {
'ha': '\U0001F1F3\U0001F1EA', # Hausa / Niger
'bs': '\U0001F1E7\U0001F1E6', # Bosnian / Bosnia & Herzegovina
'jp': '\U0001F1EF\U0001F1F5', # Japanese
'ua': '\U0001F1FA\U0001F1E6', # Ukrainian
'he': '\U0001F1EE\U0001F1F1', # Hebrew
}
def main():
load_engines(settings['engines'])
# traits_map = EngineTraitsMap.from_data()
traits_map = fetch_traits_map()
sxng_tag_list = filter_locales(traits_map)
write_languages_file(sxng_tag_list)
def fetch_traits_map():
"""Fetchs supported languages for each engine and writes json file with those."""
network.set_timeout_for_thread(10.0)
def log(msg):
print(msg)
traits_map = EngineTraitsMap.fetch_traits(log=log)
print("fetched properties from %s engines" % len(traits_map))
print("write json file: %s" % traits_map.ENGINE_TRAITS_FILE)
traits_map.save_data()
return traits_map
def filter_locales(traits_map: EngineTraitsMap):
"""Filter language & region tags by a threshold."""
min_eng_per_region = 11
min_eng_per_lang = 13
_ = {}
for eng in traits_map.values():
for reg in eng.regions.keys():
_[reg] = _.get(reg, 0) + 1
regions = set(k for k, v in _.items() if v >= min_eng_per_region)
lang_from_region = set(k.split('-')[0] for k in regions)
_ = {}
for eng in traits_map.values():
for lang in eng.languages.keys():
# ignore script types like zh_Hant, zh_Hans or sr_Latin, pa_Arab (they
# already counted by existence of 'zh' or 'sr', 'pa')
if '_' in lang:
# print("ignore %s" % lang)
continue
_[lang] = _.get(lang, 0) + 1
languages = set(k for k, v in _.items() if v >= min_eng_per_lang)
sxng_tag_list = set()
sxng_tag_list.update(regions)
sxng_tag_list.update(lang_from_region)
sxng_tag_list.update(languages)
return sxng_tag_list
def write_languages_file(sxng_tag_list):
language_codes = []
for sxng_tag in sorted(sxng_tag_list):
sxng_locale: babel.Locale = babel.Locale.parse(sxng_tag, sep='-')
flag = get_unicode_flag(sxng_locale) or ''
item = (
sxng_tag,
sxng_locale.get_language_name().title(),
sxng_locale.get_territory_name() or '',
sxng_locale.english_name.split(' (')[0],
UnicodeEscape(flag),
)
language_codes.append(item)
language_codes = tuple(language_codes)
with open(languages_file, 'w', encoding='utf-8') as new_file:
file_content = "{header} {language_codes}{footer}".format(
header=languages_file_header,
language_codes=pformat(language_codes, width=120, indent=4)[1:-1],
footer=languages_file_footer,
)
new_file.write(file_content)
new_file.close()
class UnicodeEscape(str):
"""Escape unicode string in :py:obj:`pprint.pformat`"""
def __repr__(self):
return "'" + "".join([chr(c) for c in self.encode('unicode-escape')]) + "'"
def get_unicode_flag(locale: babel.Locale):
"""Determine a unicode flag (emoji) that fits to the ``locale``"""
emoji = lang2emoji.get(locale.language)
if emoji:
return emoji
if not locale.territory:
return '\U0001F310'
emoji = lang2emoji.get(locale.territory.lower())
if emoji:
return emoji
try:
c1 = lookup('REGIONAL INDICATOR SYMBOL LETTER ' + locale.territory[0])
c2 = lookup('REGIONAL INDICATOR SYMBOL LETTER ' + locale.territory[1])
# print("OK : %s --> %s%s" % (locale, c1, c2))
except KeyError as exc:
print("ERROR: %s --> %s" % (locale, exc))
return None
return c1 + c2
if __name__ == "__main__":
main()
+163
View File
@@ -0,0 +1,163 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Update :origin:`searx/data/external_bangs.json` using the duckduckgo bangs
(:origin:`CI Update data ... <.github/workflows/data-update.yml>`).
https://duckduckgo.com/newbang loads:
* a javascript which provides the bang version ( https://duckduckgo.com/bv1.js )
* a JSON file which contains the bangs ( https://duckduckgo.com/bang.v260.js for example )
This script loads the javascript, then the bangs.
The javascript URL may change in the future ( for example
https://duckduckgo.com/bv2.js ), but most probably it will requires to update
RE_BANG_VERSION
"""
# pylint: disable=C0116
import json
import re
from os.path import join
import httpx
from searx import searx_dir # pylint: disable=E0401 C0413
from searx.external_bang import LEAF_KEY
# from https://duckduckgo.com/newbang
URL_BV1 = 'https://duckduckgo.com/bv1.js'
RE_BANG_VERSION = re.compile(r'\/bang\.v([0-9]+)\.js')
HTTPS_COLON = 'https:'
HTTP_COLON = 'http:'
def get_bang_url():
response = httpx.get(URL_BV1)
response.raise_for_status()
r = RE_BANG_VERSION.findall(response.text)
return f'https://duckduckgo.com/bang.v{r[0]}.js', r[0]
def fetch_ddg_bangs(url):
response = httpx.get(url)
response.raise_for_status()
return json.loads(response.content.decode())
def merge_when_no_leaf(node):
"""Minimize the number of nodes
``A -> B -> C``
- ``B`` is child of ``A``
- ``C`` is child of ``B``
If there are no ``C`` equals to ``<LEAF_KEY>``, then each ``C`` are merged
into ``A``. For example (5 nodes)::
d -> d -> g -> <LEAF_KEY> (ddg)
-> i -> g -> <LEAF_KEY> (dig)
becomes (3 noodes)::
d -> dg -> <LEAF_KEY>
-> ig -> <LEAF_KEY>
"""
restart = False
if not isinstance(node, dict):
return
# create a copy of the keys so node can be modified
keys = list(node.keys())
for key in keys:
if key == LEAF_KEY:
continue
value = node[key]
value_keys = list(value.keys())
if LEAF_KEY not in value_keys:
for value_key in value_keys:
node[key + value_key] = value[value_key]
merge_when_no_leaf(node[key + value_key])
del node[key]
restart = True
else:
merge_when_no_leaf(value)
if restart:
merge_when_no_leaf(node)
def optimize_leaf(parent, parent_key, node):
if not isinstance(node, dict):
return
if len(node) == 1 and LEAF_KEY in node and parent is not None:
parent[parent_key] = node[LEAF_KEY]
else:
for key, value in node.items():
optimize_leaf(node, key, value)
def parse_ddg_bangs(ddg_bangs):
bang_trie = {}
bang_urls = {}
for bang_definition in ddg_bangs:
# bang_list
bang_url = bang_definition['u']
if '{{{s}}}' not in bang_url:
# ignore invalid bang
continue
bang_url = bang_url.replace('{{{s}}}', chr(2))
# only for the https protocol: "https://example.com" becomes "//example.com"
if bang_url.startswith(HTTPS_COLON + '//'):
bang_url = bang_url[len(HTTPS_COLON) :]
#
if bang_url.startswith(HTTP_COLON + '//') and bang_url[len(HTTP_COLON) :] in bang_urls:
# if the bang_url uses the http:// protocol, and the same URL exists in https://
# then reuse the https:// bang definition. (written //example.com)
bang_def_output = bang_urls[bang_url[len(HTTP_COLON) :]]
else:
# normal use case : new http:// URL or https:// URL (without "https:", see above)
bang_rank = str(bang_definition['r'])
bang_def_output = bang_url + chr(1) + bang_rank
bang_def_output = bang_urls.setdefault(bang_url, bang_def_output)
bang_urls[bang_url] = bang_def_output
# bang name
bang = bang_definition['t']
# bang_trie
t = bang_trie
for bang_letter in bang:
t = t.setdefault(bang_letter, {})
t = t.setdefault(LEAF_KEY, bang_def_output)
# optimize the trie
merge_when_no_leaf(bang_trie)
optimize_leaf(None, None, bang_trie)
return bang_trie
def get_bangs_filename():
return join(join(searx_dir, "data"), "external_bangs.json")
if __name__ == '__main__':
bangs_url, bangs_version = get_bang_url()
print(f'fetch bangs from {bangs_url}')
output = {'version': bangs_version, 'trie': parse_ddg_bangs(fetch_ddg_bangs(bangs_url))}
with open(get_bangs_filename(), 'w', encoding="utf8") as fp:
json.dump(output, fp, ensure_ascii=False, indent=4)
@@ -0,0 +1,84 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fetch firefox useragent signatures
Output file: :origin:`searx/data/useragents.json` (:origin:`CI Update data ...
<.github/workflows/data-update.yml>`).
"""
# pylint: disable=use-dict-literal
import json
import re
from os.path import join
from urllib.parse import urlparse, urljoin
from packaging.version import parse
import requests
from lxml import html
from searx import searx_dir
URL = 'https://ftp.mozilla.org/pub/firefox/releases/'
RELEASE_PATH = '/pub/firefox/releases/'
NORMAL_REGEX = re.compile(r'^[0-9]+\.[0-9](\.[0-9])?$')
# BETA_REGEX = re.compile(r'.*[0-9]b([0-9\-a-z]+)$')
# ESR_REGEX = re.compile(r'^[0-9]+\.[0-9](\.[0-9])?esr$')
#
useragents = {
# fmt: off
"versions": (),
"os": ('Windows NT 10.0; Win64; x64',
'X11; Linux x86_64'),
"ua": "Mozilla/5.0 ({os}; rv:{version}) Gecko/20100101 Firefox/{version}",
# fmt: on
}
def fetch_firefox_versions():
resp = requests.get(URL, timeout=2.0)
if resp.status_code != 200:
# pylint: disable=broad-exception-raised
raise Exception("Error fetching firefox versions, HTTP code " + resp.status_code)
dom = html.fromstring(resp.text)
versions = []
for link in dom.xpath('//a/@href'):
url = urlparse(urljoin(URL, link))
path = url.path
if path.startswith(RELEASE_PATH):
version = path[len(RELEASE_PATH) : -1]
if NORMAL_REGEX.match(version):
versions.append(parse(version))
list.sort(versions, reverse=True)
return versions
def fetch_firefox_last_versions():
versions = fetch_firefox_versions()
result = []
major_last = versions[0].major
major_list = (major_last, major_last - 1)
for version in versions:
major_current = version.major
minor_current = version.minor
if major_current in major_list:
user_agent_version = f'{major_current}.{minor_current}'
if user_agent_version not in result:
result.append(user_agent_version)
return result
def get_useragents_filename():
return join(join(searx_dir, "data"), "useragents.json")
if __name__ == '__main__':
useragents["versions"] = fetch_firefox_last_versions()
with open(get_useragents_filename(), "w", encoding='utf-8') as f:
json.dump(useragents, f, indent=4, ensure_ascii=False)
+218
View File
@@ -0,0 +1,218 @@
#!/usr/bin/env python
# lint: pylint
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Fetch OSM keys and tags.
To get the i18n names, the scripts uses `Wikidata Query Service`_ instead of for
example `OSM tags API`_ (sidenote: the actual change log from
map.atownsend.org.uk_ might be useful to normalize OSM tags).
Output file: :origin:`searx/data/osm_keys_tags` (:origin:`CI Update data ...
<.github/workflows/data-update.yml>`).
.. _Wikidata Query Service: https://query.wikidata.org/
.. _OSM tags API: https://taginfo.openstreetmap.org/taginfo/apidoc
.. _map.atownsend.org.uk: https://map.atownsend.org.uk/maps/map/changelog.html
:py:obj:`SPARQL_TAGS_REQUEST` :
Wikidata SPARQL query that returns *type-categories* and *types*. The
returned tag is ``Tag:{category}={type}`` (see :py:func:`get_tags`).
Example:
- https://taginfo.openstreetmap.org/tags/building=house#overview
- https://wiki.openstreetmap.org/wiki/Tag:building%3Dhouse
at the bottom of the infobox (right side), there is a link to wikidata:
https://www.wikidata.org/wiki/Q3947
see property "OpenStreetMap tag or key" (P1282)
- https://wiki.openstreetmap.org/wiki/Tag%3Abuilding%3Dbungalow
https://www.wikidata.org/wiki/Q850107
:py:obj:`SPARQL_KEYS_REQUEST` :
Wikidata SPARQL query that returns *keys*. Example with "payment":
- https://wiki.openstreetmap.org/wiki/Key%3Apayment
at the bottom of infobox (right side), there is a link to wikidata:
https://www.wikidata.org/wiki/Q1148747
link made using the "OpenStreetMap tag or key" property (P1282)
to be confirm: there is a one wiki page per key ?
- https://taginfo.openstreetmap.org/keys/payment#values
- https://taginfo.openstreetmap.org/keys/payment:cash#values
``rdfs:label`` get all the labels without language selection
(as opposed to SERVICE ``wikibase:label``).
"""
import json
import collections
from pathlib import Path
from searx import searx_dir
from searx.network import set_timeout_for_thread
from searx.engines import wikidata, set_loggers
from searx.sxng_locales import sxng_locales
from searx.engines.openstreetmap import get_key_rank, VALUE_TO_LINK
set_loggers(wikidata, 'wikidata')
SPARQL_TAGS_REQUEST = """
SELECT ?tag ?item ?itemLabel WHERE {
?item wdt:P1282 ?tag .
?item rdfs:label ?itemLabel .
FILTER(STRSTARTS(?tag, 'Tag'))
}
GROUP BY ?tag ?item ?itemLabel
ORDER BY ?tag ?item ?itemLabel
"""
SPARQL_KEYS_REQUEST = """
SELECT ?key ?item ?itemLabel WHERE {
?item wdt:P1282 ?key .
?item rdfs:label ?itemLabel .
FILTER(STRSTARTS(?key, 'Key'))
}
GROUP BY ?key ?item ?itemLabel
ORDER BY ?key ?item ?itemLabel
"""
LANGUAGES = [l[0].lower() for l in sxng_locales]
PRESET_KEYS = {
('wikidata',): {'en': 'Wikidata'},
('wikipedia',): {'en': 'Wikipedia'},
('email',): {'en': 'Email'},
('facebook',): {'en': 'Facebook'},
('fax',): {'en': 'Fax'},
('internet_access', 'ssid'): {'en': 'Wi-Fi'},
}
INCLUDED_KEYS = {('addr',)}
def get_preset_keys():
results = collections.OrderedDict()
for keys, value in PRESET_KEYS.items():
r = results
for k in keys:
r = r.setdefault(k, {})
r.setdefault('*', value)
return results
def get_keys():
results = get_preset_keys()
response = wikidata.send_wikidata_query(SPARQL_KEYS_REQUEST)
for key in response['results']['bindings']:
keys = key['key']['value'].split(':')[1:]
if keys[0] == 'currency' and len(keys) > 1:
# special case in openstreetmap.py
continue
if keys[0] == 'contact' and len(keys) > 1:
# label for the key "contact.email" is "Email"
# whatever the language
r = results.setdefault('contact', {})
r[keys[1]] = {'*': {'en': keys[1]}}
continue
if tuple(keys) in PRESET_KEYS:
# skip presets (already set above)
continue
if (
get_key_rank(':'.join(keys)) is None
and ':'.join(keys) not in VALUE_TO_LINK
and tuple(keys) not in INCLUDED_KEYS
):
# keep only keys that will be displayed by openstreetmap.py
continue
label = key['itemLabel']['value'].lower()
lang = key['itemLabel']['xml:lang']
r = results
for k in keys:
r = r.setdefault(k, {})
r = r.setdefault('*', {})
if lang in LANGUAGES:
r.setdefault(lang, label)
# special cases
results['delivery']['covid19']['*'].clear()
for k, v in results['delivery']['*'].items():
results['delivery']['covid19']['*'][k] = v + ' (COVID19)'
results['opening_hours']['covid19']['*'].clear()
for k, v in results['opening_hours']['*'].items():
results['opening_hours']['covid19']['*'][k] = v + ' (COVID19)'
return results
def get_tags():
results = collections.OrderedDict()
response = wikidata.send_wikidata_query(SPARQL_TAGS_REQUEST)
for tag in response['results']['bindings']:
tag_names = tag['tag']['value'].split(':')[1].split('=')
if len(tag_names) == 2:
tag_category, tag_type = tag_names
else:
tag_category, tag_type = tag_names[0], ''
label = tag['itemLabel']['value'].lower()
lang = tag['itemLabel']['xml:lang']
if lang in LANGUAGES:
results.setdefault(tag_category, {}).setdefault(tag_type, {}).setdefault(lang, label)
return results
def optimize_data_lang(translations):
language_to_delete = []
# remove "zh-hk" entry if the value is the same as "zh"
# same for "en-ca" / "en" etc...
for language in translations:
if '-' in language:
base_language = language.split('-')[0]
if translations.get(base_language) == translations.get(language):
language_to_delete.append(language)
for language in language_to_delete:
del translations[language]
language_to_delete = []
# remove entries that have the same value than the "en" entry
value_en = translations.get('en')
if value_en:
for language, value in translations.items():
if language != 'en' and value == value_en:
language_to_delete.append(language)
for language in language_to_delete:
del translations[language]
def optimize_tags(data):
for v in data.values():
for translations in v.values():
optimize_data_lang(translations)
return data
def optimize_keys(data):
for k, v in data.items():
if k == '*':
optimize_data_lang(v)
elif isinstance(v, dict):
optimize_keys(v)
return data
def get_osm_tags_filename():
return Path(searx_dir) / "data" / "osm_keys_tags.json"
if __name__ == '__main__':
set_timeout_for_thread(60)
result = {
'keys': optimize_keys(get_keys()),
'tags': optimize_tags(get_tags()),
}
with open(get_osm_tags_filename(), 'w', encoding="utf8") as f:
json.dump(result, f, indent=4, ensure_ascii=False, sort_keys=True)
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Update pygments style
Call this script after each upgrade of pygments
"""
# pylint: disable=C0116
# set path
from os.path import join
import pygments
from pygments.formatters import HtmlFormatter # pylint: disable=E0611
from pygments.style import Style
from pygments.token import Comment, Error, Generic, Keyword, Literal, Name, Operator, Text
from searx import searx_dir
CSSCLASS = '.code-highlight'
RULE_CODE_LINENOS = """ .linenos {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: default;
&::selection {
background: transparent; /* WebKit/Blink Browsers */
}
&::-moz-selection {
background: transparent; /* Gecko Browsers */
}
margin-right: 8px;
text-align: right;
}"""
def get_output_filename(relative_name):
return join(searx_dir, relative_name)
def get_css(cssclass, style):
result = f"""/*
this file is generated automatically by searxng_extra/update/update_pygments.py
using pygments version {pygments.__version__}
*/\n\n"""
css_text = HtmlFormatter(style=style).get_style_defs(cssclass)
result += cssclass + RULE_CODE_LINENOS + '\n\n'
for line in css_text.splitlines():
if ' ' in line and not line.startswith(cssclass):
line = cssclass + ' ' + line
result += line + '\n'
return result
def main():
fname = 'static/themes/simple/src/generated/pygments.less'
print("update: %s" % fname)
with open(get_output_filename(fname), 'w') as f:
f.write(get_css(CSSCLASS, 'default'))
if __name__ == '__main__':
main()
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env python
# SPDX-License-Identifier: AGPL-3.0-or-later
# lint: pylint
# pylint: disable=missing-module-docstring
"""Fetch units from :origin:`searx/engines/wikidata.py` engine.
Output file: :origin:`searx/data/wikidata_units.json` (:origin:`CI Update data
... <.github/workflows/data-update.yml>`).
"""
import json
import collections
# set path
from os.path import join
from searx import searx_dir
from searx.engines import wikidata, set_loggers
set_loggers(wikidata, 'wikidata')
# the response contains duplicate ?item with the different ?symbol
# "ORDER BY ?item DESC(?rank) ?symbol" provides a deterministic result
# even if a ?item has different ?symbol of the same rank.
# A deterministic result
# see:
# * https://www.wikidata.org/wiki/Help:Ranking
# * https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format ("Statement representation" section)
# * https://w.wiki/32BT
# see the result for https://www.wikidata.org/wiki/Q11582
# there are multiple symbols the same rank
SARQL_REQUEST = """
SELECT DISTINCT ?item ?symbol
WHERE
{
?item wdt:P31/wdt:P279 wd:Q47574 .
?item p:P5061 ?symbolP .
?symbolP ps:P5061 ?symbol ;
wikibase:rank ?rank .
FILTER(LANG(?symbol) = "en").
}
ORDER BY ?item DESC(?rank) ?symbol
"""
def get_data():
results = collections.OrderedDict()
response = wikidata.send_wikidata_query(SARQL_REQUEST)
for unit in response['results']['bindings']:
name = unit['item']['value'].replace('http://www.wikidata.org/entity/', '')
unit = unit['symbol']['value']
if name not in results:
# ignore duplicate: always use the first one
results[name] = unit
return results
def get_wikidata_units_filename():
return join(join(searx_dir, "data"), "wikidata_units.json")
if __name__ == '__main__':
with open(get_wikidata_units_filename(), 'w', encoding="utf8") as f:
json.dump(get_data(), f, indent=4, ensure_ascii=False)