9 enhancement con financialdatasettool e yfinance #11
32
.env.example
32
.env.example
@@ -1,4 +1,4 @@
|
|||||||
###########################################################################
|
###############################################################################
|
||||||
# Configurazioni per i modelli di linguaggio
|
# Configurazioni per i modelli di linguaggio
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
@@ -13,19 +13,11 @@ OLLAMA_MODELS_PATH=
|
|||||||
# Configurazioni per gli agenti di mercato
|
# Configurazioni per gli agenti di mercato
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
# Coinbase CDP API per Market Agent
|
# https://portal.cdp.coinbase.com/access/api
|
||||||
# Ottenibili da: https://portal.cdp.coinbase.com/access/api
|
|
||||||
# IMPORTANTE: Usare le credenziali CDP (NON Exchange legacy)
|
|
||||||
# - COINBASE_API_KEY: organizations/{org_id}/apiKeys/{key_id}
|
|
||||||
# - COINBASE_API_SECRET: La private key completa (inclusi BEGIN/END)
|
|
||||||
# - NON serve COINBASE_PASSPHRASE (solo per Exchange legacy)
|
|
||||||
COINBASE_API_KEY=
|
COINBASE_API_KEY=
|
||||||
COINBASE_API_SECRET=
|
COINBASE_API_SECRET=
|
||||||
|
|
||||||
# CryptoCompare API per Market Agent
|
# https://www.cryptocompare.com/cryptopian/api-keys
|
||||||
# Ottenibile da: https://www.cryptocompare.com/cryptopian/api-keys
|
|
||||||
# NOTA: API legacy, potrebbe essere deprecata in futuro
|
|
||||||
# Funzionalità limitata: get_all_products() non supportato
|
|
||||||
CRYPTOCOMPARE_API_KEY=
|
CRYPTOCOMPARE_API_KEY=
|
||||||
|
|
||||||
# Binance API per Market Agent
|
# Binance API per Market Agent
|
||||||
@@ -33,3 +25,21 @@ CRYPTOCOMPARE_API_KEY=
|
|||||||
# Supporta sia API autenticate che pubbliche (PublicBinance)
|
# Supporta sia API autenticate che pubbliche (PublicBinance)
|
||||||
BINANCE_API_KEY=
|
BINANCE_API_KEY=
|
||||||
BINANCE_API_SECRET=
|
BINANCE_API_SECRET=
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Configurazioni per gli agenti di notizie
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
# https://newsapi.org/docs
|
||||||
|
NEWS_API_KEY=
|
||||||
|
|
||||||
|
# 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=
|
||||||
|
|||||||
16
demos/news_api.py
Normal file
16
demos/news_api.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#### FOR ALL FILES OUTSIDE src/ FOLDER ####
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||||
|
###########################################
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from app.news import NewsApiWrapper
|
||||||
|
|
||||||
|
def main():
|
||||||
|
api = NewsApiWrapper()
|
||||||
|
print("ok")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
load_dotenv()
|
||||||
|
main()
|
||||||
@@ -10,25 +10,32 @@ requires-python = "==3.12.*"
|
|||||||
# Per ogni roba ho fatto un commento per evitare di dimenticarmi cosa fa chi.
|
# 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.
|
# Inoltre ho messo una emoji per indicare se è raccomandato o meno.
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# ✅ per i test
|
"pytest", # Test
|
||||||
"pytest",
|
"dotenv", # Gestire variabili d'ambiente (generalmente API keys od opzioni)
|
||||||
# ✅ per gestire variabili d'ambiente (generalmente API keys od opzioni)
|
"gradio", # UI web semplice con user_input e output
|
||||||
"dotenv",
|
|
||||||
# 🟡 per fare scraping di pagine web
|
|
||||||
#"bs4",
|
|
||||||
# ✅ per fare una UI web semplice con user_input e output
|
|
||||||
"gradio",
|
|
||||||
|
|
||||||
# ✅ 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
|
# 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
|
# oltre a questa è necessario installare anche le librerie specifiche per i modelli che si vogliono usare
|
||||||
"agno",
|
"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",
|
"google-genai",
|
||||||
"ollama",
|
"ollama",
|
||||||
|
|
||||||
# ✅ per interagire con API di exchange di criptovalute
|
# API di exchange di criptovalute
|
||||||
"coinbase-advanced-py",
|
"coinbase-advanced-py",
|
||||||
"python-binance",
|
"python-binance",
|
||||||
|
|
||||||
|
# API di notizie
|
||||||
|
"newsapi-python",
|
||||||
|
"gnews",
|
||||||
|
"ddgs",
|
||||||
|
|
||||||
|
# API di social media
|
||||||
|
"praw", # Reddit
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|||||||
32
src/app/news/__init__.py
Normal file
32
src/app/news/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from app.utils.wrapper_handler import WrapperHandler
|
||||||
|
from .base import NewsWrapper, Article
|
||||||
|
from .news_api import NewsApiWrapper
|
||||||
|
from .gnews_api import GoogleNewsWrapper
|
||||||
|
from .cryptopanic_api import CryptoPanicWrapper
|
||||||
|
from .duckduckgo import DuckDuckGoWrapper
|
||||||
|
|
||||||
|
__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:
|
||||||
|
- GoogleNewsWrapper
|
||||||
|
- 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 = [GoogleNewsWrapper, DuckDuckGoWrapper, NewsApiWrapper, CryptoPanicWrapper]
|
||||||
|
self.wrapper_handler: WrapperHandler[NewsWrapper] = WrapperHandler.build_wrappers(wrappers)
|
||||||
|
|
||||||
|
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))
|
||||||
35
src/app/news/base.py
Normal file
35
src/app/news/base.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Article(BaseModel):
|
||||||
|
source: str = ""
|
||||||
|
time: str = ""
|
||||||
|
title: str = ""
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
class NewsWrapper:
|
||||||
|
"""
|
||||||
|
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")
|
||||||
|
|
||||||
77
src/app/news/cryptopanic_api.py
Normal file
77
src/app/news/cryptopanic_api.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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):
|
||||||
|
"""
|
||||||
|
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"
|
||||||
|
|
||||||
|
# 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, 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
|
||||||
|
|
||||||
|
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]
|
||||||
32
src/app/news/duckduckgo.py
Normal file
32
src/app/news/duckduckgo.py
Normal file
@@ -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]
|
||||||
|
|
||||||
36
src/app/news/gnews_api.py
Normal file
36
src/app/news/gnews_api.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from gnews import GNews
|
||||||
|
from .base import Article, NewsWrapper
|
||||||
|
|
||||||
|
def result_to_article(result: dict) -> Article:
|
||||||
|
article = Article()
|
||||||
|
article.source = result.get("source", "")
|
||||||
|
article.time = result.get("publishedAt", "")
|
||||||
|
article.title = result.get("title", "")
|
||||||
|
article.description = result.get("description", "")
|
||||||
|
return 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()
|
||||||
|
|
||||||
|
articles = []
|
||||||
|
for result in results:
|
||||||
|
article = result_to_article(result)
|
||||||
|
articles.append(article)
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def get_latest_news(self, query: str, total: int = 100) -> list[Article]:
|
||||||
|
gnews = GNews(language='en', max_results=total, period='7d')
|
||||||
|
results = gnews.get_news(query)
|
||||||
|
|
||||||
|
articles = []
|
||||||
|
for result in results:
|
||||||
|
article = result_to_article(result)
|
||||||
|
articles.append(article)
|
||||||
|
return articles
|
||||||
50
src/app/news/news_api.py
Normal file
50
src/app/news/news_api.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import os
|
||||||
|
import newsapi
|
||||||
|
from .base import Article, NewsWrapper
|
||||||
|
|
||||||
|
def result_to_article(result: dict) -> Article:
|
||||||
|
article = Article()
|
||||||
|
article.source = result.get("source", {}).get("name", "")
|
||||||
|
article.time = result.get("publishedAt", "")
|
||||||
|
article.title = result.get("title", "")
|
||||||
|
article.description = result.get("description", "")
|
||||||
|
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"
|
||||||
|
|
||||||
|
self.client = newsapi.NewsApiClient(api_key=api_key)
|
||||||
|
self.category = "business" # Cryptocurrency is under business
|
||||||
|
self.language = "en" # TODO Only English articles for now?
|
||||||
|
self.max_page_size = 100
|
||||||
|
|
||||||
|
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="", 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
|
||||||
|
|
||||||
|
def get_latest_news(self, query: str, 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):
|
||||||
|
everything = self.client.get_everything(q=query, language=self.language, sort_by="publishedAt", page_size=page_size, page=page)
|
||||||
|
results = [result_to_article(article) for article in everything.get("articles", [])]
|
||||||
|
articles.extend(results)
|
||||||
|
return articles
|
||||||
|
|
||||||
1
src/app/social/__init.py
Normal file
1
src/app/social/__init.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .base import SocialWrapper
|
||||||
22
src/app/social/base.py
Normal file
22
src/app/social/base.py
Normal file
@@ -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
|
||||||
53
src/app/social/reddit.py
Normal file
53
src/app/social/reddit.py
Normal file
@@ -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]
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ def unified_checks(model: AppModels, input):
|
|||||||
for item in content.portfolio:
|
for item in content.portfolio:
|
||||||
assert item.asset not in (None, "", "null")
|
assert item.asset not in (None, "", "null")
|
||||||
assert isinstance(item.asset, str)
|
assert isinstance(item.asset, str)
|
||||||
assert item.percentage > 0
|
assert item.percentage >= 0.0
|
||||||
assert item.percentage <= 100
|
assert item.percentage <= 100.0
|
||||||
assert isinstance(item.percentage, (int, float))
|
assert isinstance(item.percentage, (int, float))
|
||||||
assert item.motivation not in (None, "", "null")
|
assert item.motivation not in (None, "", "null")
|
||||||
assert isinstance(item.motivation, str)
|
assert isinstance(item.motivation, str)
|
||||||
@@ -41,6 +41,7 @@ class TestPredictor:
|
|||||||
def test_gemini_model_output(self, inputs):
|
def test_gemini_model_output(self, inputs):
|
||||||
unified_checks(AppModels.GEMINI, inputs)
|
unified_checks(AppModels.GEMINI, inputs)
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
def test_ollama_qwen_model_output(self, inputs):
|
def test_ollama_qwen_model_output(self, inputs):
|
||||||
unified_checks(AppModels.OLLAMA_QWEN, inputs)
|
unified_checks(AppModels.OLLAMA_QWEN, inputs)
|
||||||
|
|
||||||
|
|||||||
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.
|
Configurazione pytest per i test del progetto upo-appAI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import pytest
|
import pytest
|
||||||
from pathlib import Path
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Aggiungi il path src al PYTHONPATH per tutti i test
|
|
||||||
src_path = Path(__file__).parent.parent / "src"
|
|
||||||
sys.path.insert(0, str(src_path))
|
|
||||||
|
|
||||||
# Carica le variabili d'ambiente per tutti i test
|
# Carica le variabili d'ambiente per tutti i test
|
||||||
from dotenv import load_dotenv
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +20,10 @@ def pytest_configure(config:pytest.Config):
|
|||||||
("gemini", "marks tests that use Gemini model"),
|
("gemini", "marks tests that use Gemini model"),
|
||||||
("ollama_gpt", "marks tests that use Ollama GPT model"),
|
("ollama_gpt", "marks tests that use Ollama GPT model"),
|
||||||
("ollama_qwen", "marks tests that use Ollama Qwen 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"),
|
||||||
]
|
]
|
||||||
for marker in markers:
|
for marker in markers:
|
||||||
line = f"{marker[0]}: {marker[1]}"
|
line = f"{marker[0]}: {marker[1]}"
|
||||||
@@ -35,7 +33,6 @@ def pytest_collection_modifyitems(config, items):
|
|||||||
"""Modifica automaticamente gli item di test aggiungendogli marker basati sul nome"""
|
"""Modifica automaticamente gli item di test aggiungendogli marker basati sul nome"""
|
||||||
|
|
||||||
markers_to_add = {
|
markers_to_add = {
|
||||||
"api": pytest.mark.api,
|
|
||||||
"coinbase": pytest.mark.api,
|
"coinbase": pytest.mark.api,
|
||||||
"cryptocompare": pytest.mark.api,
|
"cryptocompare": pytest.mark.api,
|
||||||
"overview": pytest.mark.slow,
|
"overview": pytest.mark.slow,
|
||||||
@@ -50,3 +47,13 @@ def pytest_collection_modifyitems(config, items):
|
|||||||
for key, marker in markers_to_add.items():
|
for key, marker in markers_to_add.items():
|
||||||
if key in name:
|
if key in name:
|
||||||
item.add_marker(marker)
|
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)
|
||||||
|
|||||||
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)
|
||||||
257
uv.lock
generated
257
uv.lock
generated
@@ -130,6 +130,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -156,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.5.2"
|
version = "5.5.2"
|
||||||
@@ -310,6 +340,30 @@ 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" },
|
{ 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"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "docstring-parser"
|
name = "docstring-parser"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -344,6 +398,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" },
|
{ url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "feedparser"
|
||||||
|
version = "6.0.12"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "sgmllib3k" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ffmpy"
|
name = "ffmpy"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -421,6 +487,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gnews"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
|
{ name = "dnspython" },
|
||||||
|
{ name = "feedparser" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6c/65/d4b19ebde3edd4d0cb63660fe61e9777de1dd35ea819cb72a5b53002bb97/gnews-0.4.2.tar.gz", hash = "sha256:5016cf5299f42ea072adb295abe5e9f093c5c422da2c12e6661d1dcdbc56d011", size = 24847, upload-time = "2025-07-27T13:46:54.717Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6f/77/00b21cce68b6041e78edf23efbc95eea6a4555cd474594b7360d1b9e4444/gnews-0.4.2-py3-none-any.whl", hash = "sha256:ed1fa603a7edeb3886925e756b114afb1e0c5b7b9f56fe5ebeedeeb730d2a9c4", size = 18142, upload-time = "2025-07-27T13:46:53.848Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-auth"
|
name = "google-auth"
|
||||||
version = "2.40.3"
|
version = "2.40.3"
|
||||||
@@ -528,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "hf-xet"
|
name = "hf-xet"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@@ -543,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -571,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.35.0"
|
version = "0.35.0"
|
||||||
@@ -590,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
@@ -620,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@@ -686,6 +836,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
{ url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "newsapi-python"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/4b/12fb9495211fc5a6d3a96968759c1a48444124a1654aaf65d0de80b46794/newsapi-python-0.2.7.tar.gz", hash = "sha256:a4b66d5dd9892198cdaa476f7542f2625cdd218e5e3121c8f880b2ace717a3c2", size = 7485, upload-time = "2023-03-02T13:15:35.89Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/47/e3b099102f0c826d37841d2266e19f1568dcf58ba86e4c6948e2a124f91d/newsapi_python-0.2.7-py2.py3-none-any.whl", hash = "sha256:11d34013a24d92ca7b7cbdac84ed2d504862b1e22467bc2a9a6913a70962318e", size = 7942, upload-time = "2023-03-02T13:15:34.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
@@ -799,6 +961,48 @@ 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" },
|
{ 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"
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "propcache"
|
name = "propcache"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1152,6 +1356,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sgmllib3k"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" }
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@@ -1188,6 +1398,24 @@ 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" },
|
{ 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"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "starlette"
|
name = "starlette"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
@@ -1288,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "upo-app-ai"
|
name = "upo-app-ai"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -1295,10 +1535,14 @@ source = { virtual = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "agno" },
|
{ name = "agno" },
|
||||||
{ name = "coinbase-advanced-py" },
|
{ name = "coinbase-advanced-py" },
|
||||||
|
{ name = "ddgs" },
|
||||||
{ name = "dotenv" },
|
{ name = "dotenv" },
|
||||||
|
{ name = "gnews" },
|
||||||
{ name = "google-genai" },
|
{ name = "google-genai" },
|
||||||
{ name = "gradio" },
|
{ name = "gradio" },
|
||||||
|
{ name = "newsapi-python" },
|
||||||
{ name = "ollama" },
|
{ name = "ollama" },
|
||||||
|
{ name = "praw" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "python-binance" },
|
{ name = "python-binance" },
|
||||||
]
|
]
|
||||||
@@ -1307,10 +1551,14 @@ dependencies = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "agno" },
|
{ name = "agno" },
|
||||||
{ name = "coinbase-advanced-py" },
|
{ name = "coinbase-advanced-py" },
|
||||||
|
{ name = "ddgs" },
|
||||||
{ name = "dotenv" },
|
{ name = "dotenv" },
|
||||||
|
{ name = "gnews" },
|
||||||
{ name = "google-genai" },
|
{ name = "google-genai" },
|
||||||
{ name = "gradio" },
|
{ name = "gradio" },
|
||||||
|
{ name = "newsapi-python" },
|
||||||
{ name = "ollama" },
|
{ name = "ollama" },
|
||||||
|
{ name = "praw" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "python-binance" },
|
{ name = "python-binance" },
|
||||||
]
|
]
|
||||||
@@ -1337,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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "websockets"
|
name = "websockets"
|
||||||
version = "13.1"
|
version = "13.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user