diff --git a/.env.example b/.env.example index d5d5a38..f59ad82 100644 --- a/.env.example +++ b/.env.example @@ -10,12 +10,10 @@ GOOGLE_API_KEY= # Configurazioni per gli agenti di mercato ############################################################################### -# Coinbase CDP API per Market Agent # Ottenibili da: https://portal.cdp.coinbase.com/access/api CDP_API_KEY_NAME= CDP_API_PRIVATE_KEY= -# CryptoCompare API per Market Agent (alternativa) # Ottenibile da: https://www.cryptocompare.com/cryptopian/api-keys CRYPTOCOMPARE_API_KEY= @@ -26,6 +24,9 @@ BINANCE_API_SECRET= ############################################################################### # Configurazioni per gli agenti di notizie ############################################################################### + # Ottenibile da: https://newsapi.org/docs NEWS_API_KEY= +# Ottenibile da: https://cryptopanic.com/developers/api/ +CRYPTOPANIC_API_KEY= diff --git a/src/app/news/__init__.py b/src/app/news/__init__.py index 957453d..f130e6b 100644 --- a/src/app/news/__init__.py +++ b/src/app/news/__init__.py @@ -1,4 +1,5 @@ from .news_api import NewsApiWrapper from .gnews_api import GnewsWrapper +from .cryptopanic_api import CryptoPanicWrapper -__all__ = ["NewsApiWrapper", "GnewsWrapper"] \ No newline at end of file +__all__ = ["NewsApiWrapper", "GnewsWrapper", "CryptoPanicWrapper"] \ No newline at end of file diff --git a/src/app/news/cryptopanic_api.py b/src/app/news/cryptopanic_api.py new file mode 100644 index 0000000..2e9270c --- /dev/null +++ b/src/app/news/cryptopanic_api.py @@ -0,0 +1,70 @@ +import os +import requests +from enum import Enum +from .base import NewsWrapper, Article + +class CryptoPanicFilter(Enum): + RISING = "rising" + HOT = "hot" + BULLISH = "bullish" + BEARISH = "bearish" + IMPORTANT = "important" + SAVED = "saved" + LOL = "lol" + ANY = "" + +class CryptoPanicKind(Enum): + NEWS = "news" + MEDIA = "media" + ALL = "all" + +def get_articles(response: dict) -> list[Article]: + articles = [] + if 'results' in response: + for item in response['results']: + article = Article() + article.source = item.get('source', {}).get('title', '') + article.time = item.get('published_at', '') + article.title = item.get('title', '') + article.description = item.get('description', '') + articles.append(article) + return articles + +class CryptoPanicWrapper(NewsWrapper): + def __init__(self): + self.api_key = os.getenv("CRYPTOPANIC_API_KEY", "") + assert self.api_key, "CRYPTOPANIC_API_KEY environment variable not set" + + # Set here for the future, but currently not needed + plan_type = os.getenv("CRYPTOPANIC_API_PLAN", "developer").lower() + assert plan_type in ["developer", "growth", "enterprise"], "Invalid CRYPTOPANIC_API_PLAN value" + + self.base_url = f"https://cryptopanic.com/api/{plan_type}/v2" + self.filter = CryptoPanicFilter.ANY + self.kind = CryptoPanicKind.NEWS + + def get_base_params(self) -> dict[str, str]: + params = {} + params['public'] = 'true' # recommended for app and bots + params['auth_token'] = self.api_key + params['kind'] = self.kind.value + if self.filter != CryptoPanicFilter.ANY: + params['filter'] = self.filter.value + return params + + def set_filter(self, filter: CryptoPanicFilter): + self.filter = filter + + def get_top_headlines(self, query: str, total: int = 100) -> list[Article]: + params = self.get_base_params() + params['currencies'] = query + + response = requests.get(f"{self.base_url}/posts/", params=params) + assert response.status_code == 200, f"Error fetching data: {response}" + + 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/tests/api/test_cryptopanic_api.py b/tests/api/test_cryptopanic_api.py new file mode 100644 index 0000000..dda2f77 --- /dev/null +++ b/tests/api/test_cryptopanic_api.py @@ -0,0 +1,38 @@ +import os +import pytest +from app.news import CryptoPanicWrapper + + +@pytest.mark.limited +@pytest.mark.news +@pytest.mark.api +@pytest.mark.skipif(not os.getenv("CRYPTOPANIC_API_KEY"), reason="CRYPTOPANIC_API_KEY not set") +class TestCryptoPanicAPI: + + def test_crypto_panic_api_initialization(self): + crypto = CryptoPanicWrapper() + assert crypto is not None + + def test_crypto_panic_api_get_latest_news(self): + crypto = CryptoPanicWrapper() + articles = crypto.get_latest_news(query="", 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 != "" + + # 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) + # 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_gnews_api.py index 49c8418..1013fa7 100644 --- a/tests/api/test_gnews_api.py +++ b/tests/api/test_gnews_api.py @@ -16,10 +16,10 @@ class TestGnewsAPI: assert isinstance(articles, list) assert len(articles) == 2 for article in articles: - assert hasattr(article, 'source') - assert hasattr(article, 'time') - assert hasattr(article, 'title') - assert hasattr(article, 'description') + 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_gnews_api_get_top_headlines(self): news_api = GnewsWrapper() @@ -27,8 +27,8 @@ class TestGnewsAPI: assert isinstance(articles, list) assert len(articles) == 2 for article in articles: - assert hasattr(article, 'source') - assert hasattr(article, 'time') - assert hasattr(article, 'title') - assert hasattr(article, 'description') + 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_news_api.py b/tests/api/test_news_api.py index 4778d5b..da8f607 100644 --- a/tests/api/test_news_api.py +++ b/tests/api/test_news_api.py @@ -1,9 +1,11 @@ +import os import pytest from app.news import NewsApiWrapper @pytest.mark.news @pytest.mark.api +@pytest.mark.skipif(not os.getenv("NEWS_API_KEY"), reason="NEWS_API_KEY not set") class TestNewsAPI: def test_news_api_initialization(self): @@ -16,20 +18,20 @@ class TestNewsAPI: assert isinstance(articles, list) assert len(articles) > 0 # Ensure we got some articles (apparently it doesn't always return the requested number) for article in articles: - assert hasattr(article, 'source') - assert hasattr(article, 'time') - assert hasattr(article, 'title') - assert hasattr(article, 'description') + 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_api = NewsApiWrapper() articles = news_api.get_top_headlines(query="crypto", total=2) assert isinstance(articles, list) - assert len(articles) > 0 # Ensure we got some articles (apparently it doesn't always return the requested number) + # assert len(articles) > 0 # apparently it doesn't always return SOME articles for article in articles: - assert hasattr(article, 'source') - assert hasattr(article, 'time') - assert hasattr(article, 'title') - assert hasattr(article, 'description') + 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/conftest.py b/tests/conftest.py index 21502d1..5584992 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,6 +21,7 @@ def pytest_configure(config:pytest.Config): ("ollama_gpt", "marks tests that use Ollama GPT model"), ("ollama_qwen", "marks tests that use Ollama Qwen model"), ("news", "marks tests that use news"), + ("limited", "marks tests that have limited execution due to API constraints"), ] for marker in markers: line = f"{marker[0]}: {marker[1]}" @@ -44,3 +45,13 @@ def pytest_collection_modifyitems(config, items): for key, marker in markers_to_add.items(): if key in name: item.add_marker(marker) + + # Rimuovo i test "limited" e "slow" se non richiesti esplicitamente + mark_to_remove = ['limited', 'slow'] + for mark in mark_to_remove: + markexpr = getattr(config.option, "markexpr", None) + if markexpr and mark in markexpr.lower(): + continue + + new_mark = (f"({markexpr}) and " if markexpr else "") + f"not {mark}" + setattr(config.option, "markexpr", new_mark)