Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-duckduckgo-search for openSUSE:Factory checked in at 2025-04-30 19:05:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-duckduckgo-search (Old) and /work/SRC/openSUSE:Factory/.python-duckduckgo-search.new.30101 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-duckduckgo-search" Wed Apr 30 19:05:48 2025 rev:4 rq:1273675 version:8.0.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-duckduckgo-search/python-duckduckgo-search.changes 2025-01-02 19:24:22.899797131 +0100 +++ /work/SRC/openSUSE:Factory/.python-duckduckgo-search.new.30101/python-duckduckgo-search.changes 2025-04-30 19:06:01.930453060 +0200 @@ -1,0 +2,71 @@ +Wed Apr 30 11:05:28 UTC 2025 - Felix Stegmeier <felix.stegme...@suse.com> + +- Update to 8.0.1: + * refactor: remove dead code + * refactor: remove dead code + * refactor: bump to primp=0.15.0 + +- Update to 8.0.0: + * Chat moved to duckai package. + * feat(chat): remove chat + * fix(typing): fix typing in cli and tests + +- Update to 7.5.5: + * fix(chat): add _chat_xfe + +- Update to 7.5.4: + * fix(chat): x-vqd-hash-1 = "" + +- Update to 7.5.3: + * DDGS.chat: bugfix by @deedy5 in #294 + +- Update to 7.5.2: + * fix(temp): don't set Client.headers + * Full Changelog: v7.5.1...v7.5.2 + +- Update to 7.5.1: + * Bugfix DDGS.text() payload by @deedy5 in #291 + +- Update to 7.5.0: + * chore: bump primp to v0.14.0 + * tests: sleep 2 seconds between tests + * feat(chat): add mistral-small-3 + * feat(chat): stream response + * feat(cli chat): stream response + +- Update to 7.4.5: + * Chat: bugfix ConversationLimitException by @deedy5 in #288 + +- Update to 7.4.4: + * DDGS.chat: add mistral-small-3 by @deedy5 in #285 + * fix(patch): patch only while sending request + +- Update to 7.4.3: + * feat: patch httpcore + * feat: improve ssl_context + +- Update to 7.4.2: + * Use httpx for requests by @deedy5 in #282 + * Cli(chat): stream answer, add DDGS.chat_yield (response message generator) by @deedy5 in #283 + +- Update to 7.3.2: + * DDGS.chat: add llama-3.3-70b model by @deedy5 in #281 + +- Update to 7.3.1: + * DDGS.chat: add o3-mini model by @deedy5 in #280 + +- Update to 7.3.0: + * clarify exceptions in README by @vpoulailleau in #275 + * Drop Support for Python 3.8 by @deedy5 in #276 + * Bump primp to v0.11.0 + * Add _impersonates_os + +- Update to 7.2.1: + * Bump primp to v0.10.0 + * DDGS._impersonates: add "firefox_128" + +- Update to 7.2.1: + * Bump primp to v0.10.0 + * DDGS._impersonates: add "firefox_128" + +------------------------------------------------------------------- Old: ---- duckduckgo_search-7.1.1.tar.gz New: ---- duckduckgo_search-8.0.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-duckduckgo-search.spec ++++++ --- /var/tmp/diff_new_pack.mRj9wI/_old 2025-04-30 19:06:02.502476912 +0200 +++ /var/tmp/diff_new_pack.mRj9wI/_new 2025-04-30 19:06:02.502476912 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-duckduckgo-search -Version: 7.1.1 +Version: 8.0.1 Release: 0 Summary: Search using the DuckDuckGo.com search engine License: MIT @@ -31,17 +31,17 @@ BuildRequires: %{python_module wheel} BuildRequires: python-rpm-macros # SECTION test requirements -BuildRequires: %{python_module click >= 8.1.7} -BuildRequires: %{python_module primp >= 0.6.3} +BuildRequires: %{python_module click >= 8.1.8} +BuildRequires: %{python_module primp >= 0.15.0} # /SECTION BuildRequires: fdupes -Requires: python-click >= 8.1.7 -Requires: python-primp >= 0.6.3 -Suggests: python-lxml >= 5.2.2 -Suggests: python-mypy >= 1.11.1 -Suggests: python-pytest >= 8.3.1 -Suggests: python-pytest-asyncio >= 0.23.8 -Suggests: python-ruff >= 0.6.1 +Requires: python-click >= 8.1.8 +Requires: python-lxml >= 5.3.0 +Requires: python-primp >= 0.15.0 +Suggests: python-mypy >= 1.14.1 +Suggests: python-pytest >= 8.3.4 +Suggests: python-pytest-dependency >= 0.6.0 +Suggests: python-ruff >= 0.9.2 Requires(post): update-alternatives Requires(postun): update-alternatives BuildArch: noarch ++++++ duckduckgo_search-7.1.1.tar.gz -> duckduckgo_search-8.0.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/PKG-INFO new/duckduckgo_search-8.0.1/PKG-INFO --- old/duckduckgo_search-7.1.1/PKG-INFO 2024-12-27 00:22:58.106326300 +0100 +++ new/duckduckgo_search-8.0.1/PKG-INFO 2025-04-17 14:37:28.462133000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: duckduckgo_search -Version: 7.1.1 +Version: 8.0.1 Summary: Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine. Author: deedy5 License: MIT License @@ -11,7 +11,6 @@ Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 @@ -20,22 +19,25 @@ Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search Classifier: Topic :: Software Development :: Libraries :: Python Modules -Requires-Python: >=3.8 +Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE.md -Requires-Dist: click>=8.1.7 -Requires-Dist: primp>=0.9.2 +Requires-Dist: click>=8.1.8 +Requires-Dist: primp>=0.15.0 Requires-Dist: lxml>=5.3.0 Provides-Extra: dev -Requires-Dist: mypy>=1.13.0; extra == "dev" +Requires-Dist: mypy>=1.14.1; extra == "dev" Requires-Dist: pytest>=8.3.4; extra == "dev" Requires-Dist: pytest-dependency>=0.6.0; extra == "dev" -Requires-Dist: ruff>=0.8.3; extra == "dev" +Requires-Dist: ruff>=0.9.2; extra == "dev" +Dynamic: license-file - [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) + [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) # Duckduckgo_search<a name="TOP"></a> -AI chat and search for text, news, images and videos using the DuckDuckGo.com search engine. +Search for text, news, images and videos using the DuckDuckGo.com search engine. + +:bangbang: AI chat moved to [duckai](https://pypi.org/project/duckai) package ## Table of Contents * [Install](#install) @@ -45,11 +47,10 @@ * [DDGS class](#ddgs-class) * [Proxy](#proxy) * [Exceptions](#exceptions) -* [1. chat() - AI chat](#1-chat---ai-chat) -* [2. text() - text search](#2-text---text-search-by-duckduckgocom) -* [3. images() - image search](#3-images---image-search-by-duckduckgocom) -* [4. videos() - video search](#4-videos---video-search-by-duckduckgocom) -* [5. news() - news search](#5-news---news-search-by-duckduckgocom) +* [1. text() - text search](#2-text---text-search-by-duckduckgocom) +* [2. images() - image search](#3-images---image-search-by-duckduckgocom) +* [3. videos() - video search](#4-videos---video-search-by-duckduckgocom) +* [4. news() - news search](#5-news---news-search-by-duckduckgocom) * [Disclaimer](#disclaimer) ## Install @@ -64,8 +65,6 @@ ``` CLI examples: ```python3 -# AI chat -ddgs chat # text search ddgs text -k "Assyrian siege of Jerusalem" # find and download pdf files via proxy @@ -227,38 +226,24 @@ ## Exceptions +```python +from duckduckgo_search.exceptions import ( + ConversationLimitException, + DuckDuckGoSearchException, + RatelimitException, + TimeoutException, +) +``` + Exceptions: - `DuckDuckGoSearchException`: Base exception for duckduckgo_search errors. - `RatelimitException`: Inherits from DuckDuckGoSearchException, raised for exceeding API request rate limits. - `TimeoutException`: Inherits from DuckDuckGoSearchException, raised for API request timeouts. - - -[Go To TOP](#TOP) - -## 1. chat() - AI chat - -```python -def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str: - """Initiates a chat session with DuckDuckGo AI. - - Args: - keywords (str): The initial message or question to send to the AI. - model (str): The model to use: "gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b". - Defaults to "gpt-4o-mini". - timeout (int): Timeout value for the HTTP client. Defaults to 30. - - Returns: - str: The response from the AI. - """ -``` -***Example*** -```python -results = DDGS().chat("summarize Daniel Defoe's The Consolidator", model='claude-3-haiku') -``` +- `ConversationLimitException`: Inherits from DuckDuckGoSearchException, raised for conversation limit during API requests to AI endpoint. [Go To TOP](#TOP) -## 2. text() - text search by duckduckgo.com +## 1. text() - text search by duckduckgo.com ```python def text( @@ -276,12 +261,10 @@ region: wt-wt, us-en, uk-en, ru-ru, etc. Defaults to "wt-wt". safesearch: on, moderate, off. Defaults to "moderate". timelimit: d, w, m, y. Defaults to None. - backend: auto, api, html, lite. Defaults to auto. + backend: auto, html, lite. Defaults to auto. auto - try all backends in random order, - api - collect data from https://duckduckgo.com, html - collect data from https://html.duckduckgo.com, - lite - collect data from https://lite.duckduckgo.com, - ecosia - collect data from https://www.ecosia.com. + lite - collect data from https://lite.duckduckgo.com. max_results: max number of results. If None, returns results only from the first response. Defaults to None. Returns: @@ -305,7 +288,7 @@ [Go To TOP](#TOP) -## 3. images() - image search by duckduckgo.com +## 2. images() - image search by duckduckgo.com ```python def images( @@ -372,7 +355,7 @@ [Go To TOP](#TOP) -## 4. videos() - video search by duckduckgo.com +## 3. videos() - video search by duckduckgo.com ```python def videos( @@ -439,7 +422,7 @@ [Go To TOP](#TOP) -## 5. news() - news search by duckduckgo.com +## 4. news() - news search by duckduckgo.com ```python def news( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/README.md new/duckduckgo_search-8.0.1/README.md --- old/duckduckgo_search-7.1.1/README.md 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/README.md 2025-04-17 14:37:17.000000000 +0200 @@ -1,7 +1,9 @@ - [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) + [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) # Duckduckgo_search<a name="TOP"></a> -AI chat and search for text, news, images and videos using the DuckDuckGo.com search engine. +Search for text, news, images and videos using the DuckDuckGo.com search engine. + +:bangbang: AI chat moved to [duckai](https://pypi.org/project/duckai) package ## Table of Contents * [Install](#install) @@ -11,11 +13,10 @@ * [DDGS class](#ddgs-class) * [Proxy](#proxy) * [Exceptions](#exceptions) -* [1. chat() - AI chat](#1-chat---ai-chat) -* [2. text() - text search](#2-text---text-search-by-duckduckgocom) -* [3. images() - image search](#3-images---image-search-by-duckduckgocom) -* [4. videos() - video search](#4-videos---video-search-by-duckduckgocom) -* [5. news() - news search](#5-news---news-search-by-duckduckgocom) +* [1. text() - text search](#2-text---text-search-by-duckduckgocom) +* [2. images() - image search](#3-images---image-search-by-duckduckgocom) +* [3. videos() - video search](#4-videos---video-search-by-duckduckgocom) +* [4. news() - news search](#5-news---news-search-by-duckduckgocom) * [Disclaimer](#disclaimer) ## Install @@ -30,8 +31,6 @@ ``` CLI examples: ```python3 -# AI chat -ddgs chat # text search ddgs text -k "Assyrian siege of Jerusalem" # find and download pdf files via proxy @@ -193,38 +192,24 @@ ## Exceptions +```python +from duckduckgo_search.exceptions import ( + ConversationLimitException, + DuckDuckGoSearchException, + RatelimitException, + TimeoutException, +) +``` + Exceptions: - `DuckDuckGoSearchException`: Base exception for duckduckgo_search errors. - `RatelimitException`: Inherits from DuckDuckGoSearchException, raised for exceeding API request rate limits. - `TimeoutException`: Inherits from DuckDuckGoSearchException, raised for API request timeouts. - - -[Go To TOP](#TOP) - -## 1. chat() - AI chat - -```python -def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str: - """Initiates a chat session with DuckDuckGo AI. - - Args: - keywords (str): The initial message or question to send to the AI. - model (str): The model to use: "gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b". - Defaults to "gpt-4o-mini". - timeout (int): Timeout value for the HTTP client. Defaults to 30. - - Returns: - str: The response from the AI. - """ -``` -***Example*** -```python -results = DDGS().chat("summarize Daniel Defoe's The Consolidator", model='claude-3-haiku') -``` +- `ConversationLimitException`: Inherits from DuckDuckGoSearchException, raised for conversation limit during API requests to AI endpoint. [Go To TOP](#TOP) -## 2. text() - text search by duckduckgo.com +## 1. text() - text search by duckduckgo.com ```python def text( @@ -242,12 +227,10 @@ region: wt-wt, us-en, uk-en, ru-ru, etc. Defaults to "wt-wt". safesearch: on, moderate, off. Defaults to "moderate". timelimit: d, w, m, y. Defaults to None. - backend: auto, api, html, lite. Defaults to auto. + backend: auto, html, lite. Defaults to auto. auto - try all backends in random order, - api - collect data from https://duckduckgo.com, html - collect data from https://html.duckduckgo.com, - lite - collect data from https://lite.duckduckgo.com, - ecosia - collect data from https://www.ecosia.com. + lite - collect data from https://lite.duckduckgo.com. max_results: max number of results. If None, returns results only from the first response. Defaults to None. Returns: @@ -271,7 +254,7 @@ [Go To TOP](#TOP) -## 3. images() - image search by duckduckgo.com +## 2. images() - image search by duckduckgo.com ```python def images( @@ -338,7 +321,7 @@ [Go To TOP](#TOP) -## 4. videos() - video search by duckduckgo.com +## 3. videos() - video search by duckduckgo.com ```python def videos( @@ -405,7 +388,7 @@ [Go To TOP](#TOP) -## 5. news() - news search by duckduckgo.com +## 4. news() - news search by duckduckgo.com ```python def news( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/duckduckgo_search/cli.py new/duckduckgo_search-8.0.1/duckduckgo_search/cli.py --- old/duckduckgo_search-7.1.1/duckduckgo_search/cli.py 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/duckduckgo_search/cli.py 2025-04-17 14:37:17.000000000 +0200 @@ -1,7 +1,8 @@ +from __future__ import annotations + import csv import logging import os -import sys from concurrent.futures import ThreadPoolExecutor, as_completed from datetime import datetime from pathlib import Path @@ -11,7 +12,7 @@ import primp from .duckduckgo_search import DDGS -from .utils import _expand_proxy_tb_alias, json_dumps, json_loads +from .utils import _expand_proxy_tb_alias, json_dumps from .version import __version__ logger = logging.getLogger(__name__) @@ -36,7 +37,7 @@ } -def _save_data(keywords, data, function_name, filename): +def _save_data(keywords: str, data: list[dict[str, str]], function_name: str, filename: str | None) -> None: filename, ext = filename.rsplit(".", 1) if filename and filename.endswith((".csv", ".json")) else (None, filename) filename = filename if filename else f"{function_name}_{keywords}_{datetime.now():%Y%m%d_%H%M%S}" if ext == "csv": @@ -45,12 +46,12 @@ _save_json(f"{filename}.{ext}", data) -def _save_json(jsonfile, data): +def _save_json(jsonfile: str | Path, data: list[dict[str, str]]) -> None: with open(jsonfile, "w", encoding="utf-8") as file: file.write(json_dumps(data)) -def _save_csv(csvfile, data): +def _save_csv(csvfile: str | Path, data: list[dict[str, str]]) -> None: with open(csvfile, "w", newline="", encoding="utf-8") as file: if data: headers = data[0].keys() @@ -59,7 +60,7 @@ writer.writerows(data) -def _print_data(data): +def _print_data(data: list[dict[str, str]]) -> None: if data: for i, e in enumerate(data, start=1): click.secho(f"{i}.\t {'=' * 78}", bg="black", fg="white") @@ -76,7 +77,7 @@ input() -def _sanitize_keywords(keywords): +def _sanitize_keywords(keywords: str) -> str: keywords = ( keywords.replace("filetype", "") .replace(":", "") @@ -90,9 +91,11 @@ return keywords -def _download_file(url, dir_path, filename, proxy, verify): +def _download_file(url: str, dir_path: str, filename: str, proxy: str | None, verify: bool) -> None: try: - resp = primp.Client(proxy=proxy, impersonate="chrome_131", timeout=10, verify=verify).get(url) + resp = primp.Client(proxy=proxy, impersonate="random", impersonate_os="random", timeout=10, verify=verify).get( + url + ) if resp.status_code == 200: with open(os.path.join(dir_path, filename[:200]), "wb") as file: file.write(resp.content) @@ -100,7 +103,15 @@ logger.debug(f"download_file url={url} {type(ex).__name__} {ex}") -def _download_results(keywords, results, function_name, proxy=None, threads=None, verify=True, pathname=None): +def _download_results( + keywords: str, + results: list[dict[str, str]], + function_name: str, + proxy: str | None = None, + threads: int | None = None, + verify: bool = True, + pathname: str | None = None, +) -> None: path = pathname if pathname else f"{function_name}_{keywords}_{datetime.now():%Y%m%d_%H%M%S}" os.makedirs(path, exist_ok=True) @@ -113,7 +124,7 @@ f = executor.submit(_download_file, url, path, f"{i}_{filename}", proxy, verify) futures.append(f) - with click.progressbar( + with click.progressbar( # type: ignore length=len(futures), label="Downloading", show_percent=True, show_pos=True, width=50 ) as bar: for future in as_completed(futures): @@ -122,12 +133,12 @@ @click.group(chain=True) -def cli(): +def cli() -> None: """duckduckgo_search CLI tool""" pass -def safe_entry_point(): +def safe_entry_point() -> None: try: cli() except Exception as ex: @@ -135,60 +146,12 @@ @cli.command() -def version(): +def version() -> str: print(__version__) return __version__ @cli.command() -@click.option("-l", "--load", is_flag=True, default=False, help="load the last conversation from the json cache") -@click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150") -@click.option("-ml", "--multiline", is_flag=True, default=False, help="multi-line input") -@click.option("-t", "--timeout", default=30, help="timeout value for the HTTP client") -@click.option("-v", "--verify", default=True, help="verify SSL when making the request") -@click.option( - "-m", - "--model", - prompt="""DuckDuckGo AI chat. Choose a model: -[1]: gpt-4o-mini -[2]: claude-3-haiku -[3]: llama-3.1-70b -[4]: mixtral-8x7b -""", - type=click.Choice(["1", "2", "3", "4"]), - show_choices=False, - default="1", -) -def chat(load, proxy, multiline, timeout, verify, model): - """CLI function to perform an interactive AI chat using DuckDuckGo API.""" - client = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify) - model = ["gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b"][int(model) - 1] - - cache_file = "ddgs_chat_conversation.json" - if load and Path(cache_file).exists(): - with open(cache_file) as f: - cache = json_loads(f.read()) - client._chat_vqd = cache.get("vqd", None) - client._chat_messages = cache.get("messages", []) - client._chat_tokens_count = cache.get("tokens", 0) - - while True: - print(f"{'-'*78}\nYou[{model=} tokens={client._chat_tokens_count}]: ", end="") - if multiline: - print(f"""[multiline, send message: ctrl+{"Z" if sys.platform == "win32" else "D"}]""") - user_input = sys.stdin.read() - print("...") - else: - user_input = input() - if user_input.strip(): - resp_answer = client.chat(keywords=user_input, model=model, timeout=timeout) - click.secho(f"AI: {resp_answer}", fg="green") - - cache = {"vqd": client._chat_vqd, "tokens": client._chat_tokens_count, "messages": client._chat_messages} - _save_json(cache_file, cache) - - -@cli.command() @click.option("-k", "--keywords", required=True, help="text search, keywords for query") @click.option("-r", "--region", default="wt-wt", help="wt-wt, us-en, ru-ru, etc. -region https://duckduckgo.com/params") @click.option("-s", "--safesearch", default="moderate", type=click.Choice(["on", "moderate", "off"])) @@ -197,24 +160,24 @@ @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)") @click.option("-d", "--download", is_flag=True, default=False, help="download results. -dd to set custom directory") @click.option("-dd", "--download-directory", help="Specify custom download directory") -@click.option("-b", "--backend", default="auto", type=click.Choice(["auto", "api", "html", "lite", "ecosia"])) +@click.option("-b", "--backend", default="auto", type=click.Choice(["auto", "html", "lite"])) @click.option("-th", "--threads", default=10, help="download threads, default=10") @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150") @click.option("-v", "--verify", default=True, help="verify SSL when making the request") def text( - keywords, - region, - safesearch, - timelimit, - backend, - output, - download, - download_directory, - threads, - max_results, - proxy, - verify, -): + keywords: str, + region: str, + safesearch: str, + timelimit: str | None, + backend: str, + output: str | None, + download: bool, + download_directory: str | None, + threads: int, + max_results: int | None, + proxy: str | None, + verify: bool, +) -> None: """CLI function to perform a text search using DuckDuckGo API.""" data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).text( keywords=keywords, @@ -284,23 +247,23 @@ @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150") @click.option("-v", "--verify", default=True, help="verify SSL when making the request") def images( - keywords, - region, - safesearch, - timelimit, - size, - color, - type_image, - layout, - license_image, - download, - download_directory, - threads, - max_results, - output, - proxy, - verify, -): + keywords: str, + region: str, + safesearch: str, + timelimit: str | None, + size: str | None, + color: str | None, + type_image: str | None, + layout: str | None, + license_image: str | None, + download: bool, + download_directory: str | None, + threads: int, + max_results: int | None, + output: str | None, + proxy: str | None, + verify: bool, +) -> None: """CLI function to perform a images search using DuckDuckGo API.""" data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).images( keywords=keywords, @@ -344,8 +307,18 @@ @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150") @click.option("-v", "--verify", default=True, help="verify SSL when making the request") def videos( - keywords, region, safesearch, timelimit, resolution, duration, license_videos, max_results, output, proxy, verify -): + keywords: str, + region: str, + safesearch: str, + timelimit: str | None, + resolution: str | None, + duration: str | None, + license_videos: str | None, + max_results: int | None, + output: str | None, + proxy: str | None, + verify: bool, +) -> None: """CLI function to perform a videos search using DuckDuckGo API.""" data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).videos( keywords=keywords, @@ -373,7 +346,16 @@ @click.option("-o", "--output", help="csv, json or filename.csv|json (save the results to a csv or json file)") @click.option("-p", "--proxy", help="the proxy to send requests, example: socks5://127.0.0.1:9150") @click.option("-v", "--verify", default=True, help="verify SSL when making the request") -def news(keywords, region, safesearch, timelimit, max_results, output, proxy, verify): +def news( + keywords: str, + region: str, + safesearch: str, + timelimit: str | None, + max_results: int | None, + output: str | None, + proxy: str | None, + verify: bool, +) -> None: """CLI function to perform a news search using DuckDuckGo API.""" data = DDGS(proxy=_expand_proxy_tb_alias(proxy), verify=verify).news( keywords=keywords, region=region, safesearch=safesearch, timelimit=timelimit, max_results=max_results diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/duckduckgo_search/duckduckgo_search.py new/duckduckgo_search-8.0.1/duckduckgo_search/duckduckgo_search.py --- old/duckduckgo_search-7.1.1/duckduckgo_search/duckduckgo_search.py 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/duckduckgo_search/duckduckgo_search.py 2025-04-17 14:37:17.000000000 +0200 @@ -6,17 +6,17 @@ from datetime import datetime, timezone from functools import cached_property from itertools import cycle -from random import choice, shuffle +from random import shuffle from time import sleep, time from types import TracebackType -from typing import cast +from typing import Any, Literal -import primp # type: ignore +import primp from lxml.etree import _Element from lxml.html import HTMLParser as LHTMLParser from lxml.html import document_fromstring -from .exceptions import ConversationLimitException, DuckDuckGoSearchException, RatelimitException, TimeoutException +from .exceptions import DuckDuckGoSearchException, RatelimitException, TimeoutException from .utils import ( _expand_proxy_tb_alias, _extract_vqd, @@ -31,20 +31,6 @@ class DDGS: """DuckDuckgo_search class to get search results from duckduckgo.com.""" - _impersonates = ( - "chrome_100", "chrome_101", "chrome_104", "chrome_105", "chrome_106", "chrome_107", - "chrome_108", "chrome_109", "chrome_114", "chrome_116", "chrome_117", "chrome_118", - "chrome_119", "chrome_120", "chrome_123", "chrome_124", "chrome_126", "chrome_127", - "chrome_128", "chrome_129", "chrome_130", "chrome_131", - "safari_ios_16.5", "safari_ios_17.2", "safari_ios_17.4.1", "safari_ios_18.1.1", - "safari_15.3", "safari_15.5", "safari_15.6.1", "safari_16", "safari_16.5", - "safari_17.0", "safari_17.2.1", "safari_17.4.1", "safari_17.5", - "safari_18", "safari_18.2", - "safari_ipad_18", - "edge_101", "edge_122", "edge_127", "edge_131", - "firefox_109", "firefox_133", - ) # fmt: skip - def __init__( self, headers: dict[str, str] | None = None, @@ -70,19 +56,18 @@ self.proxy = proxies.get("http") or proxies.get("https") if isinstance(proxies, dict) else proxies self.headers = headers if headers else {} self.headers["Referer"] = "https://duckduckgo.com/" + self.timeout = timeout self.client = primp.Client( - headers=self.headers, + # headers=self.headers, proxy=self.proxy, - timeout=timeout, + timeout=self.timeout, cookie_store=True, referer=True, - impersonate=choice(self._impersonates), + impersonate="random", + impersonate_os="random", follow_redirects=False, verify=verify, ) - self._chat_messages: list[dict[str, str]] = [] - self._chat_tokens_count = 0 - self._chat_vqd: str = "" self.sleep_timestamp = 0.0 def __enter__(self) -> DDGS: @@ -109,99 +94,45 @@ def _get_url( self, - method: str, + method: Literal["GET", "HEAD", "OPTIONS", "DELETE", "POST", "PUT", "PATCH"], url: str, params: dict[str, str] | None = None, content: bytes | None = None, - data: dict[str, str] | bytes | None = None, + data: dict[str, str] | None = None, + headers: dict[str, str] | None = None, cookies: dict[str, str] | None = None, - ) -> bytes: + json: Any = None, + timeout: float | None = None, + ) -> Any: self._sleep() try: - resp = self.client.request(method, url, params=params, content=content, data=data, cookies=cookies) + resp = self.client.request( + method, + url, + params=params, + content=content, + data=data, + headers=headers, + cookies=cookies, + json=json, + timeout=timeout or self.timeout, + ) except Exception as ex: if "time" in str(ex).lower(): raise TimeoutException(f"{url} {type(ex).__name__}: {ex}") from ex raise DuckDuckGoSearchException(f"{url} {type(ex).__name__}: {ex}") from ex - logger.debug(f"_get_url() {resp.url} {resp.status_code} {len(resp.content)}") + logger.debug(f"_get_url() {resp.url} {resp.status_code}") if resp.status_code == 200: - return cast(bytes, resp.content) - elif resp.status_code in (202, 301, 403): + return resp + elif resp.status_code in (202, 301, 403, 400, 429, 418): raise RatelimitException(f"{resp.url} {resp.status_code} Ratelimit") raise DuckDuckGoSearchException(f"{resp.url} return None. {params=} {content=} {data=}") def _get_vqd(self, keywords: str) -> str: """Get vqd value for a search query.""" - resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords}) + resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords}).content return _extract_vqd(resp_content, keywords) - def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str: - """Initiates a chat session with DuckDuckGo AI. - - Args: - keywords (str): The initial message or question to send to the AI. - model (str): The model to use: "gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b". - Defaults to "gpt-4o-mini". - timeout (int): Timeout value for the HTTP client. Defaults to 20. - - Returns: - str: The response from the AI. - """ - models_deprecated = { - "gpt-3.5": "gpt-4o-mini", - "llama-3-70b": "llama-3.1-70b", - } - if model in models_deprecated: - logger.info(f"{model=} is deprecated, using {models_deprecated[model]}") - model = models_deprecated[model] - models = { - "claude-3-haiku": "claude-3-haiku-20240307", - "gpt-4o-mini": "gpt-4o-mini", - "llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1", - } - # vqd - if not self._chat_vqd: - resp = self.client.get("https://duckduckgo.com/duckchat/v1/status", headers={"x-vqd-accept": "1"}) - self._chat_vqd = resp.headers.get("x-vqd-4", "") - - self._chat_messages.append({"role": "user", "content": keywords}) - self._chat_tokens_count += len(keywords) // 4 if len(keywords) >= 4 else 1 # approximate number of tokens - - json_data = { - "model": models[model], - "messages": self._chat_messages, - } - resp = self.client.post( - "https://duckduckgo.com/duckchat/v1/chat", - headers={"x-vqd-4": self._chat_vqd}, - json=json_data, - timeout=timeout, - ) - self._chat_vqd = resp.headers.get("x-vqd-4", "") - - data = ",".join(x for line in resp.text.rstrip("[DONE]LIMT_CVRSA\n").split("data:") if (x := line.strip())) - data = json_loads("[" + data + "]") - - results = [] - for x in data: - if x.get("action") == "error": - err_message = x.get("type", "") - if x.get("status") == 429: - raise ( - ConversationLimitException(err_message) - if err_message == "ERR_CONVERSATION_LIMIT" - else RatelimitException(err_message) - ) - raise DuckDuckGoSearchException(err_message) - elif message := x.get("message"): - results.append(message) - result = "".join(results) - - self._chat_messages.append({"role": "assistant", "content": result}) - self._chat_tokens_count += len(results) - return result - def text( self, keywords: str, @@ -218,11 +149,10 @@ region: wt-wt, us-en, uk-en, ru-ru, etc. Defaults to "wt-wt". safesearch: on, moderate, off. Defaults to "moderate". timelimit: d, w, m, y. Defaults to None. - backend: auto, api, html, lite. Defaults to auto. + backend: auto, html, lite. Defaults to auto. auto - try all backends in random order, html - collect data from https://html.duckduckgo.com, - lite - collect data from https://lite.duckduckgo.com, - ecosia - collect data from https://www.ecosia.com. + lite - collect data from https://lite.duckduckgo.com. max_results: max number of results. If None, returns results only from the first response. Defaults to None. Returns: @@ -233,10 +163,10 @@ RatelimitException: Inherits from DuckDuckGoSearchException, raised for exceeding API request rate limits. TimeoutException: Inherits from DuckDuckGoSearchException, raised for API request timeouts. """ - if backend == "api": - warnings.warn("'api' backend is deprecated, using backend='auto'", stacklevel=2) + if backend in ("api", "ecosia"): + warnings.warn(f"{backend=} is deprecated, using backend='auto'", stacklevel=2) backend = "auto" - backends = ["html", "lite", "ecosia"] if backend == "auto" else [backend] + backends = ["html", "lite"] if backend == "auto" else [backend] shuffle(backends) results, err = [], None @@ -246,8 +176,6 @@ results = self._text_html(keywords, region, timelimit, max_results) elif b == "lite": results = self._text_lite(keywords, region, timelimit, max_results) - elif b == "ecosia": - results = self._text_ecosia(keywords, region, safesearch, max_results) return results except Exception as ex: logger.info(f"Error to search using {b} backend: {ex}") @@ -266,12 +194,8 @@ payload = { "q": keywords, - "s": "0", - "o": "json", - "api": "d.js", - "vqd": "", + "b": "", "kl": region, - "bing_market": region, } if timelimit: payload["df"] = timelimit @@ -280,7 +204,7 @@ results: list[dict[str, str]] = [] for _ in range(5): - resp_content = self._get_url("POST", "https://html.duckduckgo.com/html", data=payload) + resp_content = self._get_url("POST", "https://html.duckduckgo.com/html", data=payload).content if b"No results." in resp_content: return results @@ -338,12 +262,7 @@ payload = { "q": keywords, - "s": "0", - "o": "json", - "api": "d.js", - "vqd": "", "kl": region, - "bing_market": region, } if timelimit: payload["df"] = timelimit @@ -352,7 +271,7 @@ results: list[dict[str, str]] = [] for _ in range(5): - resp_content = self._get_url("POST", "https://lite.duckduckgo.com/lite/", data=payload) + resp_content = self._get_url("POST", "https://lite.duckduckgo.com/lite/", data=payload).content if b"No more results." in resp_content: return results @@ -397,85 +316,15 @@ if max_results and len(results) >= max_results: return results - next_page_s = tree.xpath("//form[./input[contains(@value, 'ext')]]/input[@name='s']/@value") - if not next_page_s or not max_results: - return results - elif isinstance(next_page_s, list): - payload["s"] = str(next_page_s[0]) - - return results - - def _text_ecosia( - self, - keywords: str, - region: str = "wt-wt", - safesearch: str = "moderate", - max_results: int | None = None, - ) -> list[dict[str, str]]: - assert keywords, "keywords is mandatory" - - payload = { - "q": keywords, - } - cookies = { - "a": "0", - "as": "0", - "cs": "1", - "dt": "pc", - "f": "y" if safesearch == "on" else "n" if safesearch == "off" else "i", - "fr": "0", - "fs": "1", - "l": "en", - "lt": f"{int(time() * 1000)}", - "mc": f"{region[3:]}-{region[:2]}", - "nf": "0", - "nt": "0", - "pz": "0", - "t": "6", - "tt": "", - "tu": "auto", - "wu": "auto", - "ma": "1", - } - - cache = set() - results: list[dict[str, str]] = [] - - for _ in range(5): - resp_content = self._get_url("GET", "https://www.ecosia.org/search", params=payload, cookies=cookies) - if b"Unfortunately we didn\xe2\x80\x99t find any results for" in resp_content: - return results - - tree = document_fromstring(resp_content, self.parser) - elements = tree.xpath("//div[@class='result__body']") - if not isinstance(elements, list): - return results - - for e in elements: - if isinstance(e, _Element): - hrefxpath = e.xpath(".//div[@class='result__title']/a/@href") - href = str(hrefxpath[0]) if hrefxpath and isinstance(hrefxpath, list) else None - if href and href not in cache: - cache.add(href) - titlexpath = e.xpath(".//div[@class='result__title']/a/h2/text()") - title = str(titlexpath[0]) if titlexpath and isinstance(titlexpath, list) else "" - bodyxpath = e.xpath(".//div[@class='result__description']//text()") - body = "".join(str(x) for x in bodyxpath) if bodyxpath and isinstance(bodyxpath, list) else "" - results.append( - { - "title": _normalize(title.strip()), - "href": _normalize_url(href), - "body": _normalize(body.strip()), - } - ) - if max_results and len(results) >= max_results: - return results - - npx = tree.xpath("//div[contains(@class, 'pagination')]//a[contains(@data-test-id, 'next')]/@href") + npx = tree.xpath("//form[./input[contains(@value, 'ext')]]") if not npx or not max_results: return results - if isinstance(npx, list): - payload["p"] = str(npx[-1]).split("p=")[1].split("&")[0] + next_page = npx[-1] if isinstance(npx, list) else None + if isinstance(next_page, _Element): + names = next_page.xpath('.//input[@type="hidden"]/@name') + values = next_page.xpath('.//input[@type="hidden"]/@value') + if isinstance(names, list) and isinstance(values, list): + payload = {str(n): str(v) for n, v in zip(names, values)} return results @@ -543,7 +392,9 @@ results: list[dict[str, str]] = [] for _ in range(5): - resp_content = self._get_url("GET", "https://duckduckgo.com/i.js", params=payload) + resp_content = self._get_url( + "GET", "https://duckduckgo.com/i.js", params=payload, headers={"Referer": "https://duckduckgo.com/"} + ).content resp_json = json_loads(resp_content) page_data = resp_json.get("results", []) @@ -623,7 +474,7 @@ results: list[dict[str, str]] = [] for _ in range(8): - resp_content = self._get_url("GET", "https://duckduckgo.com/v.js", params=payload) + resp_content = self._get_url("GET", "https://duckduckgo.com/v.js", params=payload).content resp_json = json_loads(resp_content) page_data = resp_json.get("results", []) @@ -685,7 +536,7 @@ results: list[dict[str, str]] = [] for _ in range(5): - resp_content = self._get_url("GET", "https://duckduckgo.com/news.js", params=payload) + resp_content = self._get_url("GET", "https://duckduckgo.com/news.js", params=payload).content resp_json = json_loads(resp_content) page_data = resp_json.get("results", []) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/duckduckgo_search/version.py new/duckduckgo_search-8.0.1/duckduckgo_search/version.py --- old/duckduckgo_search-7.1.1/duckduckgo_search/version.py 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/duckduckgo_search/version.py 2025-04-17 14:37:17.000000000 +0200 @@ -1 +1 @@ -__version__ = "7.1.1" +__version__ = "8.0.1" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/duckduckgo_search.egg-info/PKG-INFO new/duckduckgo_search-8.0.1/duckduckgo_search.egg-info/PKG-INFO --- old/duckduckgo_search-7.1.1/duckduckgo_search.egg-info/PKG-INFO 2024-12-27 00:22:58.000000000 +0100 +++ new/duckduckgo_search-8.0.1/duckduckgo_search.egg-info/PKG-INFO 2025-04-17 14:37:28.000000000 +0200 @@ -1,6 +1,6 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: duckduckgo_search -Version: 7.1.1 +Version: 8.0.1 Summary: Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine. Author: deedy5 License: MIT License @@ -11,7 +11,6 @@ Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 @@ -20,22 +19,25 @@ Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search Classifier: Topic :: Software Development :: Libraries :: Python Modules -Requires-Python: >=3.8 +Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE.md -Requires-Dist: click>=8.1.7 -Requires-Dist: primp>=0.9.2 +Requires-Dist: click>=8.1.8 +Requires-Dist: primp>=0.15.0 Requires-Dist: lxml>=5.3.0 Provides-Extra: dev -Requires-Dist: mypy>=1.13.0; extra == "dev" +Requires-Dist: mypy>=1.14.1; extra == "dev" Requires-Dist: pytest>=8.3.4; extra == "dev" Requires-Dist: pytest-dependency>=0.6.0; extra == "dev" -Requires-Dist: ruff>=0.8.3; extra == "dev" +Requires-Dist: ruff>=0.9.2; extra == "dev" +Dynamic: license-file - [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) [](https://pepy.tech/project/duckduckgo-search) + [](https://github.com/deedy5/duckduckgo_search/releases) [](https://pypi.org/project/duckduckgo-search) # Duckduckgo_search<a name="TOP"></a> -AI chat and search for text, news, images and videos using the DuckDuckGo.com search engine. +Search for text, news, images and videos using the DuckDuckGo.com search engine. + +:bangbang: AI chat moved to [duckai](https://pypi.org/project/duckai) package ## Table of Contents * [Install](#install) @@ -45,11 +47,10 @@ * [DDGS class](#ddgs-class) * [Proxy](#proxy) * [Exceptions](#exceptions) -* [1. chat() - AI chat](#1-chat---ai-chat) -* [2. text() - text search](#2-text---text-search-by-duckduckgocom) -* [3. images() - image search](#3-images---image-search-by-duckduckgocom) -* [4. videos() - video search](#4-videos---video-search-by-duckduckgocom) -* [5. news() - news search](#5-news---news-search-by-duckduckgocom) +* [1. text() - text search](#2-text---text-search-by-duckduckgocom) +* [2. images() - image search](#3-images---image-search-by-duckduckgocom) +* [3. videos() - video search](#4-videos---video-search-by-duckduckgocom) +* [4. news() - news search](#5-news---news-search-by-duckduckgocom) * [Disclaimer](#disclaimer) ## Install @@ -64,8 +65,6 @@ ``` CLI examples: ```python3 -# AI chat -ddgs chat # text search ddgs text -k "Assyrian siege of Jerusalem" # find and download pdf files via proxy @@ -227,38 +226,24 @@ ## Exceptions +```python +from duckduckgo_search.exceptions import ( + ConversationLimitException, + DuckDuckGoSearchException, + RatelimitException, + TimeoutException, +) +``` + Exceptions: - `DuckDuckGoSearchException`: Base exception for duckduckgo_search errors. - `RatelimitException`: Inherits from DuckDuckGoSearchException, raised for exceeding API request rate limits. - `TimeoutException`: Inherits from DuckDuckGoSearchException, raised for API request timeouts. - - -[Go To TOP](#TOP) - -## 1. chat() - AI chat - -```python -def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30) -> str: - """Initiates a chat session with DuckDuckGo AI. - - Args: - keywords (str): The initial message or question to send to the AI. - model (str): The model to use: "gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b". - Defaults to "gpt-4o-mini". - timeout (int): Timeout value for the HTTP client. Defaults to 30. - - Returns: - str: The response from the AI. - """ -``` -***Example*** -```python -results = DDGS().chat("summarize Daniel Defoe's The Consolidator", model='claude-3-haiku') -``` +- `ConversationLimitException`: Inherits from DuckDuckGoSearchException, raised for conversation limit during API requests to AI endpoint. [Go To TOP](#TOP) -## 2. text() - text search by duckduckgo.com +## 1. text() - text search by duckduckgo.com ```python def text( @@ -276,12 +261,10 @@ region: wt-wt, us-en, uk-en, ru-ru, etc. Defaults to "wt-wt". safesearch: on, moderate, off. Defaults to "moderate". timelimit: d, w, m, y. Defaults to None. - backend: auto, api, html, lite. Defaults to auto. + backend: auto, html, lite. Defaults to auto. auto - try all backends in random order, - api - collect data from https://duckduckgo.com, html - collect data from https://html.duckduckgo.com, - lite - collect data from https://lite.duckduckgo.com, - ecosia - collect data from https://www.ecosia.com. + lite - collect data from https://lite.duckduckgo.com. max_results: max number of results. If None, returns results only from the first response. Defaults to None. Returns: @@ -305,7 +288,7 @@ [Go To TOP](#TOP) -## 3. images() - image search by duckduckgo.com +## 2. images() - image search by duckduckgo.com ```python def images( @@ -372,7 +355,7 @@ [Go To TOP](#TOP) -## 4. videos() - video search by duckduckgo.com +## 3. videos() - video search by duckduckgo.com ```python def videos( @@ -439,7 +422,7 @@ [Go To TOP](#TOP) -## 5. news() - news search by duckduckgo.com +## 4. news() - news search by duckduckgo.com ```python def news( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/duckduckgo_search.egg-info/requires.txt new/duckduckgo_search-8.0.1/duckduckgo_search.egg-info/requires.txt --- old/duckduckgo_search-7.1.1/duckduckgo_search.egg-info/requires.txt 2024-12-27 00:22:58.000000000 +0100 +++ new/duckduckgo_search-8.0.1/duckduckgo_search.egg-info/requires.txt 2025-04-17 14:37:28.000000000 +0200 @@ -1,9 +1,9 @@ -click>=8.1.7 -primp>=0.9.2 +click>=8.1.8 +primp>=0.15.0 lxml>=5.3.0 [dev] -mypy>=1.13.0 +mypy>=1.14.1 pytest>=8.3.4 pytest-dependency>=0.6.0 -ruff>=0.8.3 +ruff>=0.9.2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/pyproject.toml new/duckduckgo_search-8.0.1/pyproject.toml --- old/duckduckgo_search-7.1.1/pyproject.toml 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/pyproject.toml 2025-04-17 14:37:17.000000000 +0200 @@ -6,7 +6,7 @@ name = "duckduckgo_search" description = "Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine." readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = {text = "MIT License"} keywords = ["python", "duckduckgo"] authors = [ @@ -18,7 +18,6 @@ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -29,8 +28,8 @@ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "click>=8.1.7", - "primp>=0.9.2", + "click>=8.1.8", + "primp>=0.15.0", "lxml>=5.3.0", ] dynamic = ["version"] @@ -46,10 +45,10 @@ [project.optional-dependencies] dev = [ - "mypy>=1.13.0", + "mypy>=1.14.1", "pytest>=8.3.4", "pytest-dependency>=0.6.0", - "ruff>=0.8.3", + "ruff>=0.9.2", ] [tool.ruff] @@ -67,6 +66,6 @@ ] [tool.mypy] -python_version = "3.8" +python_version = "3.9" strict = true -exclude = ['cli\.py$', '__main__\.py$', "tests/", "build/"] +exclude = ["build/"] \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/tests/test_cli.py new/duckduckgo_search-8.0.1/tests/test_cli.py --- old/duckduckgo_search-7.1.1/tests/test_cli.py 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/tests/test_cli.py 2025-04-17 14:37:17.000000000 +0200 @@ -1,7 +1,10 @@ +from __future__ import annotations + import os import pathlib import shutil import time +from pathlib import Path import pytest from click.testing import CliRunner @@ -10,76 +13,71 @@ from duckduckgo_search.cli import _download_results, _save_csv, _save_json, cli runner = CliRunner() -TEXT_RESULTS = None -IMAGES_RESULTS = None +TEXT_RESULTS = [] +IMAGES_RESULTS = [] @pytest.fixture(autouse=True) -def pause_between_tests(): - time.sleep(1) +def pause_between_tests() -> None: + time.sleep(2) -def test_version_command(): +def test_version_command() -> None: result = runner.invoke(cli, ["version"]) assert result.output.strip() == __version__ -def test_chat_command(): - result = runner.invoke(cli, ["chat"]) - assert "chat" in result.output - - -def test_text_command(): +def test_text_command() -> None: result = runner.invoke(cli, ["text", "-k", "python"]) assert "title" in result.output -def test_images_command(): +def test_images_command() -> None: result = runner.invoke(cli, ["images", "-k", "cat"]) assert "title" in result.output -def test_news_command(): +def test_news_command() -> None: result = runner.invoke(cli, ["news", "-k", "usa"]) assert "title" in result.output -def test_videos_command(): +def test_videos_command() -> None: result = runner.invoke(cli, ["videos", "-k", "dog"]) assert "title" in result.output @pytest.mark.dependency() -def test_get_text(): +def test_get_text() -> None: global TEXT_RESULTS TEXT_RESULTS = DDGS().text("test") assert TEXT_RESULTS @pytest.mark.dependency() -def test_get_images(): +def test_get_images() -> None: global IMAGES_RESULTS IMAGES_RESULTS = DDGS().images("test") assert IMAGES_RESULTS -@pytest.mark.dependency(depends=["test_get_data"]) -def test_save_csv(tmp_path): +@pytest.mark.dependency(depends=["test_get_text"]) +def test_save_csv(tmp_path: Path) -> None: temp_file = tmp_path / "test_csv.csv" - _save_csv(temp_file, RESULTS) + _save_csv(temp_file, TEXT_RESULTS) assert temp_file.exists() -@pytest.mark.dependency(depends=["test_get_data"]) -def test_save_json(tmp_path): +@pytest.mark.dependency(depends=["test_get_text"]) +def test_save_json(tmp_path: Path) -> None: temp_file = tmp_path / "test_json.json" - _save_json(temp_file, RESULTS) + _save_json(temp_file, TEXT_RESULTS) assert temp_file.exists() -@pytest.mark.dependency(depends=["test_get_data"]) -def test_text_download(): +@pytest.mark.dependency(depends=["test_get_text"]) +def test_text_download() -> None: pathname = pathlib.Path("text_downloads") - _download_results(test_text_download, TEXT_RESULTS, function_name="text", pathname=str(pathname)) + _download_results(f"{test_text_download}", TEXT_RESULTS, function_name="text", pathname=str(pathname)) assert pathname.is_dir() and pathname.iterdir() for file in pathname.iterdir(): assert file.is_file() @@ -87,9 +85,9 @@ @pytest.mark.dependency(depends=["test_get_images"]) -def test_images_download(): +def test_images_download() -> None: pathname = pathlib.Path("images_downloads") - _download_results(test_images_download, IMAGES_RESULTS, function_name="images", pathname=str(pathname)) + _download_results(f"{test_images_download}", IMAGES_RESULTS, function_name="images", pathname=str(pathname)) assert pathname.is_dir() and pathname.iterdir() for file in pathname.iterdir(): assert file.is_file() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/duckduckgo_search-7.1.1/tests/test_duckduckgo_search.py new/duckduckgo_search-8.0.1/tests/test_duckduckgo_search.py --- old/duckduckgo_search-7.1.1/tests/test_duckduckgo_search.py 2024-12-27 00:22:48.000000000 +0100 +++ new/duckduckgo_search-8.0.1/tests/test_duckduckgo_search.py 2025-04-17 14:37:17.000000000 +0200 @@ -5,47 +5,36 @@ @pytest.fixture(autouse=True) -def pause_between_tests(): - time.sleep(1) +def pause_between_tests() -> None: + time.sleep(2) -def test_context_manager(): +def test_context_manager() -> None: with DDGS() as ddgs: results = ddgs.news("cars", max_results=30) assert 20 <= len(results) <= 30 -@pytest.mark.parametrize("model", ["gpt-4o-mini", "claude-3-haiku", "llama-3.1-70b", "mixtral-8x7b"]) -def test_chat(model): - results = DDGS().chat("cat", model=model) - assert len(results) >= 1 - - -def test_text_html(): +def test_text_html() -> None: results = DDGS().text("eagle", backend="html", region="br-pt", timelimit="y", max_results=20) assert 15 <= len(results) <= 20 -def test_text_lite(): +def test_text_lite() -> None: results = DDGS().text("dog", backend="lite", region="br-pt", timelimit="y", max_results=20) assert 15 <= len(results) <= 20 -def test_text_ecosia(): - results = DDGS().text("cat", backend="ecosia", region="br-pt", safesearch="off", max_results=20) - assert 15 <= len(results) <= 20 - - -def test_images(): +def test_images() -> None: results = DDGS().images("flower", max_results=200) assert 85 <= len(results) <= 200 -def test_videos(): +def test_videos() -> None: results = DDGS().videos("sea", max_results=40) assert 30 <= len(results) <= 40 -def test_news(): +def test_news() -> None: results = DDGS().news("tesla", max_results=30) assert 20 <= len(results) <= 30