Husnain Rasheed commited on
Commit
e66582e
·
verified ·
1 Parent(s): db4af16

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +316 -230
main.py CHANGED
@@ -1,320 +1,406 @@
1
- import time
2
- import json
 
 
 
 
3
  import base64
4
- from typing import List, Optional, Dict, Any
5
- from urllib.parse import urlencode, urlparse, parse_qs
6
-
7
- import uvicorn
8
- from fastapi import FastAPI, HTTPException, Query, Request, Response
9
- from pydantic import BaseModel, Field
10
- from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
11
- from curl_cffi.requests import AsyncSession
12
  from bs4 import BeautifulSoup
 
13
 
 
 
 
 
 
14
 
15
- # --- Pydantic Models for API Responses ---
16
-
17
- class SearchResultMetadata(BaseModel):
18
- """
19
- Defines the structure for rich metadata associated with a search result,
20
- such as sitelinks and the display URL.
21
- """
22
- sitelinks: Optional[List[Dict[str, str]]] = Field(
23
- None, description="A list of sitelinks (title and URL) found under the main result."
24
- )
25
- displayed_url: Optional[str] = Field(
26
- None, description="The user-friendly display URL or breadcrumb shown on the search page."
27
- )
28
 
29
  class BingSearchResult(BaseModel):
30
- """Represents a single text search result from Bing."""
31
- url: str = Field(..., description="The direct URL of the search result.")
32
- title: str = Field(..., description="The title of the search result.")
33
- description: str = Field(..., description="A brief description or snippet of the search result.")
34
- metadata: SearchResultMetadata = Field(
35
- default_factory=SearchResultMetadata, description="Additional rich metadata scraped for the result."
36
- )
37
 
38
  class BingImageResult(BaseModel):
39
- """Represents a single image search result from Bing."""
40
- title: str = Field(..., description="The title or description of the image.")
41
- image_url: str = Field(..., description="The direct URL to the full-size image.")
42
- thumbnail_url: str = Field(..., description="The URL to the thumbnail of the image.")
43
- page_url: str = Field(..., description="The URL of the page where the image was found.")
44
- source: str = Field(..., description="The source or domain of the image.")
45
 
46
  class BingNewsResult(BaseModel):
47
- """Represents a single news article search result from Bing."""
48
- title: str = Field(..., description="The headline of the news article.")
49
- url: str = Field(..., description="The URL to the full news article.")
50
- description: str = Field(..., description="A snippet from the news article.")
51
- source: str = Field("", description="The publisher or source of the news article.")
52
-
53
-
54
- # --- Custom Middleware for Response Headers ---
55
-
56
- class CustomHeaderMiddleware(BaseHTTPMiddleware):
57
- """
58
- This middleware adds custom headers to every API response, including
59
- the processing time and a 'Powered-By' header.
60
- """
61
- async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
62
- start_time = time.time()
63
- response = await call_next(request)
64
- process_time = time.time() - start_time
65
- response.headers["X-Response-Time"] = f"{process_time:.4f}s"
66
- response.headers["X-Powered-By"] = "NiansuhAI"
67
- return response
68
-
69
-
70
- # --- Bing Search Service ---
71
 
72
  class BingSearch:
73
- """
74
- An asynchronous service class for scraping search results from Bing.
75
- It handles text, image, news, and suggestion searches using curl_cffi
76
- for efficient, non-blocking HTTP requests.
77
- """
78
 
79
  def __init__(
80
  self,
81
- timeout: int = 15,
82
  proxies: Optional[Dict[str, str]] = None,
 
83
  lang: str = "en-US",
 
84
  impersonate: str = "chrome110"
85
  ):
86
  self.timeout = timeout
87
- self.proxies = proxies or {}
 
88
  self.lang = lang
 
89
  self._base_url = "https://www.bing.com"
90
- self.session = AsyncSession(
91
  proxies=self.proxies,
 
92
  timeout=self.timeout,
93
  impersonate=impersonate
94
  )
95
- # Use a realistic User-Agent to mimic a real browser
96
  self.session.headers.update({
97
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
98
- "Accept-Language": "en-US,en;q=0.9",
99
  })
100
 
101
- async def _fetch_html(self, url: str) -> str:
102
- """Asynchronously fetches HTML content from a given URL."""
103
- try:
104
- resp = await self.session.get(url)
105
- resp.raise_for_status()
106
- return resp.text
107
- except Exception as e:
108
- # Raise an HTTPException that FastAPI can handle gracefully
109
- raise HTTPException(status_code=502, detail=f"Failed to fetch Bing content: {e}")
110
-
111
- def _parse_url(self, url: Optional[str]) -> str:
112
- """Decodes Bing's redirect URLs to find the actual destination URL."""
113
- if not url:
114
- return ""
 
 
 
 
 
 
 
 
 
115
  try:
116
  parsed_url = urlparse(url)
117
  query_params = parse_qs(parsed_url.query)
118
  if "u" in query_params:
119
- encoded_url = query_params["u"][0].split("&")[0]
120
- # Decode the Base64-encoded URL
121
- decoded_bytes = base64.urlsafe_b64decode(encoded_url + '===')
122
- return decoded_bytes.decode('utf-8', errors='ignore')
123
- except (KeyError, IndexError, Exception):
124
- # Fallback to the original URL if parsing fails
125
- return url
126
- return url
 
 
127
 
128
- async def text(
129
- self, keywords: str, region: Optional[str], max_results: int
 
 
 
 
 
 
130
  ) -> List[BingSearchResult]:
131
- """Performs a text search and scrapes the results page."""
132
  if not keywords:
133
- raise ValueError("Search keywords cannot be empty.")
134
-
135
- params = {"q": keywords, "form": "QBLH"}
136
- url = f'{self._base_url}/search?{urlencode(params)}'
137
- if region:
138
- url += f"&setmkt={region}"
139
 
140
- html = await self._fetch_html(url)
141
- soup = BeautifulSoup(html, "html.parser")
142
  fetched_results = []
 
143
 
144
- for result in soup.select('li.b_algo'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  if len(fetched_results) >= max_results:
146
  break
147
 
148
- title_tag = result.find('h2')
149
- link_tag = title_tag.find('a') if title_tag else None
150
- if not link_tag or not link_tag.has_attr('href'):
151
- continue
152
-
153
- url_val = self._parse_url(link_tag.get('href'))
154
- title = link_tag.get_text(strip=True)
155
- description = result.find('p').get_text(strip=True) if result.find('p') else ""
156
 
157
- sitelinks = [
158
- {"title": a.get_text(strip=True), "url": self._parse_url(a.get('href'))}
159
- for a in result.select('ul.b_vlist li a')
160
- ]
161
- displayed_url = result.cite.get_text(strip=True) if result.cite else None
162
- metadata = SearchResultMetadata(sitelinks=sitelinks or None, displayed_url=displayed_url)
163
 
164
- if url_val and title:
165
- fetched_results.append(
166
- BingSearchResult(url=url_val, title=title, description=description, metadata=metadata)
167
- )
168
- return fetched_results
169
 
170
- async def suggestions(self, query: str, region: Optional[str]) -> List[str]:
171
- """Fetches auto-complete suggestions for a given query."""
172
  if not query:
173
- raise ValueError("Search query cannot be empty.")
174
- params = {"query": query, "mkt": region or "en-US"}
 
 
 
175
  url = f"https://api.bing.com/osjson.aspx?{urlencode(params)}"
176
  try:
177
- resp = await self.session.get(url)
178
  resp.raise_for_status()
179
  data = resp.json()
180
- return data[1] if isinstance(data, list) and len(data) > 1 else []
 
 
181
  except Exception as e:
182
- raise HTTPException(status_code=502, detail=f"Failed to fetch suggestions: {e}")
 
 
 
183
 
184
- async def images(self, keywords: str, max_results: int) -> List[BingImageResult]:
185
- """Performs an image search and scrapes the results."""
 
 
 
 
 
186
  if not keywords:
187
- raise ValueError("Search keywords cannot be empty.")
188
-
189
- params = {"q": keywords, "count": max_results}
 
 
 
 
 
 
 
 
 
 
 
 
190
  url = f"{self._base_url}/images/search?{urlencode(params)}"
191
- html = await self._fetch_html(url)
 
 
 
 
 
 
 
 
192
  soup = BeautifulSoup(html, "html.parser")
193
  results = []
194
-
195
  for item in soup.select("a.iusc"):
196
- if len(results) >= max_results:
197
- break
198
  try:
199
- meta = json.loads(item["m"])
200
- if "murl" in meta:
201
- results.append(
202
- BingImageResult(
203
- title=meta.get("t", ""),
204
- image_url=meta["murl"],
205
- thumbnail_url=meta.get("turl", ""),
206
- page_url=meta.get("purl", ""),
207
- source=urlparse(meta.get("purl", "")).netloc
208
- )
209
- )
210
- except (json.JSONDecodeError, KeyError):
211
  continue
212
- return results
213
 
214
- async def news(self, keywords: str, region: Optional[str], max_results: int) -> List[BingNewsResult]:
215
- """Performs a news search and scrapes the results."""
 
 
 
 
 
216
  if not keywords:
217
- raise ValueError("Search keywords cannot be empty.")
218
-
219
- params = {"q": keywords, "form": "QBNH"}
 
 
 
 
 
 
 
 
 
220
  if region:
221
  params["mkt"] = region
222
-
223
  url = f"{self._base_url}/news/search?{urlencode(params)}"
224
- html = await self._fetch_html(url)
225
- soup = BeautifulSoup(html, "html.parser")
 
 
 
 
 
 
 
226
  results = []
227
-
228
- for item in soup.select("div.news-card"):
229
- if len(results) >= max_results:
230
- break
231
- a_tag = item.find("a", class_="title")
232
- snippet_tag = item.find("div", class_="snippet")
233
  source_tag = item.find("div", class_="source")
234
-
235
- if a_tag and a_tag.has_attr('href'):
236
- results.append(
237
- BingNewsResult(
238
- title=a_tag.get_text(strip=True),
239
- url=a_tag['href'],
240
- description=snippet_tag.get_text(strip=True) if snippet_tag else "",
241
- source=source_tag.get_text(strip=True).split('·')[0].strip() if source_tag else "",
242
- )
243
- )
244
- return results
245
-
246
-
247
- # --- FastAPI Application Setup ---
248
-
249
- app = FastAPI(
250
- title="Bing Search API",
251
- description="An advanced, asynchronous FastAPI wrapper to scrape Bing search results, powered by NiansuhAI.",
252
- version="3.1.0",
253
- )
254
-
255
- app.add_middleware(CustomHeaderMiddleware)
256
- bing_search_service = BingSearch()
257
-
258
-
259
- # --- API Endpoints ---
260
-
261
- @app.get("/search", response_model=List[BingSearchResult], summary="Perform a text search with rich metadata")
262
  async def text_search(
263
  query: str = Query(..., description="The search keywords."),
264
- region: Optional[str] = Query(None, description="The market/region for the search (e.g., 'en-US')."),
265
- max_results: int = Query(10, ge=1, le=30, description="Maximum number of results to return."),
 
266
  ):
267
  """
268
- Performs a text search on Bing and returns a list of results,
269
- each enriched with metadata like sitelinks.
270
  """
271
  try:
272
- return await bing_search_service.text(keywords=query, region=region, max_results=max_results)
273
- except ValueError as e:
274
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
275
  except Exception as e:
276
- # Catch-all for any other unexpected errors
277
- raise HTTPException(status_code=500, detail=f"An unexpected internal error occurred: {e}")
278
 
279
- @app.get("/suggestions", response_model=List[str], summary="Get real-time search suggestions")
280
  async def get_suggestions(
281
- query: str = Query(..., description="The partial search query for which to fetch suggestions."),
282
  region: Optional[str] = Query(None, description="The region for the suggestions (e.g., 'en-US')."),
283
  ):
284
- """Fetches real-time search suggestions from Bing's autocomplete service."""
 
 
285
  try:
286
- return await bing_search_service.suggestions(query=query, region=region)
287
- except ValueError as e:
288
- raise HTTPException(status_code=400, detail=str(e))
 
289
 
290
- @app.get("/images", response_model=List[BingImageResult], summary="Perform an image search")
291
  async def image_search(
292
  query: str = Query(..., description="The search keywords for images."),
293
- max_results: int = Query(20, ge=1, le=100, description="Maximum number of image results to return."),
 
 
294
  ):
295
- """Performs an image search on Bing and returns a list of image results."""
 
 
296
  try:
297
- return await bing_search_service.images(keywords=query, max_results=max_results)
298
- except ValueError as e:
299
- raise HTTPException(status_code=400, detail=str(e))
 
 
 
 
 
 
300
 
301
- @app.get("/news", response_model=List[BingNewsResult], summary="Perform a news search")
302
  async def news_search(
303
- query: str = Query(..., description="The search keywords for news articles."),
304
- region: Optional[str] = Query(None, description="The region for the news search (e.g., 'en-US')."),
305
- max_results: int = Query(15, ge=1, le=50, description="Maximum number of news results to return."),
 
306
  ):
307
- """Performs a news search on Bing and returns a list of recent articles."""
 
 
308
  try:
309
- return await bing_search_service.news(keywords=query, region=region, max_results=max_results)
310
- except ValueError as e:
311
- raise HTTPException(status_code=400, detail=str(e))
312
-
313
- @app.get("/", include_in_schema=False)
314
- async def root():
315
- return {"message": "Bing Search API is running. Visit /docs for documentation."}
316
-
 
317
 
318
  if __name__ == "__main__":
319
- # Standard entry point to run the FastAPI application using Uvicorn
320
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
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="Bing Search API",
16
+ description="A FastAPI wrapper for the BingSearch 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
33
+ url: str
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,
49
  proxies: Optional[Dict[str, str]] = None,
50
+ verify: bool = True,
51
  lang: str = "en-US",
52
+ sleep_interval: float = 0.0,
53
  impersonate: str = "chrome110"
54
  ):
55
  self.timeout = timeout
56
+ self.proxies = proxies if proxies else {}
57
+ self.verify = verify
58
  self.lang = lang
59
+ self.sleep_interval = sleep_interval
60
  self._base_url = "https://www.bing.com"
61
+ self.session = Session(
62
  proxies=self.proxies,
63
+ verify=self.verify,
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
88
+ if next_page_tag and next_page_tag.get('href'):
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
+
135
+ while current_url and len(fetched_results) < max_results:
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
147
+
148
+ link_tag = title_tag.find('a')
149
+ if not link_tag or not link_tag.has_attr('href'):
150
+ continue
151
+
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
181
+
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:
189
+ sleep(self.sleep_interval)
 
 
 
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)