search-api-webui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/__init__.py +0 -0
- backend/app.py +146 -0
- backend/providers/__init__.py +34 -0
- backend/providers/base.py +25 -0
- backend/providers/generic.py +125 -0
- backend/providers/querit.py +80 -0
- backend/providers.yaml +19 -0
- backend/static/assets/index-CF13bI2g.js +181 -0
- backend/static/assets/index-DLyBd1PD.css +1 -0
- backend/static/index.html +13 -0
- search_api_webui-0.1.0.dist-info/METADATA +93 -0
- search_api_webui-0.1.0.dist-info/RECORD +15 -0
- search_api_webui-0.1.0.dist-info/WHEEL +4 -0
- search_api_webui-0.1.0.dist-info/entry_points.txt +2 -0
- search_api_webui-0.1.0.dist-info/licenses/LICENSE +7 -0
backend/__init__.py
ADDED
|
File without changes
|
backend/app.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from flask import Flask, request, jsonify, send_from_directory
|
|
4
|
+
from flask_cors import CORS
|
|
5
|
+
from backend.providers import load_providers
|
|
6
|
+
|
|
7
|
+
app = Flask(__name__, static_folder='static')
|
|
8
|
+
CORS(app)
|
|
9
|
+
|
|
10
|
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
11
|
+
PROVIDERS_YAML = os.path.join(BASE_DIR, 'providers.yaml')
|
|
12
|
+
USER_CONFIG_JSON = os.path.join(BASE_DIR, 'user_config.json')
|
|
13
|
+
|
|
14
|
+
provider_map = load_providers(PROVIDERS_YAML)
|
|
15
|
+
|
|
16
|
+
def get_stored_config():
|
|
17
|
+
if not os.path.exists(USER_CONFIG_JSON):
|
|
18
|
+
return {}
|
|
19
|
+
try:
|
|
20
|
+
with open(USER_CONFIG_JSON, 'r') as f:
|
|
21
|
+
return json.load(f)
|
|
22
|
+
except:
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
def save_stored_config(config_dict):
|
|
26
|
+
with open(USER_CONFIG_JSON, 'w') as f:
|
|
27
|
+
json.dump(config_dict, f, indent=2)
|
|
28
|
+
|
|
29
|
+
@app.route('/api/providers', methods=['GET'])
|
|
30
|
+
def get_providers_list():
|
|
31
|
+
stored_config = get_stored_config()
|
|
32
|
+
providers_info = []
|
|
33
|
+
|
|
34
|
+
for name, provider_instance in provider_map.items():
|
|
35
|
+
config_details = provider_instance.config
|
|
36
|
+
|
|
37
|
+
user_conf = stored_config.get(name, {})
|
|
38
|
+
|
|
39
|
+
if isinstance(user_conf, str):
|
|
40
|
+
user_conf = {'api_key': user_conf}
|
|
41
|
+
|
|
42
|
+
has_key = bool(user_conf.get('api_key'))
|
|
43
|
+
|
|
44
|
+
providers_info.append({
|
|
45
|
+
"name": name,
|
|
46
|
+
"has_key": has_key,
|
|
47
|
+
"details": config_details,
|
|
48
|
+
"user_settings": {
|
|
49
|
+
"api_url": user_conf.get('api_url', ''),
|
|
50
|
+
"limit": user_conf.get('limit', '10'),
|
|
51
|
+
"language": user_conf.get('language', 'en-US')
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return jsonify(providers_info)
|
|
55
|
+
|
|
56
|
+
@app.route('/api/config', methods=['POST'])
|
|
57
|
+
def update_config():
|
|
58
|
+
data = request.json
|
|
59
|
+
provider_name = data.get('provider')
|
|
60
|
+
|
|
61
|
+
if not provider_name:
|
|
62
|
+
return jsonify({"error": "Provider name is required"}), 400
|
|
63
|
+
|
|
64
|
+
if 'api_key' not in data:
|
|
65
|
+
return jsonify({"error": "API Key field is missing"}), 400
|
|
66
|
+
|
|
67
|
+
api_key = data.get('api_key')
|
|
68
|
+
|
|
69
|
+
api_url = data.get('api_url', '').strip()
|
|
70
|
+
limit = data.get('limit', '10')
|
|
71
|
+
language = data.get('language', 'en-US')
|
|
72
|
+
|
|
73
|
+
all_config = get_stored_config()
|
|
74
|
+
|
|
75
|
+
if provider_name in all_config and isinstance(all_config[provider_name], str):
|
|
76
|
+
all_config[provider_name] = {'api_key': all_config[provider_name]}
|
|
77
|
+
|
|
78
|
+
if not api_key:
|
|
79
|
+
if provider_name in all_config:
|
|
80
|
+
all_config[provider_name]['api_key'] = ""
|
|
81
|
+
else:
|
|
82
|
+
if provider_name not in all_config:
|
|
83
|
+
all_config[provider_name] = {}
|
|
84
|
+
|
|
85
|
+
all_config[provider_name]['api_key'] = api_key
|
|
86
|
+
all_config[provider_name]['api_url'] = api_url
|
|
87
|
+
all_config[provider_name]['limit'] = limit
|
|
88
|
+
all_config[provider_name]['language'] = language
|
|
89
|
+
|
|
90
|
+
save_stored_config(all_config)
|
|
91
|
+
return jsonify({"status": "success"})
|
|
92
|
+
|
|
93
|
+
@app.route('/api/search', methods=['POST'])
|
|
94
|
+
def search_api():
|
|
95
|
+
data = request.json
|
|
96
|
+
query = data.get('query')
|
|
97
|
+
provider_name = data.get('provider', 'querit')
|
|
98
|
+
|
|
99
|
+
api_key = data.get('api_key')
|
|
100
|
+
|
|
101
|
+
stored_config = get_stored_config()
|
|
102
|
+
provider_config = stored_config.get(provider_name, {})
|
|
103
|
+
|
|
104
|
+
if isinstance(provider_config, str):
|
|
105
|
+
provider_config = {'api_key': provider_config}
|
|
106
|
+
|
|
107
|
+
if not api_key:
|
|
108
|
+
api_key = provider_config.get('api_key')
|
|
109
|
+
|
|
110
|
+
if not api_key:
|
|
111
|
+
return jsonify({"error": f"API Key for {provider_name} is missing. Please configure it."}), 401
|
|
112
|
+
|
|
113
|
+
provider = provider_map.get(provider_name)
|
|
114
|
+
if not provider:
|
|
115
|
+
return jsonify({"error": "Provider not found"}), 404
|
|
116
|
+
|
|
117
|
+
search_kwargs = {
|
|
118
|
+
'api_url': provider_config.get('api_url'),
|
|
119
|
+
'limit': provider_config.get('limit'),
|
|
120
|
+
'language': provider_config.get('language')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
result = provider.search(query, api_key, **search_kwargs)
|
|
124
|
+
return jsonify(result)
|
|
125
|
+
|
|
126
|
+
# Host React Frontend
|
|
127
|
+
@app.route('/', defaults={'path': ''})
|
|
128
|
+
@app.route('/<path:path>')
|
|
129
|
+
def serve(path):
|
|
130
|
+
if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
|
|
131
|
+
return send_from_directory(app.static_folder, path)
|
|
132
|
+
else:
|
|
133
|
+
return send_from_directory(app.static_folder, 'index.html')
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
import argparse
|
|
137
|
+
parser = argparse.ArgumentParser(description="Search API WebUI")
|
|
138
|
+
parser.add_argument("--port", type=int, default=8889, help="Port to run the server on")
|
|
139
|
+
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to run the server on")
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
app.run(host=args.host, port=args.port)
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
main()
|
|
146
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
from .generic import GenericProvider
|
|
4
|
+
from .querit import QueritSdkProvider
|
|
5
|
+
|
|
6
|
+
def load_providers(file_path='providers.yaml'):
|
|
7
|
+
"""
|
|
8
|
+
Parses the YAML configuration file and instantiates the appropriate provider classes.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
file_path (str): Path to the providers configuration file.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict: A dictionary mapping provider names to their initialized instances.
|
|
15
|
+
"""
|
|
16
|
+
if not os.path.exists(file_path):
|
|
17
|
+
print(f"Warning: Provider config file not found at {file_path}")
|
|
18
|
+
return {}
|
|
19
|
+
|
|
20
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
21
|
+
configs = yaml.safe_load(f)
|
|
22
|
+
|
|
23
|
+
providers = {}
|
|
24
|
+
for name, conf in configs.items():
|
|
25
|
+
conf['name'] = name
|
|
26
|
+
provider_type = conf.get('type', 'generic')
|
|
27
|
+
|
|
28
|
+
# Instantiate specific provider based on type or name
|
|
29
|
+
if name == 'querit' or provider_type == 'querit_sdk':
|
|
30
|
+
providers[name] = QueritSdkProvider(conf)
|
|
31
|
+
else:
|
|
32
|
+
providers[name] = GenericProvider(conf)
|
|
33
|
+
|
|
34
|
+
return providers
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class BaseProvider(ABC):
|
|
4
|
+
"""
|
|
5
|
+
Abstract base class for all search providers.
|
|
6
|
+
Enforces a standard interface for executing search queries.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def search(self, query, api_key, **kwargs):
|
|
11
|
+
"""
|
|
12
|
+
Execute a search request against the provider.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
query (str): The search keywords.
|
|
16
|
+
api_key (str): The API Key required for authentication.
|
|
17
|
+
**kwargs: Arbitrary keyword arguments (e.g., 'limit', 'language', 'api_url').
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict: A standardized dictionary containing:
|
|
21
|
+
- 'results': List of dicts with 'title', 'url', 'snippet'.
|
|
22
|
+
- 'metrics': Dict with 'latency_ms' and 'size_bytes'.
|
|
23
|
+
- 'error': (Optional) Error message string if occurred.
|
|
24
|
+
"""
|
|
25
|
+
pass
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
import jmespath
|
|
4
|
+
from .base import BaseProvider
|
|
5
|
+
|
|
6
|
+
class GenericProvider(BaseProvider):
|
|
7
|
+
"""
|
|
8
|
+
A generic search provider driven by YAML configuration.
|
|
9
|
+
It constructs HTTP requests dynamically and maps responses using JMESPath.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, config):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the provider with a configuration dictionary.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
config (dict): Configuration containing url, headers, params, and mapping rules.
|
|
18
|
+
"""
|
|
19
|
+
self.config = config
|
|
20
|
+
|
|
21
|
+
def _fill_template(self, template_obj, **kwargs):
|
|
22
|
+
"""
|
|
23
|
+
Recursively replaces placeholders (e.g., {query}) in dictionaries or strings
|
|
24
|
+
with values provided in kwargs.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
template_obj (dict | str): The structure containing placeholders.
|
|
28
|
+
**kwargs: Key-value pairs to inject into the template.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
The structure with placeholders replaced by actual values.
|
|
32
|
+
"""
|
|
33
|
+
if isinstance(template_obj, str):
|
|
34
|
+
# Treat None values as empty strings to prevent "None" appearing in URLs
|
|
35
|
+
safe_kwargs = {k: (v if v is not None else '') for k, v in kwargs.items()}
|
|
36
|
+
try:
|
|
37
|
+
return template_obj.format(**safe_kwargs)
|
|
38
|
+
except KeyError:
|
|
39
|
+
# Return original string if a placeholder key is missing in kwargs
|
|
40
|
+
return template_obj
|
|
41
|
+
elif isinstance(template_obj, dict):
|
|
42
|
+
return {k: self._fill_template(v, **kwargs) for k, v in template_obj.items()}
|
|
43
|
+
return template_obj
|
|
44
|
+
|
|
45
|
+
def search(self, query, api_key, **kwargs):
|
|
46
|
+
# 1. Extract parameters with defaults
|
|
47
|
+
limit = kwargs.get('limit', '10')
|
|
48
|
+
language = kwargs.get('language', 'en-US')
|
|
49
|
+
custom_url = kwargs.get('api_url', '').strip()
|
|
50
|
+
|
|
51
|
+
# 2. Determine configuration
|
|
52
|
+
url = custom_url if custom_url else self.config.get('url')
|
|
53
|
+
method = self.config.get('method', 'GET')
|
|
54
|
+
|
|
55
|
+
# 3. Prepare context for template injection
|
|
56
|
+
context = {
|
|
57
|
+
'query': query,
|
|
58
|
+
'api_key': api_key,
|
|
59
|
+
'limit': limit,
|
|
60
|
+
'language': language
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# 4. construct request components
|
|
64
|
+
headers = self._fill_template(self.config.get('headers', {}), **context)
|
|
65
|
+
params = self._fill_template(self.config.get('params', {}), **context)
|
|
66
|
+
json_body = self._fill_template(self.config.get('payload', {}), **context)
|
|
67
|
+
|
|
68
|
+
# Logging (Masking sensitive API keys)
|
|
69
|
+
print(f'[{self.config.get("name", "Unknown")}] Search:')
|
|
70
|
+
print(f' URL: {url} | Method: {method}')
|
|
71
|
+
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
req_args = {'headers': headers, 'timeout': 30}
|
|
76
|
+
if params:
|
|
77
|
+
req_args['params'] = params
|
|
78
|
+
if json_body:
|
|
79
|
+
req_args['json'] = json_body
|
|
80
|
+
|
|
81
|
+
if method.upper() == 'GET':
|
|
82
|
+
response = requests.get(url, **req_args)
|
|
83
|
+
else:
|
|
84
|
+
response = requests.post(url, **req_args)
|
|
85
|
+
|
|
86
|
+
response.raise_for_status()
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Request Error: {e}")
|
|
89
|
+
return {
|
|
90
|
+
"error": str(e),
|
|
91
|
+
"results": [],
|
|
92
|
+
"metrics": {"latency_ms": 0, "size_bytes": 0}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
end_time = time.time()
|
|
96
|
+
|
|
97
|
+
# 5. Parse and Normalize Response
|
|
98
|
+
try:
|
|
99
|
+
raw_data = response.json()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"JSON Parse Error: {e}")
|
|
102
|
+
raw_data = {}
|
|
103
|
+
|
|
104
|
+
mapping = self.config.get('response_mapping', {})
|
|
105
|
+
# Use JMESPath to find the list of results
|
|
106
|
+
root_list = jmespath.search(mapping.get('root_path', '@'), raw_data) or []
|
|
107
|
+
|
|
108
|
+
normalized_results = []
|
|
109
|
+
field_map = mapping.get('fields', {})
|
|
110
|
+
|
|
111
|
+
for item in root_list:
|
|
112
|
+
entry = {}
|
|
113
|
+
# Map specific fields (title, url, etc.) based on config
|
|
114
|
+
for std_key, source_path in field_map.items():
|
|
115
|
+
val = jmespath.search(source_path, item)
|
|
116
|
+
entry[std_key] = val if val else ""
|
|
117
|
+
normalized_results.append(entry)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"results": normalized_results,
|
|
121
|
+
"metrics": {
|
|
122
|
+
"latency_ms": round((end_time - start_time) * 1000, 2),
|
|
123
|
+
"size_bytes": len(response.content)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
from querit import QueritClient
|
|
4
|
+
from querit.models.request import SearchRequest
|
|
5
|
+
from querit.errors import QueritError
|
|
6
|
+
from .base import BaseProvider
|
|
7
|
+
|
|
8
|
+
class QueritSdkProvider(BaseProvider):
|
|
9
|
+
"""
|
|
10
|
+
Specialized provider implementation using the official Querit Python SDK.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
self.config = config
|
|
15
|
+
|
|
16
|
+
def search(self, query, api_key, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Executes a search using the Querit SDK.
|
|
19
|
+
Handles the 'Bearer' prefix logic internally within the SDK.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
# Initialize client with the raw API key
|
|
23
|
+
client = QueritClient(
|
|
24
|
+
api_key=api_key.strip(),
|
|
25
|
+
timeout=30
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
limit = int(kwargs.get('limit', 10))
|
|
29
|
+
|
|
30
|
+
request_model = SearchRequest(
|
|
31
|
+
query=query,
|
|
32
|
+
count=limit,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
print(f'[Querit SDK] Searching: {query} (Limit: {limit})')
|
|
36
|
+
|
|
37
|
+
start_time = time.time()
|
|
38
|
+
|
|
39
|
+
# Execute search via SDK
|
|
40
|
+
response = client.search(request_model)
|
|
41
|
+
|
|
42
|
+
end_time = time.time()
|
|
43
|
+
|
|
44
|
+
# Normalize results to standard format
|
|
45
|
+
normalized_results = []
|
|
46
|
+
if response.results:
|
|
47
|
+
for item in response.results:
|
|
48
|
+
# Use getattr to safely access SDK object attributes
|
|
49
|
+
normalized_results.append({
|
|
50
|
+
"title": getattr(item, 'title', ''),
|
|
51
|
+
"url": getattr(item, 'url', ''),
|
|
52
|
+
# Fallback to description if snippet is missing
|
|
53
|
+
"snippet": getattr(item, 'snippet', '') or getattr(item, 'description', '')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Calculate estimated size for metrics (approximate JSON size)
|
|
57
|
+
estimated_size = len(json.dumps([r for r in normalized_results]))
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"results": normalized_results,
|
|
61
|
+
"metrics": {
|
|
62
|
+
"latency_ms": round((end_time - start_time) * 1000, 2),
|
|
63
|
+
"size_bytes": estimated_size
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
except QueritError as e:
|
|
68
|
+
print(f"Querit SDK Error: {e}")
|
|
69
|
+
return {
|
|
70
|
+
"error": f"Querit SDK Error: {str(e)}",
|
|
71
|
+
"results": [],
|
|
72
|
+
"metrics": {"latency_ms": 0, "size_bytes": 0}
|
|
73
|
+
}
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Unexpected Error: {e}")
|
|
76
|
+
return {
|
|
77
|
+
"error": f"Error: {str(e)}",
|
|
78
|
+
"results": [],
|
|
79
|
+
"metrics": {"latency_ms": 0, "size_bytes": 0}
|
|
80
|
+
}
|
backend/providers.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
querit:
|
|
2
|
+
type: "querit_sdk"
|
|
3
|
+
description: "Official Querit Search via Python SDK"
|
|
4
|
+
default_limit: 10
|
|
5
|
+
|
|
6
|
+
ydc_search:
|
|
7
|
+
url: "https://ydc-index.io/v1/search"
|
|
8
|
+
method: "GET"
|
|
9
|
+
headers:
|
|
10
|
+
X-API-Key: "{api_key}"
|
|
11
|
+
params:
|
|
12
|
+
query: "{query}"
|
|
13
|
+
payload: {}
|
|
14
|
+
response_mapping:
|
|
15
|
+
root_path: "results.web"
|
|
16
|
+
fields:
|
|
17
|
+
title: "title"
|
|
18
|
+
url: "url"
|
|
19
|
+
snippet: "snippets[0] || description"
|