webscout 4.3__py3-none-any.whl → 4.5__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.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

webscout/cli.py CHANGED
@@ -20,8 +20,10 @@ from rich.table import Table
20
20
  from rich.style import Style
21
21
  from rich.text import Text
22
22
  from rich.align import Align
23
- from rich.progress import track
23
+ from rich.progress import track, Progress
24
24
  from rich.prompt import Prompt, Confirm
25
+ from rich.columns import Columns
26
+ from pyfiglet import figlet_format
25
27
 
26
28
  logger = logging.getLogger(__name__)
27
29
 
@@ -45,13 +47,12 @@ COLORS = {
45
47
  }
46
48
 
47
49
  def _print_data(data):
48
- """Prints data using rich panels and markdown, asynchronously."""
50
+ """Prints data using rich panels and markdown."""
49
51
  console = Console()
50
52
  if data:
51
53
  for i, e in enumerate(data, start=1):
52
- # Create a table for each result
53
- table = Table(title=f"{i}.", show_lines=True)
54
- table.add_column("Key", style="cyan", no_wrap=True)
54
+ table = Table(show_header=False, show_lines=True, expand=True, box=None) # Removed duplicate title
55
+ table.add_column("Key", style="cyan", no_wrap=True, width=15)
55
56
  table.add_column("Value", style="white")
56
57
 
57
58
  for j, (k, v) in enumerate(e.items(), start=1):
@@ -59,21 +60,22 @@ def _print_data(data):
59
60
  width = 300 if k in ("content", "href", "image", "source", "thumbnail", "url") else 78
60
61
  k = "language" if k == "detected_language" else k
61
62
  text = click.wrap_text(
62
- f"{v}", width=width, initial_indent="", subsequent_indent=" " * 12, preserve_paragraphs=True
63
- )
63
+ f"{v}", width=width, initial_indent="", subsequent_indent=" " * 18, preserve_paragraphs=True
64
+ ).replace("\n", "\n\n")
64
65
  else:
65
66
  text = v
66
67
  table.add_row(k, text)
67
68
 
68
- # Wrap the table in a panel with a title
69
- console.print(Panel(Align(table, align="left"), title=f"Result {i}", expand=False))
70
- console.print("\n")
69
+ # Only the Panel has the title now
70
+ console.print(Panel(table, title=f"Result {i}", expand=False, style="green on black"))
71
+ console.print("\n")
72
+
71
73
 
72
74
  def _sanitize_keywords(keywords):
73
75
  """Sanitizes keywords for file names and paths. Removes invalid characters like ':'. """
74
76
  keywords = (
75
77
  keywords.replace("filetype", "")
76
- .replace(":", "") # Remove colons
78
+ .replace(":", "")
77
79
  .replace('"', "'")
78
80
  .replace("site", "")
79
81
  .replace(" ", "_")
@@ -86,8 +88,8 @@ def _sanitize_keywords(keywords):
86
88
  @click.group(chain=True)
87
89
  def cli():
88
90
  """webscout CLI tool - Search the web with a rich UI."""
89
- pass
90
-
91
+ console = Console()
92
+ console.print(f"[bold blue]{figlet_format('Webscout')}[/]\n", justify="center")
91
93
 
92
94
  def safe_entry_point():
93
95
  try:
@@ -100,7 +102,7 @@ def safe_entry_point():
100
102
  def version():
101
103
  """Shows the current version of webscout."""
102
104
  console = Console()
103
- console.print(Panel(Text(f"webscout v{__version__}", style="cyan"), title="Version"))
105
+ console.print(Panel(Text(f"webscout v{__version__}", style="cyan"), title="Version", expand=False))
104
106
 
105
107
 
106
108
  @cli.command()
@@ -111,16 +113,15 @@ def chat(proxy):
111
113
  client = WEBS(proxy=proxy)
112
114
 
113
115
  console = Console()
114
- console.print(Panel(Text("Available AI Models:", style="cyan"), title="DuckDuckGo AI Chat"))
115
- for idx, model in enumerate(models, start=1):
116
- console.print(f"{idx}. {model}")
117
- chosen_model_idx = Prompt.ask("Choose a model by entering its number [1]", choices=[str(i) for i in range(1, len(models) + 1)], default="1")
116
+ console.print(Panel(Text("Available AI Models:", style="cyan"), title="DuckDuckGo AI Chat", expand=False))
117
+ console.print(Columns([Panel(Text(model, justify="center"), expand=True) for model in models]))
118
+ chosen_model_idx = Prompt.ask("[bold cyan]Choose a model by entering its number[/] [1]", choices=[str(i) for i in range(1, len(models) + 1)], default="1")
118
119
  chosen_model_idx = int(chosen_model_idx) - 1
119
120
  model = models[chosen_model_idx]
120
- console.print(f"Using model: {model}")
121
+ console.print(f"[bold green]Using model:[/] {model}")
121
122
 
122
123
  while True:
123
- user_input = input(f"{'-'*78}\nYou: ")
124
+ user_input = Prompt.ask(f"{'-'*78}\n[bold blue]You:[/]")
124
125
  if not user_input.strip():
125
126
  break
126
127
 
@@ -129,7 +130,7 @@ def chat(proxy):
129
130
  console.print(Panel(Text(f"AI: {text}", style="green"), title="AI Response"))
130
131
 
131
132
  if "exit" in user_input.lower() or "quit" in user_input.lower():
132
- console.print(Panel(Text("Exiting chat session.", style="cyan"), title="Goodbye"))
133
+ console.print(Panel(Text("Exiting chat session.", style="cyan"), title="Goodbye", expand=False))
133
134
  break
134
135
 
135
136
 
webscout/utils.py CHANGED
@@ -4,23 +4,33 @@ from html import unescape
4
4
  from math import atan2, cos, radians, sin, sqrt
5
5
  from typing import Any, Dict, List, Union
6
6
  from urllib.parse import unquote
7
- import orjson
8
7
 
9
8
  from .exceptions import WebscoutE
10
9
 
10
+ try:
11
+ HAS_ORJSON = True
12
+ import orjson
13
+ except ImportError:
14
+ HAS_ORJSON = False
15
+ import json
16
+
11
17
  REGEX_STRIP_TAGS = re.compile("<.*?>")
12
18
 
13
19
 
14
20
  def json_dumps(obj: Any) -> str:
15
21
  try:
16
- return orjson.dumps(obj).decode("utf-8")
22
+ return (
23
+ orjson.dumps(obj, option=orjson.OPT_INDENT_2).decode()
24
+ if HAS_ORJSON
25
+ else json.dumps(obj, ensure_ascii=False, indent=2)
26
+ )
17
27
  except Exception as ex:
18
28
  raise WebscoutE(f"{type(ex).__name__}: {ex}") from ex
19
29
 
20
30
 
21
31
  def json_loads(obj: Union[str, bytes]) -> Any:
22
32
  try:
23
- return orjson.loads(obj)
33
+ return orjson.loads(obj) if HAS_ORJSON else json.loads(obj)
24
34
  except Exception as ex:
25
35
  raise WebscoutE(f"{type(ex).__name__}: {ex}") from ex
26
36
 
webscout/voice.py CHANGED
@@ -24,4 +24,11 @@ def play_audio(message: str, voice: str = "Brian") -> typing.Union[str, typing.N
24
24
  result = requests.get(url=url, headers=headers)
25
25
  return result.content
26
26
  except:
27
- return None
27
+ return None
28
+
29
+ if __name__ == "__main__":
30
+ # Example usage of the play_audio function
31
+ message = "Hello, world!"
32
+ voice = "Brian"
33
+ audio_result = play_audio(message, voice)
34
+ print(audio_result)
webscout/webai.py CHANGED
@@ -37,11 +37,13 @@ from dotenv import load_dotenv
37
37
  import g4f
38
38
  import webscout
39
39
  import webscout.AIutel
40
+ from pyfiglet import figlet_format
40
41
 
41
42
  init_colorama(autoreset=True)
42
43
 
43
44
  load_dotenv() # loads .env variables
44
45
 
46
+ console = Console()
45
47
  logging.basicConfig(
46
48
  format="%(asctime)s - %(levelname)s : %(message)s ",
47
49
  datefmt="%H:%M:%S",
@@ -763,7 +765,20 @@ class Main(cmd.Cmd):
763
765
  model=getOr(model, "Phind Model"),
764
766
  quiet=quiet,
765
767
  )
768
+ elif provider == "andi":
769
+ from webscout import AndiSearch
766
770
 
771
+ self.bot = AndiSearch(
772
+ is_conversation=disable_conversation,
773
+ max_tokens=max_tokens,
774
+ timeout=timeout,
775
+ intro=intro,
776
+ filepath=filepath,
777
+ update_file=update_file,
778
+ proxies=proxies,
779
+ history_offset=history_offset,
780
+ act=awesome_prompt,
781
+ )
767
782
  elif provider == "blackboxai":
768
783
 
769
784
  from webscout import BLACKBOXAI
@@ -2606,6 +2621,7 @@ def make_commands():
2606
2621
  # @this.handle_exception
2607
2622
  def main(*args):
2608
2623
  """Fireup console programmically"""
2624
+ console.print(f"[bold green]{figlet_format('WebAI')}[/]\n", justify="center")
2609
2625
  sys.argv += list(args)
2610
2626
  args = sys.argv
2611
2627
  if len(args) == 1:
@@ -5,11 +5,14 @@ from datetime import datetime, timezone
5
5
  from decimal import Decimal
6
6
  from functools import cached_property
7
7
  from itertools import cycle, islice
8
+ from random import choice
8
9
  from threading import Event
9
10
  from types import TracebackType
10
11
  from typing import Dict, List, Optional, Tuple, Type, Union, cast
11
12
 
12
- import pyreqwest_impersonate as pri # type: ignore
13
+ import pyreqwest_impersonate as pri
14
+
15
+ from .utils import _calculate_distance, _extract_vqd, _normalize, _normalize_url, _text_extract_json, json_loads # type: ignore
13
16
 
14
17
  try:
15
18
  from lxml.etree import _Element
@@ -20,15 +23,8 @@ try:
20
23
  except ImportError:
21
24
  LXML_AVAILABLE = False
22
25
 
23
- from .exceptions import WebscoutE, RatelimitE, TimeoutE
24
- from .utils import (
25
- _calculate_distance,
26
- _extract_vqd,
27
- _normalize,
28
- _normalize_url,
29
- _text_extract_json,
30
- json_loads,
31
- )
26
+ from .exceptions import *
27
+
32
28
 
33
29
  logger = logging.getLogger("webscout.WEBS")
34
30
 
@@ -37,6 +33,15 @@ class WEBS:
37
33
  """webscout class to get search results from duckduckgo.com."""
38
34
 
39
35
  _executor: ThreadPoolExecutor = ThreadPoolExecutor()
36
+ _impersonates = (
37
+ "chrome_99", "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", "chrome_108",
38
+ "chrome_107", "chrome_109", "chrome_114", "chrome_116", "chrome_117", "chrome_118", "chrome_119",
39
+ "chrome_120", #"chrome_123", "chrome_124", "chrome_126",
40
+ "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_15.3", "safari_15.5",
41
+ "safari_15.6.1", "safari_16", "safari_16.5", "safari_17.2.1", "safari_17.4.1", "safari_17.5",
42
+ #"okhttp_3.9", "okhttp_3.11", "okhttp_3.13", "okhttp_3.14", "okhttp_4.9", "okhttp_4.10", "okhttp_5",
43
+ "edge_99", "edge_101", "edge_122",
44
+ ) # fmt: skip
40
45
 
41
46
  def __init__(
42
47
  self,
@@ -66,7 +71,7 @@ class WEBS:
66
71
  timeout=timeout,
67
72
  cookie_store=True,
68
73
  referer=True,
69
- impersonate="chrome_124",
74
+ impersonate=choice(self._impersonates),
70
75
  follow_redirects=False,
71
76
  verify=False,
72
77
  )
@@ -120,13 +125,14 @@ class WEBS:
120
125
  resp_content = self._get_url("POST", "https://duckduckgo.com", data={"q": keywords})
121
126
  return _extract_vqd(resp_content, keywords)
122
127
 
123
- def chat(self, keywords: str, model: str = "gpt-3.5") -> str:
128
+ def chat(self, keywords: str, model: str = "gpt-3.5", timeout: int = 20) -> str:
124
129
  """Initiates a chat session with DuckDuckGo AI.
125
130
 
126
131
  Args:
127
132
  keywords (str): The initial message or question to send to the AI.
128
133
  model (str): The model to use: "gpt-3.5", "claude-3-haiku", "llama-3-70b", "mixtral-8x7b".
129
134
  Defaults to "gpt-3.5".
135
+ timeout (int): Timeout value for the HTTP client. Defaults to 20.
130
136
 
131
137
  Returns:
132
138
  str: The response from the AI.
@@ -136,6 +142,7 @@ class WEBS:
136
142
  "gpt-3.5": "gpt-3.5-turbo-0125",
137
143
  "llama-3-70b": "meta-llama/Llama-3-70b-chat-hf",
138
144
  "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1",
145
+ "gpt-4o-mini": "gpt-4o-mini",
139
146
  }
140
147
  # vqd
141
148
  if not self._chat_vqd:
@@ -149,18 +156,16 @@ class WEBS:
149
156
  "messages": self._chat_messages,
150
157
  }
151
158
  resp = self.client.post(
152
- "https://duckduckgo.com/duckchat/v1/chat", headers={"x-vqd-4": self._chat_vqd}, json=json_data
159
+ "https://duckduckgo.com/duckchat/v1/chat",
160
+ headers={"x-vqd-4": self._chat_vqd},
161
+ json=json_data,
162
+ timeout=timeout,
153
163
  )
154
164
  self._chat_vqd = resp.headers.get("x-vqd-4", "")
155
165
 
156
- messages = []
157
- for line in resp.text.replace("data: ", "").replace("[DONE]", "").split("\n\n"):
158
- x = line.strip()
159
- if x:
160
- j = json_loads(x)
161
- message = j.get("message", "")
162
- messages.append(message)
163
- result = "".join(messages)
166
+ data = ",".join(x for line in resp.text.rstrip("[DONE]\n").split("data:") if (x := line.strip()))
167
+ result = "".join(x.get("message", "") for x in json_loads("[" + data + "]"))
168
+
164
169
  self._chat_messages.append({"role": "assistant", "content": result})
165
170
  return result
166
171
 
@@ -347,7 +352,7 @@ class WEBS:
347
352
  for e in elements:
348
353
  if isinstance(e, _Element):
349
354
  hrefxpath = e.xpath("./a/@href")
350
- href = str(hrefxpath[0]) if isinstance(hrefxpath, List) else None
355
+ href = str(hrefxpath[0]) if hrefxpath and isinstance(hrefxpath, List) else None
351
356
  if (
352
357
  href
353
358
  and href not in cache
@@ -357,9 +362,9 @@ class WEBS:
357
362
  ):
358
363
  cache.add(href)
359
364
  titlexpath = e.xpath("./h2/a/text()")
360
- title = str(titlexpath[0]) if isinstance(titlexpath, List) else ""
365
+ title = str(titlexpath[0]) if titlexpath and isinstance(titlexpath, List) else ""
361
366
  bodyxpath = e.xpath("./a//text()")
362
- body = "".join(str(x) for x in bodyxpath) if isinstance(bodyxpath, List) else ""
367
+ body = "".join(str(x) for x in bodyxpath) if bodyxpath and isinstance(bodyxpath, List) else ""
363
368
  result = {
364
369
  "title": _normalize(title),
365
370
  "href": _normalize_url(href),
@@ -449,10 +454,14 @@ class WEBS:
449
454
  else:
450
455
  cache.add(href)
451
456
  titlexpath = e.xpath(".//a//text()")
452
- title = str(titlexpath[0]) if isinstance(titlexpath, List) else ""
457
+ title = str(titlexpath[0]) if titlexpath and isinstance(titlexpath, List) else ""
453
458
  elif i == 2:
454
459
  bodyxpath = e.xpath(".//td[@class='result-snippet']//text()")
455
- body = "".join(str(x) for x in bodyxpath) if isinstance(bodyxpath, List) else ""
460
+ body = (
461
+ "".join(str(x) for x in bodyxpath).strip()
462
+ if bodyxpath and isinstance(bodyxpath, List)
463
+ else ""
464
+ )
456
465
  if href:
457
466
  result = {
458
467
  "title": _normalize(title),