From dfe3b4ad902f345ceafe57b1f61b080581315895 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Tue, 30 Sep 2025 12:24:43 +0200 Subject: [PATCH] Add DuckDuckGo and Google News wrappers; refactor CryptoPanic and NewsAPI - Implemented DuckDuckGoWrapper for news retrieval using DuckDuckGo tools. - Added GoogleNewsWrapper for accessing Google News RSS feed. - Refactored CryptoPanicWrapper to unify get_top_headlines and get_latest_news methods. - Updated NewsApiWrapper to simplify top headlines retrieval. - Added tests for DuckDuckGo and Google News wrappers. - Enhanced documentation for CryptoPanicWrapper and NewsApiWrapper. - Created base module for social media integrations. --- pyproject.toml | 1 + src/app/news/__init__.py | 24 +++- src/app/news/base.py | 27 +++- src/app/news/cryptopanic_api.py | 15 +- src/app/news/duckduckgo.py | 32 +++++ src/app/news/gnews_api.py | 9 +- src/app/news/news_api.py | 10 +- src/app/social/__init.py | 1 + src/app/social/base.py | 0 tests/api/test_cryptopanic_api.py | 2 +- tests/api/test_duckduckgo_news.py | 35 +++++ ...{test_gnews_api.py => test_google_news.py} | 12 +- tests/api/test_news_api.py | 2 +- uv.lock | 128 ++++++++++++++++++ 14 files changed, 274 insertions(+), 24 deletions(-) create mode 100644 src/app/news/duckduckgo.py create mode 100644 src/app/social/__init.py create mode 100644 src/app/social/base.py create mode 100644 tests/api/test_duckduckgo_news.py rename tests/api/{test_gnews_api.py => test_google_news.py} (82%) diff --git a/pyproject.toml b/pyproject.toml index b83de19..8f4cc87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ # ✅ per interagire con API di notizie "newsapi-python", "gnews", + "ddgs", ] [tool.pytest.ini_options] diff --git a/src/app/news/__init__.py b/src/app/news/__init__.py index 8b0fd04..3218296 100644 --- a/src/app/news/__init__.py +++ b/src/app/news/__init__.py @@ -1,18 +1,32 @@ from app.utils.wrapper_handler import WrapperHandler from .base import NewsWrapper, Article from .news_api import NewsApiWrapper -from .gnews_api import GnewsWrapper +from .gnews_api import GoogleNewsWrapper from .cryptopanic_api import CryptoPanicWrapper +from .duckduckgo import DuckDuckGoWrapper -__all__ = ["NewsApiWrapper", "GnewsWrapper", "CryptoPanicWrapper"] +__all__ = ["NewsApiWrapper", "GoogleNewsWrapper", "CryptoPanicWrapper", "DuckDuckGoWrapper"] class NewsAPIs(NewsWrapper): + """ + A wrapper class that aggregates multiple news API wrappers and tries them in order until one succeeds. + This class uses the WrapperHandler to manage multiple NewsWrapper instances. + It includes, and tries, the following news API wrappers in this order: + - GnewsWrapper + - DuckDuckGoWrapper + - NewsApiWrapper + - CryptoPanicWrapper + + It provides methods to get top headlines and latest news by delegating the calls to the first successful wrapper. + If all wrappers fail, it raises an exception. + """ + def __init__(self): - wrappers = [GnewsWrapper, NewsApiWrapper, CryptoPanicWrapper] + wrappers = [GoogleNewsWrapper, DuckDuckGoWrapper, NewsApiWrapper, CryptoPanicWrapper] self.wrapper_handler: WrapperHandler[NewsWrapper] = WrapperHandler.build_wrappers(wrappers) - def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: - return self.wrapper_handler.try_call(lambda w: w.get_top_headlines(query, total)) + def get_top_headlines(self, total: int = 100) -> list[Article]: + return self.wrapper_handler.try_call(lambda w: w.get_top_headlines(total)) def get_latest_news(self, query: str, total: int = 100) -> list[Article]: return self.wrapper_handler.try_call(lambda w: w.get_latest_news(query, total)) diff --git a/src/app/news/base.py b/src/app/news/base.py index 8b3f55d..0a8f6be 100644 --- a/src/app/news/base.py +++ b/src/app/news/base.py @@ -7,8 +7,29 @@ class Article(BaseModel): description: str = "" class NewsWrapper: - def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: - raise NotImplementedError("This method should be overridden by subclasses") - def get_latest_news(self, query: str, total: int = 100) -> list[Article]: + """ + Base class for news API wrappers. + All news API wrappers should inherit from this class and implement the methods. + """ + + def get_top_headlines(self, total: int = 100) -> list[Article]: + """ + Get top headlines, optionally limited by total. + Args: + total (int): The maximum number of articles to return. + Returns: + list[Article]: A list of Article objects. + """ + raise NotImplementedError("This method should be overridden by subclasses") + + def get_latest_news(self, query: str, total: int = 100) -> list[Article]: + """ + Get latest news based on a query. + Args: + query (str): The search query. + total (int): The maximum number of articles to return. + Returns: + list[Article]: A list of Article objects. + """ raise NotImplementedError("This method should be overridden by subclasses") diff --git a/src/app/news/cryptopanic_api.py b/src/app/news/cryptopanic_api.py index 2e9270c..a949c69 100644 --- a/src/app/news/cryptopanic_api.py +++ b/src/app/news/cryptopanic_api.py @@ -31,6 +31,13 @@ def get_articles(response: dict) -> list[Article]: return articles class CryptoPanicWrapper(NewsWrapper): + """ + A wrapper for the CryptoPanic API (Documentation: https://cryptopanic.com/developers/api/) + Requires an API key set in the environment variable CRYPTOPANIC_API_KEY. + It is free to use, but has rate limits and restrictions based on the plan type (the free plan is 'developer' with 100 req/month). + Supports different plan types via the CRYPTOPANIC_API_PLAN environment variable (developer, growth, enterprise). + """ + def __init__(self): self.api_key = os.getenv("CRYPTOPANIC_API_KEY", "") assert self.api_key, "CRYPTOPANIC_API_KEY environment variable not set" @@ -55,7 +62,10 @@ class CryptoPanicWrapper(NewsWrapper): def set_filter(self, filter: CryptoPanicFilter): self.filter = filter - def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: + def get_top_headlines(self, total: int = 100) -> list[Article]: + return self.get_latest_news("", total) # same endpoint so just call the other method + + def get_latest_news(self, query: str, total: int = 100) -> list[Article]: params = self.get_base_params() params['currencies'] = query @@ -65,6 +75,3 @@ class CryptoPanicWrapper(NewsWrapper): json_response = response.json() articles = get_articles(json_response) return articles[:total] - - def get_latest_news(self, query: str, total: int = 100) -> list[Article]: - return self.get_top_headlines(query, total) # same endpoint for both, so just call it diff --git a/src/app/news/duckduckgo.py b/src/app/news/duckduckgo.py new file mode 100644 index 0000000..3a7c0bf --- /dev/null +++ b/src/app/news/duckduckgo.py @@ -0,0 +1,32 @@ +import json +from .base import Article, NewsWrapper +from agno.tools.duckduckgo import DuckDuckGoTools + +def create_article(result: dict) -> Article: + article = Article() + article.source = result.get("source", "") + article.time = result.get("date", "") + article.title = result.get("title", "") + article.description = result.get("body", "") + return article + +class DuckDuckGoWrapper(NewsWrapper): + """ + A wrapper for DuckDuckGo News search using the Tool from agno.tools.duckduckgo. + It can be rewritten to use direct API calls if needed in the future, but currently is easy to write and use. + """ + + def __init__(self): + self.tool = DuckDuckGoTools() + self.query = "crypto" + + def get_top_headlines(self, total: int = 100) -> list[Article]: + results = self.tool.duckduckgo_news(self.query, max_results=total) + json_results = json.loads(results) + return [create_article(result) for result in json_results] + + def get_latest_news(self, query: str, total: int = 100) -> list[Article]: + results = self.tool.duckduckgo_news(query or self.query, max_results=total) + json_results = json.loads(results) + return [create_article(result) for result in json_results] + diff --git a/src/app/news/gnews_api.py b/src/app/news/gnews_api.py index 53451c9..2e35f46 100644 --- a/src/app/news/gnews_api.py +++ b/src/app/news/gnews_api.py @@ -9,8 +9,13 @@ def result_to_article(result: dict) -> Article: article.description = result.get("description", "") return article -class GnewsWrapper(NewsWrapper): - def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: +class GoogleNewsWrapper(NewsWrapper): + """ + A wrapper for the Google News RSS Feed (Documentation: https://github.com/ranahaani/GNews/?tab=readme-ov-file#about-gnews) + It does not require an API key and is free to use. + """ + + def get_top_headlines(self, total: int = 100) -> list[Article]: gnews = GNews(language='en', max_results=total, period='7d') results = gnews.get_top_news() diff --git a/src/app/news/news_api.py b/src/app/news/news_api.py index 9629ecd..0e6d684 100644 --- a/src/app/news/news_api.py +++ b/src/app/news/news_api.py @@ -11,6 +11,12 @@ def result_to_article(result: dict) -> Article: return article class NewsApiWrapper(NewsWrapper): + """ + A wrapper for the NewsAPI (Documentation: https://newsapi.org/docs/get-started) + Requires an API key set in the environment variable NEWS_API_KEY. + It is free to use, but has rate limits and restrictions based on the plan type (the free plan is 'developer' with 100 req/day). + """ + def __init__(self): api_key = os.getenv("NEWS_API_KEY") assert api_key is not None, "NEWS_API_KEY environment variable not set" @@ -20,13 +26,13 @@ class NewsApiWrapper(NewsWrapper): self.language = "en" # TODO Only English articles for now? self.max_page_size = 100 - def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: + def get_top_headlines(self, total: int = 100) -> list[Article]: page_size = min(self.max_page_size, total) pages = (total // page_size) + (1 if total % page_size > 0 else 0) articles = [] for page in range(1, pages + 1): - headlines = self.client.get_top_headlines(q=query, category=self.category, language=self.language, page_size=page_size, page=page) + headlines = self.client.get_top_headlines(q="", category=self.category, language=self.language, page_size=page_size, page=page) results = [result_to_article(article) for article in headlines.get("articles", [])] articles.extend(results) return articles diff --git a/src/app/social/__init.py b/src/app/social/__init.py new file mode 100644 index 0000000..0d46bc8 --- /dev/null +++ b/src/app/social/__init.py @@ -0,0 +1 @@ +from .base import SocialWrapper \ No newline at end of file diff --git a/src/app/social/base.py b/src/app/social/base.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/api/test_cryptopanic_api.py b/tests/api/test_cryptopanic_api.py index dda2f77..c8020d3 100644 --- a/tests/api/test_cryptopanic_api.py +++ b/tests/api/test_cryptopanic_api.py @@ -27,7 +27,7 @@ class TestCryptoPanicAPI: # Useless since both methods use the same endpoint # def test_crypto_panic_api_get_top_headlines(self): # crypto = CryptoPanicWrapper() - # articles = crypto.get_top_headlines(query="crypto", total=2) + # articles = crypto.get_top_headlines(total=2) # assert isinstance(articles, list) # assert len(articles) == 2 # for article in articles: diff --git a/tests/api/test_duckduckgo_news.py b/tests/api/test_duckduckgo_news.py new file mode 100644 index 0000000..ea38272 --- /dev/null +++ b/tests/api/test_duckduckgo_news.py @@ -0,0 +1,35 @@ +import pytest +from app.news import DuckDuckGoWrapper + + +@pytest.mark.news +@pytest.mark.api +class TestDuckDuckGoNews: + + def test_news_api_initialization(self): + news = DuckDuckGoWrapper() + assert news.tool is not None + + def test_news_api_get_latest_news(self): + news = DuckDuckGoWrapper() + articles = news.get_latest_news(query="crypto", total=2) + assert isinstance(articles, list) + assert len(articles) == 2 + for article in articles: + assert article.source is not None or article.source != "" + assert article.time is not None or article.time != "" + assert article.title is not None or article.title != "" + assert article.description is not None or article.description != "" + + + def test_news_api_get_top_headlines(self): + news = DuckDuckGoWrapper() + articles = news.get_top_headlines(total=2) + assert isinstance(articles, list) + assert len(articles) == 2 + for article in articles: + assert article.source is not None or article.source != "" + assert article.time is not None or article.time != "" + assert article.title is not None or article.title != "" + assert article.description is not None or article.description != "" + diff --git a/tests/api/test_gnews_api.py b/tests/api/test_google_news.py similarity index 82% rename from tests/api/test_gnews_api.py rename to tests/api/test_google_news.py index 1013fa7..c7750f3 100644 --- a/tests/api/test_gnews_api.py +++ b/tests/api/test_google_news.py @@ -1,17 +1,17 @@ import pytest -from app.news import GnewsWrapper +from app.news import GoogleNewsWrapper @pytest.mark.news @pytest.mark.api -class TestGnewsAPI: +class TestGoogleNews: def test_gnews_api_initialization(self): - gnews_api = GnewsWrapper() + gnews_api = GoogleNewsWrapper() assert gnews_api is not None def test_gnews_api_get_latest_news(self): - gnews_api = GnewsWrapper() + gnews_api = GoogleNewsWrapper() articles = gnews_api.get_latest_news(query="crypto", total=2) assert isinstance(articles, list) assert len(articles) == 2 @@ -22,8 +22,8 @@ class TestGnewsAPI: assert article.description is not None or article.description != "" def test_gnews_api_get_top_headlines(self): - news_api = GnewsWrapper() - articles = news_api.get_top_headlines(query="crypto", total=2) + news_api = GoogleNewsWrapper() + articles = news_api.get_top_headlines(total=2) assert isinstance(articles, list) assert len(articles) == 2 for article in articles: diff --git a/tests/api/test_news_api.py b/tests/api/test_news_api.py index da8f607..927419b 100644 --- a/tests/api/test_news_api.py +++ b/tests/api/test_news_api.py @@ -26,7 +26,7 @@ class TestNewsAPI: def test_news_api_get_top_headlines(self): news_api = NewsApiWrapper() - articles = news_api.get_top_headlines(query="crypto", total=2) + articles = news_api.get_top_headlines(total=2) assert isinstance(articles, list) # assert len(articles) > 0 # apparently it doesn't always return SOME articles for article in articles: diff --git a/uv.lock b/uv.lock index 7eb69ba..ce4499b 100644 --- a/uv.lock +++ b/uv.lock @@ -169,6 +169,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload-time = "2023-09-07T14:04:17.83Z" }, ] +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786, upload-time = "2023-09-14T14:21:57.72Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165, upload-time = "2023-09-14T14:21:59.613Z" }, + { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895, upload-time = "2023-09-14T14:22:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834, upload-time = "2023-09-14T14:22:03.571Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731, upload-time = "2023-09-14T14:22:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783, upload-time = "2023-09-14T14:22:07.096Z" }, +] + [[package]] name = "cachetools" version = "5.5.2" @@ -323,6 +340,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, ] +[[package]] +name = "ddgs" +version = "9.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "httpx", extra = ["brotli", "http2", "socks"] }, + { name = "lxml" }, + { name = "primp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/45/7a408de2cd89855403ea18ed776f12c291eabe7dd54bc5b00f7cdb43f8ba/ddgs-9.6.0.tar.gz", hash = "sha256:8caf555d4282c1cf5c15969994ad55f4239bd15e97cf004a5da8f1cad37529bf", size = 35865, upload-time = "2025-09-17T13:27:10.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/cd/ef820662e0d87f46b829bba7e2324c7978e0153692bbd2f08f7746049708/ddgs-9.6.0-py3-none-any.whl", hash = "sha256:24120f1b672fd3a28309db029e7038eb3054381730aea7a08d51bb909dd55520", size = 41558, upload-time = "2025-09-17T13:27:08.99Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -577,6 +609,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + [[package]] name = "hf-xet" version = "1.1.10" @@ -592,6 +637,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" }, ] +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -620,6 +674,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[package.optional-dependencies] +brotli = [ + { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, + { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, +] +http2 = [ + { name = "h2" }, +] +socks = [ + { name = "socksio" }, +] + [[package]] name = "huggingface-hub" version = "0.35.0" @@ -639,6 +705,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/85/a18508becfa01f1e4351b5e18651b06d210dbd96debccd48a452acccb901/huggingface_hub-0.35.0-py3-none-any.whl", hash = "sha256:f2e2f693bca9a26530b1c0b9bcd4c1495644dad698e6a0060f90e22e772c31e9", size = 563436, upload-time = "2025-09-16T13:49:30.627Z" }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -669,6 +744,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "lxml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, + { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -860,6 +961,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "primp" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/0b/a87556189da4de1fc6360ca1aa05e8335509633f836cdd06dd17f0743300/primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a", size = 113022, upload-time = "2025-04-17T11:41:05.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/5a/146ac964b99ea7657ad67eb66f770be6577dfe9200cb28f9a95baffd6c3f/primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f", size = 3178914, upload-time = "2025-04-17T11:40:59.558Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/cc2321e32db3ce64d6e32950d5bcbea01861db97bfb20b5394affc45b387/primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299", size = 2955079, upload-time = "2025-04-17T11:40:57.398Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7b/cbd5d999a07ff2a21465975d4eb477ae6f69765e8fe8c9087dab250180d8/primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161", size = 3281018, upload-time = "2025-04-17T11:40:55.308Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6e/a6221c612e61303aec2bcac3f0a02e8b67aee8c0db7bdc174aeb8010f975/primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080", size = 3255229, upload-time = "2025-04-17T11:40:47.811Z" }, + { url = "https://files.pythonhosted.org/packages/3b/54/bfeef5aca613dc660a69d0760a26c6b8747d8fdb5a7f20cb2cee53c9862f/primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83", size = 3014522, upload-time = "2025-04-17T11:40:50.191Z" }, + { url = "https://files.pythonhosted.org/packages/ac/96/84078e09f16a1dad208f2fe0f8a81be2cf36e024675b0f9eec0c2f6e2182/primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260", size = 3418567, upload-time = "2025-04-17T11:41:01.595Z" }, + { url = "https://files.pythonhosted.org/packages/6c/80/8a7a9587d3eb85be3d0b64319f2f690c90eb7953e3f73a9ddd9e46c8dc42/primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8", size = 3606279, upload-time = "2025-04-17T11:41:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/0c/dd/f0183ed0145e58cf9d286c1b2c14f63ccee987a4ff79ac85acc31b5d86bd/primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32", size = 3149967, upload-time = "2025-04-17T11:41:07.067Z" }, +] + [[package]] name = "propcache" version = "0.3.2" @@ -1255,6 +1372,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" }, +] + [[package]] name = "soupsieve" version = "2.8" @@ -1371,6 +1497,7 @@ source = { virtual = "." } dependencies = [ { name = "agno" }, { name = "coinbase-advanced-py" }, + { name = "ddgs" }, { name = "dotenv" }, { name = "gnews" }, { name = "google-genai" }, @@ -1385,6 +1512,7 @@ dependencies = [ requires-dist = [ { name = "agno" }, { name = "coinbase-advanced-py" }, + { name = "ddgs" }, { name = "dotenv" }, { name = "gnews" }, { name = "google-genai" },