on master help sign in
Feature/standalone searx update (#1591)

* chg: dev: update standalone_searx parent d8a5df721b33dd8a7cc9e21dba4060f21d629f69 author rachmadaniHaryono <foreturiga@gmail.com> 1603896594 +0800 committer rachmadaniHaryono <foreturiga@gmail.com> 1603896619 +0800 chg: dev: debug engine_shortcuts chg: dev: only initilize if engine is given chg: dev: split main chg: dev: standalone_searx chg: dev: update standalone_searx chg: doc: remove unnecessary log chg: test: differentiate travis chg: test: disable shortcut chg: test: use default engine settings fix: dev: category choices fix: dev: duplicate engine shortcut fix: dev: travis python3 fix: test: use empty string as shortcut fix: test: apkm fix: test: engine shortcut fix: test: mypy fix: test: parameter fix: test: pep8 fix: test: py2 compatibilities fix: test: searx settings fix: test: travis engines new: dev: deduplicate engine new: dev: main receive engines parameter new: dev: parse_argument accept engines parameter new: dev: split search query from get_result func new: test: basic result case Suggestions: use RawTextQuery to make the suggestions URLs. Update all themes accordingly. * new: doc: searx import and init * chg: dev: parse_argument - doc - run on __main__ - simple parse_args * chg: doc: module * chg: dev: import section - remove unused python path modification - new required package * chg: dev: script run - parse_argument func return directly parsed results - main func return dict instead json text - dump directly on sys.stdout.write * chg: dev: get_search_query and get_search_query func * chg: dev: main func - move inner function outside - return dict instead of json text * new: dev: add utils to doc sys path * new: doc: standalone_searx * fix: doc: run script * chg: dev: mypy type hint * chg: dev: SearchQuery don't have attr engines * chg: dev: reset engines __init__ * chg: test: unit test update * chg: dev: pylint and flake8 * new: test: standalone_searx * chg: dev: main func and doc * chg: dev: import and type hint * new: dev: main func - remove get_result func - single func which just translate dict * chg: test: put mypy on dev requirement * chg: doc: update * new: doc: add standalone_searx module member * chg: doc: shell command line * chg: dev: remove mypy * chg: doc: module docstring

commitc03e4c86bc49d6ef4664c038066d9f1c16e7dafc authorrachmadani haryono <rachmadaniHaryono@users.noreply.github.com> dateWed 4 Nov 2020, 20:38 (6 years ago) parent1b42d42
5 files changed+315−71
docs/conf.py+1 docs/utils/index.rst+1 docs/utils/standalone_searx.py.rst+11 tests/unit/test_standalone_searx.py+118 utils/standalone_searx.py+184−71
docs/conf.py+1
@@ -87,6 +87,7 @@ issues_github_path = "searx/searx"
8787# HTML -----------------------------------------------------------------
8888
8989sys.path.append(os.path.abspath('_themes'))
90+sys.path.insert(0, os.path.abspath("../utils/"))
9091html_theme_path = ['_themes']
9192html_theme = "searx"
9293
docs/utils/index.rst+1
@@ -16,6 +16,7 @@ developers.
1616 filtron.sh
1717 morty.sh
1818 lxc.sh
19+ standalone_searx.py
1920
2021.. _toolboxing common:
2122
docs/utils/standalone_searx.py.rstnew file+11
@@ -0,0 +1,11 @@
1+
2+.. _standalone_searx.py:
3+
4+=============================
5+``utils/standalone_searx.py``
6+=============================
7+
8+.. automodule:: standalone_searx
9+ :members:
10+
11+
tests/unit/test_standalone_searx.pynew file+118
@@ -0,0 +1,118 @@
1+# -*- coding: utf-8 -*-
2+"""Test utils/standalone_searx.py"""
3+import datetime
4+import importlib.util
5+import sys
6+
7+from mock import Mock, patch
8+from nose2.tools import params
9+
10+from searx.testing import SearxTestCase
11+
12+
13+def get_standalone_searx_module():
14+ """Get standalone_searx module."""
15+ module_name = 'utils.standalone_searx'
16+ filename = 'utils/standalone_searx.py'
17+ spec = importlib.util.spec_from_file_location(module_name, filename)
18+ sas = importlib.util.module_from_spec(spec)
19+ spec.loader.exec_module(sas)
20+ return sas
21+
22+
23+class StandaloneSearx(SearxTestCase):
24+ """Unit test for standalone_searx."""
25+
26+ def test_parse_argument_no_args(self):
27+ """Test parse argument without args."""
28+ sas = get_standalone_searx_module()
29+ with patch.object(sys, 'argv', ['standalone_searx']), \
30+ self.assertRaises(SystemExit):
31+ sas.parse_argument()
32+
33+ def test_parse_argument_basic_args(self):
34+ """Test parse argument with basic args."""
35+ sas = get_standalone_searx_module()
36+ query = 'red box'
37+ exp_dict = {
38+ 'query': query, 'category': 'general', 'lang': 'all', 'pageno': 1,
39+ 'safesearch': '0', 'timerange': None}
40+ args = ['standalone_searx', query]
41+ with patch.object(sys, 'argv', args):
42+ res = sas.parse_argument()
43+ self.assertEqual(exp_dict, vars(res))
44+ res2 = sas.parse_argument(args[1:])
45+ self.assertEqual(exp_dict, vars(res2))
46+
47+ def test_to_dict(self):
48+ """test to_dict."""
49+ sas = get_standalone_searx_module()
50+ self.assertEqual(
51+ sas.to_dict(
52+ sas.get_search_query(sas.parse_argument(['red box']))),
53+ {
54+ 'search': {
55+ 'q': 'red box', 'pageno': 1, 'lang': 'all',
56+ 'safesearch': 0, 'timerange': None
57+ },
58+ 'results': [], 'infoboxes': [], 'suggestions': [],
59+ 'answers': [], 'paging': False, 'results_number': 0
60+ }
61+ )
62+
63+ def test_to_dict_with_mock(self):
64+ """test to dict."""
65+ sas = get_standalone_searx_module()
66+ with patch.object(sas.searx.search, 'Search') as mock_s:
67+ m_search = mock_s().search()
68+ m_sq = Mock()
69+ self.assertEqual(
70+ sas.to_dict(m_sq),
71+ {
72+ 'answers': [],
73+ 'infoboxes': m_search.infoboxes,
74+ 'paging': m_search.paging,
75+ 'results': m_search.get_ordered_results(),
76+ 'results_number': m_search.results_number(),
77+ 'search': {
78+ 'lang': m_sq.lang,
79+ 'pageno': m_sq.pageno,
80+ 'q': m_sq.query,
81+ 'safesearch': m_sq.safesearch,
82+ 'timerange': m_sq.time_range,
83+ },
84+ 'suggestions': []
85+ }
86+ )
87+
88+ def test_get_search_query(self):
89+ """test get_search_query."""
90+ sas = get_standalone_searx_module()
91+ args = sas.parse_argument(['rain', ])
92+ search_q = sas.get_search_query(args)
93+ self.assertTrue(search_q)
94+ self.assertEqual(str(search_q), 'rain;[]')
95+
96+ def test_no_parsed_url(self):
97+ """test no_parsed_url func"""
98+ sas = get_standalone_searx_module()
99+ self.assertEqual(
100+ sas.no_parsed_url([{'parsed_url': 'http://example.com'}]),
101+ [{}]
102+ )
103+
104+ @params(
105+ (datetime.datetime(2020, 1, 1), '2020-01-01T00:00:00'),
106+ ('a'.encode('utf8'), 'a'),
107+ (set([1]), [1])
108+ )
109+ def test_json_serial(self, arg, exp_res):
110+ """test json_serial func"""
111+ sas = get_standalone_searx_module()
112+ self.assertEqual(sas.json_serial(arg), exp_res)
113+
114+ def test_json_serial_error(self):
115+ """test error on json_serial."""
116+ sas = get_standalone_searx_module()
117+ with self.assertRaises(TypeError):
118+ sas.json_serial('a')
utils/standalone_searx.py+184−71
@@ -1,5 +1,63 @@
11#!/usr/bin/env python
2+"""Script to run searx from terminal.
23
4+Getting categories without initiate the engine will only return `['general']`
5+
6+>>> import searx.engines
7+... list(searx.engines.categories.keys())
8+['general']
9+>>> import searx
10+... searx.engines.initialize_engines(searx.settings['engines'])
11+... list(searx.engines.categories.keys())
12+['general', 'it', 'science', 'images', 'news', 'videos', 'music', 'files', 'social media', 'map']
13+
14+Example to use this script:
15+
16+.. code:: bash
17+
18+ $ SEARX_DEBUG=1 python3 utils/standalone_searx.py rain
19+
20+Example to run it from python:
21+
22+>>> import importlib
23+... import json
24+... import sys
25+... import searx
26+... import searx.engines
27+... search_query = 'rain'
28+... # initialize engines
29+... searx.engines.initialize_engines(searx.settings['engines'])
30+... # load engines categories once instead of each time the function called
31+... engine_cs = list(searx.engines.categories.keys())
32+... # load module
33+... spec = importlib.util.spec_from_file_location(
34+... 'utils.standalone_searx', 'utils/standalone_searx.py')
35+... sas = importlib.util.module_from_spec(spec)
36+... spec.loader.exec_module(sas)
37+... # use function from module
38+... prog_args = sas.parse_argument([search_query], category_choices=engine_cs)
39+... search_q = sas.get_search_query(prog_args, engine_categories=engine_cs)
40+... res_dict = sas.to_dict(search_q)
41+... sys.stdout.write(json.dumps(
42+... res_dict, sort_keys=True, indent=4, ensure_ascii=False,
43+... default=sas.json_serial))
44+{
45+ "answers": [],
46+ "infoboxes": [ {...} ],
47+ "paging": true,
48+ "results": [... ],
49+ "results_number": 820000000.0,
50+ "search": {
51+ "lang": "all",
52+ "pageno": 1,
53+ "q": "rain",
54+ "safesearch": 0,
55+ "timerange": null
56+ },
57+ "suggestions": [...]
58+}
59+""" # noqa: E501
60+# pylint: disable=pointless-string-statement
361'''
462searx is free software: you can redistribute it and/or modify
563it under the terms of the GNU Affero General Public License as published by
@@ -16,90 +74,145 @@ along with searx. If not, see < http://www.gnu.org/licenses/ >.
1674
1775(C) 2016- by Alexandre Flament, <alex@al-f.net>
1876'''
19−
20−# set path
21−from sys import path
22−from os.path import realpath, dirname
23−path.append(realpath(dirname(realpath(__file__)) + '/../'))
24−
25−# initialization
26−from json import dumps
27−from searx import settings
77+# pylint: disable=wrong-import-position
78+import argparse
2879import sys
29−import codecs
30−import searx.query
31−import searx.search
80+from datetime import datetime
81+from json import dumps
82+from typing import Any, Dict, List, Optional
83+
84+import searx
3285import searx.engines
33−import searx.webapdater
3486import searx.preferences
87+import searx.query
88+import searx.search
3589import searx.webadapter
36−import argparse
3790
38−searx.engines.initialize_engines(settings['engines'])
39−
40−# command line parsing
41−parser = argparse.ArgumentParser(description='Standalone searx.')
42−parser.add_argument('query', type=str,
43− help='Text query')
44−parser.add_argument('--category', type=str, nargs='?',
45− choices=searx.engines.categories.keys(),
46− default='general',
47− help='Search category')
48−parser.add_argument('--lang', type=str, nargs='?',default='all',
49− help='Search language')
50−parser.add_argument('--pageno', type=int, nargs='?', default=1,
51− help='Page number starting from 1')
52−parser.add_argument('--safesearch', type=str, nargs='?', choices=['0', '1', '2'], default='0',
53− help='Safe content filter from none to strict')
54−parser.add_argument('--timerange', type=str, nargs='?', choices=['day', 'week', 'month', 'year'],
55− help='Filter by time range')
56−args = parser.parse_args()
57−
58−# search results for the query
59−form = {
60− "q":args.query,
61− "categories":args.category.decode(),
62− "pageno":str(args.pageno),
63− "language":args.lang,
64− "time_range":args.timerange
65−}
66−preferences = searx.preferences.Preferences(['oscar'], searx.engines.categories.keys(), searx.engines.engines, [])
67−preferences.key_value_settings['safesearch'].parse(args.safesearch)
91+EngineCategoriesVar = Optional[List[str]]
6892
69−search_query, raw_text_query, _, _ = searx.webadapter.get_search_query_from_webapp(preferences, form)
70−search = searx.search.Search(search_query)
71−result_container = search.search()
7293
73−# output
74−from datetime import datetime
94+def get_search_query(
95+ args: argparse.Namespace, engine_categories: EngineCategoriesVar = None
96+) -> searx.search.SearchQuery:
97+ """Get search results for the query"""
98+ if engine_categories is None:
99+ engine_categories = list(searx.engines.categories.keys())
100+ try:
101+ category = args.category.decode('utf-8')
102+ except AttributeError:
103+ category = args.category
104+ form = {
105+ "q": args.query,
106+ "categories": category,
107+ "pageno": str(args.pageno),
108+ "language": args.lang,
109+ "time_range": args.timerange
110+ }
111+ preferences = searx.preferences.Preferences(
112+ ['oscar'], engine_categories, searx.engines.engines, [])
113+ preferences.key_value_settings['safesearch'].parse(args.safesearch)
114+
115+ search_query = searx.webadapter.get_search_query_from_webapp(
116+ preferences, form)[0]
117+ return search_query
75118
76−def no_parsed_url(results):
119+
120+def no_parsed_url(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
121+ """Remove parsed url from dict."""
77122 for result in results:
78123 del result['parsed_url']
79124 return results
80125
81−def json_serial(obj):
82− """JSON serializer for objects not serializable by default json code"""
126+
127+def json_serial(obj: Any) -> Any:
128+ """JSON serializer for objects not serializable by default json code.
129+
130+ :raise TypeError: raised when **obj** is not serializable
131+ """
83132 if isinstance(obj, datetime):
84133 serial = obj.isoformat()
85134 return serial
86− raise TypeError ("Type not serializable")
135+ if isinstance(obj, bytes):
136+ return obj.decode('utf8')
137+ if isinstance(obj, set):
138+ return list(obj)
139+ raise TypeError("Type ({}) not serializable".format(type(obj)))
87140
88−result_container_json = {
89− "search": {
90− "q": search_query.query,
91− "pageno": search_query.pageno,
92− "lang": search_query.lang,
93− "safesearch": search_query.safesearch,
94− "timerange": search_query.time_range,
95− "engines": search_query.engines
96− },
97− "results": no_parsed_url(result_container.get_ordered_results()),
98− "infoboxes": result_container.infoboxes,
99− "suggestions": list(result_container.suggestions),
100− "answers": list(result_container.answers),
101− "paging": result_container.paging,
102− "results_number": result_container.results_number()
103−}
104−sys.stdout = codecs.getwriter("UTF-8")(sys.stdout)
105−sys.stdout.write(dumps(result_container_json, sort_keys=True, indent=4, ensure_ascii=False, encoding="utf-8", default=json_serial))
141+
142+def to_dict(search_query: searx.search.SearchQuery) -> Dict[str, Any]:
143+ """Get result from parsed arguments."""
144+ result_container = searx.search.Search(search_query).search()
145+ result_container_json = {
146+ "search": {
147+ "q": search_query.query,
148+ "pageno": search_query.pageno,
149+ "lang": search_query.lang,
150+ "safesearch": search_query.safesearch,
151+ "timerange": search_query.time_range,
152+ },
153+ "results": no_parsed_url(result_container.get_ordered_results()),
154+ "infoboxes": result_container.infoboxes,
155+ "suggestions": list(result_container.suggestions),
156+ "answers": list(result_container.answers),
157+ "paging": result_container.paging,
158+ "results_number": result_container.results_number()
159+ }
160+ return result_container_json
161+
162+
163+def parse_argument(
164+ args: Optional[List[str]]=None,
165+ category_choices: EngineCategoriesVar=None
166+) -> argparse.Namespace:
167+ """Parse command line.
168+
169+ :raise SystemExit: Query argument required on `args`
170+
171+ Examples:
172+
173+ >>> import importlib
174+ ... # load module
175+ ... spec = importlib.util.spec_from_file_location(
176+ ... 'utils.standalone_searx', 'utils/standalone_searx.py')
177+ ... sas = importlib.util.module_from_spec(spec)
178+ ... spec.loader.exec_module(sas)
179+ ... sas.parse_argument()
180+ usage: ptipython [-h] [--category [{general}]] [--lang [LANG]] [--pageno [PAGENO]] [--safesearch [{0,1,2}]] [--timerange [{day,week,month,year}]]
181+ query
182+ SystemExit: 2
183+ >>> sas.parse_argument(['rain'])
184+ Namespace(category='general', lang='all', pageno=1, query='rain', safesearch='0', timerange=None)
185+ """ # noqa: E501
186+ if not category_choices:
187+ category_choices = list(searx.engines.categories.keys())
188+ parser = argparse.ArgumentParser(description='Standalone searx.')
189+ parser.add_argument('query', type=str,
190+ help='Text query')
191+ parser.add_argument('--category', type=str, nargs='?',
192+ choices=category_choices,
193+ default='general',
194+ help='Search category')
195+ parser.add_argument('--lang', type=str, nargs='?', default='all',
196+ help='Search language')
197+ parser.add_argument('--pageno', type=int, nargs='?', default=1,
198+ help='Page number starting from 1')
199+ parser.add_argument(
200+ '--safesearch', type=str, nargs='?',
201+ choices=['0', '1', '2'], default='0',
202+ help='Safe content filter from none to strict')
203+ parser.add_argument(
204+ '--timerange', type=str,
205+ nargs='?', choices=['day', 'week', 'month', 'year'],
206+ help='Filter by time range')
207+ return parser.parse_args(args)
208+
209+
210+if __name__ == '__main__':
211+ searx.engines.initialize_engines(searx.settings['engines'])
212+ engine_cs = list(searx.engines.categories.keys())
213+ prog_args = parse_argument(category_choices=engine_cs)
214+ search_q = get_search_query(prog_args, engine_categories=engine_cs)
215+ res_dict = to_dict(search_q)
216+ sys.stdout.write(dumps(
217+ res_dict, sort_keys=True, indent=4, ensure_ascii=False,
218+ default=json_serial))