Greff3 commited on
Commit
b47974e
·
verified ·
1 Parent(s): c374d16

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +166 -184
main.py CHANGED
@@ -1,32 +1,36 @@
1
  from fastapi import FastAPI, HTTPException, Query
2
- from typing import List, Optional
3
  from pydantic import BaseModel
4
  from time import sleep
5
  from curl_cffi.requests import Session
6
- from urllib.parse import urlencode, unquote, urlparse, parse_qs
7
  import base64
8
- from typing import Dict, Any
9
- from concurrent.futures import ThreadPoolExecutor
10
- from webscout.litagent import LitAgent
11
- from bs4 import BeautifulSoup
12
  import json
 
13
 
 
 
 
 
 
 
14
  app = FastAPI(
15
  title="Snapzion Search API",
16
- description="A FastAPI wrapper for the Search library with advanced features.",
17
- version="1.0.0",
18
  )
19
 
20
- # --- BingSearch Library Code ---
21
- # The provided BingSearch code is integrated here directly.
22
 
23
  class BingSearchResult(BaseModel):
 
24
  url: str
25
  title: str
26
  description: str
27
  metadata: Dict[str, Any] = {}
28
 
29
  class BingImageResult(BaseModel):
 
30
  title: str
31
  image: str
32
  thumbnail: str
@@ -34,15 +38,19 @@ class BingImageResult(BaseModel):
34
  source: str
35
 
36
  class BingNewsResult(BaseModel):
 
37
  title: str
38
  url: str
39
  description: str
40
  source: str = ""
41
 
42
- class BingSearch:
43
- """Bing search implementation with configurable parameters and advanced features."""
44
- _executor: ThreadPoolExecutor = ThreadPoolExecutor()
45
 
 
 
 
 
 
 
46
  def __init__(
47
  self,
48
  timeout: int = 10,
@@ -64,24 +72,31 @@ class BingSearch:
64
  timeout=self.timeout,
65
  impersonate=impersonate
66
  )
67
- # It's good practice to set a realistic User-Agent
68
  self.session.headers.update({
69
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36"
 
70
  })
71
 
72
- # FIX: Updated selectors to be more robust against Bing UI changes.
73
- def _selectors(self, element):
 
 
74
  selectors = {
75
- 'links': 'ol#b_results > li', # More generic selector for any list item in results
76
- 'next': 'a.sb_pagN' # Selector for the "Next" page button
 
 
77
  }
78
  return selectors[element]
79
 
80
- def _first_page(self, query):
81
- url = f'{self._base_url}/search?q={query}&search=&form=QBLH'
 
 
82
  return {'url': url, 'data': None}
83
 
84
- def _next_page(self, soup):
 
85
  selector = self._selectors('next')
86
  next_page_tag = soup.select_one(selector)
87
  url = None
@@ -89,46 +104,47 @@ class BingSearch:
89
  url = self._base_url + next_page_tag['href']
90
  return {'url': url, 'data': None}
91
 
92
- def _get_url(self, tag):
 
93
  url = tag.get('href', '')
94
- resp = url
95
  try:
96
  parsed_url = urlparse(url)
97
  query_params = parse_qs(parsed_url.query)
98
  if "u" in query_params:
99
- encoded_url = query_params["u"][0][2:]
100
- try:
101
- decoded_bytes = base64.urlsafe_b64decode(encoded_url + '===')
102
- except base64.binascii.Error as e:
103
- print(f"Error decoding Base64 string: {e}")
104
- return url
105
- resp = decoded_bytes.decode('utf-8')
106
  except Exception as e:
107
- print(f"Error decoding Base64 string: {e}")
108
- return resp
 
109
 
110
- # FIX: The entire text parsing logic is updated to handle modern Bing HTML structure.
111
  def text(
112
  self,
113
  keywords: str,
114
- region: str = None,
115
  safesearch: str = "moderate",
116
  max_results: int = 10,
117
  unique: bool = True
118
  ) -> List[BingSearchResult]:
 
119
  if not keywords:
120
  raise ValueError("Search keywords cannot be empty")
121
 
122
  fetched_results = []
123
- fetched_links = set()
124
 
125
- def fetch_page(url):
126
  try:
127
  resp = self.session.get(url)
128
  resp.raise_for_status()
129
  return resp.text
130
  except Exception as e:
131
- raise Exception(f"Bing search failed: {str(e)}")
 
132
 
133
  current_url = self._first_page(keywords)['url']
134
 
@@ -136,11 +152,9 @@ class BingSearch:
136
  html = fetch_page(current_url)
137
  soup = BeautifulSoup(html, "html.parser")
138
 
139
- # Use the more generic selector for result blocks
140
  result_blocks = soup.select(self._selectors('links'))
141
 
142
  for result in result_blocks:
143
- # Find the title and link, which are usually in an <h2> tag
144
  title_tag = result.find('h2')
145
  if not title_tag:
146
  continue
@@ -152,29 +166,30 @@ class BingSearch:
152
  url_val = self._get_url(link_tag)
153
  title = title_tag.get_text(strip=True)
154
 
155
- # Find the description, often in a div with class 'b_caption'
156
  desc_container = result.find('div', class_='b_caption')
157
- description = ''
158
- if desc_container:
159
- # Find the paragraph within the caption, or use the whole caption text
160
- desc_p = desc_container.find('p')
161
- if desc_p:
162
- description = desc_p.get_text(strip=True)
163
- else:
164
- description = desc_container.get_text(strip=True)
165
 
166
- # Fallback if no 'b_caption' is found
167
- if not description:
168
- p_tag = result.find('p')
169
- if p_tag:
170
- description = p_tag.get_text(strip=True)
171
 
172
  if url_val and title:
173
  if unique and url_val in fetched_links:
174
  continue
175
 
176
- fetched_results.append(BingSearchResult(url=url_val, title=title, description=description))
177
- fetched_links.add(url_val)
 
 
 
 
 
 
 
 
178
 
179
  if len(fetched_results) >= max_results:
180
  break
@@ -182,7 +197,6 @@ class BingSearch:
182
  if len(fetched_results) >= max_results:
183
  break
184
 
185
- # Find the next page URL
186
  next_page_info = self._next_page(soup)
187
  current_url = next_page_info['url']
188
  if current_url:
@@ -190,217 +204,185 @@ class BingSearch:
190
 
191
  return fetched_results[:max_results]
192
 
193
-
194
- def suggestions(self, query: str, region: str = None) -> List[str]:
195
  if not query:
196
  raise ValueError("Search query cannot be empty")
197
- params = {
198
- "query": query,
199
- "mkt": region if region else "en-US"
200
- }
201
  url = f"https://api.bing.com/osjson.aspx?{urlencode(params)}"
202
  try:
203
  resp = self.session.get(url)
204
  resp.raise_for_status()
205
  data = resp.json()
206
- if isinstance(data, list) and len(data) > 1 and isinstance(data[1], list):
207
- return data[1]
208
- return []
209
  except Exception as e:
210
- if hasattr(e, 'response') and e.response is not None:
211
- raise Exception(f"Bing suggestions failed with status {e.response.status_code}: {str(e)}")
212
- else:
213
- raise Exception(f"Bing suggestions failed: {str(e)}")
214
 
215
  def images(
216
- self,
217
- keywords: str,
218
- region: str = None,
219
- safesearch: str = "moderate",
220
- max_results: int = 10
221
  ) -> List[BingImageResult]:
 
222
  if not keywords:
223
  raise ValueError("Search keywords cannot be empty")
224
- safe_map = {
225
- "on": "Strict",
226
- "moderate": "Moderate",
227
- "off": "Off"
228
- }
229
- safe = safe_map.get(safesearch.lower(), "Moderate")
230
  params = {
231
- "q": keywords,
232
- "count": max_results,
233
- "setlang": self.lang,
234
- "safeSearch": safe,
235
  }
236
  if region:
237
  params["mkt"] = region
 
238
  url = f"{self._base_url}/images/search?{urlencode(params)}"
239
  try:
240
  resp = self.session.get(url)
241
  resp.raise_for_status()
242
  html = resp.text
243
  except Exception as e:
244
- if hasattr(e, 'response') and e.response is not None:
245
- raise Exception(f"Bing image search failed with status {e.response.status_code}: {str(e)}")
246
- else:
247
- raise Exception(f"Bing image search failed: {str(e)}")
248
  soup = BeautifulSoup(html, "html.parser")
249
  results = []
 
250
  for item in soup.select("a.iusc"):
 
 
251
  try:
252
- m = item.get("m")
253
- meta = json.loads(m) if m else {}
254
- image_url = meta.get("murl", "")
255
- thumb_url = meta.get("turl", "")
256
- title = meta.get("t", "")
257
- page_url = meta.get("purl", "")
258
- source = meta.get("surl", "")
259
- if image_url:
260
- results.append(BingImageResult(title=title, image=image_url, thumbnail=thumb_url, url=page_url, source=source))
261
- if len(results) >= max_results:
262
- break
263
  except Exception:
264
  continue
265
- return results[:max_results]
266
 
267
  def news(
268
- self,
269
- keywords: str,
270
- region: str = None,
271
- safesearch: str = "moderate",
272
- max_results: int = 10,
273
- ) -> List['BingNewsResult']:
274
  if not keywords:
275
  raise ValueError("Search keywords cannot be empty")
276
- safe_map = {
277
- "on": "Strict",
278
- "moderate": "Moderate",
279
- "off": "Off"
280
- }
281
- safe = safe_map.get(safesearch.lower(), "Moderate")
282
- params = {
283
- "q": keywords,
284
- "form": "QBNH",
285
- "safeSearch": safe,
286
- }
287
  if region:
288
  params["mkt"] = region
 
289
  url = f"{self._base_url}/news/search?{urlencode(params)}"
290
  try:
291
  resp = self.session.get(url)
292
  resp.raise_for_status()
293
  except Exception as e:
294
- if hasattr(e, 'response') and e.response is not None:
295
- raise Exception(f"Bing news search failed with status {e.response.status_code}: {str(e)}")
296
- else:
297
- raise Exception(f"Bing news search failed: {str(e)}")
298
  soup = BeautifulSoup(resp.text, "html.parser")
299
  results = []
300
- for item in soup.select("div.news-card, div.card, div.newsitem, div.card-content, div.t_s_main"):
301
- a_tag = item.find("a")
302
- title = a_tag.get_text(strip=True) if a_tag else ''
303
- url_val = a_tag['href'] if a_tag and a_tag.has_attr('href') else ''
304
- desc_tag = item.find("div", class_="snippet") or item.find("div", class_="news-card-snippet") or item.find("div", class_="snippetText")
305
- description = desc_tag.get_text(strip=True) if desc_tag else ''
306
- source_tag = item.find("div", class_="source")
307
- source = source_tag.get_text(strip=True) if source_tag else ''
308
- if url_val and title:
309
- results.append(BingNewsResult(title=title, url=url_val, description=description, source=source))
310
- if len(results) >= max_results:
311
- break
312
- if not results:
313
- for item in soup.select("a.title"):
314
- title = item.get_text(strip=True)
315
- url_val = item['href'] if item.has_attr('href') else ''
316
- description = ''
317
- source = ''
318
- if url_val and title:
319
- results.append(BingNewsResult(title=title, url=url_val, description=description, source=source))
320
- if len(results) >= max_results:
321
- break
322
  return results[:max_results]
323
 
324
 
 
 
325
  bing = BingSearch()
326
 
327
- @app.get("/search", response_model=List[BingSearchResult])
328
  async def text_search(
329
- query: str = Query(..., description="The search keywords."),
330
- region: Optional[str] = Query(None, description="The region for the search (e.g., 'us-US')."),
331
- safesearch: str = Query("moderate", description="Safe search level ('on', 'moderate', 'off')."),
332
- max_results: int = Query(10, description="Maximum number of results to return."),
333
  ):
334
  """
335
- Perform a text search on Bing.
336
  """
337
  try:
338
- results = bing.text(
339
- keywords=query,
340
- region=region,
341
- safesearch=safesearch,
342
- max_results=max_results,
343
- )
344
  return results
 
 
345
  except Exception as e:
 
346
  raise HTTPException(status_code=500, detail=str(e))
347
 
348
- @app.get("/suggestions", response_model=List[str])
349
  async def get_suggestions(
350
- query: str = Query(..., description="The search query for which to fetch suggestions."),
351
- region: Optional[str] = Query(None, description="The region for the suggestions (e.g., 'en-US')."),
352
  ):
353
  """
354
- Fetches search suggestions for a given query.
355
  """
356
  try:
357
- suggestions = bing.suggestions(query=query, region=region)
358
- return suggestions
 
359
  except Exception as e:
 
360
  raise HTTPException(status_code=500, detail=str(e))
361
 
362
- @app.get("/images", response_model=List[BingImageResult])
363
  async def image_search(
364
- query: str = Query(..., description="The search keywords for images."),
365
- region: Optional[str] = Query(None, description="The region for the image search (e.g., 'us-US')."),
366
- safesearch: str = Query("moderate", description="Safe search level ('on', 'moderate', 'off')."),
367
- max_results: int = Query(10, description="Maximum number of image results to return."),
368
  ):
369
  """
370
  Perform an image search on Bing.
371
  """
372
  try:
373
- results = bing.images(
374
- keywords=query,
375
- region=region,
376
- safesearch=safesearch,
377
- max_results=max_results,
378
- )
379
  return results
 
 
380
  except Exception as e:
 
381
  raise HTTPException(status_code=500, detail=str(e))
382
 
383
- @app.get("/news", response_model=List[BingNewsResult])
384
  async def news_search(
385
- query: str = Query(..., description="The search keywords for news."),
386
- region: Optional[str] = Query(None, description="The region for the news search (e.g., 'us-US')."),
387
- safesearch: str = Query("moderate", description="Safe search level ('on', 'moderate', 'off')."),
388
- max_results: int = Query(10, description="Maximum number of news results to return."),
389
  ):
390
  """
391
  Perform a news search on Bing.
392
  """
393
  try:
394
- results = bing.news(
395
- keywords=query,
396
- region=region,
397
- safesearch=safesearch,
398
- max_results=max_results,
399
- )
400
  return results
 
 
401
  except Exception as e:
 
402
  raise HTTPException(status_code=500, detail=str(e))
403
 
404
  if __name__ == "__main__":
405
  import uvicorn
 
406
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
  from fastapi import FastAPI, HTTPException, Query
2
+ from typing import List, Optional, Dict, Any
3
  from pydantic import BaseModel
4
  from time import sleep
5
  from curl_cffi.requests import Session
6
+ from urllib.parse import urlencode, urlparse, parse_qs
7
  import base64
 
 
 
 
8
  import json
9
+ import logging
10
 
11
+ # --- Setup Logging ---
12
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ # --- FastAPI App Initialization ---
17
  app = FastAPI(
18
  title="Snapzion Search API",
19
+ description="An improved FastAPI wrapper for a robust Bing Search library.",
20
+ version="1.1.0",
21
  )
22
 
23
+ # --- Pydantic Models for API Responses ---
 
24
 
25
  class BingSearchResult(BaseModel):
26
+ """Represents a single text search result."""
27
  url: str
28
  title: str
29
  description: str
30
  metadata: Dict[str, Any] = {}
31
 
32
  class BingImageResult(BaseModel):
33
+ """Represents a single image search result."""
34
  title: str
35
  image: str
36
  thumbnail: str
 
38
  source: str
39
 
40
  class BingNewsResult(BaseModel):
41
+ """Represents a single news search result."""
42
  title: str
43
  url: str
44
  description: str
45
  source: str = ""
46
 
 
 
 
47
 
48
+ # --- Improved BingSearch Library Code ---
49
+
50
+ class BingSearch:
51
+ """
52
+ Bing search implementation with more robust selectors, better error handling, and advanced features.
53
+ """
54
  def __init__(
55
  self,
56
  timeout: int = 10,
 
72
  timeout=self.timeout,
73
  impersonate=impersonate
74
  )
 
75
  self.session.headers.update({
76
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
77
+ "Accept-Language": "en-US,en;q=0.9",
78
  })
79
 
80
+ def _selectors(self, element: str) -> str:
81
+ """
82
+ Returns the CSS selector for a given element, updated for modern Bing layout.
83
+ """
84
  selectors = {
85
+ # IMPROVEMENT: Use 'li.b_algo' to specifically target organic search results
86
+ # and ignore ads, "People also ask", etc.
87
+ 'links': 'li.b_algo',
88
+ 'next': 'a.sb_pagN'
89
  }
90
  return selectors[element]
91
 
92
+ def _first_page(self, query: str) -> Dict[str, Any]:
93
+ """Constructs the URL for the first search page."""
94
+ params = {'q': query, 'form': 'QBLH', 'setlang': self.lang}
95
+ url = f'{self._base_url}/search?{urlencode(params)}'
96
  return {'url': url, 'data': None}
97
 
98
+ def _next_page(self, soup: 'BeautifulSoup') -> Dict[str, Any]:
99
+ """Finds the URL for the next page of results."""
100
  selector = self._selectors('next')
101
  next_page_tag = soup.select_one(selector)
102
  url = None
 
104
  url = self._base_url + next_page_tag['href']
105
  return {'url': url, 'data': None}
106
 
107
+ def _get_url(self, tag: 'Tag') -> str:
108
+ """Decodes the tracked URL from Bing's search results if possible."""
109
  url = tag.get('href', '')
110
+ # Bing often uses a tracking redirect; try to extract the real URL.
111
  try:
112
  parsed_url = urlparse(url)
113
  query_params = parse_qs(parsed_url.query)
114
  if "u" in query_params:
115
+ # The real URL is often Base64 encoded in the 'u' parameter.
116
+ encoded_url = query_params["u"][0][2:] # Strip leading 'a1'
117
+ # Add padding if necessary for correct decoding.
118
+ decoded_bytes = base64.urlsafe_b64decode(encoded_url + '===')
119
+ return decoded_bytes.decode('utf-8', errors='ignore')
 
 
120
  except Exception as e:
121
+ # IMPROVEMENT: Use logging instead of print for library code.
122
+ logger.warning(f"Could not decode Bing tracking URL '{url}': {e}")
123
+ return url
124
 
 
125
  def text(
126
  self,
127
  keywords: str,
128
+ region: Optional[str] = None,
129
  safesearch: str = "moderate",
130
  max_results: int = 10,
131
  unique: bool = True
132
  ) -> List[BingSearchResult]:
133
+ """Performs a standard web search."""
134
  if not keywords:
135
  raise ValueError("Search keywords cannot be empty")
136
 
137
  fetched_results = []
138
+ fetched_links = set() if unique else None
139
 
140
+ def fetch_page(url: str):
141
  try:
142
  resp = self.session.get(url)
143
  resp.raise_for_status()
144
  return resp.text
145
  except Exception as e:
146
+ logger.error(f"Failed to fetch Bing search page '{url}': {e}")
147
+ raise Exception(f"Bing search page request failed.")
148
 
149
  current_url = self._first_page(keywords)['url']
150
 
 
152
  html = fetch_page(current_url)
153
  soup = BeautifulSoup(html, "html.parser")
154
 
 
155
  result_blocks = soup.select(self._selectors('links'))
156
 
157
  for result in result_blocks:
 
158
  title_tag = result.find('h2')
159
  if not title_tag:
160
  continue
 
166
  url_val = self._get_url(link_tag)
167
  title = title_tag.get_text(strip=True)
168
 
169
+ # Find the description in its container
170
  desc_container = result.find('div', class_='b_caption')
171
+ description = desc_container.get_text(strip=True) if desc_container else ''
 
 
 
 
 
 
 
172
 
173
+ # IMPROVEMENT: Extract metadata like the display URL.
174
+ metadata = {}
175
+ cite_tag = result.find('cite')
176
+ if cite_tag:
177
+ metadata['display_url'] = cite_tag.get_text(strip=True)
178
 
179
  if url_val and title:
180
  if unique and url_val in fetched_links:
181
  continue
182
 
183
+ fetched_results.append(
184
+ BingSearchResult(
185
+ url=url_val,
186
+ title=title,
187
+ description=description,
188
+ metadata=metadata
189
+ )
190
+ )
191
+ if unique:
192
+ fetched_links.add(url_val)
193
 
194
  if len(fetched_results) >= max_results:
195
  break
 
197
  if len(fetched_results) >= max_results:
198
  break
199
 
 
200
  next_page_info = self._next_page(soup)
201
  current_url = next_page_info['url']
202
  if current_url:
 
204
 
205
  return fetched_results[:max_results]
206
 
207
+ def suggestions(self, query: str, region: Optional[str] = None) -> List[str]:
208
+ """Fetches auto-complete suggestions for a query."""
209
  if not query:
210
  raise ValueError("Search query cannot be empty")
211
+ # This uses a stable API endpoint, so it's less likely to break.
212
+ params = {"query": query, "mkt": region if region else "en-US"}
 
 
213
  url = f"https://api.bing.com/osjson.aspx?{urlencode(params)}"
214
  try:
215
  resp = self.session.get(url)
216
  resp.raise_for_status()
217
  data = resp.json()
218
+ return data[1] if isinstance(data, list) and len(data) > 1 else []
 
 
219
  except Exception as e:
220
+ logger.error(f"Bing suggestions failed for query '{query}': {e}")
221
+ raise Exception("Bing suggestions failed.")
 
 
222
 
223
  def images(
224
+ self, keywords: str, region: Optional[str] = None, safesearch: str = "moderate", max_results: int = 10
 
 
 
 
225
  ) -> List[BingImageResult]:
226
+ """Performs an image search."""
227
  if not keywords:
228
  raise ValueError("Search keywords cannot be empty")
229
+
 
 
 
 
 
230
  params = {
231
+ "q": keywords, "count": max_results, "setlang": self.lang,
232
+ "safeSearch": safesearch.capitalize(), "form": "IRFLTR"
 
 
233
  }
234
  if region:
235
  params["mkt"] = region
236
+
237
  url = f"{self._base_url}/images/search?{urlencode(params)}"
238
  try:
239
  resp = self.session.get(url)
240
  resp.raise_for_status()
241
  html = resp.text
242
  except Exception as e:
243
+ logger.error(f"Bing image search failed for '{keywords}': {e}")
244
+ raise Exception("Bing image search failed.")
245
+
 
246
  soup = BeautifulSoup(html, "html.parser")
247
  results = []
248
+ # This method of parsing JSON from an attribute is quite stable.
249
  for item in soup.select("a.iusc"):
250
+ if len(results) >= max_results:
251
+ break
252
  try:
253
+ m_data = json.loads(item.get("m", "{}"))
254
+ if m_data.get("murl"):
255
+ results.append(
256
+ BingImageResult(
257
+ title=m_data.get("t", ""), image=m_data.get("murl", ""),
258
+ thumbnail=m_data.get("turl", ""), url=m_data.get("purl", ""),
259
+ source=m_data.get("surl", "")
260
+ )
261
+ )
 
 
262
  except Exception:
263
  continue
264
+ return results
265
 
266
  def news(
267
+ self, keywords: str, region: Optional[str] = None, safesearch: str = "moderate", max_results: int = 10,
268
+ ) -> List[BingNewsResult]:
269
+ """Performs a news search."""
 
 
 
270
  if not keywords:
271
  raise ValueError("Search keywords cannot be empty")
272
+
273
+ params = {"q": keywords, "form": "QBNH", "safeSearch": safesearch.capitalize()}
 
 
 
 
 
 
 
 
 
274
  if region:
275
  params["mkt"] = region
276
+
277
  url = f"{self._base_url}/news/search?{urlencode(params)}"
278
  try:
279
  resp = self.session.get(url)
280
  resp.raise_for_status()
281
  except Exception as e:
282
+ logger.error(f"Bing news search failed for '{keywords}': {e}")
283
+ raise Exception("Bing news search failed.")
284
+
 
285
  soup = BeautifulSoup(resp.text, "html.parser")
286
  results = []
287
+ # Use a more resilient selector for news cards
288
+ for item in soup.select("div.news-card"):
289
+ if len(results) >= max_results:
290
+ break
291
+
292
+ title_tag = item.find("a", class_="title")
293
+ desc_tag = item.find("div", class_="snippet")
294
+ source_tag = item.find(class_="source")
295
+
296
+ if title_tag and title_tag.has_attr('href'):
297
+ results.append(
298
+ BingNewsResult(
299
+ title=title_tag.get_text(strip=True),
300
+ url=title_tag['href'],
301
+ description=desc_tag.get_text(strip=True) if desc_tag else "",
302
+ source=source_tag.get_text(strip=True) if source_tag else ""
303
+ )
304
+ )
 
 
 
 
305
  return results[:max_results]
306
 
307
 
308
+ # --- API Endpoints ---
309
+
310
  bing = BingSearch()
311
 
312
+ @app.get("/search", response_model=List[BingSearchResult], summary="Perform a web search")
313
  async def text_search(
314
+ query: str = Query(..., description="The search keywords.", min_length=1),
315
+ region: Optional[str] = Query(None, description="Region for the search (e.g., 'en-US')."),
316
+ safesearch: str = Query("moderate", description="Safe search: 'on', 'moderate', 'off'."),
317
+ max_results: int = Query(10, description="Maximum number of results.", gt=0, le=50),
318
  ):
319
  """
320
+ Perform a standard text search on Bing and get a list of results.
321
  """
322
  try:
323
+ results = bing.text(keywords=query, region=region, safesearch=safesearch, max_results=max_results)
 
 
 
 
 
324
  return results
325
+ except ValueError as e:
326
+ raise HTTPException(status_code=400, detail=str(e))
327
  except Exception as e:
328
+ logger.exception(f"Unhandled error in text_search for query: {query}")
329
  raise HTTPException(status_code=500, detail=str(e))
330
 
331
+ @app.get("/suggestions", response_model=List[str], summary="Get search suggestions")
332
  async def get_suggestions(
333
+ query: str = Query(..., description="The query to get suggestions for.", min_length=1),
334
+ region: Optional[str] = Query(None, description="Region for suggestions (e.g., 'en-US')."),
335
  ):
336
  """
337
+ Fetches auto-complete search suggestions for a given query.
338
  """
339
  try:
340
+ return bing.suggestions(query=query, region=region)
341
+ except ValueError as e:
342
+ raise HTTPException(status_code=400, detail=str(e))
343
  except Exception as e:
344
+ logger.exception(f"Unhandled error in get_suggestions for query: {query}")
345
  raise HTTPException(status_code=500, detail=str(e))
346
 
347
+ @app.get("/images", response_model=List[BingImageResult], summary="Perform an image search")
348
  async def image_search(
349
+ query: str = Query(..., description="The search keywords for images.", min_length=1),
350
+ region: Optional[str] = Query(None, description="Region for the search (e.g., 'en-US')."),
351
+ safesearch: str = Query("moderate", description="Safe search: 'on', 'moderate', 'off'."),
352
+ max_results: int = Query(10, description="Maximum number of results.", gt=0, le=50),
353
  ):
354
  """
355
  Perform an image search on Bing.
356
  """
357
  try:
358
+ results = bing.images(keywords=query, region=region, safesearch=safesearch, max_results=max_results)
 
 
 
 
 
359
  return results
360
+ except ValueError as e:
361
+ raise HTTPException(status_code=400, detail=str(e))
362
  except Exception as e:
363
+ logger.exception(f"Unhandled error in image_search for query: {query}")
364
  raise HTTPException(status_code=500, detail=str(e))
365
 
366
+ @app.get("/news", response_model=List[BingNewsResult], summary="Perform a news search")
367
  async def news_search(
368
+ query: str = Query(..., description="The search keywords for news.", min_length=1),
369
+ region: Optional[str] = Query(None, description="Region for the search (e.g., 'en-US')."),
370
+ safesearch: str = Query("moderate", description="Safe search: 'on', 'moderate', 'off'."),
371
+ max_results: int = Query(10, description="Maximum number of results.", gt=0, le=50),
372
  ):
373
  """
374
  Perform a news search on Bing.
375
  """
376
  try:
377
+ results = bing.news(keywords=query, region=region, safesearch=safesearch, max_results=max_results)
 
 
 
 
 
378
  return results
379
+ except ValueError as e:
380
+ raise HTTPException(status_code=400, detail=str(e))
381
  except Exception as e:
382
+ logger.exception(f"Unhandled error in news_search for query: {query}")
383
  raise HTTPException(status_code=500, detail=str(e))
384
 
385
  if __name__ == "__main__":
386
  import uvicorn
387
+ # To run: uvicorn your_filename:app --reload
388
  uvicorn.run(app, host="0.0.0.0", port=8000)