search-api-webui 0.1.7__py3-none-any.whl → 0.1.9__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.
- search_api_webui/__init__.py +0 -1
- search_api_webui/app.py +173 -56
- search_api_webui/providers/__init__.py +13 -7
- search_api_webui/providers/base.py +98 -5
- search_api_webui/providers/generic.py +128 -48
- search_api_webui/providers/querit.py +46 -34
- search_api_webui/providers.yaml +63 -5
- search_api_webui/ruff.toml +27 -0
- search_api_webui/static/AppIcon.icns +0 -0
- search_api_webui/static/assets/index-B2AadzJS.css +1 -0
- search_api_webui/static/assets/index-DxStlxhi.js +14 -0
- search_api_webui/static/index.html +2 -2
- {search_api_webui-0.1.7.dist-info → search_api_webui-0.1.9.dist-info}/METADATA +75 -13
- search_api_webui-0.1.9.dist-info/RECORD +18 -0
- search_api_webui/static/assets/index-BNPOSv2e.css +0 -1
- search_api_webui/static/assets/index-Co_gG-wr.js +0 -196
- search_api_webui-0.1.7.dist-info/RECORD +0 -16
- {search_api_webui-0.1.7.dist-info → search_api_webui-0.1.9.dist-info}/WHEEL +0 -0
- {search_api_webui-0.1.7.dist-info → search_api_webui-0.1.9.dist-info}/entry_points.txt +0 -0
- {search_api_webui-0.1.7.dist-info → search_api_webui-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -18,55 +18,81 @@
|
|
|
18
18
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
19
|
# DEALINGS IN THE SOFTWARE.
|
|
20
20
|
|
|
21
|
+
import html
|
|
22
|
+
import logging
|
|
21
23
|
import time
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
import jmespath
|
|
24
|
-
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
from .base import BaseProvider, extract_domain_from_url, parse_server_latency
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
25
32
|
|
|
26
33
|
class GenericProvider(BaseProvider):
|
|
27
|
-
|
|
34
|
+
'''
|
|
28
35
|
A generic search provider driven by YAML configuration.
|
|
29
36
|
It constructs HTTP requests dynamically and maps responses using JMESPath.
|
|
30
|
-
|
|
37
|
+
'''
|
|
31
38
|
|
|
32
39
|
def __init__(self, config):
|
|
33
|
-
|
|
40
|
+
'''
|
|
34
41
|
Initialize the provider with a configuration dictionary.
|
|
35
|
-
|
|
42
|
+
|
|
36
43
|
Args:
|
|
37
44
|
config (dict): Configuration containing url, headers, params, and mapping rules.
|
|
38
|
-
|
|
45
|
+
'''
|
|
39
46
|
self.config = config
|
|
40
47
|
self.session = requests.Session() # Persistent connection session
|
|
41
|
-
self._connection_ready = False
|
|
42
|
-
self._last_url = None
|
|
48
|
+
self._connection_ready = False # Connection ready status
|
|
49
|
+
self._last_url = None # Last used URL tracker
|
|
43
50
|
|
|
44
51
|
def _fill_template(self, template_obj, **kwargs):
|
|
45
|
-
|
|
46
|
-
Recursively replaces placeholders (e.g., {query}) in dictionaries or strings
|
|
52
|
+
'''
|
|
53
|
+
Recursively replaces placeholders (e.g., {query}) in dictionaries or strings
|
|
47
54
|
with values provided in kwargs.
|
|
48
|
-
|
|
55
|
+
|
|
49
56
|
Args:
|
|
50
57
|
template_obj (dict | str): The structure containing placeholders.
|
|
51
58
|
**kwargs: Key-value pairs to inject into the template.
|
|
52
|
-
|
|
59
|
+
|
|
53
60
|
Returns:
|
|
54
61
|
The structure with placeholders replaced by actual values.
|
|
55
|
-
|
|
62
|
+
Dict entries with empty values are removed.
|
|
63
|
+
Pure numeric placeholders (e.g., "{limit}") are converted to int/float.
|
|
64
|
+
'''
|
|
56
65
|
if isinstance(template_obj, str):
|
|
57
66
|
# Treat None values as empty strings to prevent "None" appearing in URLs
|
|
58
67
|
safe_kwargs = {k: (v if v is not None else '') for k, v in kwargs.items()}
|
|
59
68
|
try:
|
|
60
|
-
|
|
69
|
+
result = template_obj.format(**safe_kwargs)
|
|
70
|
+
# Convert to number if the result is a numeric string
|
|
71
|
+
# and the original template was a pure placeholder like "{limit}"
|
|
72
|
+
if template_obj.strip().startswith('{') and template_obj.strip().endswith('}'):
|
|
73
|
+
try:
|
|
74
|
+
# Try int first, then float
|
|
75
|
+
if '.' in result:
|
|
76
|
+
return float(result)
|
|
77
|
+
return int(result)
|
|
78
|
+
except (ValueError, AttributeError):
|
|
79
|
+
pass
|
|
80
|
+
return result
|
|
61
81
|
except KeyError:
|
|
62
82
|
# Return original string if a placeholder key is missing in kwargs
|
|
63
83
|
return template_obj
|
|
64
84
|
elif isinstance(template_obj, dict):
|
|
65
|
-
|
|
85
|
+
result = {}
|
|
86
|
+
for k, v in template_obj.items():
|
|
87
|
+
filled = self._fill_template(v, **kwargs)
|
|
88
|
+
# Skip entries with empty values (e.g., empty strings after template fill)
|
|
89
|
+
if filled != '':
|
|
90
|
+
result[k] = filled
|
|
91
|
+
return result
|
|
66
92
|
return template_obj
|
|
67
93
|
|
|
68
94
|
def _ensure_connection(self, url, headers):
|
|
69
|
-
|
|
95
|
+
'''
|
|
70
96
|
Pre-warm HTTPS connection and verify availability.
|
|
71
97
|
Uses lightweight HEAD request to verify connection without fetching response body.
|
|
72
98
|
|
|
@@ -76,7 +102,7 @@ class GenericProvider(BaseProvider):
|
|
|
76
102
|
|
|
77
103
|
Returns:
|
|
78
104
|
bool: Whether connection is ready
|
|
79
|
-
|
|
105
|
+
'''
|
|
80
106
|
# Re-warm if URL changed or connection not ready
|
|
81
107
|
if url != self._last_url or not self._connection_ready:
|
|
82
108
|
try:
|
|
@@ -84,53 +110,64 @@ class GenericProvider(BaseProvider):
|
|
|
84
110
|
self.session.head(url, headers=headers, timeout=5)
|
|
85
111
|
self._connection_ready = True
|
|
86
112
|
self._last_url = url
|
|
87
|
-
|
|
113
|
+
logger.debug('[Connection Pool] Connected to: %s', url)
|
|
88
114
|
except Exception as e:
|
|
89
115
|
self._connection_ready = False
|
|
90
|
-
|
|
116
|
+
logger.warning('[Connection Pool] Connection warm-up failed: %s', e)
|
|
91
117
|
raise
|
|
92
118
|
|
|
93
119
|
def search(self, query, api_key, **kwargs):
|
|
120
|
+
'''
|
|
121
|
+
Perform search using the configured API.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
query: Search query string
|
|
125
|
+
api_key: API key for authentication
|
|
126
|
+
**kwargs: Additional parameters (limit, language, api_url)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: Search results with 'results' and 'metrics' keys
|
|
130
|
+
'''
|
|
94
131
|
# 1. Extract parameters with defaults
|
|
95
132
|
limit = kwargs.get('limit', '10')
|
|
96
|
-
language = kwargs.get('language'
|
|
97
|
-
custom_url = kwargs.get('api_url'
|
|
133
|
+
language = kwargs.get('language')
|
|
134
|
+
custom_url = kwargs.get('api_url')
|
|
98
135
|
|
|
99
136
|
# 2. Determine configuration
|
|
100
|
-
|
|
137
|
+
# Use custom api_url if provided, otherwise fallback to config url
|
|
138
|
+
url = custom_url.strip() if custom_url else self.config.get('url')
|
|
101
139
|
method = self.config.get('method', 'GET')
|
|
102
|
-
|
|
140
|
+
|
|
103
141
|
# 3. Prepare context for template injection
|
|
104
142
|
context = {
|
|
105
143
|
'query': query,
|
|
106
144
|
'api_key': api_key,
|
|
107
145
|
'limit': limit,
|
|
108
|
-
'language': language
|
|
109
146
|
}
|
|
147
|
+
# Only add language to context if provided
|
|
148
|
+
if language:
|
|
149
|
+
context['language'] = language
|
|
110
150
|
|
|
111
151
|
# 4. construct request components
|
|
112
152
|
headers = self._fill_template(self.config.get('headers', {}), **context)
|
|
113
153
|
params = self._fill_template(self.config.get('params', {}), **context)
|
|
114
154
|
json_body = self._fill_template(self.config.get('payload', {}), **context)
|
|
115
155
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
logger.info('[%s] Search: URL=%s | Method=%s',
|
|
157
|
+
self.config.get('name', 'Unknown'), url, method)
|
|
158
|
+
|
|
120
159
|
# Ensure connection is pre-warmed (use HEAD request to verify availability)
|
|
121
160
|
# Pre-warming is not counted in request latency, only verifies connection
|
|
122
161
|
try:
|
|
123
162
|
self._ensure_connection(url, headers)
|
|
124
163
|
except Exception as e:
|
|
125
|
-
|
|
164
|
+
logger.error('Connection Warm-up Error: %s', e)
|
|
126
165
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
166
|
+
'error': f'Connection failed: {str(e)}',
|
|
167
|
+
'results': [],
|
|
168
|
+
'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
|
|
130
169
|
}
|
|
131
170
|
|
|
132
|
-
start_time = time.time()
|
|
133
|
-
|
|
134
171
|
try:
|
|
135
172
|
req_args = {'headers': headers, 'timeout': 30}
|
|
136
173
|
if params:
|
|
@@ -139,48 +176,91 @@ class GenericProvider(BaseProvider):
|
|
|
139
176
|
req_args['json'] = json_body
|
|
140
177
|
|
|
141
178
|
# Use Session to send request (connection is reused)
|
|
179
|
+
start_time = time.time()
|
|
142
180
|
if method.upper() == 'GET':
|
|
143
181
|
response = self.session.get(url, **req_args)
|
|
144
182
|
else:
|
|
145
183
|
response = self.session.post(url, **req_args)
|
|
184
|
+
end_time = time.time()
|
|
146
185
|
|
|
147
186
|
response.raise_for_status()
|
|
148
187
|
except Exception as e:
|
|
149
|
-
|
|
188
|
+
logger.error('Request Error: %s', e, f"args: {req_args}")
|
|
150
189
|
return {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
190
|
+
'error': str(e),
|
|
191
|
+
'results': [],
|
|
192
|
+
'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
|
|
154
193
|
}
|
|
155
194
|
|
|
156
|
-
end_time = time.time()
|
|
157
|
-
|
|
158
195
|
# 5. Parse and Normalize Response
|
|
159
196
|
try:
|
|
160
197
|
raw_data = response.json()
|
|
161
198
|
except Exception as e:
|
|
162
|
-
|
|
199
|
+
logger.error('JSON Parse Error: %s', e)
|
|
163
200
|
raw_data = {}
|
|
164
201
|
|
|
202
|
+
logger.debug('Full response: %s', raw_data)
|
|
203
|
+
|
|
165
204
|
mapping = self.config.get('response_mapping', {})
|
|
166
205
|
# Use JMESPath to find the list of results
|
|
167
206
|
root_list = jmespath.search(mapping.get('root_path', '@'), raw_data) or []
|
|
168
207
|
|
|
169
208
|
normalized_results = []
|
|
170
209
|
field_map = mapping.get('fields', {})
|
|
210
|
+
# Define common fields that should be extracted as-is
|
|
211
|
+
common_fields = {'title', 'url', 'site_name', 'site_icon', 'page_age'}
|
|
212
|
+
|
|
213
|
+
# Collect all JMESPath source paths that are already mapped
|
|
214
|
+
mapped_paths = set(field_map.values())
|
|
171
215
|
|
|
172
216
|
for item in root_list:
|
|
173
217
|
entry = {}
|
|
218
|
+
snippet_fields = {} # Collect unmapped fields from raw API response
|
|
219
|
+
|
|
174
220
|
# Map specific fields (title, url, etc.) based on config
|
|
175
221
|
for std_key, source_path in field_map.items():
|
|
176
222
|
val = jmespath.search(source_path, item)
|
|
177
|
-
|
|
223
|
+
# Decode HTML entities for site_name
|
|
224
|
+
if std_key == 'site_name' and val:
|
|
225
|
+
val = html.unescape(val)
|
|
226
|
+
|
|
227
|
+
# Only store common fields in entry
|
|
228
|
+
if std_key in common_fields:
|
|
229
|
+
entry[std_key] = val if val else ''
|
|
230
|
+
|
|
231
|
+
# Find all unmapped fields in the raw item
|
|
232
|
+
# These are fields that exist in the API response but are not in field_map
|
|
233
|
+
if isinstance(item, dict):
|
|
234
|
+
for key, value in item.items():
|
|
235
|
+
# Check if this key is already mapped in config
|
|
236
|
+
# Skip common nested objects and arrays for cleaner output
|
|
237
|
+
if key not in mapped_paths and value and not isinstance(value, (dict, list)):
|
|
238
|
+
snippet_fields[key] = value
|
|
239
|
+
|
|
240
|
+
# Store snippet fields as JSON structure in snippet
|
|
241
|
+
if snippet_fields:
|
|
242
|
+
entry['snippet'] = snippet_fields
|
|
243
|
+
else:
|
|
244
|
+
entry['snippet'] = ''
|
|
245
|
+
|
|
178
246
|
normalized_results.append(entry)
|
|
179
247
|
|
|
248
|
+
# Post-process: extract domain from URL if site_name is empty
|
|
249
|
+
for entry in normalized_results:
|
|
250
|
+
if not entry.get('site_name') and entry.get('url'):
|
|
251
|
+
entry['site_name'] = extract_domain_from_url(entry['url']) or ''
|
|
252
|
+
|
|
253
|
+
# Extract server latency from response if configured
|
|
254
|
+
server_latency_path = mapping.get('server_latency_path')
|
|
255
|
+
server_latency_ms = parse_server_latency(
|
|
256
|
+
jmespath.search(server_latency_path, raw_data),
|
|
257
|
+
) if server_latency_path else None
|
|
258
|
+
|
|
180
259
|
return {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
260
|
+
'results': normalized_results,
|
|
261
|
+
'metrics': {
|
|
262
|
+
'latency_ms': round((end_time - start_time) * 1000, 2),
|
|
263
|
+
'server_latency_ms': server_latency_ms,
|
|
264
|
+
'size_bytes': len(response.content),
|
|
265
|
+
},
|
|
186
266
|
}
|
|
@@ -18,32 +18,35 @@
|
|
|
18
18
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
19
|
# DEALINGS IN THE SOFTWARE.
|
|
20
20
|
|
|
21
|
-
import time
|
|
22
21
|
import json
|
|
22
|
+
import logging
|
|
23
|
+
import time
|
|
24
|
+
|
|
23
25
|
from querit import QueritClient
|
|
24
|
-
from querit.models.request import SearchRequest
|
|
25
26
|
from querit.errors import QueritError
|
|
26
|
-
from .
|
|
27
|
+
from querit.models.request import SearchRequest
|
|
28
|
+
|
|
29
|
+
from .base import BaseProvider, parse_server_latency
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
27
33
|
|
|
28
34
|
class QueritSdkProvider(BaseProvider):
|
|
29
|
-
|
|
35
|
+
'''
|
|
30
36
|
Specialized provider implementation using the official Querit Python SDK.
|
|
31
|
-
|
|
37
|
+
'''
|
|
32
38
|
|
|
33
39
|
def __init__(self, config):
|
|
34
40
|
self.config = config
|
|
35
41
|
|
|
36
42
|
def search(self, query, api_key, **kwargs):
|
|
37
|
-
|
|
43
|
+
'''
|
|
38
44
|
Executes a search using the Querit SDK.
|
|
39
45
|
Handles the 'Bearer' prefix logic internally within the SDK.
|
|
40
|
-
|
|
46
|
+
'''
|
|
41
47
|
try:
|
|
42
48
|
# Initialize client with the raw API key
|
|
43
|
-
client = QueritClient(
|
|
44
|
-
api_key=api_key.strip(),
|
|
45
|
-
timeout=30
|
|
46
|
-
)
|
|
49
|
+
client = QueritClient(api_key=api_key.strip(), timeout=30)
|
|
47
50
|
|
|
48
51
|
limit = int(kwargs.get('limit', 10))
|
|
49
52
|
|
|
@@ -52,13 +55,13 @@ class QueritSdkProvider(BaseProvider):
|
|
|
52
55
|
count=limit,
|
|
53
56
|
)
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
logger.debug(f'[Querit SDK] Searching: {query} (Limit: {limit})')
|
|
56
59
|
|
|
57
60
|
start_time = time.time()
|
|
58
|
-
|
|
61
|
+
|
|
59
62
|
# Execute search via SDK
|
|
60
63
|
response = client.search(request_model)
|
|
61
|
-
|
|
64
|
+
|
|
62
65
|
end_time = time.time()
|
|
63
66
|
|
|
64
67
|
# Normalize results to standard format
|
|
@@ -66,35 +69,44 @@ class QueritSdkProvider(BaseProvider):
|
|
|
66
69
|
if response.results:
|
|
67
70
|
for item in response.results:
|
|
68
71
|
# Use getattr to safely access SDK object attributes
|
|
69
|
-
normalized_results.append(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
normalized_results.append(
|
|
73
|
+
{
|
|
74
|
+
'title': getattr(item, 'title', ''),
|
|
75
|
+
'url': getattr(item, 'url', ''),
|
|
76
|
+
# Fallback to description if snippet is missing
|
|
77
|
+
'snippet': getattr(item, 'snippet', '') or getattr(item, 'description', ''),
|
|
78
|
+
},
|
|
79
|
+
)
|
|
75
80
|
|
|
76
81
|
# Calculate estimated size for metrics (approximate JSON size)
|
|
77
|
-
estimated_size = len(json.dumps(
|
|
82
|
+
estimated_size = len(json.dumps(list(normalized_results)))
|
|
83
|
+
|
|
84
|
+
# Extract server latency from SDK response if available
|
|
85
|
+
server_latency_ms = None
|
|
86
|
+
# The SDK may provide the server's 'took' field from the response
|
|
87
|
+
if hasattr(response, 'took') and response.took is not None:
|
|
88
|
+
server_latency_ms = parse_server_latency(response.took)
|
|
78
89
|
|
|
79
90
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
'results': normalized_results,
|
|
92
|
+
'metrics': {
|
|
93
|
+
'latency_ms': round((end_time - start_time) * 1000, 2),
|
|
94
|
+
'server_latency_ms': server_latency_ms,
|
|
95
|
+
'size_bytes': estimated_size,
|
|
96
|
+
},
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
except QueritError as e:
|
|
88
|
-
|
|
100
|
+
logger.error(f'Querit SDK Error: {e}')
|
|
89
101
|
return {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
'error': f'Querit SDK Error: {str(e)}',
|
|
103
|
+
'results': [],
|
|
104
|
+
'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
|
|
93
105
|
}
|
|
94
106
|
except Exception as e:
|
|
95
|
-
|
|
107
|
+
logger.exception(f'Unexpected Error: {e}')
|
|
96
108
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
'error': f'Error: {str(e)}',
|
|
110
|
+
'results': [],
|
|
111
|
+
'metrics': {'latency_ms': 0, 'server_latency_ms': None, 'size_bytes': 0},
|
|
100
112
|
}
|
search_api_webui/providers.yaml
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
querit:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
url: "https://api.querit.ai/v1/search"
|
|
3
|
+
method: "POST"
|
|
4
|
+
headers:
|
|
5
|
+
"Accept": "application/json"
|
|
6
|
+
"Authorization": "Bearer {api_key}"
|
|
7
|
+
"Content-Type": "application/json"
|
|
8
|
+
payload:
|
|
9
|
+
query: "{query}"
|
|
10
|
+
count: "{limit}"
|
|
11
|
+
response_mapping:
|
|
12
|
+
root_path: "results.result"
|
|
13
|
+
server_latency_path: "took"
|
|
14
|
+
fields:
|
|
15
|
+
title: "title"
|
|
16
|
+
url: "url"
|
|
17
|
+
site_name: "site_name"
|
|
18
|
+
site_icon: "site_icon"
|
|
19
|
+
page_age: "page_age"
|
|
5
20
|
|
|
6
21
|
you:
|
|
7
22
|
url: "https://ydc-index.io/v1/search"
|
|
@@ -10,10 +25,53 @@ you:
|
|
|
10
25
|
X-API-Key: "{api_key}"
|
|
11
26
|
params:
|
|
12
27
|
query: "{query}"
|
|
28
|
+
count: "{limit}"
|
|
13
29
|
payload: {}
|
|
14
30
|
response_mapping:
|
|
15
|
-
root_path: "results.web"
|
|
31
|
+
root_path: "results.web"
|
|
32
|
+
server_latency_path: "metadata.latency"
|
|
16
33
|
fields:
|
|
17
34
|
title: "title"
|
|
18
35
|
url: "url"
|
|
19
|
-
|
|
36
|
+
site_icon: "favicon_url"
|
|
37
|
+
page_age: "page_age"
|
|
38
|
+
|
|
39
|
+
brave:
|
|
40
|
+
url: "https://api.search.brave.com/res/v1/web/search"
|
|
41
|
+
method: "GET"
|
|
42
|
+
headers:
|
|
43
|
+
X-Subscription-Token: "{api_key}"
|
|
44
|
+
Accept: "application/json"
|
|
45
|
+
params:
|
|
46
|
+
q: "{query}"
|
|
47
|
+
count: "{limit}"
|
|
48
|
+
payload: {}
|
|
49
|
+
response_mapping:
|
|
50
|
+
root_path: "web.results"
|
|
51
|
+
fields:
|
|
52
|
+
title: "title"
|
|
53
|
+
url: "url"
|
|
54
|
+
page_age: "page_age"
|
|
55
|
+
|
|
56
|
+
exa:
|
|
57
|
+
url: "https://api.exa.ai/search"
|
|
58
|
+
method: "POST"
|
|
59
|
+
headers:
|
|
60
|
+
"Content-Type": "application/json"
|
|
61
|
+
"x-api-key": "{api_key}"
|
|
62
|
+
params: {}
|
|
63
|
+
payload:
|
|
64
|
+
query: "{query}"
|
|
65
|
+
numResults: "{limit}"
|
|
66
|
+
response_mapping:
|
|
67
|
+
root_path: "results"
|
|
68
|
+
fields:
|
|
69
|
+
title: "title"
|
|
70
|
+
url: "url"
|
|
71
|
+
page_age: "publishedDate"
|
|
72
|
+
site_icon: "favicon"
|
|
73
|
+
|
|
74
|
+
querit_sdk:
|
|
75
|
+
type: "querit_sdk"
|
|
76
|
+
description: "Official Querit Search via Python SDK"
|
|
77
|
+
default_limit: 10
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Ruff configuration
|
|
2
|
+
# Ruff replaces Black, flake8, and isort
|
|
3
|
+
|
|
4
|
+
line-length = 120
|
|
5
|
+
target-version = "py37"
|
|
6
|
+
|
|
7
|
+
[format]
|
|
8
|
+
quote-style = "single"
|
|
9
|
+
indent-style = "space"
|
|
10
|
+
docstring-code-format = false
|
|
11
|
+
|
|
12
|
+
[lint]
|
|
13
|
+
select = [
|
|
14
|
+
"E", # pycodestyle errors
|
|
15
|
+
"W", # pycodestyle warnings
|
|
16
|
+
"F", # pyflakes
|
|
17
|
+
"I", # isort
|
|
18
|
+
"N", # pep8-naming
|
|
19
|
+
"UP", # pyupgrade
|
|
20
|
+
"B", # flake8-bugbear
|
|
21
|
+
"C4", # flake8-comprehensions
|
|
22
|
+
"SIM", # flake8-simplify
|
|
23
|
+
"COM", # flake8-commas (trailing commas)
|
|
24
|
+
]
|
|
25
|
+
ignore = [
|
|
26
|
+
"E203", # whitespace before ':' - conflicts with Black
|
|
27
|
+
]
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.pointer-events-none{pointer-events:none}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-3{right:.75rem}.top-0{top:0}.top-3\.5{top:.875rem}.z-10{z-index:10}.col-span-2{grid-column:span 2 / span 2}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-full{height:100%}.min-h-\[500px\]{min-height:500px}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-0{border-width:0px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-t-4{border-top-width:4px}.border-dashed{border-style:dashed}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-t-transparent{border-top-color:transparent}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-100\/50{background-color:#f3f4f680}.bg-gray-400{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-200{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-purple-600{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-600{--tw-gradient-from: #2563eb var(--tw-gradient-from-position);--tw-gradient-to: rgb(37 99 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-gray-50{--tw-gradient-from: #f9fafb var(--tw-gradient-from-position);--tw-gradient-to: rgb(249 250 251 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-purple-600{--tw-gradient-from: #9333ea var(--tw-gradient-from-position);--tw-gradient-to: rgb(147 51 234 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-blue-50{--tw-gradient-to: #eff6ff var(--tw-gradient-to-position)}.to-blue-600{--tw-gradient-to: #2563eb var(--tw-gradient-to-position)}.to-indigo-600{--tw-gradient-to: #4f46e5 var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.p-2{padding:.5rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pb-2{padding-bottom:.5rem}.pl-0{padding-left:0}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-widest{letter-spacing:.1em}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-gray-950{--tw-text-opacity: 1;color:rgb(3 7 18 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-50{opacity:.5}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-gray-200{--tw-ring-opacity: 1;--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity, 1))}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-500{transition-duration:.5s}body{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.placeholder\:text-gray-500::-moz-placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.placeholder\:text-gray-500::placeholder{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-green-300:hover{--tw-bg-opacity: 1;background-color:rgb(134 239 172 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-50:hover{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-700:hover{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-purple-700:hover{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-600:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.focus\:ring-purple-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(168 85 247 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-blue-600:focus-visible{--tw-ring-opacity: 1;--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity, 1))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:underline{text-decoration-line:underline}.group:hover .group-hover\:opacity-50{opacity:.5}.data-\[side\=Left\]\:border-t-blue-500[data-side=Left]{--tw-border-opacity: 1;border-top-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.data-\[side\=Right\]\:border-t-orange-500[data-side=Right]{--tw-border-opacity: 1;border-top-color:rgb(249 115 22 / var(--tw-border-opacity, 1))}@media(min-width:768px){.md\:w-64{width:16rem}.md\:w-auto{width:auto}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}}
|