From 43b2bddba5fc89fcc53f49d598122b975cdee7aa Mon Sep 17 00:00:00 2001 From: Berack96 Date: Tue, 30 Sep 2025 15:36:37 +0200 Subject: [PATCH] Add Reddit API wrapper and related tests; update environment configuration --- .env.example | 16 +++++++++--- pyproject.toml | 20 +++++++-------- src/app/social/base.py | 22 +++++++++++++++++ src/app/social/reddit.py | 53 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_reddit.py | 24 ++++++++++++++++++ tests/conftest.py | 1 + uv.lock | 49 +++++++++++++++++++++++++++++++++++++ 7 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 src/app/social/reddit.py create mode 100644 tests/api/test_reddit.py diff --git a/.env.example b/.env.example index f59ad82..0948465 100644 --- a/.env.example +++ b/.env.example @@ -10,11 +10,11 @@ GOOGLE_API_KEY= # Configurazioni per gli agenti di mercato ############################################################################### -# Ottenibili da: https://portal.cdp.coinbase.com/access/api +# https://portal.cdp.coinbase.com/access/api CDP_API_KEY_NAME= CDP_API_PRIVATE_KEY= -# Ottenibile da: https://www.cryptocompare.com/cryptopian/api-keys +# https://www.cryptocompare.com/cryptopian/api-keys CRYPTOCOMPARE_API_KEY= # Binance API per Market Agent (alternativa) @@ -25,8 +25,16 @@ BINANCE_API_SECRET= # Configurazioni per gli agenti di notizie ############################################################################### -# Ottenibile da: https://newsapi.org/docs +# https://newsapi.org/docs NEWS_API_KEY= -# Ottenibile da: https://cryptopanic.com/developers/api/ +# https://cryptopanic.com/developers/api/ CRYPTOPANIC_API_KEY= + +############################################################################### +# Configurazioni per API di social media +############################################################################### + +# https://www.reddit.com/prefs/apps +REDDIT_API_CLIENT_ID= +REDDIT_API_CLIENT_SECRET= diff --git a/pyproject.toml b/pyproject.toml index 8f4cc87..74c0029 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,30 +10,30 @@ requires-python = "==3.12.*" # Per ogni roba ho fatto un commento per evitare di dimenticarmi cosa fa chi. # Inoltre ho messo una emoji per indicare se è raccomandato o meno. dependencies = [ - # ✅ per i test - "pytest", - # ✅ per gestire variabili d'ambiente (generalmente API keys od opzioni) - "dotenv", - # ✅ per fare una UI web semplice con input e output - "gradio", + "pytest", # Test + "dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni) + "gradio", # UI web semplice con user_input e output - # ✅ per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno + # Per costruire agenti (ovvero modelli che possono fare più cose tramite tool) https://github.com/agno-agi/agno # altamente consigliata dato che ha anche tools integrati per fare scraping, calcoli e molto altro # oltre a questa è necessario installare anche le librerie specifiche per i modelli che si vogliono usare "agno", - # ✅ Modelli supportati e installati (aggiungere qui sotto quelli che si vogliono usare) + # Modelli supportati e installati (aggiungere qui sotto quelli che si vogliono usare) "google-genai", "ollama", - # ✅ per interagire con API di exchange di criptovalute + # API di exchange di criptovalute "coinbase-advanced-py", "python-binance", - # ✅ per interagire con API di notizie + # API di notizie "newsapi-python", "gnews", "ddgs", + + # API di social media + "praw", # Reddit ] [tool.pytest.ini_options] diff --git a/src/app/social/base.py b/src/app/social/base.py index e69de29..945cdd5 100644 --- a/src/app/social/base.py +++ b/src/app/social/base.py @@ -0,0 +1,22 @@ +from pydantic import BaseModel + + +class SocialPost(BaseModel): + time: str = "" + title: str = "" + description: str = "" + comments: list["SocialComment"] = [] + + def __str__(self): + return f"Title: {self.title}\nDescription: {self.description}\nComments: {len(self.comments)}\n[{" | ".join(str(c) for c in self.comments)}]" + +class SocialComment(BaseModel): + time: str = "" + description: str = "" + + def __str__(self): + return f"Time: {self.time}\nDescription: {self.description}" + +# TODO IMPLEMENTARLO SE SI USANO PIU' WRAPPER (E QUINDI PIU' SOCIAL) +class SocialWrapper: + pass diff --git a/src/app/social/reddit.py b/src/app/social/reddit.py new file mode 100644 index 0000000..7a3c824 --- /dev/null +++ b/src/app/social/reddit.py @@ -0,0 +1,53 @@ +import os +from praw import Reddit +from praw.models import Submission, MoreComments +from .base import SocialWrapper, SocialPost, SocialComment + +MAX_COMMENTS = 5 + + +def create_social_post(post: Submission) -> SocialPost: + social = SocialPost() + social.time = str(post.created) + social.title = post.title + social.description = post.selftext + + for i, top_comment in enumerate(post.comments): + if i >= MAX_COMMENTS: + break + if isinstance(top_comment, MoreComments): #skip MoreComments objects + continue + + comment = SocialComment() + comment.time = str(top_comment.created) + comment.description = top_comment.body + social.comments.append(comment) + return social + +class RedditWrapper(SocialWrapper): + """ + A wrapper for the Reddit API using PRAW (Python Reddit API Wrapper). + Requires the following environment variables to be set: + - REDDIT_API_CLIENT_ID + - REDDIT_API_CLIENT_SECRET + You can get them by creating an app at https://www.reddit.com/prefs/apps + """ + + def __init__(self): + self.client_id = os.getenv("REDDIT_API_CLIENT_ID") + assert self.client_id is not None, "REDDIT_API_CLIENT_ID environment variable is not set" + + self.client_secret = os.getenv("REDDIT_API_CLIENT_SECRET") + assert self.client_secret is not None, "REDDIT_API_CLIENT_SECRET environment variable is not set" + + self.tool = Reddit( + client_id=self.client_id, + client_secret=self.client_secret, + user_agent="upo-appAI", + ) + + def get_top_crypto_posts(self, limit=5) -> list[SocialPost]: + subreddit = self.tool.subreddit("CryptoCurrency") + top_posts = subreddit.top(limit=limit, time_filter="week") + return [create_social_post(post) for post in top_posts] + diff --git a/tests/api/test_reddit.py b/tests/api/test_reddit.py new file mode 100644 index 0000000..84c66da --- /dev/null +++ b/tests/api/test_reddit.py @@ -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 != "" diff --git a/tests/conftest.py b/tests/conftest.py index 9bd9589..40d5aab 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"), + ("social", "marks tests that use social media"), ("limited", "marks tests that have limited execution due to API constraints"), ("wrapper", "marks tests for wrapper handler"), ] diff --git a/uv.lock b/uv.lock index ce4499b..2d7d6a1 100644 --- a/uv.lock +++ b/uv.lock @@ -961,6 +961,32 @@ 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 = "praw" +version = "7.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prawcore" }, + { name = "update-checker" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/52/7dd0b3d9ccb78e90236420ef6c51b6d9b2400a7229442f0cfcf2258cce21/praw-7.8.1.tar.gz", hash = "sha256:3c5767909f71e48853eb6335fef7b50a43cbe3da728cdfb16d3be92904b0a4d8", size = 154106, upload-time = "2024-10-25T21:49:33.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/ca/60ec131c3b43bff58261167045778b2509b83922ce8f935ac89d871bd3ea/praw-7.8.1-py3-none-any.whl", hash = "sha256:15917a81a06e20ff0aaaf1358481f4588449fa2421233040cb25e5c8202a3e2f", size = 189338, upload-time = "2024-10-25T21:49:31.109Z" }, +] + +[[package]] +name = "prawcore" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/62/d4c99cf472205f1e5da846b058435a6a7c988abf8eb6f7d632a7f32f4a77/prawcore-2.4.0.tar.gz", hash = "sha256:b7b2b5a1d04406e086ab4e79988dc794df16059862f329f4c6a43ed09986c335", size = 15862, upload-time = "2023-10-01T23:30:49.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/5c/8af904314e42d5401afcfaff69940dc448e974f80f7aa39b241a4fbf0cf1/prawcore-2.4.0-py3-none-any.whl", hash = "sha256:29af5da58d85704b439ad3c820873ad541f4535e00bb98c66f0fbcc8c603065a", size = 17203, upload-time = "2023-10-01T23:30:47.651Z" }, +] + [[package]] name = "primp" version = "0.15.0" @@ -1490,6 +1516,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, ] +[[package]] +name = "update-checker" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/0b/1bec4a6cc60d33ce93d11a7bcf1aeffc7ad0aa114986073411be31395c6f/update_checker-0.18.0.tar.gz", hash = "sha256:6a2d45bb4ac585884a6b03f9eade9161cedd9e8111545141e9aa9058932acb13", size = 6699, upload-time = "2020-08-04T07:08:50.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ba/8dd7fa5f0b1c6a8ac62f8f57f7e794160c1f86f31c6d0fb00f582372a3e4/update_checker-0.18.0-py3-none-any.whl", hash = "sha256:cbba64760a36fe2640d80d85306e8fe82b6816659190993b7bdabadee4d4bbfd", size = 7008, upload-time = "2020-08-04T07:08:49.51Z" }, +] + [[package]] name = "upo-app-ai" version = "0.1.0" @@ -1504,6 +1542,7 @@ dependencies = [ { name = "gradio" }, { name = "newsapi-python" }, { name = "ollama" }, + { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, ] @@ -1519,6 +1558,7 @@ requires-dist = [ { name = "gradio" }, { name = "newsapi-python" }, { name = "ollama" }, + { name = "praw" }, { name = "pytest" }, { name = "python-binance" }, ] @@ -1545,6 +1585,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, ] +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + [[package]] name = "websockets" version = "13.1"