Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-ddgs for openSUSE:Factory checked in at 2026-11-09 16:04:02 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-ddgs (Old) and /work/SRC/openSUSE:Factory/.python-ddgs.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-ddgs" Mon Nov 9 16:04:02 2026 rev:5 rq:1396186 version:9.16.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-ddgs/python-ddgs.changes 2026-08-20 16:16:50.334525931 +0200 +++ /work/SRC/openSUSE:Factory/.python-ddgs.new.3759290/python-ddgs.changes 2026-11-09 16:04:36.115707647 +0100 @@ -1,0 +2,10 @@ +Sun Nov 8 21:02:45 UTC 2026 - Dirk Müller <[email protected]> + +- update to 9.16.1: + * fix(engines): add one iteration to extract results from + pending futures +- update to 9.16.0: + * fix(engines): update and enable Google + * fix(engines): update DuckDuckgo + +------------------------------------------------------------------- Old: ---- ddgs-9.15.0.tar.gz New: ---- ddgs-9.16.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-ddgs.spec ++++++ --- /var/tmp/diff_new_pack.k8pGsE/_old 2026-11-09 16:04:37.893782189 +0100 +++ /var/tmp/diff_new_pack.k8pGsE/_new 2026-11-09 16:04:37.895782273 +0100 @@ -23,7 +23,7 @@ %endif Name: python-ddgs -Version: 9.15.0 +Version: 9.16.1 Release: 0 Summary: Dux Distributed Global Search. A metasearch library that aggregates results from diverse web search services License: MIT ++++++ ddgs-9.15.0.tar.gz -> ddgs-9.16.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/PKG-INFO new/ddgs-9.16.1/PKG-INFO --- old/ddgs-9.15.0/PKG-INFO 2026-08-16 07:20:52.176898200 +0200 +++ new/ddgs-9.16.1/PKG-INFO 2026-10-23 23:16:39.896275800 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ddgs -Version: 9.15.0 +Version: 9.16.1 Summary: Dux Distributed Global Search. A metasearch library that aggregates results from diverse web search services. Author: deedy5 License-Expression: MIT @@ -25,10 +25,8 @@ Description-Content-Type: text/markdown License-File: LICENSE.md Requires-Dist: click>=8.1.8 -Requires-Dist: primp>=1.2.3 +Requires-Dist: primp>=1.3.1 Requires-Dist: lxml>=4.9.4 -Requires-Dist: httpx[brotli,http2,socks]>=0.28.1 -Requires-Dist: fake-useragent>=2.2.0 Provides-Extra: dev Requires-Dist: mypy>=1.17.1; extra == "dev" Requires-Dist: prek; extra == "dev" @@ -40,12 +38,6 @@ Requires-Dist: types-pexpect; extra == "dev" Requires-Dist: types-PyYAML; extra == "dev" Requires-Dist: types-ujson; extra == "dev" -Requires-Dist: types-PySocks; extra == "dev" -Requires-Dist: types-colorama; extra == "dev" -Requires-Dist: types-decorator; extra == "dev" -Requires-Dist: types-jsonschema; extra == "dev" -Requires-Dist: types-psutil; extra == "dev" -Requires-Dist: types-pyasn1; extra == "dev" Provides-Extra: mcp Requires-Dist: mcp>=2.0; extra == "mcp" Provides-Extra: api diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/__init__.py new/ddgs-9.16.1/ddgs/__init__.py --- old/ddgs-9.15.0/ddgs/__init__.py 2026-08-16 07:20:40.928991000 +0200 +++ new/ddgs-9.16.1/ddgs/__init__.py 2026-10-23 23:16:32.206802800 +0200 @@ -8,7 +8,7 @@ import threading from typing import TYPE_CHECKING, Any, cast -__version__ = "9.15.0" +__version__ = "9.16.1" __all__ = ("DDGS",) if TYPE_CHECKING: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/ddgs.py new/ddgs-9.16.1/ddgs/ddgs.py --- old/ddgs-9.15.0/ddgs/ddgs.py 2026-08-16 07:20:40.929079500 +0200 +++ new/ddgs-9.16.1/ddgs/ddgs.py 2026-10-23 23:16:32.207218600 +0200 @@ -132,7 +132,7 @@ instances.sort(key=lambda e: (e.priority, random), reverse=True) return instances - def _search_sync( # noqa: C901 + def _search_sync( # noqa: C901, PLR0912 self, category: str, query: str, @@ -180,19 +180,20 @@ max_workers = min(max_workers, DDGS.threads) futures, err = {}, None with ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="DDGS") as executor: - for i, engine in enumerate(engines, start=1): - if engine.provider in seen_providers: - continue - future = executor.submit( - engine.search, - query, - region=region, - safesearch=safesearch, - timelimit=timelimit, - page=page, - **kwargs, - ) - futures[future] = engine + for i, engine in enumerate([*engines, None], start=1): + if engine: + if engine.provider in seen_providers: + continue + future = executor.submit( + engine.search, + query, + region=region, + safesearch=safesearch, + timelimit=timelimit, + page=page, + **kwargs, + ) + futures[future] = engine if len(futures) >= max_workers or i >= max_workers: done, not_done = wait(futures, timeout=self._timeout, return_when="FIRST_EXCEPTION") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/engines/duckduckgo.py new/ddgs-9.16.1/ddgs/engines/duckduckgo.py --- old/ddgs-9.15.0/ddgs/engines/duckduckgo.py 2026-08-16 07:20:40.929439500 +0200 +++ new/ddgs-9.16.1/ddgs/engines/duckduckgo.py 2026-10-23 23:16:32.207618200 +0200 @@ -1,18 +1,11 @@ """Duckduckgo search engine implementation.""" from collections.abc import Mapping -from typing import Any, ClassVar, TypeVar - -from fake_useragent import UserAgent +from typing import Any, ClassVar from ddgs.base import BaseSearchEngine -from ddgs.http_client2 import HttpClient2 from ddgs.results import TextResult -ua = UserAgent() - -T = TypeVar("T") - class Duckduckgo(BaseSearchEngine[TextResult]): """Duckduckgo search engine.""" @@ -27,13 +20,6 @@ items_xpath = "//div[contains(@class, 'body')]" elements_xpath: ClassVar[Mapping[str, str]] = {"title": ".//h2//text()", "href": "./a/@href", "body": "./a//text()"} - headers: ClassVar[dict[str, str]] = {"User-Agent": ua.random} - - def __init__(self, proxy: str | None = None, timeout: int | None = None, *, verify: bool = True) -> None: - """Temporary, delete when HttpClient is fixed.""" - self.http_client = HttpClient2(headers=self.headers, proxy=proxy, timeout=timeout, verify=verify) # type: ignore[assignment] - self.results: list[T] = [] # type: ignore[valid-type] - def build_payload( self, query: str, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/engines/google.py new/ddgs-9.16.1/ddgs/engines/google.py --- old/ddgs-9.15.0/ddgs/engines/google.py 2026-08-16 07:20:40.929439500 +0200 +++ new/ddgs-9.16.1/ddgs/engines/google.py 2026-10-23 23:16:32.207618200 +0200 @@ -11,43 +11,33 @@ def get_ua() -> str: - """Return one random Android Google App User-Agent string.""" - # Device templates: (Android version, device string, Chrome major version range) - devices = ( - ("5.0", "SM-G900P Build/LRX21T", 39, 60), - ("6.0", "Nexus 5 Build/MRA58N", 39, 60), - ("8.0", "Pixel 2 Build/OPD3.170816.012", 39, 60), - ) - android_ver, device, chrome_min, chrome_max = random.choice(devices) - chrome_major = random.randint(chrome_min, chrome_max) - chrome_build = random.randint(1000, 9999) - chrome_patch = random.randint(1000, 1999) - ua = ( - f"Mozilla/5.0 (Linux; Android {android_ver}; {device}) " - f"AppleWebKit/537.36 (KHTML, like Gecko) " - f"Chrome/{chrome_major}.0.{chrome_build}.{chrome_patch} Mobile Safari/537.36" - ) - return ua + bytes.fromhex("4e53544e5756").decode() + """Return one User-Agent string.""" + firmware = random.choice(("2.0617.1.0.3", "2.0625.2.0.2", "2.0635.2.0.2", "5.0706.4.0.1", "5.0819.4.0.1")) + ua = f"NokiaN72/{firmware} Series60/2.8 Profile/MIDP-2.0 Configuration/CLDC-1.1" + if random.choice((True, False)): + uc_version = random.choice(("7.9.1.120", "7.9.1.121", "7.9.1.122")) + ua += f"/UC Browser{uc_version}/27/351/UCWEB" + return ua class Google(BaseSearchEngine[TextResult]): """Google search engine.""" - disabled = True + disabled = True # !!! name = "google" category = "text" provider = "google" - search_url = "https://www.google.com/search" + search_url = "https://www.google.com/wml/search" search_method = "GET" headers_update: ClassVar[dict[str, str]] = {"User-Agent": get_ua()} - items_xpath = "//div[@data-hveid][.//h3]" + items_xpath = "//div[./div[1]/a and ./div[2][table]]" elements_xpath: ClassVar[Mapping[str, str]] = { - "title": ".//h3//text()", - "href": ".//a[.//h3]/@href", - "body": "./div/div[last()]//text()", + "title": "./div[a]/a/span[1]/text()", + "href": "./div[a]/a/@href", + "body": "./div[2][table]//text()", } def build_payload( @@ -65,6 +55,7 @@ start = (page - 1) * 10 payload = { "q": query, + "sca_esv": "1", "filter": safesearch_base[safesearch.lower()], "start": str(start), } @@ -76,12 +67,16 @@ payload["tbs"] = f"qdr:{timelimit}" return payload + def pre_process_html(self, html_text: str) -> str: + """Pre-process html_text before extracting results.""" + return html_text[html_text.find("?>") :] + def post_extract_results(self, results: list[TextResult]) -> list[TextResult]: """Post-process search results.""" post_results = [] for result in results: if result.href.startswith("/url?q="): - result.href = result.href.split("?q=")[1].split("&")[0] + result.href = result.href.split("?q=")[1].split("&")[0].split("?")[0] if result.title and result.href.startswith("http"): post_results.append(result) return post_results diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/engines/startpage.py new/ddgs-9.16.1/ddgs/engines/startpage.py --- old/ddgs-9.15.0/ddgs/engines/startpage.py 2026-08-16 07:20:40.929439500 +0200 +++ new/ddgs-9.16.1/ddgs/engines/startpage.py 2026-10-23 23:16:32.207618200 +0200 @@ -21,7 +21,7 @@ search_method = "POST" headers_update: ClassVar[dict[str, str]] = {"Referer": "https://www.startpage.com/"} - items_xpath = "//div[contains(@class, 'result')][./a]" + items_xpath = "//div[contains(@class, 'result css')][./a]" elements_xpath: ClassVar[Mapping[str, str]] = { "title": ".//h2//text()", "href": "./a/@href", @@ -68,3 +68,7 @@ payload["with_date"] = timelimit return payload + + def post_extract_results(self, results: list[TextResult]) -> list[TextResult]: + """Post-process search results.""" + return [r for r in results if not r.href.startswith("https://www.google.com")] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs/http_client2.py new/ddgs-9.16.1/ddgs/http_client2.py --- old/ddgs-9.15.0/ddgs/http_client2.py 2026-08-16 07:20:40.929991000 +0200 +++ new/ddgs-9.16.1/ddgs/http_client2.py 1970-01-01 01:00:00.000000000 +0100 @@ -1,151 +0,0 @@ -"""Temporary HTTP client for 'backend=duckduckgo'. Delete when HttpClient is fixed.""" - -import logging -import ssl -from random import SystemRandom -from types import TracebackType -from typing import TYPE_CHECKING, Any - -import h2 -import httpcore -import httpx - -from .exceptions import DDGSException, TimeoutException - -if TYPE_CHECKING: - from collections.abc import Callable - - -logger = logging.getLogger(__name__) -random = SystemRandom() - - -class Response: - """HTTP response.""" - - __slots__ = ("content", "status_code", "text") - - def __init__(self, status_code: int, content: bytes, text: str) -> None: - self.status_code = status_code - self.content = content - self.text = text - - -class HttpClient2: - """Temporary HTTP client.""" - - def __init__( - self, - headers: dict[str, str] | None = None, - proxy: str | None = None, - timeout: int | None = 10, - *, - verify: bool | str = True, - ) -> None: - """Initialize the HttpClient object. - - Args: - headers (dict, optional): headers for the HTTP client. - proxy (str, optional): proxy for the HTTP client, supports http/https/socks5 protocols. - example: "http://user:[email protected]:3128". Defaults to None. - timeout (int, optional): Timeout value for the HTTP client. Defaults to 10. - verify: (bool | str): True to verify, False to skip or str path to a PEM file. Defaults to True. - - """ - self.client = httpx.Client( - headers=headers, - proxy=proxy, - timeout=timeout, - verify=_get_random_ssl_context(verify=verify) if verify else False, - follow_redirects=False, - http2=True, - ) - - def request(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401 - """Make a request to the HTTP client.""" - with Patch(): - try: - resp = self.client.request(*args, **kwargs) - return Response(status_code=resp.status_code, content=resp.content, text=resp.text) - except Exception as ex: - if "timed out" in f"{ex}": - msg = f"Request timed out: {ex!r}" - raise TimeoutException(msg) from ex - msg = f"{type(ex).__name__}: {ex!r}" - raise DDGSException(msg) from ex - - def get(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401 - """Make a GET request to the HTTP client.""" - return self.request(*args, method="GET", **kwargs) - - def post(self, *args: Any, **kwargs: Any) -> Response: # noqa: ANN401 - """Make a POST request to the HTTP client.""" - return self.request(*args, method="POST", **kwargs) - - -# SSL -DEFAULT_CIPHERS = [ # https://developers.cloudflare.com/ssl/reference/cipher-suites/recommendations/ - "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256", - # Modern: - "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-ECDSA-CHACHA20-POLY1305", "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-RSA-CHACHA20-POLY1305", "ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384", - # Compatible: - "ECDHE-ECDSA-AES128-GCM-SHA256", "ECDHE-ECDSA-CHACHA20-POLY1305", "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-RSA-CHACHA20-POLY1305", "ECDHE-ECDSA-AES256-GCM-SHA384", "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-SHA256", "ECDHE-RSA-AES128-SHA256", "ECDHE-ECDSA-AES256-SHA384", "ECDHE-RSA-AES256-SHA384", - # Legacy: - "ECDHE-ECDSA-AES128-SHA", "ECDHE-RSA-AES128-SHA", "AES128-GCM-SHA256", "AES128-SHA256", "AES128-SHA", - "ECDHE-RSA-AES256-SHA", "AES256-GCM-SHA384", "AES256-SHA256", "AES256-SHA", "DES-CBC3-SHA", -] # fmt: skip - - -def _get_random_ssl_context(*, verify: bool | str) -> ssl.SSLContext: - ssl_context = ssl.create_default_context(cafile=verify if isinstance(verify, str) else None) - shuffled_ciphers = random.sample(DEFAULT_CIPHERS[9:], len(DEFAULT_CIPHERS) - 9) - ssl_context.set_ciphers(":".join(DEFAULT_CIPHERS[:9] + shuffled_ciphers)) - commands: list[Callable[[ssl.SSLContext], None] | None] = [ - None, - lambda context: setattr(context, "maximum_version", ssl.TLSVersion.TLSv1_2), - lambda context: setattr(context, "minimum_version", ssl.TLSVersion.TLSv1_3), - lambda context: setattr(context, "options", context.options | ssl.OP_NO_TICKET), - ] - random_command = random.choice(commands) - if random_command: - random_command(ssl_context) - return ssl_context - - -class Patch: - """Patch the HTTP2Connection._send_connection_init method.""" - - def __enter__(self) -> None: - """Enter the context manager.""" - - def _send_connection_init(self: httpcore._sync.http2.HTTP2Connection, request: httpcore.Request) -> None: - self._h2_state.local_settings = h2.settings.Settings( - client=True, - initial_values={ - h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: random.randint(100, 200), - h2.settings.SettingCodes.HEADER_TABLE_SIZE: random.randint(4000, 5000), - h2.settings.SettingCodes.MAX_FRAME_SIZE: random.randint(16384, 65535), - h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: random.randint(100, 200), - h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: random.randint(65500, 66500), - h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL: random.randint(0, 1), - h2.settings.SettingCodes.ENABLE_PUSH: random.randint(0, 1), - }, - ) - self._h2_state.initiate_connection() - self._h2_state.increment_flow_control_window(2**24) - self._write_outgoing_data(request) - - self.original_send_connection_init = httpcore._sync.http2.HTTP2Connection._send_connection_init - httpcore._sync.http2.HTTP2Connection._send_connection_init = _send_connection_init # type: ignore[method-assign] - - def __exit__( - self, - exc_type: type[BaseException] | None = None, - exc_val: BaseException | None = None, - exc_tb: TracebackType | None = None, - ) -> None: - """Exit the context manager.""" - httpcore._sync.http2.HTTP2Connection._send_connection_init = self.original_send_connection_init # type: ignore[method-assign] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs.egg-info/PKG-INFO new/ddgs-9.16.1/ddgs.egg-info/PKG-INFO --- old/ddgs-9.15.0/ddgs.egg-info/PKG-INFO 2026-08-16 07:20:52.165898300 +0200 +++ new/ddgs-9.16.1/ddgs.egg-info/PKG-INFO 2026-10-23 23:16:39.885282800 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: ddgs -Version: 9.15.0 +Version: 9.16.1 Summary: Dux Distributed Global Search. A metasearch library that aggregates results from diverse web search services. Author: deedy5 License-Expression: MIT @@ -25,10 +25,8 @@ Description-Content-Type: text/markdown License-File: LICENSE.md Requires-Dist: click>=8.1.8 -Requires-Dist: primp>=1.2.3 +Requires-Dist: primp>=1.3.1 Requires-Dist: lxml>=4.9.4 -Requires-Dist: httpx[brotli,http2,socks]>=0.28.1 -Requires-Dist: fake-useragent>=2.2.0 Provides-Extra: dev Requires-Dist: mypy>=1.17.1; extra == "dev" Requires-Dist: prek; extra == "dev" @@ -40,12 +38,6 @@ Requires-Dist: types-pexpect; extra == "dev" Requires-Dist: types-PyYAML; extra == "dev" Requires-Dist: types-ujson; extra == "dev" -Requires-Dist: types-PySocks; extra == "dev" -Requires-Dist: types-colorama; extra == "dev" -Requires-Dist: types-decorator; extra == "dev" -Requires-Dist: types-jsonschema; extra == "dev" -Requires-Dist: types-psutil; extra == "dev" -Requires-Dist: types-pyasn1; extra == "dev" Provides-Extra: mcp Requires-Dist: mcp>=2.0; extra == "mcp" Provides-Extra: api diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs.egg-info/SOURCES.txt new/ddgs-9.16.1/ddgs.egg-info/SOURCES.txt --- old/ddgs-9.15.0/ddgs.egg-info/SOURCES.txt 2026-08-16 07:20:52.171849000 +0200 +++ new/ddgs-9.16.1/ddgs.egg-info/SOURCES.txt 2026-10-23 23:16:39.890035400 +0200 @@ -7,7 +7,6 @@ ddgs/ddgs.py ddgs/exceptions.py ddgs/http_client.py -ddgs/http_client2.py ddgs/py.typed ddgs/results.py ddgs/similarity.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/ddgs.egg-info/requires.txt new/ddgs-9.16.1/ddgs.egg-info/requires.txt --- old/ddgs-9.15.0/ddgs.egg-info/requires.txt 2026-08-16 07:20:52.167670700 +0200 +++ new/ddgs-9.16.1/ddgs.egg-info/requires.txt 2026-10-23 23:16:39.886176600 +0200 @@ -1,8 +1,6 @@ click>=8.1.8 -primp>=1.2.3 +primp>=1.3.1 lxml>=4.9.4 -httpx[brotli,http2,socks]>=0.28.1 -fake-useragent>=2.2.0 [api] fastapi>=0.135.1 @@ -19,12 +17,6 @@ types-pexpect types-PyYAML types-ujson -types-PySocks -types-colorama -types-decorator -types-jsonschema -types-psutil -types-pyasn1 [mcp] mcp>=2.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/ddgs-9.15.0/pyproject.toml new/ddgs-9.16.1/pyproject.toml --- old/ddgs-9.15.0/pyproject.toml 2026-08-16 07:20:40.929991000 +0200 +++ new/ddgs-9.16.1/pyproject.toml 2026-10-23 23:16:32.208145900 +0200 @@ -28,10 +28,8 @@ ] dependencies = [ "click>=8.1.8", - "primp>=1.2.3", + "primp>=1.3.1", "lxml>=4.9.4", - "httpx[http2,socks,brotli]>=0.28.1", # temporarily - "fake-useragent>=2.2.0", ] dynamic = ["version"] @@ -65,14 +63,6 @@ "types-pexpect", "types-PyYAML", "types-ujson", - - # for mypy (httpx) - "types-PySocks", - "types-colorama", - "types-decorator", - "types-jsonschema", - "types-psutil", - "types-pyasn1" ] mcp = [ "mcp>=2.0",
