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 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"