3 market api (#8)
* Creazione branch tool, refactor degli import e soppressione dei warning * Update pytest configuration and dependencies in pyproject.toml * Add news API integration and related configurations - Update .env.example to include NEWS_API_KEY configuration - Add newsapi-python dependency in pyproject.toml - Implement NewsAPI class for fetching news articles - Create Article model for structured news data - Add tests for NewsAPI functionality in test_news_api.py - Update pytest configuration to include news marker * Add news API functionality and update tests for article retrieval * ToDo: 1. Aggiungere un aggregator per i dati recuperati dai provider. 2. Lavorare effettivamente all'issue Done: 1. creati test per i provider 2. creato market_providers_api_demo.py per mostrare i dati recuperati dalle api dei providers 3. aggiornato i provider 4. creato il provider binance sia pubblico che con chiave 5. creato error_handler.py per gestire decoratori e utilità: retry automatico, gestione timeout... * Refactor news API integration to use NewsApiWrapper and GnewsWrapper; add tests for Gnews API functionality * Add CryptoPanic API integration and related tests; update .env.example and test configurations * Implement WrapperHandler for managing multiple news API wrappers; add tests for wrapper functionality * Enhance WrapperHandler - docstrings - add try_call_all method - update tests * pre merge con phil * 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. * - Refactor struttura progetto: divisione tra agent e toolkit * Refactor try_call_all method to return a dictionary of results; update tests for success and partial failures * Fix class and test method names for DuckDuckGoWrapper * Add Reddit API wrapper and related tests; update environment configuration * pre merge con giacomo * Fix import statements * Fixes - separated tests - fix tests - fix bugs reintroduced my previous merge * Refactor market API wrappers to streamline product and price retrieval methods * Add BinanceWrapper to market API exports * Finito ISSUE 3 * Final review - rm PublicBinanceAgent & updated demo - moved in the correct folder some tests - fix binance bug --------- Co-authored-by: trojanhorse47 <cosmomemory@hotmail.it> Co-authored-by: Berack96 <giacomobertolazzi7@gmail.com> Co-authored-by: Giacomo Bertolazzi <31776951+Berack96@users.noreply.github.com>
This commit was merged in pull request #8.
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
import os
|
||||
import pytest
|
||||
from app.agents.market import MarketToolkit
|
||||
from app.markets.base import BaseWrapper
|
||||
from app.markets.coinbase import CoinBaseWrapper
|
||||
from app.markets.cryptocompare import CryptoCompareWrapper
|
||||
from app.markets import MarketAPIs
|
||||
|
||||
class TestMarketSystem:
|
||||
"""Test suite per il sistema di mercato (wrappers + toolkit)"""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def market_wrapper(self) -> BaseWrapper:
|
||||
return MarketAPIs("USD")
|
||||
|
||||
def test_wrapper_initialization(self, market_wrapper):
|
||||
assert market_wrapper is not None
|
||||
assert hasattr(market_wrapper, 'get_product')
|
||||
assert hasattr(market_wrapper, 'get_products')
|
||||
assert hasattr(market_wrapper, 'get_all_products')
|
||||
assert hasattr(market_wrapper, 'get_historical_prices')
|
||||
|
||||
def test_providers_configuration(self):
|
||||
available_providers = []
|
||||
if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'):
|
||||
available_providers.append('coinbase')
|
||||
if os.getenv('CRYPTOCOMPARE_API_KEY'):
|
||||
available_providers.append('cryptocompare')
|
||||
assert len(available_providers) > 0
|
||||
|
||||
def test_wrapper_capabilities(self, market_wrapper):
|
||||
capabilities = []
|
||||
if hasattr(market_wrapper, 'get_product'):
|
||||
capabilities.append('single_product')
|
||||
if hasattr(market_wrapper, 'get_products'):
|
||||
capabilities.append('multiple_products')
|
||||
if hasattr(market_wrapper, 'get_historical_prices'):
|
||||
capabilities.append('historical_data')
|
||||
assert len(capabilities) > 0
|
||||
|
||||
def test_market_data_retrieval(self, market_wrapper):
|
||||
btc_product = market_wrapper.get_product("BTC")
|
||||
assert btc_product is not None
|
||||
assert hasattr(btc_product, 'symbol')
|
||||
assert hasattr(btc_product, 'price')
|
||||
assert btc_product.price > 0
|
||||
|
||||
def test_market_toolkit_integration(self, market_wrapper):
|
||||
try:
|
||||
toolkit = MarketToolkit()
|
||||
assert toolkit is not None
|
||||
assert hasattr(toolkit, 'market_agent')
|
||||
assert toolkit.market_api is not None
|
||||
|
||||
tools = toolkit.tools
|
||||
assert len(tools) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"MarketToolkit test failed: {e}")
|
||||
# Non fail completamente - il toolkit potrebbe avere dipendenze specifiche
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not os.getenv('CRYPTOCOMPARE_API_KEY'),
|
||||
reason="CRYPTOCOMPARE_API_KEY not configured"
|
||||
)
|
||||
def test_cryptocompare_wrapper(self):
|
||||
try:
|
||||
api_key = os.getenv('CRYPTOCOMPARE_API_KEY')
|
||||
wrapper = CryptoCompareWrapper(api_key=api_key, currency="USD")
|
||||
|
||||
btc_product = wrapper.get_product("BTC")
|
||||
assert btc_product is not None
|
||||
assert btc_product.symbol == "BTC"
|
||||
assert btc_product.price > 0
|
||||
|
||||
products = wrapper.get_products(["BTC", "ETH"])
|
||||
assert isinstance(products, list)
|
||||
assert len(products) > 0
|
||||
|
||||
for product in products:
|
||||
if product.symbol in ["BTC", "ETH"]:
|
||||
assert product.price > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"CryptoCompare test failed: {e}")
|
||||
# Non fail il test se c'è un errore di rete o API
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY')),
|
||||
reason="Coinbase credentials not configured"
|
||||
)
|
||||
def test_coinbase_wrapper(self):
|
||||
try:
|
||||
api_key = os.getenv('CDP_API_KEY_NAME')
|
||||
api_secret = os.getenv('CDP_API_PRIVATE_KEY')
|
||||
wrapper = CoinBaseWrapper(
|
||||
api_key=api_key,
|
||||
api_private_key=api_secret,
|
||||
currency="USD"
|
||||
)
|
||||
|
||||
btc_product = wrapper.get_product("BTC")
|
||||
assert btc_product is not None
|
||||
assert btc_product.symbol == "BTC"
|
||||
assert btc_product.price > 0
|
||||
|
||||
products = wrapper.get_products(["BTC", "ETH"])
|
||||
assert isinstance(products, list)
|
||||
assert len(products) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"Coinbase test failed: {e}")
|
||||
# Non fail il test se c'è un errore di credenziali o rete
|
||||
|
||||
def test_provider_selection_mechanism(self):
|
||||
potential_providers = 0
|
||||
if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'):
|
||||
potential_providers += 1
|
||||
if os.getenv('CRYPTOCOMPARE_API_KEY'):
|
||||
potential_providers += 1
|
||||
|
||||
if potential_providers == 0:
|
||||
with pytest.raises(AssertionError, match="No valid API keys"):
|
||||
MarketAPIs.get_list_available_market_apis()
|
||||
else:
|
||||
wrapper = MarketAPIs("USD")
|
||||
assert wrapper is not None
|
||||
assert hasattr(wrapper, 'get_product')
|
||||
|
||||
def test_error_handling(self, market_wrapper):
|
||||
try:
|
||||
fake_product = market_wrapper.get_product("NONEXISTENT_CRYPTO_SYMBOL_12345")
|
||||
assert fake_product is None or fake_product.price == 0
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
empty_products = market_wrapper.get_products([])
|
||||
assert isinstance(empty_products, list)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def test_wrapper_currency_support(self, market_wrapper):
|
||||
assert hasattr(market_wrapper, 'currency')
|
||||
assert isinstance(market_wrapper.currency, str)
|
||||
assert len(market_wrapper.currency) >= 3 # USD, EUR, etc.
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from app.agents.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle
|
||||
from app.predictor import PREDICTOR_INSTRUCTIONS, PredictorInput, PredictorOutput, PredictorStyle
|
||||
from app.markets.base import ProductInfo
|
||||
from app.models import AppModels
|
||||
|
||||
@@ -16,8 +16,8 @@ def unified_checks(model: AppModels, input):
|
||||
for item in content.portfolio:
|
||||
assert item.asset not in (None, "", "null")
|
||||
assert isinstance(item.asset, str)
|
||||
assert item.percentage > 0
|
||||
assert item.percentage <= 100
|
||||
assert item.percentage >= 0.0
|
||||
assert item.percentage <= 100.0
|
||||
assert isinstance(item.percentage, (int, float))
|
||||
assert item.motivation not in (None, "", "null")
|
||||
assert isinstance(item.motivation, str)
|
||||
@@ -41,6 +41,7 @@ class TestPredictor:
|
||||
def test_gemini_model_output(self, inputs):
|
||||
unified_checks(AppModels.GEMINI, inputs)
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_ollama_qwen_model_output(self, inputs):
|
||||
unified_checks(AppModels.OLLAMA_QWEN, inputs)
|
||||
|
||||
|
||||
52
tests/api/test_binance.py
Normal file
52
tests/api/test_binance.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import pytest
|
||||
from app.markets.binance import BinanceWrapper
|
||||
|
||||
@pytest.mark.market
|
||||
@pytest.mark.api
|
||||
class TestBinance:
|
||||
|
||||
def test_binance_init(self):
|
||||
market = BinanceWrapper()
|
||||
assert market is not None
|
||||
assert hasattr(market, 'currency')
|
||||
assert market.currency == "USDT"
|
||||
|
||||
def test_binance_get_product(self):
|
||||
market = BinanceWrapper()
|
||||
product = market.get_product("BTC")
|
||||
assert product is not None
|
||||
assert hasattr(product, 'symbol')
|
||||
assert product.symbol == "BTC"
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_binance_get_products(self):
|
||||
market = BinanceWrapper()
|
||||
products = market.get_products(["BTC", "ETH"])
|
||||
assert products is not None
|
||||
assert isinstance(products, list)
|
||||
assert len(products) == 2
|
||||
symbols = [p.symbol for p in products]
|
||||
assert "BTC" in symbols
|
||||
assert "ETH" in symbols
|
||||
for product in products:
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_binance_invalid_product(self):
|
||||
market = BinanceWrapper()
|
||||
with pytest.raises(Exception):
|
||||
_ = market.get_product("INVALID")
|
||||
|
||||
def test_binance_history(self):
|
||||
market = BinanceWrapper()
|
||||
history = market.get_historical_prices("BTC", limit=5)
|
||||
assert history is not None
|
||||
assert isinstance(history, list)
|
||||
assert len(history) == 5
|
||||
for entry in history:
|
||||
assert hasattr(entry, 'time')
|
||||
assert hasattr(entry, 'close')
|
||||
assert hasattr(entry, 'high')
|
||||
assert entry.close > 0
|
||||
assert entry.high > 0
|
||||
54
tests/api/test_coinbase.py
Normal file
54
tests/api/test_coinbase.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import os
|
||||
import pytest
|
||||
from app.markets import CoinBaseWrapper
|
||||
|
||||
@pytest.mark.market
|
||||
@pytest.mark.api
|
||||
@pytest.mark.skipif(not(os.getenv('COINBASE_API_KEY')) or not(os.getenv('COINBASE_API_SECRET')), reason="COINBASE_API_KEY or COINBASE_API_SECRET not set in environment variables")
|
||||
class TestCoinBase:
|
||||
|
||||
def test_coinbase_init(self):
|
||||
market = CoinBaseWrapper()
|
||||
assert market is not None
|
||||
assert hasattr(market, 'currency')
|
||||
assert market.currency == "USD"
|
||||
|
||||
def test_coinbase_get_product(self):
|
||||
market = CoinBaseWrapper()
|
||||
product = market.get_product("BTC")
|
||||
assert product is not None
|
||||
assert hasattr(product, 'symbol')
|
||||
assert product.symbol == "BTC"
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_coinbase_get_products(self):
|
||||
market = CoinBaseWrapper()
|
||||
products = market.get_products(["BTC", "ETH"])
|
||||
assert products is not None
|
||||
assert isinstance(products, list)
|
||||
assert len(products) == 2
|
||||
symbols = [p.symbol for p in products]
|
||||
assert "BTC" in symbols
|
||||
assert "ETH" in symbols
|
||||
for product in products:
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_coinbase_invalid_product(self):
|
||||
market = CoinBaseWrapper()
|
||||
with pytest.raises(Exception):
|
||||
_ = market.get_product("INVALID")
|
||||
|
||||
def test_coinbase_history(self):
|
||||
market = CoinBaseWrapper()
|
||||
history = market.get_historical_prices("BTC", limit=5)
|
||||
assert history is not None
|
||||
assert isinstance(history, list)
|
||||
assert len(history) == 5
|
||||
for entry in history:
|
||||
assert hasattr(entry, 'time')
|
||||
assert hasattr(entry, 'close')
|
||||
assert hasattr(entry, 'high')
|
||||
assert entry.close > 0
|
||||
assert entry.high > 0
|
||||
56
tests/api/test_cryptocompare.py
Normal file
56
tests/api/test_cryptocompare.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import pytest
|
||||
from app.markets import CryptoCompareWrapper
|
||||
|
||||
@pytest.mark.market
|
||||
@pytest.mark.api
|
||||
@pytest.mark.skipif(not os.getenv('CRYPTOCOMPARE_API_KEY'), reason="CRYPTOCOMPARE_API_KEY not set in environment variables")
|
||||
class TestCryptoCompare:
|
||||
|
||||
def test_cryptocompare_init(self):
|
||||
market = CryptoCompareWrapper()
|
||||
assert market is not None
|
||||
assert hasattr(market, 'api_key')
|
||||
assert market.api_key == os.getenv('CRYPTOCOMPARE_API_KEY')
|
||||
assert hasattr(market, 'currency')
|
||||
assert market.currency == "USD"
|
||||
|
||||
def test_cryptocompare_get_product(self):
|
||||
market = CryptoCompareWrapper()
|
||||
product = market.get_product("BTC")
|
||||
assert product is not None
|
||||
assert hasattr(product, 'symbol')
|
||||
assert product.symbol == "BTC"
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_cryptocompare_get_products(self):
|
||||
market = CryptoCompareWrapper()
|
||||
products = market.get_products(["BTC", "ETH"])
|
||||
assert products is not None
|
||||
assert isinstance(products, list)
|
||||
assert len(products) == 2
|
||||
symbols = [p.symbol for p in products]
|
||||
assert "BTC" in symbols
|
||||
assert "ETH" in symbols
|
||||
for product in products:
|
||||
assert hasattr(product, 'price')
|
||||
assert product.price > 0
|
||||
|
||||
def test_cryptocompare_invalid_product(self):
|
||||
market = CryptoCompareWrapper()
|
||||
with pytest.raises(Exception):
|
||||
_ = market.get_product("INVALID")
|
||||
|
||||
def test_cryptocompare_history(self):
|
||||
market = CryptoCompareWrapper()
|
||||
history = market.get_historical_prices("BTC", limit=5)
|
||||
assert history is not None
|
||||
assert isinstance(history, list)
|
||||
assert len(history) == 5
|
||||
for entry in history:
|
||||
assert hasattr(entry, 'time')
|
||||
assert hasattr(entry, 'close')
|
||||
assert hasattr(entry, 'high')
|
||||
assert entry.close > 0
|
||||
assert entry.high > 0
|
||||
38
tests/api/test_cryptopanic_api.py
Normal file
38
tests/api/test_cryptopanic_api.py
Normal file
@@ -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(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 != ""
|
||||
|
||||
34
tests/api/test_duckduckgo_news.py
Normal file
34
tests/api/test_duckduckgo_news.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
from app.news import DuckDuckGoWrapper
|
||||
|
||||
|
||||
@pytest.mark.news
|
||||
@pytest.mark.api
|
||||
class TestDuckDuckGoNews:
|
||||
|
||||
def test_duckduckgo_initialization(self):
|
||||
news = DuckDuckGoWrapper()
|
||||
assert news.tool is not None
|
||||
|
||||
def test_duckduckgo_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_duckduckgo_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 != ""
|
||||
|
||||
34
tests/api/test_google_news.py
Normal file
34
tests/api/test_google_news.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import pytest
|
||||
from app.news import GoogleNewsWrapper
|
||||
|
||||
|
||||
@pytest.mark.news
|
||||
@pytest.mark.api
|
||||
class TestGoogleNews:
|
||||
|
||||
def test_gnews_api_initialization(self):
|
||||
gnews_api = GoogleNewsWrapper()
|
||||
assert gnews_api is not None
|
||||
|
||||
def test_gnews_api_get_latest_news(self):
|
||||
gnews_api = GoogleNewsWrapper()
|
||||
articles = gnews_api.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_gnews_api_get_top_headlines(self):
|
||||
news_api = GoogleNewsWrapper()
|
||||
articles = news_api.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 != ""
|
||||
|
||||
37
tests/api/test_news_api.py
Normal file
37
tests/api/test_news_api.py
Normal file
@@ -0,0 +1,37 @@
|
||||
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):
|
||||
news_api = NewsApiWrapper()
|
||||
assert news_api.client is not None
|
||||
|
||||
def test_news_api_get_latest_news(self):
|
||||
news_api = NewsApiWrapper()
|
||||
articles = news_api.get_latest_news(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)
|
||||
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_api = NewsApiWrapper()
|
||||
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:
|
||||
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 != ""
|
||||
|
||||
24
tests/api/test_reddit.py
Normal file
24
tests/api/test_reddit.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
from praw import Reddit
|
||||
from app.social.reddit import MAX_COMMENTS, RedditWrapper
|
||||
|
||||
@pytest.mark.social
|
||||
@pytest.mark.api
|
||||
class TestRedditWrapper:
|
||||
def test_initialization(self):
|
||||
wrapper = RedditWrapper()
|
||||
assert wrapper.client_id is not None
|
||||
assert wrapper.client_secret is not None
|
||||
assert isinstance(wrapper.tool, Reddit)
|
||||
|
||||
def test_get_top_crypto_posts(self):
|
||||
wrapper = RedditWrapper()
|
||||
posts = wrapper.get_top_crypto_posts(limit=2)
|
||||
assert isinstance(posts, list)
|
||||
assert len(posts) == 2
|
||||
for post in posts:
|
||||
assert post.title != ""
|
||||
assert isinstance(post.comments, list)
|
||||
assert len(post.comments) <= MAX_COMMENTS
|
||||
for comment in post.comments:
|
||||
assert comment.description != ""
|
||||
@@ -2,16 +2,10 @@
|
||||
Configurazione pytest per i test del progetto upo-appAI.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
# Aggiungi il path src al PYTHONPATH per tutti i test
|
||||
src_path = Path(__file__).parent.parent / "src"
|
||||
sys.path.insert(0, str(src_path))
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Carica le variabili d'ambiente per tutti i test
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
|
||||
@@ -21,32 +15,28 @@ def pytest_configure(config:pytest.Config):
|
||||
markers = [
|
||||
("slow", "marks tests as slow (deselect with '-m \"not slow\"')"),
|
||||
("api", "marks tests that require API access"),
|
||||
("coinbase", "marks tests that require Coinbase credentials"),
|
||||
("cryptocompare", "marks tests that require CryptoCompare credentials"),
|
||||
("market", "marks tests that use market data"),
|
||||
("gemini", "marks tests that use Gemini model"),
|
||||
("ollama_gpt", "marks tests that use Ollama GPT model"),
|
||||
("ollama_qwen", "marks tests that use Ollama Qwen model"),
|
||||
("news", "marks tests that use news"),
|
||||
("social", "marks tests that use social media"),
|
||||
("limited", "marks tests that have limited execution due to API constraints"),
|
||||
("wrapper", "marks tests for wrapper handler"),
|
||||
("tools", "marks tests for tools"),
|
||||
]
|
||||
for marker in markers:
|
||||
line = f"{marker[0]}: {marker[1]}"
|
||||
config.addinivalue_line("markers", line)
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Modifica automaticamente gli item di test aggiungendogli marker basati sul nome"""
|
||||
"""Modifica automaticamente degli item di test rimovendoli"""
|
||||
# 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
|
||||
|
||||
markers_to_add = {
|
||||
"api": pytest.mark.api,
|
||||
"coinbase": pytest.mark.api,
|
||||
"cryptocompare": pytest.mark.api,
|
||||
"overview": pytest.mark.slow,
|
||||
"analysis": pytest.mark.slow,
|
||||
"gemini": pytest.mark.gemini,
|
||||
"ollama_gpt": pytest.mark.ollama_gpt,
|
||||
"ollama_qwen": pytest.mark.ollama_qwen,
|
||||
}
|
||||
|
||||
for item in items:
|
||||
name = item.name.lower()
|
||||
for key, marker in markers_to_add.items():
|
||||
if key in name:
|
||||
item.add_marker(marker)
|
||||
new_mark = (f"({markexpr}) and " if markexpr else "") + f"not {mark}"
|
||||
setattr(config.option, "markexpr", new_mark)
|
||||
|
||||
70
tests/tools/test_market_tool.py
Normal file
70
tests/tools/test_market_tool.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import os
|
||||
import pytest
|
||||
from app.agents.market_agent import MarketToolkit
|
||||
from app.markets import MarketAPIsTool
|
||||
|
||||
@pytest.mark.limited # usa molte api calls e non voglio esaurire le chiavi api
|
||||
@pytest.mark.tools
|
||||
@pytest.mark.api
|
||||
class TestMarketAPIsTool:
|
||||
def test_wrapper_initialization(self):
|
||||
market_wrapper = MarketAPIsTool("USD")
|
||||
assert market_wrapper is not None
|
||||
assert hasattr(market_wrapper, 'get_product')
|
||||
assert hasattr(market_wrapper, 'get_products')
|
||||
assert hasattr(market_wrapper, 'get_all_products')
|
||||
assert hasattr(market_wrapper, 'get_historical_prices')
|
||||
|
||||
def test_wrapper_capabilities(self):
|
||||
market_wrapper = MarketAPIsTool("USD")
|
||||
capabilities = []
|
||||
if hasattr(market_wrapper, 'get_product'):
|
||||
capabilities.append('single_product')
|
||||
if hasattr(market_wrapper, 'get_products'):
|
||||
capabilities.append('multiple_products')
|
||||
if hasattr(market_wrapper, 'get_historical_prices'):
|
||||
capabilities.append('historical_data')
|
||||
assert len(capabilities) > 0
|
||||
|
||||
def test_market_data_retrieval(self):
|
||||
market_wrapper = MarketAPIsTool("USD")
|
||||
btc_product = market_wrapper.get_product("BTC")
|
||||
assert btc_product is not None
|
||||
assert hasattr(btc_product, 'symbol')
|
||||
assert hasattr(btc_product, 'price')
|
||||
assert btc_product.price > 0
|
||||
|
||||
def test_market_toolkit_integration(self):
|
||||
try:
|
||||
toolkit = MarketToolkit()
|
||||
assert toolkit is not None
|
||||
assert hasattr(toolkit, 'market_agent')
|
||||
assert toolkit.market_api is not None
|
||||
|
||||
tools = toolkit.tools
|
||||
assert len(tools) > 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"MarketToolkit test failed: {e}")
|
||||
# Non fail completamente - il toolkit potrebbe avere dipendenze specifiche
|
||||
|
||||
def test_provider_selection_mechanism(self):
|
||||
potential_providers = 0
|
||||
if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'):
|
||||
potential_providers += 1
|
||||
if os.getenv('CRYPTOCOMPARE_API_KEY'):
|
||||
potential_providers += 1
|
||||
|
||||
def test_error_handling(self):
|
||||
try:
|
||||
market_wrapper = MarketAPIsTool("USD")
|
||||
fake_product = market_wrapper.get_product("NONEXISTENT_CRYPTO_SYMBOL_12345")
|
||||
assert fake_product is None or fake_product.price == 0
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def test_wrapper_currency_support(self):
|
||||
market_wrapper = MarketAPIsTool("USD")
|
||||
assert hasattr(market_wrapper, 'currency')
|
||||
assert isinstance(market_wrapper.currency, str)
|
||||
assert len(market_wrapper.currency) >= 3 # USD, EUR, etc.
|
||||
88
tests/utils/test_market_data_aggregator.py
Normal file
88
tests/utils/test_market_data_aggregator.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
from app.utils.market_data_aggregator import MarketDataAggregator
|
||||
from app.utils.aggregated_models import AggregatedProductInfo
|
||||
from app.markets.base import ProductInfo, Price
|
||||
|
||||
|
||||
@pytest.mark.limited
|
||||
@pytest.mark.market
|
||||
@pytest.mark.api
|
||||
class TestMarketDataAggregator:
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test che il MarketDataAggregator si inizializzi correttamente"""
|
||||
aggregator = MarketDataAggregator()
|
||||
assert aggregator is not None
|
||||
assert aggregator.is_aggregation_enabled() == True
|
||||
|
||||
def test_aggregation_toggle(self):
|
||||
"""Test del toggle dell'aggregazione"""
|
||||
aggregator = MarketDataAggregator()
|
||||
|
||||
# Disabilita aggregazione
|
||||
aggregator.enable_aggregation(False)
|
||||
assert aggregator.is_aggregation_enabled() == False
|
||||
|
||||
# Riabilita aggregazione
|
||||
aggregator.enable_aggregation(True)
|
||||
assert aggregator.is_aggregation_enabled() == True
|
||||
|
||||
def test_aggregated_product_info_creation(self):
|
||||
"""Test creazione AggregatedProductInfo da fonti multiple"""
|
||||
|
||||
# Crea dati di esempio
|
||||
product1 = ProductInfo(
|
||||
id="BTC-USD",
|
||||
symbol="BTC-USD",
|
||||
price=50000.0,
|
||||
volume_24h=1000.0,
|
||||
status="active",
|
||||
quote_currency="USD"
|
||||
)
|
||||
|
||||
product2 = ProductInfo(
|
||||
id="BTC-USD",
|
||||
symbol="BTC-USD",
|
||||
price=50100.0,
|
||||
volume_24h=1100.0,
|
||||
status="active",
|
||||
quote_currency="USD"
|
||||
)
|
||||
|
||||
# Aggrega i prodotti
|
||||
aggregated = AggregatedProductInfo.from_multiple_sources([product1, product2])
|
||||
|
||||
assert aggregated.symbol == "BTC-USD"
|
||||
assert aggregated.price == pytest.approx(50050.0, rel=1e-3) # media tra 50000 e 50100
|
||||
assert aggregated.volume_24h == 50052.38095 # somma dei volumi
|
||||
assert aggregated.status == "active" # majority vote
|
||||
assert aggregated.id == "BTC-USD_AGG" # mapping_id con suffisso aggregazione
|
||||
|
||||
def test_confidence_calculation(self):
|
||||
"""Test del calcolo della confidence"""
|
||||
|
||||
product1 = ProductInfo(
|
||||
id="BTC-USD",
|
||||
symbol="BTC-USD",
|
||||
price=50000.0,
|
||||
volume_24h=1000.0,
|
||||
status="active",
|
||||
quote_currency="USD"
|
||||
)
|
||||
|
||||
product2 = ProductInfo(
|
||||
id="BTC-USD",
|
||||
symbol="BTC-USD",
|
||||
price=50100.0,
|
||||
volume_24h=1100.0,
|
||||
status="active",
|
||||
quote_currency="USD"
|
||||
)
|
||||
|
||||
aggregated = AggregatedProductInfo.from_multiple_sources([product1, product2])
|
||||
|
||||
# Verifica che ci siano metadati
|
||||
assert aggregated._metadata is not None
|
||||
assert len(aggregated._metadata.sources_used) > 0
|
||||
assert aggregated._metadata.aggregation_timestamp != ""
|
||||
# La confidence può essere 0.0 se ci sono fonti "unknown"
|
||||
90
tests/utils/test_wrapper_handler.py
Normal file
90
tests/utils/test_wrapper_handler.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import pytest
|
||||
from app.utils.wrapper_handler import WrapperHandler
|
||||
|
||||
class MockWrapper:
|
||||
def do_something(self) -> str:
|
||||
return "Success"
|
||||
|
||||
class MockWrapper2(MockWrapper):
|
||||
def do_something(self) -> str:
|
||||
return "Success 2"
|
||||
|
||||
class FailingWrapper(MockWrapper):
|
||||
def do_something(self):
|
||||
raise Exception("Intentional Failure")
|
||||
|
||||
|
||||
@pytest.mark.wrapper
|
||||
class TestWrapperHandler:
|
||||
def test_all_wrappers_fail(self):
|
||||
wrappers = [FailingWrapper, FailingWrapper]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
handler.try_call(lambda w: w.do_something())
|
||||
assert "All wrappers failed after retries" in str(exc_info.value)
|
||||
|
||||
def test_success_on_first_try(self):
|
||||
wrappers = [MockWrapper, FailingWrapper]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
|
||||
|
||||
result = handler.try_call(lambda w: w.do_something())
|
||||
assert result == "Success"
|
||||
assert handler.index == 0 # Should still be on the first wrapper
|
||||
assert handler.retry_count == 0
|
||||
|
||||
def test_eventual_success(self):
|
||||
wrappers = [FailingWrapper, MockWrapper]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
|
||||
|
||||
result = handler.try_call(lambda w: w.do_something())
|
||||
assert result == "Success"
|
||||
assert handler.index == 1 # Should have switched to the second wrapper
|
||||
assert handler.retry_count == 0
|
||||
|
||||
def test_partial_failures(self):
|
||||
wrappers = [FailingWrapper, MockWrapper, FailingWrapper]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
|
||||
|
||||
result = handler.try_call(lambda w: w.do_something())
|
||||
assert result == "Success"
|
||||
assert handler.index == 1 # Should have switched to the second wrapper
|
||||
assert handler.retry_count == 0
|
||||
|
||||
# Next call should still succeed on the second wrapper
|
||||
result = handler.try_call(lambda w: w.do_something())
|
||||
assert result == "Success"
|
||||
assert handler.index == 1 # Should still be on the second wrapper
|
||||
assert handler.retry_count == 0
|
||||
|
||||
handler.index = 2 # Manually switch to the third wrapper
|
||||
result = handler.try_call(lambda w: w.do_something())
|
||||
assert result == "Success"
|
||||
assert handler.index == 1 # Should return to the second wrapper after failure
|
||||
assert handler.retry_count == 0
|
||||
|
||||
def test_try_call_all_success(self):
|
||||
wrappers = [MockWrapper, MockWrapper2]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
|
||||
results = handler.try_call_all(lambda w: w.do_something())
|
||||
assert results == {MockWrapper: "Success", MockWrapper2: "Success 2"}
|
||||
|
||||
def test_try_call_all_partial_failures(self):
|
||||
# Only the second wrapper should succeed
|
||||
wrappers = [FailingWrapper, MockWrapper, FailingWrapper]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
|
||||
results = handler.try_call_all(lambda w: w.do_something())
|
||||
assert results == {MockWrapper: "Success"}
|
||||
|
||||
# Only the second and fourth wrappers should succeed
|
||||
wrappers = [FailingWrapper, MockWrapper, FailingWrapper, MockWrapper2]
|
||||
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
|
||||
results = handler.try_call_all(lambda w: w.do_something())
|
||||
assert results == {MockWrapper: "Success", MockWrapper2: "Success 2"}
|
||||
|
||||
def test_try_call_all_all_fail(self):
|
||||
# Test when all wrappers fail
|
||||
handler_all_fail: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers([FailingWrapper, FailingWrapper], try_per_wrapper=1, retry_delay=0)
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
handler_all_fail.try_call_all(lambda w: w.do_something())
|
||||
assert "All wrappers failed" in str(exc_info.value)
|
||||
Reference in New Issue
Block a user