From 41001580f5a656cd7f37447d7f13a0430368e2bf Mon Sep 17 00:00:00 2001 From: Daviddanillo Date: Sat, 16 May 2026 19:01:44 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20adiciona=20integra=C3=A7=C3=A3o=20com?= =?UTF-8?q?=20NewsApi=20para=20resumos=20economicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + src/main.py | 48 +++++++++++++++------ src/noticias.py | 59 ++++++++++++++++++++++++++ tests/test_noticias.py | 95 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 src/noticias.py create mode 100644 tests/test_noticias.py diff --git a/requirements.txt b/requirements.txt index fcc4c4e..bf00d43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pytest>=8.0.0 ruff>=0.4.0 +requests>=2.31.0 diff --git a/src/main.py b/src/main.py index e0fefcc..8e57a80 100644 --- a/src/main.py +++ b/src/main.py @@ -9,19 +9,21 @@ resumo_por_categoria, salvar_dados, ) +from src.noticias import buscar_noticias_economicas def exibir_menu() -> None: - print("\n╔════════════════════════════════╗") - print("║ Gerenciador de Gastos ║") - print("╠══════════════════════════════════╣") - print("║ 1. Adicionar gasto ║") - print("║ 2. Listar gastos ║") - print("║ 3. Remover gasto ║") - print("║ 4. Resumo por categoria ║") - print("║ 5. Ver total gasto ║") - print("║ 0. Sair ║") - print("╚══════════════════════════════════╝") + print("\n" + "=" * 40) + print(" Gerenciador de Gastos") + print("=" * 40) + print(" 1. Adicionar gasto") + print(" 2. Listar gastos") + print(" 3. Remover gasto") + print(" 4. Resumo por categoria") + print(" 5. Ver total gasto") + print(" 6. Resumo econômico do dia") + print(" 0. Sair") + print("=" * 40) def exibir_gastos(gastos: list[dict]) -> None: @@ -42,7 +44,6 @@ def fluxo_adicionar(gastos: list[dict]) -> None: descricao = input(" Descrição: ").strip() categoria = input(" Categoria (ex: alimentação, transporte): ").strip() valor_str = input(" Valor (R$): ").strip().replace(",", ".") - try: valor = float(valor_str) gasto = adicionar_gasto(gastos, descricao, valor, categoria) @@ -79,6 +80,25 @@ def fluxo_resumo(gastos: list[dict]) -> None: print(f" {cat:<20} R$ {total:>8.2f}") +def fluxo_noticias() -> None: + """Busca e exibe as principais notícias econômicas do dia.""" + print("\n --- Resumo Econômico do Dia ---") + print(" Buscando notícias...") + try: + noticias = buscar_noticias_economicas() + if not noticias: + print(" Nenhuma notícia encontrada no momento.") + return + for i, n in enumerate(noticias, 1): + print(f"\n [{i}] {n['titulo']}") + print(f" Fonte: {n['fonte']}") + except ValueError as e: + print(f"\n Configuração pendente: {e}") + print(" Dica: defina a variável de ambiente NEWSAPI_KEY com sua chave gratuita.") + except ConnectionError as e: + print(f"\n Não foi possível buscar notícias: {e}") + + def main() -> None: gastos = carregar_dados() @@ -98,12 +118,14 @@ def main() -> None: fluxo_resumo(gastos) elif opcao == "5": total = calcular_total(gastos) - print(f"\n Total gasto: R$ {total:.2f}") + print(f"\n Total gasto: R$ {total:.2f}") + elif opcao == "6": + fluxo_noticias() elif opcao == "0": print("\n Até logo! \n") break else: - print("\n Opção inválida.") + print("\n Opção inválida.") if __name__ == "__main__": diff --git a/src/noticias.py b/src/noticias.py new file mode 100644 index 0000000..e945c1a --- /dev/null +++ b/src/noticias.py @@ -0,0 +1,59 @@ +"""Integração com a NewsAPI para exibir resumo econômico do dia.""" + +import os + +import requests + +NEWSAPI_URL = "https://newsapi.org/v2/everything" +NEWSAPI_KEY = os.getenv("NEWSAPI_KEY", "0315b6055a57461bb41004994dd2abe8") +TIMEOUT = 5 + + +def buscar_noticias_economicas(api_key: str = NEWSAPI_KEY, quantidade: int = 3) -> list[dict]: + """Busca as principais notícias econômicas do dia via NewsAPI. + + Args: + api_key: Chave da NewsAPI. + quantidade: Número de notícias a retornar. + + Returns: + Lista de dicionários com 'titulo' e 'fonte'. + + Raises: + ValueError: Se a API key estiver vazia. + ConnectionError: Se a requisição falhar. + """ + if not api_key: + raise ValueError("NEWSAPI_KEY não configurada. Defina a variável de ambiente.") + + params = { + "q": "economia brasil finanças", + "language": "pt", + "sortBy": "publishedAt", + "pageSize": quantidade, + "apiKey": api_key, + } + + try: + response = requests.get(NEWSAPI_URL, params=params, timeout=TIMEOUT) + response.raise_for_status() + dados = response.json() + + if dados.get("status") != "ok": + raise ConnectionError(f"Erro da API: {dados.get('message', 'desconhecido')}") + + artigos = dados.get("articles", []) + return [ + { + "titulo": a.get("title", "Sem título"), + "fonte": a.get("source", {}).get("name", "Desconhecida"), + } + for a in artigos + ] + + except requests.exceptions.Timeout: + raise ConnectionError("Tempo de conexão esgotado ao acessar a NewsAPI.") + except requests.exceptions.ConnectionError: + raise ConnectionError("Sem conexão com a internet.") + except requests.exceptions.HTTPError as e: + raise ConnectionError(f"Erro HTTP: {e}") diff --git a/tests/test_noticias.py b/tests/test_noticias.py new file mode 100644 index 0000000..ecb3e5a --- /dev/null +++ b/tests/test_noticias.py @@ -0,0 +1,95 @@ +"""Testes de integração para o módulo de notícias econômicas.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from src.noticias import buscar_noticias_economicas + +# --------------------------------------------------------------------------- +# Dados simulados (mock) — não dependem de internet nem de API key real +# --------------------------------------------------------------------------- + +RESPOSTA_MOCK = { + "status": "ok", + "totalResults": 2, + "articles": [ + { + "title": "Dólar cai frente ao real após dados do IPCA", + "source": {"name": "Valor Econômico"}, + }, + { + "title": "Bolsa fecha em alta puxada por commodities", + "source": {"name": "InfoMoney"}, + }, + ], +} + + +# --------------------------------------------------------------------------- +# Testes +# --------------------------------------------------------------------------- + + +def test_buscar_noticias_retorna_lista(monkeypatch): + """Verifica que a função retorna uma lista com os campos corretos.""" + mock_response = MagicMock() + mock_response.json.return_value = RESPOSTA_MOCK + mock_response.raise_for_status = MagicMock() + + with patch("src.noticias.requests.get", return_value=mock_response): + resultado = buscar_noticias_economicas(api_key="fake-key-teste") + + assert isinstance(resultado, list) + assert len(resultado) == 2 + assert resultado[0]["titulo"] == "Dólar cai frente ao real após dados do IPCA" + assert resultado[0]["fonte"] == "Valor Econômico" + + +def test_buscar_noticias_campos_presentes(monkeypatch): + """Verifica que cada notícia possui os campos 'titulo' e 'fonte'.""" + mock_response = MagicMock() + mock_response.json.return_value = RESPOSTA_MOCK + mock_response.raise_for_status = MagicMock() + + with patch("src.noticias.requests.get", return_value=mock_response): + resultado = buscar_noticias_economicas(api_key="fake-key-teste") + + for noticia in resultado: + assert "titulo" in noticia + assert "fonte" in noticia + + +def test_buscar_noticias_sem_api_key(): + """Verifica que ValueError é levantado quando a API key está vazia.""" + with pytest.raises(ValueError, match="NEWSAPI_KEY"): + buscar_noticias_economicas(api_key="") + + +def test_buscar_noticias_timeout(): + """Verifica que ConnectionError é levantado em caso de timeout.""" + import requests as req + + with patch("src.noticias.requests.get", side_effect=req.exceptions.Timeout): + with pytest.raises(ConnectionError, match="Tempo de conexão"): + buscar_noticias_economicas(api_key="fake-key-teste") + + +def test_buscar_noticias_sem_internet(): + """Verifica que ConnectionError é levantado quando não há conexão.""" + import requests as req + + with patch("src.noticias.requests.get", side_effect=req.exceptions.ConnectionError): + with pytest.raises(ConnectionError, match="internet"): + buscar_noticias_economicas(api_key="fake-key-teste") + + +def test_buscar_noticias_status_erro(): + """Verifica que ConnectionError é levantado quando a API retorna status de erro.""" + mock_response = MagicMock() + mock_response.json.return_value = {"status": "error", "message": "apiKey inválida"} + mock_response.raise_for_status = MagicMock() + + with patch("src.noticias.requests.get", return_value=mock_response): + with pytest.raises(ConnectionError, match="Erro da API"): + buscar_noticias_economicas(api_key="fake-key-errada")