Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pytest>=8.0.0
ruff>=0.4.0
requests>=2.31.0
48 changes: 35 additions & 13 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand All @@ -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__":
Expand Down
59 changes: 59 additions & 0 deletions src/noticias.py
Original file line number Diff line number Diff line change
@@ -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}")
95 changes: 95 additions & 0 deletions tests/test_noticias.py
Original file line number Diff line number Diff line change
@@ -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")
Loading