🌗 ← Índice

🐍 Python P18 — Desafio Final Mês 2 (CLI com API externa)


Data: 14/08/2026

Nível: Intermediário (Mês 2 — POO)




🎯 Objetivos





🏗️ O Projeto: `clima.py` — CLI de previsão do tempo


Uma ferramenta de terminal que consulta a Open-Meteo (API gratuita, sem chave) e mostra a previsão do tempo para uma cidade.


🌐 API gratuita: `https://api.open-meteo.com/v1/forecast?latitude=...&longitude=...`

Passo 0 — Descobrir coordenadas


A Open-Meteo tem um serviço de geocodificação gratuito:



https://geocoding-api.open-meteo.com/v1/search?name=Sao%20Paulo&count=1

Resposta (JSON):


{"results": [{"name": "São Paulo", "latitude": -23.55, "longitude": -46.63}]}

Passo 1 — Estrutura do projeto



clima/
├── clima.py          # CLI principal
├── api_clima.py      # classe ClimaAPI (encapsula requests)
└── test_api_clima.py # testes

Passo 2 — `api_clima.py`



import requests


class ClimaAPI:
    BASE = "https://api.open-meteo.com/v1/forecast"

    def __init__(self, timeout=10):
        self.timeout = timeout

    def buscar_coordenadas(self, cidade):
        url = "https://geocoding-api.open-meteo.com/v1/search"
        resposta = requests.get(url, params={"name": cidade, "count": 1}, timeout=self.timeout)
        resposta.raise_for_status()
        resultados = resposta.json().get("results", [])
        if not resultados:
            raise ValueError(f"Cidade '{cidade}' não encontrada")
        return resultados[0]["latitude"], resultados[0]["longitude"]

    def previsao(self, cidade):
        lat, lon = self.buscar_coordenadas(cidade)
        resposta = requests.get(
            self.BASE,
            params={
                "latitude": lat,
                "longitude": lon,
                "hourly": "temperature_2m",
                "forecast_days": 1,
                "timezone": "America/Sao_Paulo",
            },
            timeout=self.timeout,
        )
        resposta.raise_for_status()
        return resposta.json()

Passo 3 — `clima.py` (CLI)



import sys
from datetime import datetime
from api_clima import ClimaAPI


def exibir_previsao(dados):
    horarios = dados["hourly"]["time"]
    temperaturas = dados["hourly"]["temperature_2m"]
    agora = datetime.now()

    print(f"\n🌤️  Previsão para hoje — {agora.strftime('%d/%m/%Y')}\n")
    for hora, temp in zip(horarios, temperaturas):
        hora_obj = datetime.fromisoformat(hora)
        if hora_obj.hour % 4 == 0:          # mostra a cada 4h
            print(f"  {hora_obj.strftime('%H:%M')}  →  {temp:.1f}°C")


def main():
    if len(sys.argv) < 2:
        print("Uso: python clima.py 'Nome da Cidade'")
        sys.exit(1)

    cidade = " ".join(sys.argv[1:])
    api = ClimaAPI()
    try:
        dados = api.previsao(cidade)
        exibir_previsao(dados)
    except ValueError as erro:
        print(f"❌ {erro}")
        sys.exit(1)
    except requests.exceptions.RequestException as erro:
        print(f"❌ Erro de rede: {erro}")
        sys.exit(1)


if __name__ == "__main__":
    import requests
    main()

Passo 4 — Rodar



python clima.py "São Paulo"
# 🌤️  Previsão para hoje — 14/08/2026
#   00:00  →  16.2°C
#   04:00  →  14.8°C
#   ...



✍️ Exercícios


  1. 1. Adicione a umidade à previsão (parâmetro `hourly: "temperature_2m,relative_humidity_2m"`).
  2. 2. Trate o caso de cidade com nome composto (`"Rio de Janeiro"` — teste com e sem aspas).
  3. 3. Adicione a opção `--dias N` para mostrar N dias (parâmetro `forecast_days`).



🧪 Desafio


Escreva testes para ClimaAPI:





✅ Correção (testes)



# test_api_clima.py
import pytest
from unittest.mock import patch, MagicMock
from api_clima import ClimaAPI


def test_buscar_coordenadas_sucesso():
    resposta_fake = MagicMock()
    resposta_fake.json.return_value = {
        "results": [{"name": "São Paulo", "latitude": -23.55, "longitude": -46.63}]
    }
    with patch("requests.get", return_value=resposta_fake) as mock_get:
        api = ClimaAPI()
        lat, lon = api.buscar_coordenadas("São Paulo")

    assert lat == -23.55
    assert lon == -46.63
    mock_get.assert_called_once()


def test_buscar_coordenadas_nao_encontrada():
    resposta_fake = MagicMock()
    resposta_fake.json.return_value = {}
    with patch("requests.get", return_value=resposta_fake):
        api = ClimaAPI()
        with pytest.raises(ValueError, match="não encontrada"):
            api.buscar_coordenadas("Atlantida")


def test_previsao_monta_params():
    resposta_geo = MagicMock()
    resposta_geo.json.return_value = {"results": [{"latitude": -23.55, "longitude": -46.63}]}
    resposta_previsao = MagicMock()
    resposta_previsao.json.return_value = {"hourly": {"time": [], "temperature_2m": []}}

    def fake_get(url, **kwargs):
        if "geocoding" in url:
            return resposta_geo
        return resposta_previsao

    with patch("requests.get", side_effect=fake_get) as mock_get:
        api = ClimaAPI()
        dados = api.previsao("São Paulo")

    # a 2ª chamada deve ter forecast_days e timezone
    segunda_chamada = mock_get.call_args_list[1]
    params = segunda_chamada.kwargs["params"]
    assert params["forecast_days"] == 1
    assert "America/Sao_Paulo" in params["timezone"]

💡 Resultado esperado: 3 passed — e a CLI funciona de verdade na rede real.




🏆 Marco do Mês 2 concluído!


Você construiu uma CLI real que consome API externa — combinando POO, requests, datetime, tratamento de erros e testes. Isso é exatamente o perfil de automação do Mês 3: ferramentas úteis para o seu dia a dia (e para as suas VPSs!).





HB Estudos · ← Voltar ao índice de Python